mirror of
https://github.com/roytam1/palemoon27.git
synced 2026-05-26 05:37:11 +00:00
import changes from rmottola/Arctic-Fox:
- remove mobile-android (cf8ef1e27) - remove also android examples (94f68c0e5) - remove android mozglue (d0114f339)
This commit is contained in:
@@ -95,7 +95,6 @@ MACH_MODULES = [
|
||||
'tools/docs/mach_commands.py',
|
||||
'tools/mercurial/mach_commands.py',
|
||||
'tools/mach_commands.py',
|
||||
'mobile/android/mach_commands.py',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
# Ensure ANDROID_SDK is defined before including this file.
|
||||
# We use common android defaults for boot class path and java version.
|
||||
ifndef ANDROID_SDK
|
||||
$(error ANDROID_SDK must be defined before including android-common.mk)
|
||||
endif
|
||||
|
||||
# DEBUG_JARSIGNER always debug signs.
|
||||
DEBUG_JARSIGNER=$(PYTHON) $(abspath $(topsrcdir)/mobile/android/debug_sign_tool.py) \
|
||||
--keytool=$(KEYTOOL) \
|
||||
--jarsigner=$(JARSIGNER) \
|
||||
$(NULL)
|
||||
|
||||
# RELEASE_JARSIGNER release signs if possible.
|
||||
ifdef MOZ_SIGN_CMD
|
||||
RELEASE_JARSIGNER := $(MOZ_SIGN_CMD) -f jar
|
||||
else
|
||||
RELEASE_JARSIGNER := $(DEBUG_JARSIGNER)
|
||||
endif
|
||||
|
||||
# $(1) is the full path to input: foo-debug-unsigned-unaligned.apk.
|
||||
# $(2) is the full path to output: foo.apk.
|
||||
# Use this like: $(call RELEASE_SIGN_ANDROID_APK,foo-debug-unsigned-unaligned.apk,foo.apk)
|
||||
RELEASE_SIGN_ANDROID_APK = \
|
||||
cp $(1) $(2)-unaligned.apk && \
|
||||
$(RELEASE_JARSIGNER) $(2)-unaligned.apk && \
|
||||
$(ZIPALIGN) -f -v 4 $(2)-unaligned.apk $(2) && \
|
||||
$(RM) $(2)-unaligned.apk
|
||||
|
||||
# For Android, this defaults to $(ANDROID_SDK)/android.jar
|
||||
ifndef JAVA_BOOTCLASSPATH
|
||||
JAVA_BOOTCLASSPATH = $(ANDROID_SDK)/android.jar
|
||||
endif
|
||||
|
||||
# For Android, we default to 1.7
|
||||
ifndef JAVA_VERSION
|
||||
JAVA_VERSION = 1.7
|
||||
endif
|
||||
|
||||
JAVAC_FLAGS = \
|
||||
-target $(JAVA_VERSION) \
|
||||
-source $(JAVA_VERSION) \
|
||||
$(if $(JAVA_CLASSPATH),-classpath $(JAVA_CLASSPATH),) \
|
||||
-bootclasspath $(JAVA_BOOTCLASSPATH) \
|
||||
-encoding UTF8 \
|
||||
-g:source,lines \
|
||||
-Werror \
|
||||
$(NULL)
|
||||
@@ -4053,30 +4053,6 @@ fi
|
||||
AC_SUBST(MOZ_BING_API_CLIENTID)
|
||||
AC_SUBST(MOZ_BING_API_KEY)
|
||||
|
||||
# Whether to include optional-but-large font files in the final APK.
|
||||
# We want this in mobile/android/confvars.sh, so it goes early.
|
||||
MOZ_ARG_DISABLE_BOOL(android-include-fonts,
|
||||
[ --disable-android-include-fonts
|
||||
Disable the inclusion of fonts into the final APK],
|
||||
MOZ_ANDROID_EXCLUDE_FONTS=1)
|
||||
|
||||
if test -n "$MOZ_ANDROID_EXCLUDE_FONTS"; then
|
||||
AC_DEFINE(MOZ_ANDROID_EXCLUDE_FONTS)
|
||||
fi
|
||||
AC_SUBST(MOZ_ANDROID_EXCLUDE_FONTS)
|
||||
|
||||
# Whether this APK is destined for resource constrained devices.
|
||||
# We want this in mobile/android/confvars.sh, so it goes early.
|
||||
MOZ_ARG_ENABLE_BOOL(android-resource-constrained,
|
||||
[ --enable-android-resource-constrained
|
||||
Exclude hi-res images and similar from the final APK],
|
||||
MOZ_ANDROID_RESOURCE_CONSTRAINED=1)
|
||||
|
||||
if test -n "$MOZ_ANDROID_RESOURCE_CONSTRAINED"; then
|
||||
AC_DEFINE(MOZ_ANDROID_RESOURCE_CONSTRAINED)
|
||||
fi
|
||||
AC_SUBST(MOZ_ANDROID_RESOURCE_CONSTRAINED)
|
||||
|
||||
# Allow the application to influence configure with a confvars.sh script.
|
||||
AC_MSG_CHECKING([if app-specific confvars.sh exists])
|
||||
if test -f "${srcdir}/${MOZ_BUILD_APP}/confvars.sh" ; then
|
||||
@@ -4165,28 +4141,6 @@ WINNT|Darwin|Android)
|
||||
;;
|
||||
esac
|
||||
|
||||
dnl ========================================================
|
||||
dnl Check Android SDK version depending on mobile target.
|
||||
dnl ========================================================
|
||||
|
||||
if test -z "$gonkdir" ; then
|
||||
# Minimum Android SDK API Level we require.
|
||||
case "$MOZ_BUILD_APP" in
|
||||
mobile/android)
|
||||
android_min_api_level=20
|
||||
case "$target" in
|
||||
*-android*|*-linuxandroid*)
|
||||
:
|
||||
;;
|
||||
*)
|
||||
AC_MSG_ERROR([You must specify --target=arm-linux-androideabi (or some other valid android target) when building with --enable-application=mobile/android. See https://wiki.mozilla.org/Mobile/Fennec/Android#Setup_Fennec_mozconfig for more information about the necessary options])
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
|
||||
MOZ_ANDROID_SDK($android_min_api_level)
|
||||
fi
|
||||
|
||||
dnl ========================================================
|
||||
dnl =
|
||||
@@ -4217,7 +4171,6 @@ MOZ_ARG_HEADER(Toolkit Options)
|
||||
-o "$_DEFAULT_TOOLKIT" = "cairo-qt" \
|
||||
-o "$_DEFAULT_TOOLKIT" = "cairo-cocoa" \
|
||||
-o "$_DEFAULT_TOOLKIT" = "cairo-uikit" \
|
||||
-o "$_DEFAULT_TOOLKIT" = "cairo-android" \
|
||||
-o "$_DEFAULT_TOOLKIT" = "cairo-gonk"
|
||||
then
|
||||
dnl nglayout only supports building with one toolkit,
|
||||
|
||||
@@ -1,988 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Activity;
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.ContentUris;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import android.telephony.SmsManager;
|
||||
import android.telephony.SmsMessage;
|
||||
|
||||
import static android.telephony.SmsMessage.MessageClass;
|
||||
|
||||
/**
|
||||
* This class is returning unique ids for PendingIntent requestCode attribute.
|
||||
* There are only |Integer.MAX_VALUE - Integer.MIN_VALUE| unique IDs available,
|
||||
* and they wrap around.
|
||||
*/
|
||||
class PendingIntentUID
|
||||
{
|
||||
static private int sUID = Integer.MIN_VALUE;
|
||||
|
||||
static public int generate() { return sUID++; }
|
||||
}
|
||||
|
||||
/**
|
||||
* The envelope class contains all information that are needed to keep track of
|
||||
* a sent SMS.
|
||||
*/
|
||||
class Envelope
|
||||
{
|
||||
enum SubParts {
|
||||
SENT_PART,
|
||||
DELIVERED_PART
|
||||
}
|
||||
|
||||
protected int mId;
|
||||
protected int mMessageId;
|
||||
protected long mMessageTimestamp;
|
||||
|
||||
/**
|
||||
* Number of sent/delivered remaining parts.
|
||||
* @note The array has much slots as SubParts items.
|
||||
*/
|
||||
protected int[] mRemainingParts;
|
||||
|
||||
/**
|
||||
* Whether sending/delivering is currently failing.
|
||||
* @note The array has much slots as SubParts items.
|
||||
*/
|
||||
protected boolean[] mFailing;
|
||||
|
||||
/**
|
||||
* Error type (only for sent).
|
||||
*/
|
||||
protected int mError;
|
||||
|
||||
public Envelope(int aId, int aParts) {
|
||||
mId = aId;
|
||||
mMessageId = -1;
|
||||
mMessageTimestamp = 0;
|
||||
mError = GoannaSmsManager.kNoError;
|
||||
|
||||
int size = Envelope.SubParts.values().length;
|
||||
mRemainingParts = new int[size];
|
||||
mFailing = new boolean[size];
|
||||
|
||||
for (int i=0; i<size; ++i) {
|
||||
mRemainingParts[i] = aParts;
|
||||
mFailing[i] = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void decreaseRemainingParts(Envelope.SubParts aType) {
|
||||
--mRemainingParts[aType.ordinal()];
|
||||
|
||||
if (mRemainingParts[SubParts.SENT_PART.ordinal()] >
|
||||
mRemainingParts[SubParts.DELIVERED_PART.ordinal()]) {
|
||||
Log.e("GoannaSmsManager", "Delivered more parts than we sent!?");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean arePartsRemaining(Envelope.SubParts aType) {
|
||||
return mRemainingParts[aType.ordinal()] != 0;
|
||||
}
|
||||
|
||||
public void markAsFailed(Envelope.SubParts aType) {
|
||||
mFailing[aType.ordinal()] = true;
|
||||
}
|
||||
|
||||
public boolean isFailing(Envelope.SubParts aType) {
|
||||
return mFailing[aType.ordinal()];
|
||||
}
|
||||
|
||||
public int getMessageId() {
|
||||
return mMessageId;
|
||||
}
|
||||
|
||||
public void setMessageId(int aMessageId) {
|
||||
mMessageId = aMessageId;
|
||||
}
|
||||
|
||||
public long getMessageTimestamp() {
|
||||
return mMessageTimestamp;
|
||||
}
|
||||
|
||||
public void setMessageTimestamp(long aMessageTimestamp) {
|
||||
mMessageTimestamp = aMessageTimestamp;
|
||||
}
|
||||
|
||||
public int getError() {
|
||||
return mError;
|
||||
}
|
||||
|
||||
public void setError(int aError) {
|
||||
mError = aError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Postman class is a singleton that manages Envelope instances.
|
||||
*/
|
||||
class Postman
|
||||
{
|
||||
public static final int kUnknownEnvelopeId = -1;
|
||||
|
||||
private static final Postman sInstance = new Postman();
|
||||
|
||||
private ArrayList<Envelope> mEnvelopes = new ArrayList<Envelope>(1);
|
||||
|
||||
private Postman() {}
|
||||
|
||||
public static Postman getInstance() {
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public int createEnvelope(int aParts) {
|
||||
/*
|
||||
* We are going to create the envelope in the first empty slot in the array
|
||||
* list. If there is no empty slot, we create a new one.
|
||||
*/
|
||||
int size = mEnvelopes.size();
|
||||
|
||||
for (int i=0; i<size; ++i) {
|
||||
if (mEnvelopes.get(i) == null) {
|
||||
mEnvelopes.set(i, new Envelope(i, aParts));
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
mEnvelopes.add(new Envelope(size, aParts));
|
||||
return size;
|
||||
}
|
||||
|
||||
public Envelope getEnvelope(int aId) {
|
||||
if (aId < 0 || mEnvelopes.size() <= aId) {
|
||||
Log.e("GoannaSmsManager", "Trying to get an unknown Envelope!");
|
||||
return null;
|
||||
}
|
||||
|
||||
Envelope envelope = mEnvelopes.get(aId);
|
||||
if (envelope == null) {
|
||||
Log.e("GoannaSmsManager", "Trying to get an empty Envelope!");
|
||||
}
|
||||
|
||||
return envelope;
|
||||
}
|
||||
|
||||
public void destroyEnvelope(int aId) {
|
||||
if (aId < 0 || mEnvelopes.size() <= aId) {
|
||||
Log.e("GoannaSmsManager", "Trying to destroy an unknown Envelope!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (mEnvelopes.set(aId, null) == null) {
|
||||
Log.e("GoannaSmsManager", "Trying to destroy an empty Envelope!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SmsIOThread extends Thread {
|
||||
private final static SmsIOThread sInstance = new SmsIOThread();
|
||||
|
||||
private Handler mHandler;
|
||||
|
||||
public static SmsIOThread getInstance() {
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public boolean execute(Runnable r) {
|
||||
return mHandler.post(r);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
Looper.prepare();
|
||||
|
||||
mHandler = new Handler();
|
||||
|
||||
Looper.loop();
|
||||
}
|
||||
}
|
||||
|
||||
class MessagesListManager
|
||||
{
|
||||
private static final MessagesListManager sInstance = new MessagesListManager();
|
||||
|
||||
public static MessagesListManager getInstance() {
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
private ArrayList<Cursor> mCursors = new ArrayList<Cursor>(0);
|
||||
|
||||
public int add(Cursor aCursor) {
|
||||
int size = mCursors.size();
|
||||
|
||||
for (int i=0; i<size; ++i) {
|
||||
if (mCursors.get(i) == null) {
|
||||
mCursors.set(i, aCursor);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
mCursors.add(aCursor);
|
||||
return size;
|
||||
}
|
||||
|
||||
public Cursor get(int aId) {
|
||||
if (aId < 0 || mCursors.size() <= aId) {
|
||||
Log.e("GoannaSmsManager", "Trying to get an unknown list!");
|
||||
return null;
|
||||
}
|
||||
|
||||
Cursor cursor = mCursors.get(aId);
|
||||
if (cursor == null) {
|
||||
Log.e("GoannaSmsManager", "Trying to get an empty list!");
|
||||
}
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public void remove(int aId) {
|
||||
if (aId < 0 || mCursors.size() <= aId) {
|
||||
Log.e("GoannaSmsManager", "Trying to destroy an unknown list!");
|
||||
return;
|
||||
}
|
||||
|
||||
Cursor cursor = mCursors.set(aId, null);
|
||||
if (cursor == null) {
|
||||
Log.e("GoannaSmsManager", "Trying to destroy an empty list!");
|
||||
return;
|
||||
}
|
||||
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
for (int i=0; i<mCursors.size(); ++i) {
|
||||
Cursor c = mCursors.get(i);
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
|
||||
mCursors.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public class GoannaSmsManager
|
||||
extends BroadcastReceiver
|
||||
implements ISmsManager
|
||||
{
|
||||
public final static String ACTION_SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED";
|
||||
public final static String ACTION_SMS_SENT = "org.mozilla.goanna.SMS_SENT";
|
||||
public final static String ACTION_SMS_DELIVERED = "org.mozilla.goanna.SMS_DELIVERED";
|
||||
|
||||
/*
|
||||
* Make sure that the following error codes are in sync with the ones
|
||||
* defined in dom/mobilemessage/interfaces/nsIMobileMessageCallback.idl. They are owned
|
||||
* owned by the interface.
|
||||
*/
|
||||
public final static int kNoError = 0;
|
||||
public final static int kNoSignalError = 1;
|
||||
public final static int kNotFoundError = 2;
|
||||
public final static int kUnknownError = 3;
|
||||
public final static int kInternalError = 4;
|
||||
public final static int kNoSimCardError = 5;
|
||||
public final static int kRadioDisabledError = 6;
|
||||
public final static int kInvalidAddressError = 7;
|
||||
public final static int kFdnCheckError = 8;
|
||||
public final static int kNonActiveSimCardError = 9;
|
||||
public final static int kStorageFullError = 10;
|
||||
public final static int kSimNotMatchedError = 11;
|
||||
|
||||
private final static int kMaxMessageSize = 160;
|
||||
|
||||
private final static Uri kSmsContentUri = Uri.parse("content://sms");
|
||||
private final static Uri kSmsSentContentUri = Uri.parse("content://sms/sent");
|
||||
|
||||
private final static int kSmsTypeInbox = 1;
|
||||
private final static int kSmsTypeSentbox = 2;
|
||||
|
||||
/*
|
||||
* Keep the following state codes in syng with |DeliveryState| in:
|
||||
* dom/mobilemessage/Types.h
|
||||
*/
|
||||
private final static int kDeliveryStateSent = 0;
|
||||
private final static int kDeliveryStateReceived = 1;
|
||||
private final static int kDeliveryStateSending = 2;
|
||||
private final static int kDeliveryStateError = 3;
|
||||
private final static int kDeliveryStateUnknown = 4;
|
||||
private final static int kDeliveryStateNotDownloaded = 5;
|
||||
private final static int kDeliveryStateEndGuard = 6;
|
||||
|
||||
/*
|
||||
* Keep the following status codes in sync with |DeliveryStatus| in:
|
||||
* dom/mobilemessage/Types.h
|
||||
*/
|
||||
private final static int kDeliveryStatusNotApplicable = 0;
|
||||
private final static int kDeliveryStatusSuccess = 1;
|
||||
private final static int kDeliveryStatusPending = 2;
|
||||
private final static int kDeliveryStatusError = 3;
|
||||
|
||||
/*
|
||||
* android.provider.Telephony.Sms.STATUS_*. Duplicated because they're not
|
||||
* part of Android public API.
|
||||
*/
|
||||
private final static int kInternalDeliveryStatusNone = -1;
|
||||
private final static int kInternalDeliveryStatusComplete = 0;
|
||||
private final static int kInternalDeliveryStatusPending = 32;
|
||||
private final static int kInternalDeliveryStatusFailed = 64;
|
||||
|
||||
/*
|
||||
* Keep the following values in sync with |MessageClass| in:
|
||||
* dom/mobilemessage/Types.h
|
||||
*/
|
||||
private final static int kMessageClassNormal = 0;
|
||||
private final static int kMessageClassClass0 = 1;
|
||||
private final static int kMessageClassClass1 = 2;
|
||||
private final static int kMessageClassClass2 = 3;
|
||||
private final static int kMessageClassClass3 = 4;
|
||||
|
||||
private final static String[] kRequiredMessageRows = new String[] { "_id", "address", "body", "date", "type", "status" };
|
||||
|
||||
public GoannaSmsManager() {
|
||||
SmsIOThread.getInstance().start();
|
||||
}
|
||||
|
||||
public void start() {
|
||||
IntentFilter smsFilter = new IntentFilter();
|
||||
smsFilter.addAction(GoannaSmsManager.ACTION_SMS_RECEIVED);
|
||||
smsFilter.addAction(GoannaSmsManager.ACTION_SMS_SENT);
|
||||
smsFilter.addAction(GoannaSmsManager.ACTION_SMS_DELIVERED);
|
||||
|
||||
GoannaApp.mAppContext.registerReceiver(this, smsFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent.getAction().equals(ACTION_SMS_RECEIVED)) {
|
||||
// TODO: Try to find the receiver number to be able to populate
|
||||
// SmsMessage.receiver.
|
||||
// TODO: Get the id and the date from the stock app saved message.
|
||||
// Using the stock app saved message require us to wait for it to
|
||||
// be saved which can lead to race conditions.
|
||||
|
||||
Bundle bundle = intent.getExtras();
|
||||
|
||||
if (bundle == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object[] pdus = (Object[]) bundle.get("pdus");
|
||||
|
||||
for (int i=0; i<pdus.length; ++i) {
|
||||
SmsMessage msg = SmsMessage.createFromPdu((byte[])pdus[i]);
|
||||
|
||||
GoannaAppShell.notifySmsReceived(msg.getDisplayOriginatingAddress(),
|
||||
msg.getDisplayMessageBody(),
|
||||
getGoannaMessageClass(msg.getMessageClass()),
|
||||
System.currentTimeMillis());
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (intent.getAction().equals(ACTION_SMS_SENT) ||
|
||||
intent.getAction().equals(ACTION_SMS_DELIVERED)) {
|
||||
Bundle bundle = intent.getExtras();
|
||||
|
||||
if (bundle == null || !bundle.containsKey("envelopeId") ||
|
||||
!bundle.containsKey("number") || !bundle.containsKey("message") ||
|
||||
!bundle.containsKey("requestId")) {
|
||||
Log.e("GoannaSmsManager", "Got an invalid ACTION_SMS_SENT/ACTION_SMS_DELIVERED!");
|
||||
return;
|
||||
}
|
||||
|
||||
int envelopeId = bundle.getInt("envelopeId");
|
||||
Postman postman = Postman.getInstance();
|
||||
|
||||
Envelope envelope = postman.getEnvelope(envelopeId);
|
||||
if (envelope == null) {
|
||||
Log.e("GoannaSmsManager", "Got an invalid envelope id (or Envelope has been destroyed)!");
|
||||
return;
|
||||
}
|
||||
|
||||
Envelope.SubParts part = intent.getAction().equals(ACTION_SMS_SENT)
|
||||
? Envelope.SubParts.SENT_PART
|
||||
: Envelope.SubParts.DELIVERED_PART;
|
||||
envelope.decreaseRemainingParts(part);
|
||||
|
||||
|
||||
if (getResultCode() != Activity.RESULT_OK) {
|
||||
switch (getResultCode()) {
|
||||
case SmsManager.RESULT_ERROR_NULL_PDU:
|
||||
envelope.setError(kInternalError);
|
||||
break;
|
||||
case SmsManager.RESULT_ERROR_NO_SERVICE:
|
||||
case SmsManager.RESULT_ERROR_RADIO_OFF:
|
||||
envelope.setError(kNoSignalError);
|
||||
break;
|
||||
case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
|
||||
default:
|
||||
envelope.setError(kUnknownError);
|
||||
break;
|
||||
}
|
||||
envelope.markAsFailed(part);
|
||||
Log.i("GoannaSmsManager", "SMS part sending failed!");
|
||||
}
|
||||
|
||||
if (envelope.arePartsRemaining(part)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (envelope.isFailing(part)) {
|
||||
if (part == Envelope.SubParts.SENT_PART) {
|
||||
GoannaAppShell.notifySmsSendFailed(envelope.getError(),
|
||||
bundle.getInt("requestId"));
|
||||
Log.i("GoannaSmsManager", "SMS sending failed!");
|
||||
} else {
|
||||
GoannaAppShell.notifySmsDelivery(envelope.getMessageId(),
|
||||
kDeliveryStatusError,
|
||||
bundle.getString("number"),
|
||||
bundle.getString("message"),
|
||||
envelope.getMessageTimestamp());
|
||||
Log.i("GoannaSmsManager", "SMS delivery failed!");
|
||||
}
|
||||
} else {
|
||||
if (part == Envelope.SubParts.SENT_PART) {
|
||||
String number = bundle.getString("number");
|
||||
String message = bundle.getString("message");
|
||||
long timestamp = System.currentTimeMillis();
|
||||
|
||||
int id = saveSentMessage(number, message, timestamp);
|
||||
|
||||
GoannaAppShell.notifySmsSent(id, number, message, timestamp,
|
||||
bundle.getInt("requestId"));
|
||||
|
||||
envelope.setMessageId(id);
|
||||
envelope.setMessageTimestamp(timestamp);
|
||||
|
||||
Log.i("GoannaSmsManager", "SMS sending was successfull!");
|
||||
} else {
|
||||
GoannaAppShell.notifySmsDelivery(envelope.getMessageId(),
|
||||
kDeliveryStatusSuccess,
|
||||
bundle.getString("number"),
|
||||
bundle.getString("message"),
|
||||
envelope.getMessageTimestamp());
|
||||
Log.i("GoannaSmsManager", "SMS succesfully delivered!");
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy the envelope object only if the SMS has been sent and delivered.
|
||||
if (!envelope.arePartsRemaining(Envelope.SubParts.SENT_PART) &&
|
||||
!envelope.arePartsRemaining(Envelope.SubParts.DELIVERED_PART)) {
|
||||
postman.destroyEnvelope(envelopeId);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void send(String aNumber, String aMessage, int aRequestId) {
|
||||
int envelopeId = Postman.kUnknownEnvelopeId;
|
||||
|
||||
try {
|
||||
SmsManager sm = SmsManager.getDefault();
|
||||
|
||||
Intent sentIntent = new Intent(ACTION_SMS_SENT);
|
||||
Intent deliveredIntent = new Intent(ACTION_SMS_DELIVERED);
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("number", aNumber);
|
||||
bundle.putString("message", aMessage);
|
||||
bundle.putInt("requestId", aRequestId);
|
||||
|
||||
if (aMessage.length() <= kMaxMessageSize) {
|
||||
envelopeId = Postman.getInstance().createEnvelope(1);
|
||||
bundle.putInt("envelopeId", envelopeId);
|
||||
|
||||
sentIntent.putExtras(bundle);
|
||||
deliveredIntent.putExtras(bundle);
|
||||
|
||||
/*
|
||||
* There are a few things to know about getBroadcast and pending intents:
|
||||
* - the pending intents are in a shared pool maintained by the system;
|
||||
* - each pending intent is identified by a token;
|
||||
* - when a new pending intent is created, if it has the same token as
|
||||
* another intent in the pool, one of them has to be removed.
|
||||
*
|
||||
* To prevent having a hard time because of this situation, we give a
|
||||
* unique id to all pending intents we are creating. This unique id is
|
||||
* generated by GetPendingIntentUID().
|
||||
*/
|
||||
PendingIntent sentPendingIntent =
|
||||
PendingIntent.getBroadcast(GoannaApp.mAppContext,
|
||||
PendingIntentUID.generate(), sentIntent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
|
||||
PendingIntent deliveredPendingIntent =
|
||||
PendingIntent.getBroadcast(GoannaApp.mAppContext,
|
||||
PendingIntentUID.generate(), deliveredIntent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
|
||||
sm.sendTextMessage(aNumber, "", aMessage,
|
||||
sentPendingIntent, deliveredPendingIntent);
|
||||
} else {
|
||||
ArrayList<String> parts = sm.divideMessage(aMessage);
|
||||
envelopeId = Postman.getInstance().createEnvelope(parts.size());
|
||||
bundle.putInt("envelopeId", envelopeId);
|
||||
|
||||
sentIntent.putExtras(bundle);
|
||||
deliveredIntent.putExtras(bundle);
|
||||
|
||||
ArrayList<PendingIntent> sentPendingIntents =
|
||||
new ArrayList<PendingIntent>(parts.size());
|
||||
ArrayList<PendingIntent> deliveredPendingIntents =
|
||||
new ArrayList<PendingIntent>(parts.size());
|
||||
|
||||
for (int i=0; i<parts.size(); ++i) {
|
||||
sentPendingIntents.add(
|
||||
PendingIntent.getBroadcast(GoannaApp.mAppContext,
|
||||
PendingIntentUID.generate(), sentIntent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
);
|
||||
|
||||
deliveredPendingIntents.add(
|
||||
PendingIntent.getBroadcast(GoannaApp.mAppContext,
|
||||
PendingIntentUID.generate(), deliveredIntent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
);
|
||||
}
|
||||
|
||||
sm.sendMultipartTextMessage(aNumber, "", parts, sentPendingIntents,
|
||||
deliveredPendingIntents);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("GoannaSmsManager", "Failed to send an SMS: ", e);
|
||||
|
||||
if (envelopeId != Postman.kUnknownEnvelopeId) {
|
||||
Postman.getInstance().destroyEnvelope(envelopeId);
|
||||
}
|
||||
|
||||
GoannaAppShell.notifySmsSendFailed(kUnknownError, aRequestId);
|
||||
}
|
||||
}
|
||||
|
||||
public int saveSentMessage(String aRecipient, String aBody, long aDate) {
|
||||
try {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("address", aRecipient);
|
||||
values.put("body", aBody);
|
||||
values.put("date", aDate);
|
||||
// Always 'PENDING' because we always request status report.
|
||||
values.put("status", kInternalDeliveryStatusPending);
|
||||
|
||||
ContentResolver cr = GoannaApp.mAppContext.getContentResolver();
|
||||
Uri uri = cr.insert(kSmsSentContentUri, values);
|
||||
|
||||
long id = ContentUris.parseId(uri);
|
||||
|
||||
// The DOM API takes a 32bits unsigned int for the id. It's unlikely that
|
||||
// we happen to need more than that but it doesn't cost to check.
|
||||
if (id > Integer.MAX_VALUE) {
|
||||
throw new IdTooHighException();
|
||||
}
|
||||
|
||||
return (int)id;
|
||||
} catch (IdTooHighException e) {
|
||||
Log.e("GoannaSmsManager", "The id we received is higher than the higher allowed value.");
|
||||
return -1;
|
||||
} catch (Exception e) {
|
||||
Log.e("GoannaSmsManager", "Something went wrong when trying to write a sent message: " + e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public void getMessage(int aMessageId, int aRequestId) {
|
||||
class GetMessageRunnable implements Runnable {
|
||||
private int mMessageId;
|
||||
private int mRequestId;
|
||||
|
||||
GetMessageRunnable(int aMessageId, int aRequestId) {
|
||||
mMessageId = aMessageId;
|
||||
mRequestId = aRequestId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
ContentResolver cr = GoannaApp.mAppContext.getContentResolver();
|
||||
Uri message = ContentUris.withAppendedId(kSmsContentUri, mMessageId);
|
||||
|
||||
cursor = cr.query(message, kRequiredMessageRows, null, null, null);
|
||||
if (cursor == null || cursor.getCount() == 0) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (cursor.getCount() != 1) {
|
||||
throw new TooManyResultsException();
|
||||
}
|
||||
|
||||
cursor.moveToFirst();
|
||||
|
||||
if (cursor.getInt(cursor.getColumnIndex("_id")) != mMessageId) {
|
||||
throw new UnmatchingIdException();
|
||||
}
|
||||
|
||||
int type = cursor.getInt(cursor.getColumnIndex("type"));
|
||||
int deliveryStatus;
|
||||
String sender = "";
|
||||
String receiver = "";
|
||||
|
||||
if (type == kSmsTypeInbox) {
|
||||
deliveryStatus = kDeliveryStatusSuccess;
|
||||
sender = cursor.getString(cursor.getColumnIndex("address"));
|
||||
} else if (type == kSmsTypeSentbox) {
|
||||
deliveryStatus = getGoannaDeliveryStatus(cursor.getInt(cursor.getColumnIndex("status")));
|
||||
receiver = cursor.getString(cursor.getColumnIndex("address"));
|
||||
} else {
|
||||
throw new InvalidTypeException();
|
||||
}
|
||||
|
||||
GoannaAppShell.notifyGetSms(cursor.getInt(cursor.getColumnIndex("_id")),
|
||||
deliveryStatus,
|
||||
receiver, sender,
|
||||
cursor.getString(cursor.getColumnIndex("body")),
|
||||
cursor.getLong(cursor.getColumnIndex("date")),
|
||||
mRequestId);
|
||||
} catch (NotFoundException e) {
|
||||
Log.i("GoannaSmsManager", "Message id " + mMessageId + " not found");
|
||||
GoannaAppShell.notifyGetSmsFailed(kNotFoundError, mRequestId);
|
||||
} catch (UnmatchingIdException e) {
|
||||
Log.e("GoannaSmsManager", "Requested message id (" + mMessageId +
|
||||
") is different from the one we got.");
|
||||
GoannaAppShell.notifyGetSmsFailed(kUnknownError, mRequestId);
|
||||
} catch (TooManyResultsException e) {
|
||||
Log.e("GoannaSmsManager", "Get too many results for id " + mMessageId);
|
||||
GoannaAppShell.notifyGetSmsFailed(kUnknownError, mRequestId);
|
||||
} catch (InvalidTypeException e) {
|
||||
Log.i("GoannaSmsManager", "Message has an invalid type, we ignore it.");
|
||||
GoannaAppShell.notifyGetSmsFailed(kNotFoundError, mRequestId);
|
||||
} catch (Exception e) {
|
||||
Log.e("GoannaSmsManager", "Error while trying to get message: " + e);
|
||||
GoannaAppShell.notifyGetSmsFailed(kUnknownError, mRequestId);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!SmsIOThread.getInstance().execute(new GetMessageRunnable(aMessageId, aRequestId))) {
|
||||
Log.e("GoannaSmsManager", "Failed to add GetMessageRunnable to the SmsIOThread");
|
||||
GoannaAppShell.notifyGetSmsFailed(kUnknownError, aRequestId);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteMessage(int aMessageId, int aRequestId) {
|
||||
class DeleteMessageRunnable implements Runnable {
|
||||
private int mMessageId;
|
||||
private int mRequestId;
|
||||
|
||||
DeleteMessageRunnable(int aMessageId, int aRequestId) {
|
||||
mMessageId = aMessageId;
|
||||
mRequestId = aRequestId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
ContentResolver cr = GoannaApp.mAppContext.getContentResolver();
|
||||
Uri message = ContentUris.withAppendedId(kSmsContentUri, mMessageId);
|
||||
|
||||
int count = cr.delete(message, null, null);
|
||||
|
||||
if (count > 1) {
|
||||
throw new TooManyResultsException();
|
||||
}
|
||||
|
||||
GoannaAppShell.notifySmsDeleted(count == 1, mRequestId);
|
||||
} catch (TooManyResultsException e) {
|
||||
Log.e("GoannaSmsManager", "Delete more than one message? " + e);
|
||||
GoannaAppShell.notifySmsDeleteFailed(kUnknownError, mRequestId);
|
||||
} catch (Exception e) {
|
||||
Log.e("GoannaSmsManager", "Error while trying to delete a message: " + e);
|
||||
GoannaAppShell.notifySmsDeleteFailed(kUnknownError, mRequestId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!SmsIOThread.getInstance().execute(new DeleteMessageRunnable(aMessageId, aRequestId))) {
|
||||
Log.e("GoannaSmsManager", "Failed to add GetMessageRunnable to the SmsIOThread");
|
||||
GoannaAppShell.notifySmsDeleteFailed(kUnknownError, aRequestId);
|
||||
}
|
||||
}
|
||||
|
||||
public void createMessageList(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, int aDeliveryState, boolean aReverse, int aRequestId) {
|
||||
class CreateMessageListRunnable implements Runnable {
|
||||
private long mStartDate;
|
||||
private long mEndDate;
|
||||
private String[] mNumbers;
|
||||
private int mNumbersCount;
|
||||
private int mDeliveryState;
|
||||
private boolean mReverse;
|
||||
private int mRequestId;
|
||||
|
||||
CreateMessageListRunnable(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, int aDeliveryState, boolean aReverse, int aRequestId) {
|
||||
mStartDate = aStartDate;
|
||||
mEndDate = aEndDate;
|
||||
mNumbers = aNumbers;
|
||||
mNumbersCount = aNumbersCount;
|
||||
mDeliveryState = aDeliveryState;
|
||||
mReverse = aReverse;
|
||||
mRequestId = aRequestId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Cursor cursor = null;
|
||||
boolean closeCursor = true;
|
||||
|
||||
try {
|
||||
// TODO: should use the |selectionArgs| argument in |ContentResolver.query()|.
|
||||
ArrayList<String> restrictions = new ArrayList<String>();
|
||||
|
||||
if (mStartDate != 0) {
|
||||
restrictions.add("date >= " + mStartDate);
|
||||
}
|
||||
|
||||
if (mEndDate != 0) {
|
||||
restrictions.add("date <= " + mEndDate);
|
||||
}
|
||||
|
||||
if (mNumbersCount > 0) {
|
||||
String numberRestriction = "address IN ('" + mNumbers[0] + "'";
|
||||
|
||||
for (int i=1; i<mNumbersCount; ++i) {
|
||||
numberRestriction += ", '" + mNumbers[i] + "'";
|
||||
}
|
||||
numberRestriction += ")";
|
||||
|
||||
restrictions.add(numberRestriction);
|
||||
}
|
||||
|
||||
if (mDeliveryState == kDeliveryStateUnknown) {
|
||||
restrictions.add("type IN ('" + kSmsTypeSentbox + "', '" + kSmsTypeInbox + "')");
|
||||
} else if (mDeliveryState == kDeliveryStateSent) {
|
||||
restrictions.add("type = " + kSmsTypeSentbox);
|
||||
} else if (mDeliveryState == kDeliveryStateReceived) {
|
||||
restrictions.add("type = " + kSmsTypeInbox);
|
||||
} else {
|
||||
throw new UnexpectedDeliveryStateException();
|
||||
}
|
||||
|
||||
String restrictionText = restrictions.size() > 0 ? restrictions.get(0) : "";
|
||||
|
||||
for (int i=1; i<restrictions.size(); ++i) {
|
||||
restrictionText += " AND " + restrictions.get(i);
|
||||
}
|
||||
|
||||
ContentResolver cr = GoannaApp.mAppContext.getContentResolver();
|
||||
cursor = cr.query(kSmsContentUri, kRequiredMessageRows, restrictionText, null,
|
||||
mReverse ? "date DESC" : "date ASC");
|
||||
|
||||
if (cursor.getCount() == 0) {
|
||||
GoannaAppShell.notifyNoMessageInList(mRequestId);
|
||||
return;
|
||||
}
|
||||
|
||||
cursor.moveToFirst();
|
||||
|
||||
int type = cursor.getInt(cursor.getColumnIndex("type"));
|
||||
int deliveryStatus;
|
||||
String sender = "";
|
||||
String receiver = "";
|
||||
|
||||
if (type == kSmsTypeInbox) {
|
||||
deliveryStatus = kDeliveryStatusSuccess;
|
||||
sender = cursor.getString(cursor.getColumnIndex("address"));
|
||||
} else if (type == kSmsTypeSentbox) {
|
||||
deliveryStatus = getGoannaDeliveryStatus(cursor.getInt(cursor.getColumnIndex("status")));
|
||||
receiver = cursor.getString(cursor.getColumnIndex("address"));
|
||||
} else {
|
||||
throw new UnexpectedDeliveryStateException();
|
||||
}
|
||||
|
||||
int listId = MessagesListManager.getInstance().add(cursor);
|
||||
closeCursor = false;
|
||||
GoannaAppShell.notifyListCreated(listId,
|
||||
cursor.getInt(cursor.getColumnIndex("_id")),
|
||||
deliveryStatus,
|
||||
receiver, sender,
|
||||
cursor.getString(cursor.getColumnIndex("body")),
|
||||
cursor.getLong(cursor.getColumnIndex("date")),
|
||||
mRequestId);
|
||||
} catch (UnexpectedDeliveryStateException e) {
|
||||
Log.e("GoannaSmsManager", "Unexcepted delivery state type: " + e);
|
||||
GoannaAppShell.notifyReadingMessageListFailed(kUnknownError, mRequestId);
|
||||
} catch (Exception e) {
|
||||
Log.e("GoannaSmsManager", "Error while trying to create a message list cursor: " + e);
|
||||
GoannaAppShell.notifyReadingMessageListFailed(kUnknownError, mRequestId);
|
||||
} finally {
|
||||
// Close the cursor if MessagesListManager isn't taking care of it.
|
||||
// We could also just check if it is in the MessagesListManager list but
|
||||
// that would be less efficient.
|
||||
if (cursor != null && closeCursor) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!SmsIOThread.getInstance().execute(new CreateMessageListRunnable(aStartDate, aEndDate, aNumbers, aNumbersCount, aDeliveryState, aReverse, aRequestId))) {
|
||||
Log.e("GoannaSmsManager", "Failed to add CreateMessageListRunnable to the SmsIOThread");
|
||||
GoannaAppShell.notifyReadingMessageListFailed(kUnknownError, aRequestId);
|
||||
}
|
||||
}
|
||||
|
||||
public void getNextMessageInList(int aListId, int aRequestId) {
|
||||
class GetNextMessageInListRunnable implements Runnable {
|
||||
private int mListId;
|
||||
private int mRequestId;
|
||||
|
||||
GetNextMessageInListRunnable(int aListId, int aRequestId) {
|
||||
mListId = aListId;
|
||||
mRequestId = aRequestId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Cursor cursor = MessagesListManager.getInstance().get(mListId);
|
||||
|
||||
if (!cursor.moveToNext()) {
|
||||
MessagesListManager.getInstance().remove(mListId);
|
||||
GoannaAppShell.notifyNoMessageInList(mRequestId);
|
||||
return;
|
||||
}
|
||||
|
||||
int type = cursor.getInt(cursor.getColumnIndex("type"));
|
||||
int deliveryStatus;
|
||||
String sender = "";
|
||||
String receiver = "";
|
||||
|
||||
if (type == kSmsTypeInbox) {
|
||||
deliveryStatus = kDeliveryStatusSuccess;
|
||||
sender = cursor.getString(cursor.getColumnIndex("address"));
|
||||
} else if (type == kSmsTypeSentbox) {
|
||||
deliveryStatus = getGoannaDeliveryStatus(cursor.getInt(cursor.getColumnIndex("status")));
|
||||
receiver = cursor.getString(cursor.getColumnIndex("address"));
|
||||
} else {
|
||||
throw new UnexpectedDeliveryStateException();
|
||||
}
|
||||
|
||||
int listId = MessagesListManager.getInstance().add(cursor);
|
||||
GoannaAppShell.notifyGotNextMessage(cursor.getInt(cursor.getColumnIndex("_id")),
|
||||
deliveryStatus,
|
||||
receiver, sender,
|
||||
cursor.getString(cursor.getColumnIndex("body")),
|
||||
cursor.getLong(cursor.getColumnIndex("date")),
|
||||
mRequestId);
|
||||
} catch (UnexpectedDeliveryStateException e) {
|
||||
Log.e("GoannaSmsManager", "Unexcepted delivery state type: " + e);
|
||||
GoannaAppShell.notifyReadingMessageListFailed(kUnknownError, mRequestId);
|
||||
} catch (Exception e) {
|
||||
Log.e("GoannaSmsManager", "Error while trying to get the next message of a list: " + e);
|
||||
GoannaAppShell.notifyReadingMessageListFailed(kUnknownError, mRequestId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!SmsIOThread.getInstance().execute(new GetNextMessageInListRunnable(aListId, aRequestId))) {
|
||||
Log.e("GoannaSmsManager", "Failed to add GetNextMessageInListRunnable to the SmsIOThread");
|
||||
GoannaAppShell.notifyReadingMessageListFailed(kUnknownError, aRequestId);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearMessageList(int aListId) {
|
||||
MessagesListManager.getInstance().remove(aListId);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
GoannaApp.mAppContext.unregisterReceiver(this);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
SmsIOThread.getInstance().interrupt();
|
||||
MessagesListManager.getInstance().clear();
|
||||
}
|
||||
|
||||
private int getGoannaDeliveryStatus(int aDeliveryStatus) {
|
||||
if (aDeliveryStatus == kInternalDeliveryStatusNone) {
|
||||
return kDeliveryStatusNotApplicable;
|
||||
}
|
||||
if (aDeliveryStatus >= kInternalDeliveryStatusFailed) {
|
||||
return kDeliveryStatusError;
|
||||
}
|
||||
if (aDeliveryStatus >= kInternalDeliveryStatusPending) {
|
||||
return kDeliveryStatusPending;
|
||||
}
|
||||
return kDeliveryStatusSuccess;
|
||||
}
|
||||
|
||||
private int getGoannaMessageClass(MessageClass aMessageClass) {
|
||||
switch (aMessageClass) {
|
||||
case UNKNOWN:
|
||||
return kMessageClassNormal;
|
||||
case CLASS_0:
|
||||
return kMessageClassClass0;
|
||||
case CLASS_1:
|
||||
return kMessageClassClass1;
|
||||
case CLASS_2:
|
||||
return kMessageClassClass2;
|
||||
case CLASS_3:
|
||||
return kMessageClassClass3;
|
||||
}
|
||||
}
|
||||
|
||||
class IdTooHighException extends Exception {
|
||||
private static final long serialVersionUID = 395697882128640L;
|
||||
}
|
||||
|
||||
class InvalidTypeException extends Exception {
|
||||
private static final long serialVersionUID = 23359904803795434L;
|
||||
}
|
||||
|
||||
class NotFoundException extends Exception {
|
||||
private static final long serialVersionUID = 266226999371957426L;
|
||||
}
|
||||
|
||||
class TooManyResultsException extends Exception {
|
||||
private static final long serialVersionUID = 48899777673841920L;
|
||||
}
|
||||
|
||||
class UnexpectedDeliveryStateException extends Exception {
|
||||
private static final long serialVersionUID = 5044567998961920L;
|
||||
}
|
||||
|
||||
class UnmatchingIdException extends Exception {
|
||||
private static final long serialVersionUID = 1935649715512128L;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
#filter substitution
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.mozilla.goannaviewexample"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
<uses-sdk android:minSdkVersion="8"
|
||||
android:targetSdkVersion="@ANDROID_TARGET_SDK@"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
|
||||
<application android:label="@string/app_name"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:hardwareAccelerated="true">
|
||||
<activity android:name="GoannaViewExample"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,14 +0,0 @@
|
||||
package org.mozilla.goannaviewexample;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class GoannaViewExample extends Activity {
|
||||
/** Called when the activity is first created. */
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.main);
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
PP_TARGETS = properties manifest
|
||||
|
||||
manifest = AndroidManifest.xml.in
|
||||
|
||||
include $(topsrcdir)/config/rules.mk
|
||||
|
||||
GARBAGE = \
|
||||
AndroidManifest.xml \
|
||||
proguard-project.txt \
|
||||
project.properties \
|
||||
ant.properties \
|
||||
build.xml \
|
||||
local.properties \
|
||||
goannaview_example.apk \
|
||||
$(NULL)
|
||||
|
||||
GARBAGE_DIRS = \
|
||||
assets \
|
||||
goannaview_library \
|
||||
gen \
|
||||
bin \
|
||||
libs \
|
||||
res \
|
||||
src \
|
||||
binaries \
|
||||
$(NULL)
|
||||
|
||||
ANDROID=$(ANDROID_TOOLS)/android
|
||||
|
||||
TARGET="android-$(ANDROID_TARGET_SDK)"
|
||||
|
||||
PACKAGE_DEPS = \
|
||||
assets/libxul.so \
|
||||
build.xml \
|
||||
src/org/mozilla/goannaviewexample/GoannaViewExample.java \
|
||||
$(CURDIR)/res/layout/main.xml \
|
||||
$(CURDIR)/AndroidManifest.xml \
|
||||
$(NULL)
|
||||
|
||||
$(CURDIR)/res/layout/main.xml: $(srcdir)/main.xml
|
||||
$(NSINSTALL) $(srcdir)/main.xml res/layout/
|
||||
|
||||
src/org/mozilla/goannaviewexample/GoannaViewExample.java: $(srcdir)/GoannaViewExample.java
|
||||
$(NSINSTALL) $(srcdir)/GoannaViewExample.java src/org/mozilla/goannaviewexample/
|
||||
|
||||
assets/libxul.so: $(DIST)/goannaview_library/goannaview_assets.zip FORCE
|
||||
$(UNZIP) -o $(DIST)/goannaview_library/goannaview_assets.zip
|
||||
|
||||
build.xml: $(CURDIR)/AndroidManifest.xml
|
||||
mv AndroidManifest.xml AndroidManifest.xml.save
|
||||
$(ANDROID) create project --name GoannaViewExample --target $(TARGET) --path $(CURDIR) --activity GoannaViewExample --package org.mozilla.goannaviewexample
|
||||
$(ANDROID) update project --target $(TARGET) --path $(CURDIR) --library $(DEPTH)/mobile/android/goannaview_library
|
||||
$(RM) $(CURDIR)/res/layout/main.xml
|
||||
$(NSINSTALL) $(srcdir)/main.xml res/layout/
|
||||
$(RM) AndroidManifest.xml
|
||||
mv AndroidManifest.xml.save AndroidManifest.xml
|
||||
echo jar.libs.dir=libs >> project.properties
|
||||
|
||||
bin/GoannaViewExample-debug.apk: $(PACKAGE_DEPS)
|
||||
ant debug
|
||||
|
||||
goannaview_example.apk: bin/GoannaViewExample-debug.apk
|
||||
cp $< $@
|
||||
|
||||
package: goannaview_example.apk FORCE
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:goanna="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
>
|
||||
<org.mozilla.goanna.GoannaView android:id="@+id/goanna_view"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
goanna:url="about:mozilla"/>
|
||||
</LinearLayout>
|
||||
@@ -6,9 +6,6 @@
|
||||
|
||||
DIRS += ['components', 'browser']
|
||||
|
||||
if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android':
|
||||
DIRS += ['android/goannaview_example']
|
||||
|
||||
TEST_DIRS += ['test']
|
||||
|
||||
if CONFIG['ENABLE_TESTS']:
|
||||
|
||||
@@ -1,373 +0,0 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
@@ -1,11 +0,0 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
include $(topsrcdir)/config/rules.mk
|
||||
include $(topsrcdir)/testing/testsuite-targets.mk
|
||||
|
||||
package-mobile-tests:
|
||||
$(MAKE) stage-mochitest DIST_BIN=$(DEPTH)/$(DIST)/bin/xulrunner
|
||||
$(NSINSTALL) -D $(DIST)/$(PKG_PATH)
|
||||
@(cd $(PKG_STAGE) && tar $(TAR_CREATE_FLAGS) - *) | bzip2 -f > $(DIST)/$(PKG_PATH)$(TEST_PACKAGE)
|
||||
@@ -1,21 +0,0 @@
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
if not CONFIG['LIBXUL_SDK']:
|
||||
include('/toolkit/toolkit.mozbuild')
|
||||
|
||||
elif CONFIG['ENABLE_TESTS']:
|
||||
DIRS += ['/testing/mochitest']
|
||||
|
||||
if CONFIG['ENABLE_TESTS']:
|
||||
DIRS += ['/testing/instrumentation']
|
||||
|
||||
if CONFIG['MOZ_EXTENSIONS']:
|
||||
DIRS += ['/extensions']
|
||||
|
||||
DIRS += [
|
||||
'/%s' % CONFIG['MOZ_BRANDING_DIRECTORY'],
|
||||
'/mobile/android',
|
||||
]
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,819 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#filter substitution
|
||||
|
||||
// For browser.xml binding
|
||||
//
|
||||
// cacheRatio* is a ratio that determines the amount of pixels to cache. The
|
||||
// ratio is multiplied by the viewport width or height to get the displayport's
|
||||
// width or height, respectively.
|
||||
//
|
||||
// (divide integer value by 1000 to get the ratio)
|
||||
//
|
||||
// For instance: cachePercentageWidth is 1500
|
||||
// viewport height is 500
|
||||
// => display port height will be 500 * 1.5 = 750
|
||||
//
|
||||
pref("toolkit.browser.cacheRatioWidth", 2000);
|
||||
pref("toolkit.browser.cacheRatioHeight", 3000);
|
||||
|
||||
// How long before a content view (a handle to a remote scrollable object)
|
||||
// expires.
|
||||
pref("toolkit.browser.contentViewExpire", 3000);
|
||||
|
||||
pref("toolkit.defaultChromeURI", "chrome://browser/content/browser.xul");
|
||||
pref("browser.chromeURL", "chrome://browser/content/");
|
||||
|
||||
// If a tab has not been active for this long (seconds), then it may be
|
||||
// turned into a zombie tab to preemptively free up memory. -1 disables time-based
|
||||
// expiration (but low-memory conditions may still require the tab to be zombified).
|
||||
pref("browser.tabs.expireTime", 900);
|
||||
|
||||
// From libpref/src/init/all.js, extended to allow a slightly wider zoom range.
|
||||
pref("zoom.minPercent", 20);
|
||||
pref("zoom.maxPercent", 400);
|
||||
pref("toolkit.zoomManager.zoomValues", ".2,.3,.5,.67,.8,.9,1,1.1,1.2,1.33,1.5,1.7,2,2.4,3,4");
|
||||
|
||||
// Mobile will use faster, less durable mode.
|
||||
pref("toolkit.storage.synchronous", 0);
|
||||
|
||||
pref("browser.viewport.desktopWidth", 980);
|
||||
// The default fallback zoom level to render pages at. Set to -1 to fit page; otherwise
|
||||
// the value is divided by 1000 and clamped to hard-coded min/max scale values.
|
||||
pref("browser.viewport.defaultZoom", -1);
|
||||
|
||||
/* allow scrollbars to float above chrome ui */
|
||||
pref("ui.scrollbarsCanOverlapContent", 1);
|
||||
|
||||
/* cache prefs */
|
||||
pref("browser.cache.disk.enable", true);
|
||||
pref("browser.cache.disk.capacity", 20480); // kilobytes
|
||||
pref("browser.cache.disk.max_entry_size", 4096); // kilobytes
|
||||
pref("browser.cache.disk.smart_size.enabled", true);
|
||||
pref("browser.cache.disk.smart_size.first_run", true);
|
||||
|
||||
#ifdef MOZ_PKG_SPECIAL
|
||||
// low memory devices
|
||||
pref("browser.cache.memory.enable", false);
|
||||
#else
|
||||
pref("browser.cache.memory.enable", true);
|
||||
#endif
|
||||
pref("browser.cache.memory.capacity", 1024); // kilobytes
|
||||
|
||||
pref("browser.cache.memory_limit", 5120); // 5 MB
|
||||
|
||||
/* image cache prefs */
|
||||
pref("image.cache.size", 1048576); // bytes
|
||||
pref("image.high_quality_downscaling.enabled", false);
|
||||
|
||||
/* offline cache prefs */
|
||||
pref("browser.offline-apps.notify", true);
|
||||
pref("browser.cache.offline.enable", true);
|
||||
pref("browser.cache.offline.capacity", 5120); // kilobytes
|
||||
pref("offline-apps.quota.warn", 1024); // kilobytes
|
||||
|
||||
// cache compression turned off for now - see bug #715198
|
||||
pref("browser.cache.compression_level", 0);
|
||||
|
||||
/* disable some protocol warnings */
|
||||
pref("network.protocol-handler.warn-external.tel", false);
|
||||
pref("network.protocol-handler.warn-external.sms", false);
|
||||
pref("network.protocol-handler.warn-external.mailto", false);
|
||||
pref("network.protocol-handler.warn-external.vnd.youtube", false);
|
||||
|
||||
/* http prefs */
|
||||
pref("network.http.pipelining", true);
|
||||
pref("network.http.pipelining.ssl", true);
|
||||
pref("network.http.proxy.pipelining", true);
|
||||
pref("network.http.pipelining.maxrequests" , 6);
|
||||
pref("network.http.keep-alive.timeout", 109);
|
||||
pref("network.http.max-connections", 20);
|
||||
pref("network.http.max-persistent-connections-per-server", 6);
|
||||
pref("network.http.max-persistent-connections-per-proxy", 20);
|
||||
|
||||
// spdy
|
||||
pref("network.http.spdy.push-allowance", 32768);
|
||||
|
||||
// See bug 545869 for details on why these are set the way they are
|
||||
pref("network.buffer.cache.count", 24);
|
||||
pref("network.buffer.cache.size", 16384);
|
||||
|
||||
// predictive actions
|
||||
pref("network.predictor.enabled", true);
|
||||
pref("network.predictor.max-db-size", 2097152); // bytes
|
||||
pref("network.predictor.preserve", 50); // percentage of predictor data to keep when cleaning up
|
||||
|
||||
/* history max results display */
|
||||
pref("browser.display.history.maxresults", 100);
|
||||
|
||||
/* How many times should have passed before the remote tabs list is refreshed */
|
||||
pref("browser.display.remotetabs.timeout", 10);
|
||||
|
||||
/* session history */
|
||||
pref("browser.sessionhistory.max_total_viewers", 1);
|
||||
pref("browser.sessionhistory.max_entries", 50);
|
||||
pref("browser.sessionhistory.contentViewerTimeout", 360);
|
||||
|
||||
/* session store */
|
||||
pref("browser.sessionstore.resume_session_once", false);
|
||||
pref("browser.sessionstore.resume_from_crash", true);
|
||||
pref("browser.sessionstore.interval", 10000); // milliseconds
|
||||
pref("browser.sessionstore.max_tabs_undo", 5);
|
||||
pref("browser.sessionstore.max_resumed_crashes", 1);
|
||||
pref("browser.sessionstore.recent_crashes", 0);
|
||||
pref("browser.sessionstore.privacy_level", 0); // saving data: 0 = all, 1 = unencrypted sites, 2 = never
|
||||
|
||||
/* these should help performance */
|
||||
pref("mozilla.widget.force-24bpp", true);
|
||||
pref("mozilla.widget.use-buffer-pixmap", true);
|
||||
pref("mozilla.widget.disable-native-theme", true);
|
||||
pref("layout.reflow.synthMouseMove", false);
|
||||
pref("layout.css.report_errors", false);
|
||||
|
||||
/* download manager (don't show the window or alert) */
|
||||
pref("browser.download.useDownloadDir", true);
|
||||
pref("browser.download.folderList", 1); // Default to ~/Downloads
|
||||
pref("browser.download.manager.showAlertOnComplete", false);
|
||||
pref("browser.download.manager.showAlertInterval", 2000);
|
||||
pref("browser.download.manager.retention", 2);
|
||||
pref("browser.download.manager.showWhenStarting", false);
|
||||
pref("browser.download.manager.closeWhenDone", true);
|
||||
pref("browser.download.manager.openDelay", 0);
|
||||
pref("browser.download.manager.focusWhenStarting", false);
|
||||
pref("browser.download.manager.flashCount", 2);
|
||||
pref("browser.download.manager.displayedHistoryDays", 7);
|
||||
pref("browser.download.manager.addToRecentDocs", true);
|
||||
|
||||
/* download helper */
|
||||
pref("browser.helperApps.deleteTempFileOnExit", false);
|
||||
|
||||
/* password manager */
|
||||
pref("signon.rememberSignons", true);
|
||||
pref("signon.expireMasterPassword", false);
|
||||
pref("signon.debug", false);
|
||||
|
||||
/* form helper (scroll to and optionally zoom into editable fields) */
|
||||
pref("formhelper.mode", 2); // 0 = disabled, 1 = enabled, 2 = dynamic depending on screen size
|
||||
pref("formhelper.autozoom", true);
|
||||
|
||||
/* find helper */
|
||||
pref("findhelper.autozoom", true);
|
||||
|
||||
/* autocomplete */
|
||||
pref("browser.formfill.enable", true);
|
||||
|
||||
/* spellcheck */
|
||||
pref("layout.spellcheckDefault", 0);
|
||||
|
||||
/* new html5 forms */
|
||||
pref("dom.experimental_forms", true);
|
||||
pref("dom.forms.number", true);
|
||||
|
||||
/* extension manager and xpinstall */
|
||||
pref("xpinstall.whitelist.directRequest", false);
|
||||
pref("xpinstall.whitelist.fileRequest", false);
|
||||
pref("xpinstall.whitelist.add", "addons.mozilla.org");
|
||||
pref("xpinstall.whitelist.add.180", "marketplace.firefox.com");
|
||||
|
||||
pref("extensions.enabledScopes", 1);
|
||||
pref("extensions.autoupdate.enabled", true);
|
||||
pref("extensions.autoupdate.interval", 86400);
|
||||
pref("extensions.update.enabled", false);
|
||||
pref("extensions.update.interval", 86400);
|
||||
pref("extensions.dss.enabled", false);
|
||||
pref("extensions.dss.switchPending", false);
|
||||
pref("extensions.ignoreMTimeChanges", false);
|
||||
pref("extensions.logging.enabled", false);
|
||||
pref("extensions.hideInstallButton", true);
|
||||
pref("extensions.showMismatchUI", false);
|
||||
pref("extensions.hideUpdateButton", false);
|
||||
pref("extensions.strictCompatibility", false);
|
||||
pref("extensions.minCompatibleAppVersion", "11.0");
|
||||
|
||||
pref("extensions.update.url", "https://versioncheck.addons.mozilla.org/update/VersionCheck.php?reqVersion=%REQ_VERSION%&id=%ITEM_ID%&version=%ITEM_VERSION%&maxAppVersion=%ITEM_MAXAPPVERSION%&status=%ITEM_STATUS%&appID=%APP_ID%&appVersion=%APP_VERSION%&appOS=%APP_OS%&appABI=%APP_ABI%&locale=%APP_LOCALE%¤tAppVersion=%CURRENT_APP_VERSION%&updateType=%UPDATE_TYPE%&compatMode=%COMPATIBILITY_MODE%");
|
||||
pref("extensions.update.background.url", "https://versioncheck-bg.addons.mozilla.org/update/VersionCheck.php?reqVersion=%REQ_VERSION%&id=%ITEM_ID%&version=%ITEM_VERSION%&maxAppVersion=%ITEM_MAXAPPVERSION%&status=%ITEM_STATUS%&appID=%APP_ID%&appVersion=%APP_VERSION%&appOS=%APP_OS%&appABI=%APP_ABI%&locale=%APP_LOCALE%¤tAppVersion=%CURRENT_APP_VERSION%&updateType=%UPDATE_TYPE%&compatMode=%COMPATIBILITY_MODE%");
|
||||
|
||||
/* preferences for the Get Add-ons pane */
|
||||
pref("extensions.getAddons.cache.enabled", true);
|
||||
pref("extensions.getAddons.maxResults", 15);
|
||||
pref("extensions.getAddons.recommended.browseURL", "https://addons.mozilla.org/%LOCALE%/android/recommended/");
|
||||
pref("extensions.getAddons.recommended.url", "https://services.addons.mozilla.org/%LOCALE%/android/api/%API_VERSION%/list/featured/all/%MAX_RESULTS%/%OS%/%VERSION%");
|
||||
pref("extensions.getAddons.search.browseURL", "https://addons.mozilla.org/%LOCALE%/android/search?q=%TERMS%&platform=%OS%&appver=%VERSION%");
|
||||
pref("extensions.getAddons.search.url", "https://services.addons.mozilla.org/%LOCALE%/android/api/%API_VERSION%/search/%TERMS%/all/%MAX_RESULTS%/%OS%/%VERSION%/%COMPATIBILITY_MODE%");
|
||||
pref("extensions.getAddons.browseAddons", "https://addons.mozilla.org/%LOCALE%/android/");
|
||||
pref("extensions.getAddons.get.url", "https://services.addons.mozilla.org/%LOCALE%/android/api/%API_VERSION%/search/guid:%IDS%?src=mobile&appOS=%OS%&appVersion=%VERSION%");
|
||||
pref("extensions.getAddons.getWithPerformance.url", "https://services.addons.mozilla.org/%LOCALE%/android/api/%API_VERSION%/search/guid:%IDS%?src=mobile&appOS=%OS%&appVersion=%VERSION%&tMain=%TIME_MAIN%&tFirstPaint=%TIME_FIRST_PAINT%&tSessionRestored=%TIME_SESSION_RESTORED%");
|
||||
|
||||
/* preference for the locale picker */
|
||||
pref("extensions.getLocales.get.url", "");
|
||||
pref("extensions.compatability.locales.buildid", "0");
|
||||
|
||||
/* blocklist preferences */
|
||||
pref("extensions.blocklist.enabled", true);
|
||||
pref("extensions.blocklist.interval", 86400);
|
||||
pref("extensions.blocklist.url", "https://blocklist.addons.mozilla.org/blocklist/3/%APP_ID%/%APP_VERSION%/%PRODUCT%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/%PING_COUNT%/%TOTAL_PING_COUNT%/%DAYS_SINCE_LAST_PING%/");
|
||||
pref("extensions.blocklist.detailsURL", "https://www.mozilla.com/%LOCALE%/blocklist/");
|
||||
|
||||
/* block popups by default, and notify the user about blocked popups */
|
||||
pref("dom.disable_open_during_load", true);
|
||||
pref("privacy.popups.showBrowserMessage", true);
|
||||
|
||||
/* disable opening windows with the dialog feature */
|
||||
pref("dom.disable_window_open_dialog_feature", true);
|
||||
pref("dom.disable_window_showModalDialog", true);
|
||||
pref("dom.disable_window_print", true);
|
||||
pref("dom.disable_window_find", true);
|
||||
|
||||
pref("keyword.enabled", true);
|
||||
pref("browser.fixup.domainwhitelist.localhost", true);
|
||||
|
||||
pref("accessibility.typeaheadfind", false);
|
||||
pref("accessibility.typeaheadfind.timeout", 5000);
|
||||
pref("accessibility.typeaheadfind.flashBar", 1);
|
||||
pref("accessibility.typeaheadfind.linksonly", false);
|
||||
pref("accessibility.typeaheadfind.casesensitive", 0);
|
||||
pref("accessibility.browsewithcaret_shortcut.enabled", false);
|
||||
|
||||
// Whether the character encoding menu is under the main Firefox button. This
|
||||
// preference is a string so that localizers can alter it.
|
||||
pref("browser.menu.showCharacterEncoding", "chrome://browser/locale/browser.properties");
|
||||
|
||||
// pointer to the default engine name
|
||||
pref("browser.search.defaultenginename", "chrome://browser/locale/region.properties");
|
||||
// maximum number of search suggestions, as a string because the search service expects a string pref
|
||||
pref("browser.search.param.maxSuggestions", "4");
|
||||
// SSL error page behaviour
|
||||
pref("browser.ssl_override_behavior", 2);
|
||||
pref("browser.xul.error_pages.expert_bad_cert", false);
|
||||
|
||||
// ordering of search engines in the engine list.
|
||||
pref("browser.search.order.1", "chrome://browser/locale/region.properties");
|
||||
pref("browser.search.order.2", "chrome://browser/locale/region.properties");
|
||||
pref("browser.search.order.3", "chrome://browser/locale/region.properties");
|
||||
|
||||
// Market-specific search defaults (US market only)
|
||||
pref("browser.search.geoSpecificDefaults", true);
|
||||
pref("browser.search.defaultenginename.US", "chrome://browser/locale/region.properties");
|
||||
pref("browser.search.order.US.1", "chrome://browser/locale/region.properties");
|
||||
pref("browser.search.order.US.2", "chrome://browser/locale/region.properties");
|
||||
pref("browser.search.order.US.3", "chrome://browser/locale/region.properties");
|
||||
|
||||
// disable updating
|
||||
pref("browser.search.update", false);
|
||||
|
||||
// disable search suggestions by default
|
||||
pref("browser.search.suggest.enabled", false);
|
||||
pref("browser.search.suggest.prompted", false);
|
||||
|
||||
// Tell the search service to load search plugins from the locale JAR
|
||||
pref("browser.search.loadFromJars", true);
|
||||
pref("browser.search.jarURIs", "chrome://browser/locale/searchplugins/");
|
||||
|
||||
// tell the search service that we don't really expose the "current engine"
|
||||
pref("browser.search.noCurrentEngine", true);
|
||||
|
||||
// Control media casting & mirroring features
|
||||
pref("browser.casting.enabled", true);
|
||||
#ifdef RELEASE_BUILD
|
||||
// Roku does not yet support mirroring in production
|
||||
pref("browser.mirroring.enabled.roku", false);
|
||||
// Chromecast mirroring is broken (bug 1131084)
|
||||
pref("browser.mirroring.enabled", false);
|
||||
#else
|
||||
pref("browser.mirroring.enabled.roku", true);
|
||||
pref("browser.mirroring.enabled", true);
|
||||
#endif
|
||||
|
||||
// Enable sparse localization by setting a few package locale overrides
|
||||
pref("chrome.override_package.global", "browser");
|
||||
pref("chrome.override_package.mozapps", "browser");
|
||||
pref("chrome.override_package.passwordmgr", "browser");
|
||||
|
||||
// enable xul error pages
|
||||
pref("browser.xul.error_pages.enabled", true);
|
||||
|
||||
// disable color management
|
||||
pref("gfx.color_management.mode", 0);
|
||||
|
||||
// 0=fixed margin, 1=velocity bias, 2=dynamic resolution, 3=no margins, 4=prediction bias
|
||||
pref("gfx.displayport.strategy", 1);
|
||||
|
||||
// all of the following displayport strategy prefs will be divided by 1000
|
||||
// to obtain some multiplier which is then used in the strategy.
|
||||
// fixed margin strategy options
|
||||
pref("gfx.displayport.strategy_fm.multiplier", -1); // displayport dimension multiplier
|
||||
pref("gfx.displayport.strategy_fm.danger_x", -1); // danger zone on x-axis when multiplied by viewport width
|
||||
pref("gfx.displayport.strategy_fm.danger_y", -1); // danger zone on y-axis when multiplied by viewport height
|
||||
|
||||
// velocity bias strategy options
|
||||
pref("gfx.displayport.strategy_vb.multiplier", -1); // displayport dimension multiplier
|
||||
pref("gfx.displayport.strategy_vb.threshold", -1); // velocity threshold in inches/frame
|
||||
pref("gfx.displayport.strategy_vb.reverse_buffer", -1); // fraction of buffer to keep in reverse direction from scroll
|
||||
pref("gfx.displayport.strategy_vb.danger_x_base", -1); // danger zone on x-axis when multiplied by viewport width
|
||||
pref("gfx.displayport.strategy_vb.danger_y_base", -1); // danger zone on y-axis when multiplied by viewport height
|
||||
pref("gfx.displayport.strategy_vb.danger_x_incr", -1); // additional danger zone on x-axis when multiplied by viewport width and velocity
|
||||
pref("gfx.displayport.strategy_vb.danger_y_incr", -1); // additional danger zone on y-axis when multiplied by viewport height and velocity
|
||||
|
||||
// prediction bias strategy options
|
||||
pref("gfx.displayport.strategy_pb.threshold", -1); // velocity threshold in inches/frame
|
||||
|
||||
// Allow 24-bit colour when the hardware supports it
|
||||
pref("gfx.android.rgb16.force", false);
|
||||
|
||||
// don't allow JS to move and resize existing windows
|
||||
pref("dom.disable_window_move_resize", true);
|
||||
|
||||
// prevent click image resizing for nsImageDocument
|
||||
pref("browser.enable_click_image_resizing", false);
|
||||
|
||||
// open in tab preferences
|
||||
// 0=default window, 1=current window/tab, 2=new window, 3=new tab in most window
|
||||
pref("browser.link.open_external", 3);
|
||||
pref("browser.link.open_newwindow", 3);
|
||||
// 0=force all new windows to tabs, 1=don't force, 2=only force those with no features set
|
||||
pref("browser.link.open_newwindow.restriction", 0);
|
||||
|
||||
// controls which bits of private data to clear. by default we clear them all.
|
||||
pref("privacy.item.cache", true);
|
||||
pref("privacy.item.cookies", true);
|
||||
pref("privacy.item.offlineApps", true);
|
||||
pref("privacy.item.history", true);
|
||||
pref("privacy.item.formdata", true);
|
||||
pref("privacy.item.downloads", true);
|
||||
pref("privacy.item.passwords", true);
|
||||
pref("privacy.item.sessions", true);
|
||||
pref("privacy.item.geolocation", true);
|
||||
pref("privacy.item.siteSettings", true);
|
||||
pref("privacy.item.syncAccount", true);
|
||||
|
||||
// enable geo
|
||||
pref("geo.enabled", true);
|
||||
|
||||
// content sink control -- controls responsiveness during page load
|
||||
// see https://bugzilla.mozilla.org/show_bug.cgi?id=481566#c9
|
||||
//pref("content.sink.enable_perf_mode", 2); // 0 - switch, 1 - interactive, 2 - perf
|
||||
//pref("content.sink.pending_event_mode", 0);
|
||||
//pref("content.sink.perf_deflect_count", 1000000);
|
||||
//pref("content.sink.perf_parse_time", 50000000);
|
||||
|
||||
// Disable the JS engine's gc on memory pressure, since we do one in the mobile
|
||||
// browser (bug 669346).
|
||||
pref("javascript.options.gc_on_memory_pressure", false);
|
||||
|
||||
#ifdef MOZ_PKG_SPECIAL
|
||||
// low memory devices
|
||||
pref("javascript.options.mem.gc_high_frequency_heap_growth_max", 120);
|
||||
pref("javascript.options.mem.gc_high_frequency_heap_growth_min", 120);
|
||||
pref("javascript.options.mem.gc_high_frequency_high_limit_mb", 40);
|
||||
pref("javascript.options.mem.gc_high_frequency_low_limit_mb", 10);
|
||||
pref("javascript.options.mem.gc_low_frequency_heap_growth", 120);
|
||||
pref("javascript.options.mem.high_water_mark", 16);
|
||||
pref("javascript.options.mem.gc_allocation_threshold_mb", 3);
|
||||
pref("javascript.options.mem.gc_decommit_threshold_mb", 1);
|
||||
pref("javascript.options.mem.gc_min_empty_chunk_count", 1);
|
||||
pref("javascript.options.mem.gc_max_empty_chunk_count", 2);
|
||||
#else
|
||||
pref("javascript.options.mem.high_water_mark", 32);
|
||||
#endif
|
||||
|
||||
pref("dom.max_chrome_script_run_time", 0); // disable slow script dialog for chrome
|
||||
pref("dom.max_script_run_time", 20);
|
||||
|
||||
// JS error console
|
||||
pref("devtools.errorconsole.enabled", false);
|
||||
// Absolute path to the devtools unix domain socket file used
|
||||
// to communicate with a usb cable via adb forward.
|
||||
pref("devtools.debugger.unix-domain-socket", "/data/data/@ANDROID_PACKAGE_NAME@/firefox-debugger-socket");
|
||||
|
||||
pref("font.size.inflation.minTwips", 120);
|
||||
|
||||
// When true, zooming will be enabled on all sites, even ones that declare user-scalable=no.
|
||||
pref("browser.ui.zoom.force-user-scalable", false);
|
||||
|
||||
pref("ui.zoomedview.enabled", false);
|
||||
pref("ui.zoomedview.limitReadableSize", 8); // value in layer pixels
|
||||
|
||||
pref("ui.touch.radius.enabled", false);
|
||||
pref("ui.touch.radius.leftmm", 3);
|
||||
pref("ui.touch.radius.topmm", 5);
|
||||
pref("ui.touch.radius.rightmm", 3);
|
||||
pref("ui.touch.radius.bottommm", 2);
|
||||
pref("ui.touch.radius.visitedWeight", 120);
|
||||
|
||||
pref("ui.mouse.radius.enabled", true);
|
||||
pref("ui.mouse.radius.leftmm", 3);
|
||||
pref("ui.mouse.radius.topmm", 5);
|
||||
pref("ui.mouse.radius.rightmm", 3);
|
||||
pref("ui.mouse.radius.bottommm", 2);
|
||||
pref("ui.mouse.radius.visitedWeight", 120);
|
||||
pref("ui.mouse.radius.reposition", true);
|
||||
|
||||
// The percentage of the screen that needs to be scrolled before margins are exposed.
|
||||
pref("browser.ui.show-margins-threshold", 10);
|
||||
|
||||
// Maximum distance from the point where the user pressed where we still
|
||||
// look for text to select
|
||||
pref("browser.ui.selection.distance", 250);
|
||||
|
||||
// plugins
|
||||
pref("plugin.disable", false);
|
||||
pref("dom.ipc.plugins.enabled", false);
|
||||
|
||||
// This pref isn't actually used anymore, but we're leaving this here to avoid changing
|
||||
// the default so that we can migrate a user-set pref. See bug 885357.
|
||||
pref("plugins.click_to_play", true);
|
||||
// The default value for nsIPluginTag.enabledState (STATE_CLICKTOPLAY = 1)
|
||||
pref("plugin.default.state", 1);
|
||||
|
||||
// product URLs
|
||||
// The breakpad report server to link to in about:crashes
|
||||
pref("breakpad.reportURL", "https://crash-stats.mozilla.com/report/index/");
|
||||
pref("app.support.baseURL", "http://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/");
|
||||
// Used to submit data to input from about:feedback
|
||||
pref("app.feedback.postURL", "https://input.mozilla.org/api/v1/feedback/");
|
||||
pref("app.privacyURL", "https://www.mozilla.org/privacy/firefox/");
|
||||
pref("app.creditsURL", "http://www.palemoon.org/Contributors.shtml");
|
||||
pref("app.channelURL", "http://www.mozilla.org/%LOCALE%/firefox/channel/");
|
||||
#if MOZ_UPDATE_CHANNEL == aurora
|
||||
pref("app.releaseNotesURL", "http://www.mozilla.com/%LOCALE%/mobile/%VERSION%/auroranotes/");
|
||||
#elif MOZ_UPDATE_CHANNEL == beta
|
||||
pref("app.releaseNotesURL", "http://www.mozilla.com/%LOCALE%/mobile/%VERSION%beta/releasenotes/");
|
||||
#else
|
||||
pref("app.releaseNotesURL", "http://www.mozilla.com/%LOCALE%/mobile/%VERSION%/releasenotes/");
|
||||
#endif
|
||||
#if MOZ_UPDATE_CHANNEL == beta
|
||||
pref("app.faqURL", "http://www.mozilla.com/%LOCALE%/mobile/beta/faq/");
|
||||
#else
|
||||
pref("app.faqURL", "http://www.mozilla.com/%LOCALE%/mobile/faq/");
|
||||
#endif
|
||||
pref("app.marketplaceURL", "https://marketplace.firefox.com/");
|
||||
|
||||
// Name of alternate about: page for certificate errors (when undefined, defaults to about:neterror)
|
||||
pref("security.alternate_certificate_error_page", "certerror");
|
||||
|
||||
pref("security.warn_viewing_mixed", false); // Warning is disabled. See Bug 616712.
|
||||
|
||||
// Block insecure active content on https pages
|
||||
pref("security.mixed_content.block_active_content", true);
|
||||
|
||||
// Enable pinning
|
||||
pref("security.cert_pinning.enforcement_level", 1);
|
||||
|
||||
// Override some named colors to avoid inverse OS themes
|
||||
pref("ui.-moz-dialog", "#efebe7");
|
||||
pref("ui.-moz-dialogtext", "#101010");
|
||||
pref("ui.-moz-field", "#fff");
|
||||
pref("ui.-moz-fieldtext", "#1a1a1a");
|
||||
pref("ui.-moz-buttonhoverface", "#f3f0ed");
|
||||
pref("ui.-moz-buttonhovertext", "#101010");
|
||||
pref("ui.-moz-combobox", "#fff");
|
||||
pref("ui.-moz-comboboxtext", "#101010");
|
||||
pref("ui.buttonface", "#ece7e2");
|
||||
pref("ui.buttonhighlight", "#fff");
|
||||
pref("ui.buttonshadow", "#aea194");
|
||||
pref("ui.buttontext", "#101010");
|
||||
pref("ui.captiontext", "#101010");
|
||||
pref("ui.graytext", "#b1a598");
|
||||
pref("ui.highlight", "#fad184");
|
||||
pref("ui.highlighttext", "#1a1a1a");
|
||||
pref("ui.infobackground", "#f5f5b5");
|
||||
pref("ui.infotext", "#000");
|
||||
pref("ui.menu", "#f7f5f3");
|
||||
pref("ui.menutext", "#101010");
|
||||
pref("ui.threeddarkshadow", "#000");
|
||||
pref("ui.threedface", "#ece7e2");
|
||||
pref("ui.threedhighlight", "#fff");
|
||||
pref("ui.threedlightshadow", "#ece7e2");
|
||||
pref("ui.threedshadow", "#aea194");
|
||||
pref("ui.window", "#efebe7");
|
||||
pref("ui.windowtext", "#101010");
|
||||
pref("ui.windowframe", "#efebe7");
|
||||
|
||||
/* prefs used by the update timer system (including blocklist pings) */
|
||||
pref("app.update.timerFirstInterval", 30000); // milliseconds
|
||||
pref("app.update.timerMinimumDelay", 30); // seconds
|
||||
|
||||
// used by update service to decide whether or not to
|
||||
// automatically download an update
|
||||
pref("app.update.autodownload", "wifi");
|
||||
pref("app.update.url.android", "https://aus4.mozilla.org/update/4/%PRODUCT%/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/%MOZ_VERSION%/update.xml");
|
||||
|
||||
#ifdef MOZ_UPDATER
|
||||
/* prefs used specifically for updating the app */
|
||||
pref("app.update.enabled", false);
|
||||
pref("app.update.channel", "@MOZ_UPDATE_CHANNEL@");
|
||||
|
||||
#endif
|
||||
|
||||
// replace newlines with spaces on paste into single-line text boxes
|
||||
pref("editor.singleLine.pasteNewlines", 2);
|
||||
|
||||
// threshold where a tap becomes a drag, in 1/240" reference pixels
|
||||
// The names of the preferences are to be in sync with EventStateManager.cpp
|
||||
pref("ui.dragThresholdX", 25);
|
||||
pref("ui.dragThresholdY", 25);
|
||||
|
||||
pref("layers.acceleration.disabled", false);
|
||||
pref("layers.offmainthreadcomposition.enabled", true);
|
||||
pref("layers.async-video.enabled", true);
|
||||
#ifdef MOZ_ANDROID_APZ
|
||||
pref("layers.async-pan-zoom.enabled", true);
|
||||
#endif
|
||||
pref("layers.progressive-paint", true);
|
||||
pref("layers.low-precision-buffer", true);
|
||||
pref("layers.low-precision-resolution", "0.25");
|
||||
pref("layers.low-precision-opacity", "1.0");
|
||||
// We want to limit layers for two reasons:
|
||||
// 1) We can't scroll smoothly if we have to many draw calls
|
||||
// 2) Pages that have too many layers consume too much memory and crash.
|
||||
// By limiting the number of layers on mobile we're making the main thread
|
||||
// work harder keep scrolling smooth and memory low.
|
||||
pref("layers.max-active", 20);
|
||||
|
||||
pref("notification.feature.enabled", true);
|
||||
pref("dom.webnotifications.enabled", true);
|
||||
|
||||
// prevent tooltips from showing up
|
||||
pref("browser.chrome.toolbar_tips", false);
|
||||
|
||||
// prevent video elements from preloading too much data
|
||||
pref("media.preload.default", 1); // default to preload none
|
||||
pref("media.preload.auto", 2); // preload metadata if preload=auto
|
||||
pref("media.cache_size", 32768); // 32MB media cache
|
||||
// Try to save battery by not resuming reading from a connection until we fall
|
||||
// below 10s of buffered data.
|
||||
pref("media.cache_resume_threshold", 10);
|
||||
pref("media.cache_readahead_limit", 30);
|
||||
|
||||
// Number of video frames we buffer while decoding video.
|
||||
// On Android this is decided by a similar value which varies for
|
||||
// each OMX decoder |OMX_PARAM_PORTDEFINITIONTYPE::nBufferCountMin|. This
|
||||
// number must be less than the OMX equivalent or goanna will think it is
|
||||
// chronically starved of video frames. All decoders seen so far have a value
|
||||
// of at least 4.
|
||||
pref("media.video-queue.default-size", 3);
|
||||
|
||||
// Enable the MediaCodec PlatformDecoderModule by default.
|
||||
pref("media.fragmented-mp4.enabled", true);
|
||||
pref("media.android-media-codec.enabled", true);
|
||||
pref("media.android-media-codec.preferred", true);
|
||||
|
||||
// optimize images memory usage
|
||||
pref("image.mem.decodeondraw", true);
|
||||
|
||||
// enable touch events interfaces
|
||||
pref("dom.w3c_touch_events.enabled", 1);
|
||||
|
||||
// URL for posting tiles metrics.
|
||||
#ifdef RELEASE_BUILD
|
||||
pref("browser.tiles.reportURL", "https://tiles.services.mozilla.com/v2/links/click");
|
||||
#endif
|
||||
|
||||
// True if this is the first time we are showing about:firstrun
|
||||
pref("browser.firstrun.show.uidiscovery", true);
|
||||
pref("browser.firstrun.show.localepicker", false);
|
||||
|
||||
// True if you always want dump() to work
|
||||
//
|
||||
// On Android, you also need to do the following for the output
|
||||
// to show up in logcat:
|
||||
//
|
||||
// $ adb shell stop
|
||||
// $ adb shell setprop log.redirect-stdio true
|
||||
// $ adb shell start
|
||||
pref("browser.dom.window.dump.enabled", true);
|
||||
|
||||
// SimplePush
|
||||
pref("services.push.enabled", false);
|
||||
|
||||
// controls if we want camera support
|
||||
pref("device.camera.enabled", true);
|
||||
pref("media.realtime_decoder.enabled", true);
|
||||
|
||||
pref("dom.report_all_js_exceptions", true);
|
||||
pref("javascript.options.showInConsole", true);
|
||||
|
||||
pref("full-screen-api.enabled", true);
|
||||
|
||||
pref("direct-texture.force.enabled", false);
|
||||
pref("direct-texture.force.disabled", false);
|
||||
|
||||
// This fraction in 1000ths of velocity remains after every animation frame when the velocity is low.
|
||||
pref("ui.scrolling.friction_slow", -1);
|
||||
// This fraction in 1000ths of velocity remains after every animation frame when the velocity is high.
|
||||
pref("ui.scrolling.friction_fast", -1);
|
||||
// The maximum velocity change factor between events, per ms, in 1000ths.
|
||||
// Direction changes are excluded.
|
||||
pref("ui.scrolling.max_event_acceleration", -1);
|
||||
// The rate of deceleration when the surface has overscrolled, in 1000ths.
|
||||
pref("ui.scrolling.overscroll_decel_rate", -1);
|
||||
// The fraction of the surface which can be overscrolled before it must snap back, in 1000ths.
|
||||
pref("ui.scrolling.overscroll_snap_limit", -1);
|
||||
// The minimum amount of space that must be present for an axis to be considered scrollable,
|
||||
// in 1/1000ths of pixels.
|
||||
pref("ui.scrolling.min_scrollable_distance", -1);
|
||||
// The axis lock mode for panning behaviour - set between standard, free and sticky
|
||||
pref("ui.scrolling.axis_lock_mode", "standard");
|
||||
// Negate scrollY, true will make the mouse scroll wheel move the screen the same direction as with most desktops or laptops.
|
||||
pref("ui.scrolling.negate_wheel_scrollY", true);
|
||||
// Determine the dead zone for gamepad joysticks. Higher values result in larger dead zones; use a negative value to
|
||||
// auto-detect based on reported hardware values
|
||||
pref("ui.scrolling.gamepad_dead_zone", 115);
|
||||
|
||||
// Prefs for fling acceleration
|
||||
pref("ui.scrolling.fling_accel_interval", -1);
|
||||
pref("ui.scrolling.fling_accel_base_multiplier", -1);
|
||||
pref("ui.scrolling.fling_accel_supplemental_multiplier", -1);
|
||||
|
||||
// Prefs for fling curving
|
||||
pref("ui.scrolling.fling_curve_function_x1", -1);
|
||||
pref("ui.scrolling.fling_curve_function_y1", -1);
|
||||
pref("ui.scrolling.fling_curve_function_x2", -1);
|
||||
pref("ui.scrolling.fling_curve_function_y2", -1);
|
||||
pref("ui.scrolling.fling_curve_threshold_velocity", -1);
|
||||
pref("ui.scrolling.fling_curve_max_velocity", -1);
|
||||
pref("ui.scrolling.fling_curve_newton_iterations", -1);
|
||||
|
||||
// Enable accessibility mode if platform accessibility is enabled.
|
||||
pref("accessibility.accessfu.activate", 2);
|
||||
pref("accessibility.accessfu.quicknav_modes", "Link,Heading,FormElement,Landmark,ListItem");
|
||||
// Active quicknav mode, index value of list from quicknav_modes
|
||||
pref("accessibility.accessfu.quicknav_index", 0);
|
||||
// Setting for an utterance order (0 - description first, 1 - description last).
|
||||
pref("accessibility.accessfu.utterance", 1);
|
||||
// Whether to skip images with empty alt text
|
||||
pref("accessibility.accessfu.skip_empty_images", true);
|
||||
|
||||
// Transmit UDP busy-work to the LAN when anticipating low latency
|
||||
// network reads and on wifi to mitigate 802.11 Power Save Polling delays
|
||||
pref("network.tickle-wifi.enabled", true);
|
||||
|
||||
// Mobile manages state by autodetection
|
||||
pref("network.manage-offline-status", true);
|
||||
|
||||
// increase the timeout clamp for background tabs to 15 minutes
|
||||
pref("dom.min_background_timeout_value", 900000);
|
||||
|
||||
// Media plugins for libstagefright playback on android
|
||||
pref("media.plugins.enabled", true);
|
||||
|
||||
// Stagefright's OMXCodec::CreationFlags. The interesting flag values are:
|
||||
// 0 = Let Stagefright choose hardware or software decoding (default)
|
||||
// 8 = Force software decoding
|
||||
// 16 = Force hardware decoding
|
||||
pref("media.stagefright.omxcodec.flags", 0);
|
||||
|
||||
// Coalesce touch events to prevent them from flooding the event queue
|
||||
pref("dom.event.touch.coalescing.enabled", false);
|
||||
|
||||
// default orientation for the app, default to undefined
|
||||
// the java GoannaScreenOrientationListener needs this to be defined
|
||||
pref("app.orientation.default", "");
|
||||
|
||||
// On memory pressure, release dirty but unused pages held by jemalloc
|
||||
// back to the system.
|
||||
pref("memory.free_dirty_pages", true);
|
||||
|
||||
pref("layout.imagevisibility.numscrollportwidths", 1);
|
||||
pref("layout.imagevisibility.numscrollportheights", 1);
|
||||
|
||||
pref("layers.enable-tiles", true);
|
||||
|
||||
// Enable the dynamic toolbar
|
||||
pref("browser.chrome.dynamictoolbar", true);
|
||||
|
||||
// The mode of browser titlebar
|
||||
// 0: Show a current page title.
|
||||
// 1: Show a current page url.
|
||||
pref("browser.chrome.titlebarMode", 1);
|
||||
|
||||
// Hide common parts of URLs like "www." or "http://"
|
||||
pref("browser.urlbar.trimURLs", true);
|
||||
|
||||
#ifdef MOZ_PKG_SPECIAL
|
||||
// Disable webgl on ARMv6 because running the reftests takes
|
||||
// too long for some reason (bug 843738)
|
||||
pref("webgl.disabled", true);
|
||||
#endif
|
||||
|
||||
// initial web feed readers list
|
||||
pref("browser.contentHandlers.types.0.title", "chrome://browser/locale/region.properties");
|
||||
pref("browser.contentHandlers.types.0.uri", "chrome://browser/locale/region.properties");
|
||||
pref("browser.contentHandlers.types.0.type", "application/vnd.mozilla.maybe.feed");
|
||||
pref("browser.contentHandlers.types.1.title", "chrome://browser/locale/region.properties");
|
||||
pref("browser.contentHandlers.types.1.uri", "chrome://browser/locale/region.properties");
|
||||
pref("browser.contentHandlers.types.1.type", "application/vnd.mozilla.maybe.feed");
|
||||
pref("browser.contentHandlers.types.2.title", "chrome://browser/locale/region.properties");
|
||||
pref("browser.contentHandlers.types.2.uri", "chrome://browser/locale/region.properties");
|
||||
pref("browser.contentHandlers.types.2.type", "application/vnd.mozilla.maybe.feed");
|
||||
pref("browser.contentHandlers.types.3.title", "chrome://browser/locale/region.properties");
|
||||
pref("browser.contentHandlers.types.3.uri", "chrome://browser/locale/region.properties");
|
||||
pref("browser.contentHandlers.types.3.type", "application/vnd.mozilla.maybe.feed");
|
||||
|
||||
// WebPayment
|
||||
pref("dom.mozPay.enabled", true);
|
||||
|
||||
#ifndef RELEASE_BUILD
|
||||
pref("dom.payment.provider.0.name", "Firefox Marketplace");
|
||||
pref("dom.payment.provider.0.description", "marketplace.firefox.com");
|
||||
pref("dom.payment.provider.0.uri", "https://marketplace.firefox.com/mozpay/?req=");
|
||||
pref("dom.payment.provider.0.type", "mozilla/payments/pay/v1");
|
||||
pref("dom.payment.provider.0.requestMethod", "GET");
|
||||
#endif
|
||||
|
||||
// Shortnumber matching needed for e.g. Brazil:
|
||||
// 01187654321 can be found with 87654321
|
||||
pref("dom.phonenumber.substringmatching.BR", 8);
|
||||
pref("dom.phonenumber.substringmatching.CO", 10);
|
||||
pref("dom.phonenumber.substringmatching.VE", 7);
|
||||
|
||||
// Support for the mozAudioChannel attribute on media elements is disabled in non-webapps
|
||||
pref("media.useAudioChannelService", false);
|
||||
|
||||
// Enable hardware-accelerated Skia canvas
|
||||
pref("gfx.canvas.azure.backends", "skia");
|
||||
pref("gfx.canvas.azure.accelerated", true);
|
||||
|
||||
pref("general.useragent.override.youtube.com", "Android; Tablet;#Android; Mobile;");
|
||||
|
||||
// When true, phone number linkification is enabled.
|
||||
pref("browser.ui.linkify.phone", false);
|
||||
|
||||
// Enables/disables Spatial Navigation
|
||||
pref("snav.enabled", true);
|
||||
|
||||
// This url, if changed, MUST continue to point to an https url. Pulling arbitrary content to inject into
|
||||
// this page over http opens us up to a man-in-the-middle attack that we'd rather not face. If you are a downstream
|
||||
// repackager of this code using an alternate snippet url, please keep your users safe
|
||||
pref("browser.snippets.updateUrl", "https://snippets.mozilla.com/json/%SNIPPETS_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/");
|
||||
|
||||
// How frequently we check for new snippets, in seconds (1 day)
|
||||
pref("browser.snippets.updateInterval", 86400);
|
||||
|
||||
// URL used to check for user's country code
|
||||
pref("browser.snippets.geoUrl", "https://geo.mozilla.org/country.json");
|
||||
|
||||
// URL used to ping metrics with stats about which snippets have been shown
|
||||
pref("browser.snippets.statsUrl", "https://snippets-stats.mozilla.org/mobile");
|
||||
|
||||
// These prefs require a restart to take effect.
|
||||
pref("browser.snippets.enabled", true);
|
||||
pref("browser.snippets.syncPromo.enabled", true);
|
||||
pref("browser.snippets.firstrunHomepage.enabled", true);
|
||||
|
||||
// The URL of the APK factory from which we obtain APKs for webapps.
|
||||
pref("browser.webapps.apkFactoryUrl", "https://controller.apk.firefox.com/application.apk");
|
||||
|
||||
// How frequently to check for webapp updates, in seconds (86400 is daily).
|
||||
pref("browser.webapps.updateInterval", 86400);
|
||||
|
||||
// Whether or not to check for updates. Enabled by default, but the runtime
|
||||
// disables it for webapp profiles on firstrun, so only the main Fennec process
|
||||
// checks for updates (to avoid duplicate update notifications).
|
||||
//
|
||||
// In the future, we might want to make each webapp process check for updates
|
||||
// for its own webapp, in which case we'll need to have a third state for this
|
||||
// preference. Thus it's an integer rather than a boolean.
|
||||
//
|
||||
// Possible values:
|
||||
// 0: don't check for updates
|
||||
// 1: do check for updates
|
||||
pref("browser.webapps.checkForUpdates", 1);
|
||||
|
||||
// The URL of the service that checks for updates.
|
||||
// To test updates, set this to http://apk-update-checker.paas.allizom.org,
|
||||
// which is a test server that always reports all apps as having updates.
|
||||
pref("browser.webapps.updateCheckUrl", "https://controller.apk.firefox.com/app_updates");
|
||||
|
||||
// The mode of home provider syncing.
|
||||
// 0: Sync always
|
||||
// 1: Sync only when on wifi
|
||||
pref("home.sync.updateMode", 0);
|
||||
|
||||
// How frequently to check if we should sync home provider data.
|
||||
pref("home.sync.checkIntervalSecs", 3600);
|
||||
|
||||
// Enable device storage API
|
||||
pref("device.storage.enabled", true);
|
||||
|
||||
// Enable meta-viewport support for font inflation code
|
||||
pref("dom.meta-viewport.enabled", true);
|
||||
|
||||
// Enable GMP support in the addon manager.
|
||||
pref("media.gmp-provider.enabled", true);
|
||||
|
||||
// The default color scheme in reader mode (light, dark, auto)
|
||||
// auto = color automatically adjusts according to ambient light level
|
||||
// (auto only works on platforms where the 'devicelight' event is enabled)
|
||||
pref("reader.color_scheme", "auto");
|
||||
|
||||
// Color scheme values available in reader mode UI.
|
||||
pref("reader.color_scheme.values", "[\"dark\",\"auto\",\"light\"]");
|
||||
|
||||
// Whether to use a vertical or horizontal toolbar.
|
||||
pref("reader.toolbar.vertical", false);
|
||||
|
||||
// Whether or not to display buttons related to reading list in reader view.
|
||||
pref("browser.readinglist.enabled", true);
|
||||
@@ -1,19 +0,0 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
for var in ('APP_NAME', 'APP_VERSION'):
|
||||
DEFINES[var] = CONFIG['MOZ_%s' % var]
|
||||
|
||||
for var in ('MOZ_UPDATER', 'MOZ_APP_UA_NAME', 'ANDROID_PACKAGE_NAME'):
|
||||
DEFINES[var] = CONFIG[var]
|
||||
|
||||
if CONFIG['MOZ_PKG_SPECIAL']:
|
||||
DEFINES['MOZ_PKG_SPECIAL'] = CONFIG['MOZ_PKG_SPECIAL']
|
||||
|
||||
JS_PREFERENCE_FILES += [
|
||||
'mobile.js',
|
||||
]
|
||||
|
||||
@@ -1,592 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Reader;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.goanna.AppConstants.Versions;
|
||||
import org.mozilla.goanna.util.ThreadUtils;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
public final class ANRReporter extends BroadcastReceiver
|
||||
{
|
||||
private static final boolean DEBUG = false;
|
||||
private static final String LOGTAG = "GoannaANRReporter";
|
||||
|
||||
private static final String ANR_ACTION = "android.intent.action.ANR";
|
||||
// Number of lines to search traces.txt to decide whether it's a Goanna ANR
|
||||
private static final int LINES_TO_IDENTIFY_TRACES = 10;
|
||||
// ANRs may happen because of memory pressure,
|
||||
// so don't use up too much memory here
|
||||
// Size of buffer to hold one line of text
|
||||
private static final int TRACES_LINE_SIZE = 100;
|
||||
// Size of block to use when processing traces.txt
|
||||
private static final int TRACES_BLOCK_SIZE = 2000;
|
||||
private static final String TRACES_CHARSET = "utf-8";
|
||||
private static final String PING_CHARSET = "utf-8";
|
||||
|
||||
private static final ANRReporter sInstance = new ANRReporter();
|
||||
private static int sRegisteredCount;
|
||||
private Handler mHandler;
|
||||
private volatile boolean mPendingANR;
|
||||
|
||||
private static native boolean requestNativeStack(boolean unwind);
|
||||
private static native String getNativeStack();
|
||||
private static native void releaseNativeStack();
|
||||
|
||||
public static void register(Context context) {
|
||||
if (sRegisteredCount++ != 0) {
|
||||
// Already registered
|
||||
return;
|
||||
}
|
||||
sInstance.start(context);
|
||||
}
|
||||
|
||||
public static void unregister() {
|
||||
if (sRegisteredCount == 0) {
|
||||
Log.w(LOGTAG, "register/unregister mismatch");
|
||||
return;
|
||||
}
|
||||
if (--sRegisteredCount != 0) {
|
||||
// Should still be registered
|
||||
return;
|
||||
}
|
||||
sInstance.stop();
|
||||
}
|
||||
|
||||
private void start(final Context context) {
|
||||
|
||||
Thread receiverThread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Looper.prepare();
|
||||
synchronized (ANRReporter.this) {
|
||||
mHandler = new Handler();
|
||||
ANRReporter.this.notify();
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "registering receiver");
|
||||
}
|
||||
context.registerReceiver(ANRReporter.this,
|
||||
new IntentFilter(ANR_ACTION),
|
||||
null,
|
||||
mHandler);
|
||||
Looper.loop();
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "unregistering receiver");
|
||||
}
|
||||
context.unregisterReceiver(ANRReporter.this);
|
||||
mHandler = null;
|
||||
}
|
||||
}, LOGTAG);
|
||||
|
||||
receiverThread.setDaemon(true);
|
||||
receiverThread.start();
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
synchronized (this) {
|
||||
while (mHandler == null) {
|
||||
try {
|
||||
wait(1000);
|
||||
if (mHandler == null) {
|
||||
// We timed out; just give up. The process is probably
|
||||
// quitting anyways, so we let the OS do the clean up
|
||||
Log.w(LOGTAG, "timed out waiting for handler");
|
||||
return;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Looper looper = mHandler.getLooper();
|
||||
looper.quit();
|
||||
try {
|
||||
looper.getThread().join();
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
|
||||
private ANRReporter() {
|
||||
}
|
||||
|
||||
// Return the "traces.txt" file, or null if there is no such file
|
||||
private static File getTracesFile() {
|
||||
// Check most common location first.
|
||||
File tracesFile = new File("/data/anr/traces.txt");
|
||||
if (tracesFile.isFile() && tracesFile.canRead()) {
|
||||
return tracesFile;
|
||||
}
|
||||
|
||||
// Find the traces file name if we can.
|
||||
try {
|
||||
// getprop [prop-name [default-value]]
|
||||
Process propProc = (new ProcessBuilder())
|
||||
.command("/system/bin/getprop", "dalvik.vm.stack-trace-file")
|
||||
.redirectErrorStream(true)
|
||||
.start();
|
||||
try {
|
||||
BufferedReader buf = new BufferedReader(
|
||||
new InputStreamReader(propProc.getInputStream()), TRACES_LINE_SIZE);
|
||||
String propVal = buf.readLine();
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "getprop returned " + String.valueOf(propVal));
|
||||
}
|
||||
// getprop can return empty string when the prop value is empty
|
||||
// or prop is undefined, treat both cases the same way
|
||||
if (propVal != null && propVal.length() != 0) {
|
||||
tracesFile = new File(propVal);
|
||||
if (tracesFile.isFile() && tracesFile.canRead()) {
|
||||
return tracesFile;
|
||||
} else if (DEBUG) {
|
||||
Log.d(LOGTAG, "cannot access traces file");
|
||||
}
|
||||
} else if (DEBUG) {
|
||||
Log.d(LOGTAG, "empty getprop result");
|
||||
}
|
||||
} finally {
|
||||
propProc.destroy();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(LOGTAG, e);
|
||||
} catch (ClassCastException e) {
|
||||
Log.w(LOGTAG, e); // Bug 975436
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static File getPingFile() {
|
||||
if (GoannaAppShell.getContext() == null) {
|
||||
return null;
|
||||
}
|
||||
GoannaProfile profile = GoannaAppShell.getGoannaInterface().getProfile();
|
||||
if (profile == null) {
|
||||
return null;
|
||||
}
|
||||
File profDir = profile.getDir();
|
||||
if (profDir == null) {
|
||||
return null;
|
||||
}
|
||||
File pingDir = new File(profDir, "saved-telemetry-pings");
|
||||
pingDir.mkdirs();
|
||||
if (!(pingDir.exists() && pingDir.isDirectory())) {
|
||||
return null;
|
||||
}
|
||||
return new File(pingDir, UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
// Return true if the traces file corresponds to a Goanna ANR
|
||||
private static boolean isGoannaTraces(String pkgName, File tracesFile) {
|
||||
try {
|
||||
final String END_OF_PACKAGE_NAME = "([^a-zA-Z0-9_]|$)";
|
||||
// Regex for finding our package name in the traces file
|
||||
Pattern pkgPattern = Pattern.compile(Pattern.quote(pkgName) + END_OF_PACKAGE_NAME);
|
||||
Pattern mangledPattern = null;
|
||||
if (!AppConstants.MANGLED_ANDROID_PACKAGE_NAME.equals(pkgName)) {
|
||||
mangledPattern = Pattern.compile(Pattern.quote(
|
||||
AppConstants.MANGLED_ANDROID_PACKAGE_NAME) + END_OF_PACKAGE_NAME);
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "trying to match package: " + pkgName);
|
||||
}
|
||||
BufferedReader traces = new BufferedReader(
|
||||
new FileReader(tracesFile), TRACES_BLOCK_SIZE);
|
||||
try {
|
||||
for (int count = 0; count < LINES_TO_IDENTIFY_TRACES; count++) {
|
||||
String line = traces.readLine();
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "identifying line: " + String.valueOf(line));
|
||||
}
|
||||
if (line == null) {
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "reached end of traces file");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (pkgPattern.matcher(line).find()) {
|
||||
// traces.txt file contains our package
|
||||
return true;
|
||||
}
|
||||
if (mangledPattern != null && mangledPattern.matcher(line).find()) {
|
||||
// traces.txt file contains our alternate package
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
traces.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// meh, can't even read from it right. just return false
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static long getUptimeMins() {
|
||||
|
||||
long uptimeMins = (new File("/proc/self/stat")).lastModified();
|
||||
if (uptimeMins != 0L) {
|
||||
uptimeMins = (System.currentTimeMillis() - uptimeMins) / 1000L / 60L;
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "uptime " + String.valueOf(uptimeMins));
|
||||
}
|
||||
return uptimeMins;
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "could not get uptime");
|
||||
}
|
||||
return 0L;
|
||||
}
|
||||
|
||||
/*
|
||||
a saved telemetry ping file consists of JSON in the following format,
|
||||
{
|
||||
"reason": "android-anr-report",
|
||||
"slug": "<uuid-string>",
|
||||
"payload": <json-object>
|
||||
}
|
||||
for Android ANR, our JSON payload should look like,
|
||||
{
|
||||
"ver": 1,
|
||||
"simpleMeasurements": {
|
||||
"uptime": <uptime>
|
||||
},
|
||||
"info": {
|
||||
"reason": "android-anr-report",
|
||||
"OS": "Android",
|
||||
...
|
||||
},
|
||||
"androidANR": "...",
|
||||
"androidLogcat": "..."
|
||||
}
|
||||
*/
|
||||
|
||||
private static int writePingPayload(OutputStream ping,
|
||||
String payload) throws IOException {
|
||||
byte [] data = payload.getBytes(PING_CHARSET);
|
||||
ping.write(data);
|
||||
return data.length;
|
||||
}
|
||||
|
||||
private static void fillPingHeader(OutputStream ping, String slug)
|
||||
throws IOException {
|
||||
|
||||
// ping file header
|
||||
byte [] data = ("{" +
|
||||
"\"reason\":\"android-anr-report\"," +
|
||||
"\"slug\":" + JSONObject.quote(slug) + "," +
|
||||
"\"payload\":").getBytes(PING_CHARSET);
|
||||
ping.write(data);
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "wrote ping header, size = " + String.valueOf(data.length));
|
||||
}
|
||||
|
||||
// payload start
|
||||
int size = writePingPayload(ping, ("{" +
|
||||
"\"ver\":1," +
|
||||
"\"simpleMeasurements\":{" +
|
||||
"\"uptime\":" + String.valueOf(getUptimeMins()) +
|
||||
"}," +
|
||||
"\"info\":{" +
|
||||
"\"reason\":\"android-anr-report\"," +
|
||||
"\"OS\":" + JSONObject.quote(SysInfo.getName()) + "," +
|
||||
"\"version\":\"" + String.valueOf(SysInfo.getVersion()) + "\"," +
|
||||
"\"appID\":" + JSONObject.quote(AppConstants.MOZ_APP_ID) + "," +
|
||||
"\"appVersion\":" + JSONObject.quote(AppConstants.MOZ_APP_VERSION)+ "," +
|
||||
"\"appName\":" + JSONObject.quote(AppConstants.MOZ_APP_BASENAME) + "," +
|
||||
"\"appBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," +
|
||||
"\"appUpdateChannel\":" + JSONObject.quote(AppConstants.MOZ_UPDATE_CHANNEL) + "," +
|
||||
// Technically the platform build ID may be different, but we'll never know
|
||||
"\"platformBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," +
|
||||
"\"locale\":" + JSONObject.quote(Locales.getLanguageTag(Locale.getDefault())) + "," +
|
||||
"\"cpucount\":" + String.valueOf(SysInfo.getCPUCount()) + "," +
|
||||
"\"memsize\":" + String.valueOf(SysInfo.getMemSize()) + "," +
|
||||
"\"arch\":" + JSONObject.quote(SysInfo.getArchABI()) + "," +
|
||||
"\"kernel_version\":" + JSONObject.quote(SysInfo.getKernelVersion()) + "," +
|
||||
"\"device\":" + JSONObject.quote(SysInfo.getDevice()) + "," +
|
||||
"\"manufacturer\":" + JSONObject.quote(SysInfo.getManufacturer()) + "," +
|
||||
"\"hardware\":" + JSONObject.quote(SysInfo.getHardware()) +
|
||||
"}," +
|
||||
"\"androidANR\":\""));
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "wrote metadata, size = " + String.valueOf(size));
|
||||
}
|
||||
|
||||
// We are at the start of ANR data
|
||||
}
|
||||
|
||||
// Block is a section of the larger input stream, and we want to find pattern within
|
||||
// the stream. This is straightforward if the entire pattern is within one block;
|
||||
// however, if the pattern spans across two blocks, we have to match both the start of
|
||||
// the pattern in the first block and the end of the pattern in the second block.
|
||||
// * If pattern is found in block, this method returns the index at the end of the
|
||||
// found pattern, which must always be > 0.
|
||||
// * If pattern is not found, it returns 0.
|
||||
// * If the start of the pattern matches the end of the block, it returns a number
|
||||
// < 0, which equals the negated value of how many characters in pattern are already
|
||||
// matched; when processing the next block, this number is passed in through
|
||||
// prevIndex, and the rest of the characters in pattern are matched against the
|
||||
// start of this second block. The method returns value > 0 if the rest of the
|
||||
// characters match, or 0 if they do not.
|
||||
private static int getEndPatternIndex(String block, String pattern, int prevIndex) {
|
||||
if (pattern == null || block.length() < pattern.length()) {
|
||||
// Nothing to do
|
||||
return 0;
|
||||
}
|
||||
if (prevIndex < 0) {
|
||||
// Last block ended with a partial start; now match start of block to rest of pattern
|
||||
if (block.startsWith(pattern.substring(-prevIndex, pattern.length()))) {
|
||||
// Rest of pattern matches; return index at end of pattern
|
||||
return pattern.length() + prevIndex;
|
||||
}
|
||||
// Not a match; continue with normal search
|
||||
}
|
||||
// Did not find pattern in last block; see if entire pattern is inside this block
|
||||
int index = block.indexOf(pattern);
|
||||
if (index >= 0) {
|
||||
// Found pattern; return index at end of the pattern
|
||||
return index + pattern.length();
|
||||
}
|
||||
// Block does not contain the entire pattern, but see if the end of the block
|
||||
// contains the start of pattern. To do that, we see if block ends with the
|
||||
// first n-1 characters of pattern, the first n-2 characters of pattern, etc.
|
||||
for (index = block.length() - pattern.length() + 1; index < block.length(); index++) {
|
||||
// Using index as a start, see if the rest of block contains the start of pattern
|
||||
if (block.charAt(index) == pattern.charAt(0) &&
|
||||
block.endsWith(pattern.substring(0, block.length() - index))) {
|
||||
// Found partial match; return -(number of characters matched),
|
||||
// i.e. -1 for 1 character matched, -2 for 2 characters matched, etc.
|
||||
return index - block.length();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Copy the content of reader to ping;
|
||||
// copying stops when endPattern is found in the input stream
|
||||
private static int fillPingBlock(OutputStream ping,
|
||||
Reader reader, String endPattern)
|
||||
throws IOException {
|
||||
|
||||
int total = 0;
|
||||
int endIndex = 0;
|
||||
char [] block = new char[TRACES_BLOCK_SIZE];
|
||||
for (int size = reader.read(block); size >= 0; size = reader.read(block)) {
|
||||
String stringBlock = new String(block, 0, size);
|
||||
endIndex = getEndPatternIndex(stringBlock, endPattern, endIndex);
|
||||
if (endIndex > 0) {
|
||||
// Found end pattern; clip the string
|
||||
stringBlock = stringBlock.substring(0, endIndex);
|
||||
}
|
||||
String quoted = JSONObject.quote(stringBlock);
|
||||
total += writePingPayload(ping, quoted.substring(1, quoted.length() - 1));
|
||||
if (endIndex > 0) {
|
||||
// End pattern already found; return now
|
||||
break;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static void fillLogcat(final OutputStream ping) {
|
||||
if (Versions.preJB) {
|
||||
// Logcat retrieval is not supported on pre-JB devices.
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// get the last 200 lines of logcat
|
||||
Process proc = (new ProcessBuilder())
|
||||
.command("/system/bin/logcat", "-v", "threadtime", "-t", "200", "-d", "*:D")
|
||||
.redirectErrorStream(true)
|
||||
.start();
|
||||
try {
|
||||
Reader procOut = new InputStreamReader(proc.getInputStream(), TRACES_CHARSET);
|
||||
int size = fillPingBlock(ping, procOut, null);
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "wrote logcat, size = " + String.valueOf(size));
|
||||
}
|
||||
} finally {
|
||||
proc.destroy();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// ignore because logcat is not essential
|
||||
Log.w(LOGTAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void fillPingFooter(OutputStream ping,
|
||||
boolean haveNativeStack)
|
||||
throws IOException {
|
||||
|
||||
// We are at the end of ANR data
|
||||
|
||||
int total = writePingPayload(ping, ("\"," +
|
||||
"\"androidLogcat\":\""));
|
||||
fillLogcat(ping);
|
||||
|
||||
if (haveNativeStack) {
|
||||
total += writePingPayload(ping, ("\"," +
|
||||
"\"androidNativeStack\":"));
|
||||
|
||||
String nativeStack = String.valueOf(getNativeStack());
|
||||
int size = writePingPayload(ping, nativeStack);
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "wrote native stack, size = " + String.valueOf(size));
|
||||
}
|
||||
total += size + writePingPayload(ping, "}");
|
||||
} else {
|
||||
total += writePingPayload(ping, "\"}");
|
||||
}
|
||||
|
||||
byte [] data = (
|
||||
"}").getBytes(PING_CHARSET);
|
||||
ping.write(data);
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "wrote ping footer, size = " + String.valueOf(data.length + total));
|
||||
}
|
||||
}
|
||||
|
||||
private static void processTraces(Reader traces, File pingFile) {
|
||||
|
||||
// Only get native stack if Goanna is running.
|
||||
// Also, unwinding is memory intensive, so only unwind if we have enough memory.
|
||||
final boolean haveNativeStack =
|
||||
GoannaThread.checkLaunchState(GoannaThread.LaunchState.GoannaRunning) ?
|
||||
requestNativeStack(/* unwind */ SysInfo.getMemSize() >= 640) : false;
|
||||
|
||||
try {
|
||||
OutputStream ping = new BufferedOutputStream(
|
||||
new FileOutputStream(pingFile), TRACES_BLOCK_SIZE);
|
||||
try {
|
||||
fillPingHeader(ping, pingFile.getName());
|
||||
// Traces file has the format
|
||||
// ----- pid xxx at xxx -----
|
||||
// Cmd line: org.mozilla.xxx
|
||||
// * stack trace *
|
||||
// ----- end xxx -----
|
||||
// ----- pid xxx at xxx -----
|
||||
// Cmd line: com.android.xxx
|
||||
// * stack trace *
|
||||
// ...
|
||||
// If we end the stack dump at the first end marker,
|
||||
// only Fennec stacks will be dumped
|
||||
int size = fillPingBlock(ping, traces, "\n----- end");
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "wrote traces, size = " + String.valueOf(size));
|
||||
}
|
||||
fillPingFooter(ping, haveNativeStack);
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "finished creating ping file");
|
||||
}
|
||||
return;
|
||||
} finally {
|
||||
ping.close();
|
||||
if (haveNativeStack) {
|
||||
releaseNativeStack();
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(LOGTAG, e);
|
||||
}
|
||||
// exception; delete ping file
|
||||
if (pingFile.exists()) {
|
||||
pingFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private static void processTraces(File tracesFile, File pingFile) {
|
||||
try {
|
||||
Reader traces = new InputStreamReader(
|
||||
new FileInputStream(tracesFile), TRACES_CHARSET);
|
||||
try {
|
||||
processTraces(traces, pingFile);
|
||||
} finally {
|
||||
traces.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(LOGTAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (mPendingANR) {
|
||||
// we already processed an ANR without getting unstuck; skip this one
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "skipping duplicate ANR");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ThreadUtils.getUiHandler() != null) {
|
||||
mPendingANR = true;
|
||||
// detect when the main thread gets unstuck
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// okay to reset mPendingANR on main thread
|
||||
mPendingANR = false;
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "yay we got unstuck!");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "receiving " + String.valueOf(intent));
|
||||
}
|
||||
if (!ANR_ACTION.equals(intent.getAction())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure we have a good save location first
|
||||
File pingFile = getPingFile();
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "using ping file: " + String.valueOf(pingFile));
|
||||
}
|
||||
if (pingFile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
File tracesFile = getTracesFile();
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "using traces file: " + String.valueOf(tracesFile));
|
||||
}
|
||||
if (tracesFile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We get ANR intents from all ANRs in the system, but we only want Goanna ANRs
|
||||
if (!isGoannaTraces(context.getPackageName(), tracesFile)) {
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "traces is not Goanna ANR");
|
||||
}
|
||||
return;
|
||||
}
|
||||
Log.i(LOGTAG, "processing Goanna ANR");
|
||||
processTraces(tracesFile, pingFile);
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.home.HomeConfig;
|
||||
import org.mozilla.goanna.home.HomeConfig.PanelType;
|
||||
import org.mozilla.goanna.mozglue.RobocopTarget;
|
||||
import org.mozilla.goanna.util.StringUtils;
|
||||
|
||||
public class AboutPages {
|
||||
// All of our special pages.
|
||||
public static final String ADDONS = "about:addons";
|
||||
public static final String APPS = "about:apps";
|
||||
public static final String CONFIG = "about:config";
|
||||
public static final String DOWNLOADS = "about:downloads";
|
||||
public static final String FIREFOX = "about:firefox";
|
||||
public static final String HEALTHREPORT = "about:healthreport";
|
||||
public static final String HOME = "about:home";
|
||||
public static final String PRIVATEBROWSING = "about:privatebrowsing";
|
||||
public static final String READER = "about:reader";
|
||||
public static final String UPDATER = "about:";
|
||||
|
||||
public static final String URL_FILTER = "about:%";
|
||||
|
||||
public static final String PANEL_PARAM = "panel";
|
||||
|
||||
public static final boolean isAboutPage(final String url) {
|
||||
return url != null && url.startsWith("about:");
|
||||
}
|
||||
|
||||
public static final boolean isTitlelessAboutPage(final String url) {
|
||||
return isAboutHome(url) ||
|
||||
PRIVATEBROWSING.equals(url);
|
||||
}
|
||||
|
||||
public static final boolean isAboutHome(final String url) {
|
||||
if (url == null || !url.startsWith(HOME)) {
|
||||
return false;
|
||||
}
|
||||
// We sometimes append a parameter to "about:home" to specify which page to
|
||||
// show when we open the home pager. Discard this parameter when checking
|
||||
// whether or not this URL is "about:home".
|
||||
return HOME.equals(url.split("\\?")[0]);
|
||||
}
|
||||
|
||||
public static final String getPanelIdFromAboutHomeUrl(String aboutHomeUrl) {
|
||||
return StringUtils.getQueryParameter(aboutHomeUrl, PANEL_PARAM);
|
||||
}
|
||||
|
||||
public static final boolean isAboutReader(final String url) {
|
||||
if (url == null) {
|
||||
return false;
|
||||
}
|
||||
return url.startsWith(READER);
|
||||
}
|
||||
|
||||
private static final String[] DEFAULT_ICON_PAGES = new String[] {
|
||||
ADDONS,
|
||||
CONFIG,
|
||||
DOWNLOADS,
|
||||
FIREFOX,
|
||||
HEALTHREPORT,
|
||||
UPDATER
|
||||
};
|
||||
|
||||
/**
|
||||
* Callers must not modify the returned array.
|
||||
*/
|
||||
public static String[] getDefaultIconPages() {
|
||||
return DEFAULT_ICON_PAGES;
|
||||
}
|
||||
|
||||
public static boolean isBuiltinIconPage(final String url) {
|
||||
if (url == null ||
|
||||
!url.startsWith("about:")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// about:home uses a separate search built-in icon.
|
||||
if (isAboutHome(url)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: it'd be quicker to not compare the "about:" part every time.
|
||||
for (int i = 0; i < DEFAULT_ICON_PAGES.length; ++i) {
|
||||
if (DEFAULT_ICON_PAGES[i].equals(url)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a URL that navigates to the specified built-in Home Panel.
|
||||
*
|
||||
* @param panelType to navigate to.
|
||||
* @return URL.
|
||||
* @throws IllegalArgumentException if the built-in panel type is not a built-in panel.
|
||||
*/
|
||||
@RobocopTarget
|
||||
public static String getURLForBuiltinPanelType(PanelType panelType) throws IllegalArgumentException {
|
||||
return HOME + "?panel=" + HomeConfig.getIdForBuiltinPanelType(panelType);
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.widget.GoannaPopupMenu;
|
||||
import org.mozilla.goanna.menu.GoannaMenuItem;
|
||||
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
class ActionModeCompat implements GoannaPopupMenu.OnMenuItemClickListener,
|
||||
GoannaPopupMenu.OnMenuItemLongClickListener,
|
||||
View.OnClickListener {
|
||||
private final String LOGTAG = "GoannaActionModeCompat";
|
||||
|
||||
private final Callback mCallback;
|
||||
private final ActionModeCompatView mView;
|
||||
private final Presenter mPresenter;
|
||||
|
||||
/* A set of callbacks to be called during this ActionMode's lifecycle. These will control the
|
||||
* creation, interaction with, and destruction of menuitems for the view */
|
||||
public static interface Callback {
|
||||
/* Called when action mode is first created. Implementors should use this to inflate menu resources. */
|
||||
public boolean onCreateActionMode(ActionModeCompat mode, Menu menu);
|
||||
|
||||
/* Called to refresh an action mode's action menu. Called whenever the mode is invalidated. Implementors
|
||||
* should use this to enable/disable/show/hide menu items. */
|
||||
public boolean onPrepareActionMode(ActionModeCompat mode, Menu menu);
|
||||
|
||||
/* Called to report a user click on an action button. */
|
||||
public boolean onActionItemClicked(ActionModeCompat mode, MenuItem item);
|
||||
|
||||
/* Called when an action mode is about to be exited and destroyed. */
|
||||
public void onDestroyActionMode(ActionModeCompat mode);
|
||||
}
|
||||
|
||||
/* Presenters handle the actual showing/hiding of the action mode UI in the app. Its their responsibility
|
||||
* to create an action mode, and assign it Callbacks and ActionModeCompatView's. */
|
||||
public static interface Presenter {
|
||||
/* Called when an action mode should be shown */
|
||||
public void startActionModeCompat(final Callback callback);
|
||||
|
||||
/* Called when whatever action mode is showing should be hidden */
|
||||
public void endActionModeCompat();
|
||||
}
|
||||
|
||||
public ActionModeCompat(Presenter presenter, Callback callback, ActionModeCompatView view) {
|
||||
mPresenter = presenter;
|
||||
mCallback = callback;
|
||||
|
||||
mView = view;
|
||||
mView.initForMode(this);
|
||||
}
|
||||
|
||||
public void finish() {
|
||||
// Clearing the menu will also clear the ActionItemBar
|
||||
mView.getMenu().clear();
|
||||
if (mCallback != null) {
|
||||
mCallback.onDestroyActionMode(this);
|
||||
}
|
||||
}
|
||||
|
||||
public CharSequence getTitle() {
|
||||
return mView.getTitle();
|
||||
}
|
||||
|
||||
public void setTitle(CharSequence title) {
|
||||
mView.setTitle(title);
|
||||
}
|
||||
|
||||
public void setTitle(int resId) {
|
||||
mView.setTitle(resId);
|
||||
}
|
||||
|
||||
public Menu getMenu() {
|
||||
return mView.getMenu();
|
||||
}
|
||||
|
||||
public void invalidate() {
|
||||
if (mCallback != null) {
|
||||
mCallback.onPrepareActionMode(this, mView.getMenu());
|
||||
}
|
||||
mView.invalidate();
|
||||
}
|
||||
|
||||
/* GoannaPopupMenu.OnMenuItemClickListener */
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
if (mCallback != null) {
|
||||
return mCallback.onActionItemClicked(this, item);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* GoannaPopupMenu.onMenuItemLongClickListener */
|
||||
@Override
|
||||
public boolean onMenuItemLongClick(MenuItem item) {
|
||||
showTooltip((GoannaMenuItem) item);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* View.OnClickListener*/
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
mPresenter.endActionModeCompat();
|
||||
}
|
||||
|
||||
private void showTooltip(GoannaMenuItem item) {
|
||||
// Computes the tooltip toast screen position (shown when long-tapping the menu item) with regards to the
|
||||
// menu item's position (i.e below the item and slightly to the left)
|
||||
int[] location = new int[2];
|
||||
final View view = item.getActionView();
|
||||
view.getLocationOnScreen(location);
|
||||
|
||||
int xOffset = location[0] - view.getWidth();
|
||||
int yOffset = location[1] + view.getHeight() / 2;
|
||||
|
||||
Toast toast = Toast.makeText(view.getContext(), item.getTitle(), Toast.LENGTH_SHORT);
|
||||
toast.setGravity(Gravity.TOP|Gravity.LEFT, xOffset, yOffset);
|
||||
toast.show();
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.animation.AnimationUtils;
|
||||
import org.mozilla.goanna.menu.GoannaMenu;
|
||||
import org.mozilla.goanna.widget.GoannaPopupMenu;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.ScaleAnimation;
|
||||
import android.view.animation.TranslateAnimation;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
class ActionModeCompatView extends LinearLayout implements GoannaMenu.ActionItemBarPresenter {
|
||||
private final String LOGTAG = "GoannaActionModeCompatPresenter";
|
||||
|
||||
private static final int SPEC = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
||||
|
||||
private Button mTitleView;
|
||||
private ImageButton mMenuButton;
|
||||
private ViewGroup mActionButtonBar;
|
||||
private GoannaPopupMenu mPopupMenu;
|
||||
|
||||
// Maximum number of items to show as actions
|
||||
private static final int MAX_ACTION_ITEMS = 4;
|
||||
|
||||
private int mActionButtonsWidth;
|
||||
|
||||
public ActionModeCompatView(Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public ActionModeCompatView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public ActionModeCompatView(Context context, AttributeSet attrs, int style) {
|
||||
super(context, attrs, style);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public void init(Context context) {
|
||||
LayoutInflater.from(context).inflate(R.layout.actionbar, this);
|
||||
|
||||
mTitleView = (Button) findViewById(R.id.actionmode_title);
|
||||
mMenuButton = (ImageButton) findViewById(R.id.actionbar_menu);
|
||||
mActionButtonBar = (ViewGroup) findViewById(R.id.actionbar_buttons);
|
||||
|
||||
mPopupMenu = new GoannaPopupMenu(getContext(), mMenuButton);
|
||||
((GoannaMenu) mPopupMenu.getMenu()).setActionItemBarPresenter(this);
|
||||
|
||||
mMenuButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
openMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void initForMode(final ActionModeCompat mode) {
|
||||
mTitleView.setOnClickListener(mode);
|
||||
mPopupMenu.setOnMenuItemClickListener(mode);
|
||||
mPopupMenu.setOnMenuItemLongClickListener(mode);
|
||||
}
|
||||
|
||||
public CharSequence getTitle() {
|
||||
return mTitleView.getText();
|
||||
}
|
||||
|
||||
public void setTitle(CharSequence title) {
|
||||
mTitleView.setText(title);
|
||||
}
|
||||
|
||||
public void setTitle(int resId) {
|
||||
mTitleView.setText(resId);
|
||||
}
|
||||
|
||||
public Menu getMenu() {
|
||||
return mPopupMenu.getMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidate() {
|
||||
// onFinishInflate may not have been called yet on some versions of Android
|
||||
if (mPopupMenu != null && mMenuButton != null) {
|
||||
mMenuButton.setVisibility(mPopupMenu.getMenu().hasVisibleItems() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
super.invalidate();
|
||||
}
|
||||
|
||||
/* GoannaMenu.ActionItemBarPresenter */
|
||||
@Override
|
||||
public boolean addActionItem(View actionItem) {
|
||||
final int count = mActionButtonBar.getChildCount();
|
||||
if (count >= MAX_ACTION_ITEMS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int maxWidth = mActionButtonBar.getMeasuredWidth();
|
||||
if (maxWidth == 0) {
|
||||
mActionButtonBar.measure(SPEC, SPEC);
|
||||
maxWidth = mActionButtonBar.getMeasuredWidth();
|
||||
}
|
||||
|
||||
// If the menu button is already visible, no need to account for it
|
||||
if (mMenuButton.getVisibility() == View.GONE) {
|
||||
// Since we don't know how many items will be added, we always reserve space for the overflow menu
|
||||
mMenuButton.measure(SPEC, SPEC);
|
||||
maxWidth -= mMenuButton.getMeasuredWidth();
|
||||
}
|
||||
|
||||
if (mActionButtonsWidth <= 0) {
|
||||
mActionButtonsWidth = 0;
|
||||
|
||||
// Loop over child views, measure them, and add their width to the taken width
|
||||
for (int i = 0; i < count; i++) {
|
||||
View v = mActionButtonBar.getChildAt(i);
|
||||
v.measure(SPEC, SPEC);
|
||||
mActionButtonsWidth += v.getMeasuredWidth();
|
||||
}
|
||||
}
|
||||
|
||||
actionItem.measure(SPEC, SPEC);
|
||||
int w = actionItem.getMeasuredWidth();
|
||||
if (mActionButtonsWidth + w < maxWidth) {
|
||||
// We cache the new width of our children.
|
||||
mActionButtonsWidth += w;
|
||||
mActionButtonBar.addView(actionItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* GoannaMenu.ActionItemBarPresenter */
|
||||
@Override
|
||||
public void removeActionItem(View actionItem) {
|
||||
actionItem.measure(SPEC, SPEC);
|
||||
mActionButtonsWidth -= actionItem.getMeasuredWidth();
|
||||
mActionButtonBar.removeView(actionItem);
|
||||
}
|
||||
|
||||
public void openMenu() {
|
||||
mPopupMenu.openMenu();
|
||||
}
|
||||
|
||||
public void closeMenu() {
|
||||
mPopupMenu.dismiss();
|
||||
}
|
||||
|
||||
public void animateIn() {
|
||||
long duration = AnimationUtils.getShortDuration(getContext());
|
||||
TranslateAnimation t = new TranslateAnimation(Animation.RELATIVE_TO_SELF, -0.5f, Animation.RELATIVE_TO_SELF, 0f,
|
||||
Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f);
|
||||
t.setDuration(duration);
|
||||
|
||||
ScaleAnimation s = new ScaleAnimation(1f, 1f, 0f, 1f,
|
||||
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
||||
s.setDuration((long) (duration * 1.5f));
|
||||
|
||||
mTitleView.startAnimation(t);
|
||||
mActionButtonBar.startAnimation(s);
|
||||
mMenuButton.startAnimation(s);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.util.ActivityResultHandler;
|
||||
import org.mozilla.goanna.util.ActivityResultHandlerMap;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
|
||||
public class ActivityHandlerHelper {
|
||||
private static final String LOGTAG = "GoannaActivityHandlerHelper";
|
||||
private static final ActivityResultHandlerMap mActivityResultHandlerMap = new ActivityResultHandlerMap();
|
||||
|
||||
private static int makeRequestCode(ActivityResultHandler aHandler) {
|
||||
return mActivityResultHandlerMap.put(aHandler);
|
||||
}
|
||||
|
||||
public static void startIntent(Intent intent, ActivityResultHandler activityResultHandler) {
|
||||
startIntentForActivity(GoannaAppShell.getGoannaInterface().getActivity(), intent, activityResultHandler);
|
||||
}
|
||||
|
||||
public static void startIntentForActivity(Activity activity, Intent intent, ActivityResultHandler activityResultHandler) {
|
||||
activity.startActivityForResult(intent, mActivityResultHandlerMap.put(activityResultHandler));
|
||||
}
|
||||
|
||||
|
||||
public static boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
ActivityResultHandler handler = mActivityResultHandlerMap.getAndRemove(requestCode);
|
||||
if (handler != null) {
|
||||
handler.onActivityResult(resultCode, data);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.gfx.BitmapUtils;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
|
||||
public class AlertNotification
|
||||
extends Notification
|
||||
{
|
||||
private static final String LOGTAG = "GoannaAlertNotification";
|
||||
|
||||
private final int mId;
|
||||
private final int mIcon;
|
||||
private final String mTitle;
|
||||
private final String mText;
|
||||
private final NotificationManager mNotificationManager;
|
||||
|
||||
private boolean mProgressStyle;
|
||||
private double mPrevPercent = -1;
|
||||
private String mPrevAlertText = "";
|
||||
|
||||
private static final double UPDATE_THRESHOLD = .01;
|
||||
private final Context mContext;
|
||||
|
||||
public AlertNotification(Context aContext, int aNotificationId, int aIcon,
|
||||
String aTitle, String aText, long aWhen, Uri aIconUri) {
|
||||
super(aIcon, (aText.length() > 0) ? aText : aTitle, aWhen);
|
||||
|
||||
mIcon = aIcon;
|
||||
mTitle = aTitle;
|
||||
mText = aText;
|
||||
mId = aNotificationId;
|
||||
mContext = aContext;
|
||||
|
||||
mNotificationManager = (NotificationManager) aContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
if (aIconUri == null || aIconUri.getScheme() == null)
|
||||
return;
|
||||
|
||||
// Custom view
|
||||
int layout = R.layout.notification_icon_text;
|
||||
RemoteViews view = new RemoteViews(mContext.getPackageName(), layout);
|
||||
try {
|
||||
Bitmap bm = BitmapUtils.decodeUrl(aIconUri);
|
||||
if (bm == null) {
|
||||
Log.e(LOGTAG, "failed to decode icon");
|
||||
return;
|
||||
}
|
||||
view.setImageViewBitmap(R.id.notification_image, bm);
|
||||
view.setTextViewText(R.id.notification_title, mTitle);
|
||||
if (mText.length() > 0) {
|
||||
view.setTextViewText(R.id.notification_text, mText);
|
||||
}
|
||||
contentView = view;
|
||||
} catch (Exception e) {
|
||||
Log.e(LOGTAG, "failed to create bitmap", e);
|
||||
}
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
public synchronized boolean isProgressStyle() {
|
||||
return mProgressStyle;
|
||||
}
|
||||
|
||||
public void show() {
|
||||
mNotificationManager.notify(mId, this);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
mNotificationManager.cancel(mId);
|
||||
}
|
||||
|
||||
public synchronized void updateProgress(String aAlertText, long aProgress, long aProgressMax) {
|
||||
if (!mProgressStyle) {
|
||||
// Custom view
|
||||
int layout = aAlertText.length() > 0 ? R.layout.notification_progress_text : R.layout.notification_progress;
|
||||
|
||||
RemoteViews view = new RemoteViews(mContext.getPackageName(), layout);
|
||||
view.setImageViewResource(R.id.notification_image, mIcon);
|
||||
view.setTextViewText(R.id.notification_title, mTitle);
|
||||
contentView = view;
|
||||
flags |= FLAG_ONGOING_EVENT | FLAG_ONLY_ALERT_ONCE;
|
||||
|
||||
mProgressStyle = true;
|
||||
}
|
||||
|
||||
String text;
|
||||
double percent = 0;
|
||||
if (aProgressMax > 0)
|
||||
percent = ((double)aProgress / (double)aProgressMax);
|
||||
|
||||
if (aAlertText.length() > 0)
|
||||
text = aAlertText;
|
||||
else
|
||||
text = NumberFormat.getPercentInstance().format(percent);
|
||||
|
||||
if (mPrevAlertText.equals(text) && Math.abs(mPrevPercent - percent) < UPDATE_THRESHOLD)
|
||||
return;
|
||||
|
||||
contentView.setTextViewText(R.id.notification_text, text);
|
||||
contentView.setProgressBar(R.id.notification_progressbar, (int)aProgressMax, (int)aProgress, false);
|
||||
|
||||
// Update the notification
|
||||
mNotificationManager.notify(mId, this);
|
||||
|
||||
mPrevPercent = percent;
|
||||
mPrevAlertText = text;
|
||||
}
|
||||
}
|
||||
@@ -1,391 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import org.mozilla.goanna.AppConstants.Versions;
|
||||
import org.mozilla.goanna.util.GamepadUtils;
|
||||
import org.mozilla.goanna.util.ThreadUtils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
|
||||
public class AndroidGamepadManager {
|
||||
// This is completely arbitrary.
|
||||
private static final float TRIGGER_PRESSED_THRESHOLD = 0.25f;
|
||||
private static final long POLL_TIMER_PERIOD = 1000; // milliseconds
|
||||
|
||||
private static enum Axis {
|
||||
X(MotionEvent.AXIS_X),
|
||||
Y(MotionEvent.AXIS_Y),
|
||||
Z(MotionEvent.AXIS_Z),
|
||||
RZ(MotionEvent.AXIS_RZ);
|
||||
|
||||
public final int axis;
|
||||
|
||||
private Axis(int axis) {
|
||||
this.axis = axis;
|
||||
}
|
||||
}
|
||||
|
||||
// A list of gamepad button mappings. Axes are determined at
|
||||
// runtime, as they vary by Android version.
|
||||
private static enum Trigger {
|
||||
Left(6),
|
||||
Right(7);
|
||||
|
||||
public final int button;
|
||||
|
||||
private Trigger(int button) {
|
||||
this.button = button;
|
||||
}
|
||||
}
|
||||
|
||||
private static final int FIRST_DPAD_BUTTON = 12;
|
||||
// A list of axis number, gamepad button mappings for negative, positive.
|
||||
// Button mappings are added to FIRST_DPAD_BUTTON.
|
||||
private static enum DpadAxis {
|
||||
UpDown(MotionEvent.AXIS_HAT_Y, 0, 1),
|
||||
LeftRight(MotionEvent.AXIS_HAT_X, 2, 3);
|
||||
|
||||
public final int axis;
|
||||
public final int negativeButton;
|
||||
public final int positiveButton;
|
||||
|
||||
private DpadAxis(int axis, int negativeButton, int positiveButton) {
|
||||
this.axis = axis;
|
||||
this.negativeButton = negativeButton;
|
||||
this.positiveButton = positiveButton;
|
||||
}
|
||||
}
|
||||
|
||||
private static enum Button {
|
||||
A(KeyEvent.KEYCODE_BUTTON_A),
|
||||
B(KeyEvent.KEYCODE_BUTTON_B),
|
||||
X(KeyEvent.KEYCODE_BUTTON_X),
|
||||
Y(KeyEvent.KEYCODE_BUTTON_Y),
|
||||
L1(KeyEvent.KEYCODE_BUTTON_L1),
|
||||
R1(KeyEvent.KEYCODE_BUTTON_R1),
|
||||
L2(KeyEvent.KEYCODE_BUTTON_L2),
|
||||
R2(KeyEvent.KEYCODE_BUTTON_R2),
|
||||
SELECT(KeyEvent.KEYCODE_BUTTON_SELECT),
|
||||
START(KeyEvent.KEYCODE_BUTTON_START),
|
||||
THUMBL(KeyEvent.KEYCODE_BUTTON_THUMBL),
|
||||
THUMBR(KeyEvent.KEYCODE_BUTTON_THUMBR),
|
||||
DPAD_UP(KeyEvent.KEYCODE_DPAD_UP),
|
||||
DPAD_DOWN(KeyEvent.KEYCODE_DPAD_DOWN),
|
||||
DPAD_LEFT(KeyEvent.KEYCODE_DPAD_LEFT),
|
||||
DPAD_RIGHT(KeyEvent.KEYCODE_DPAD_RIGHT);
|
||||
|
||||
public final int button;
|
||||
|
||||
private Button(int button) {
|
||||
this.button = button;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Gamepad {
|
||||
// ID from GamepadService
|
||||
public int id;
|
||||
// Retain axis state so we can determine changes.
|
||||
public float axes[];
|
||||
public boolean dpad[];
|
||||
public int triggerAxes[];
|
||||
public float triggers[];
|
||||
|
||||
public Gamepad(int serviceId, int deviceId) {
|
||||
id = serviceId;
|
||||
axes = new float[Axis.values().length];
|
||||
dpad = new boolean[4];
|
||||
triggers = new float[2];
|
||||
|
||||
InputDevice device = InputDevice.getDevice(deviceId);
|
||||
if (device != null) {
|
||||
// LTRIGGER/RTRIGGER don't seem to be exposed on older
|
||||
// versions of Android.
|
||||
if (device.getMotionRange(MotionEvent.AXIS_LTRIGGER) != null && device.getMotionRange(MotionEvent.AXIS_RTRIGGER) != null) {
|
||||
triggerAxes = new int[]{MotionEvent.AXIS_LTRIGGER,
|
||||
MotionEvent.AXIS_RTRIGGER};
|
||||
} else if (device.getMotionRange(MotionEvent.AXIS_BRAKE) != null && device.getMotionRange(MotionEvent.AXIS_GAS) != null) {
|
||||
triggerAxes = new int[]{MotionEvent.AXIS_BRAKE,
|
||||
MotionEvent.AXIS_GAS};
|
||||
} else {
|
||||
triggerAxes = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean sStarted;
|
||||
private static HashMap<Integer, Gamepad> sGamepads;
|
||||
private static HashMap<Integer, List<KeyEvent>> sPendingGamepads;
|
||||
private static InputManager.InputDeviceListener sListener;
|
||||
private static Timer sPollTimer;
|
||||
|
||||
private AndroidGamepadManager() {
|
||||
}
|
||||
|
||||
public static void startup() {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
if (!sStarted) {
|
||||
sGamepads = new HashMap<Integer, Gamepad>();
|
||||
sPendingGamepads = new HashMap<Integer, List<KeyEvent>>();
|
||||
scanForGamepads();
|
||||
addDeviceListener();
|
||||
sStarted = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void shutdown() {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
if (sStarted) {
|
||||
removeDeviceListener();
|
||||
sPendingGamepads = null;
|
||||
sGamepads = null;
|
||||
sStarted = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void gamepadAdded(int deviceId, int serviceId) {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
if (!sStarted) {
|
||||
return;
|
||||
}
|
||||
if (!sPendingGamepads.containsKey(deviceId)) {
|
||||
removeGamepad(deviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
List<KeyEvent> pending = sPendingGamepads.get(deviceId);
|
||||
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;
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createGamepadButtonEvent(gamepad.id, FIRST_DPAD_BUTTON + which, pressed, Math.abs(value)));
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean handleMotionEvent(MotionEvent ev) {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
if (!sStarted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sGamepads.containsKey(ev.getDeviceId())) {
|
||||
// Not a device we care about.
|
||||
return false;
|
||||
}
|
||||
|
||||
Gamepad gamepad = sGamepads.get(ev.getDeviceId());
|
||||
// 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.
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createGamepadAxisEvent(gamepad.id, valid, axes));
|
||||
}
|
||||
|
||||
// Map triggers to buttons.
|
||||
if (gamepad.triggerAxes != null) {
|
||||
for (Trigger trigger : Trigger.values()) {
|
||||
int i = trigger.ordinal();
|
||||
int axis = gamepad.triggerAxes[i];
|
||||
float value = deadZone(ev, axis);
|
||||
if (value != gamepad.triggers[i]) {
|
||||
gamepad.triggers[i] = value;
|
||||
boolean pressed = value > TRIGGER_PRESSED_THRESHOLD;
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createGamepadButtonEvent(gamepad.id, trigger.button, pressed, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Map d-pad to buttons.
|
||||
for (DpadAxis dpadaxis : DpadAxis.values()) {
|
||||
float value = deadZone(ev, dpadaxis.axis);
|
||||
mapDpadAxis(gamepad, value < 0.0f, value, dpadaxis.negativeButton);
|
||||
mapDpadAxis(gamepad, value > 0.0f, value, dpadaxis.positiveButton);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean handleKeyEvent(KeyEvent ev) {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
if (!sStarted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int deviceId = ev.getDeviceId();
|
||||
if (sPendingGamepads.containsKey(deviceId)) {
|
||||
// Queue up key events for pending devices.
|
||||
sPendingGamepads.get(deviceId).add(ev);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!sGamepads.containsKey(deviceId)) {
|
||||
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;
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createGamepadButtonEvent(gamepad.id, key, pressed, pressed ? 1.0f : 0.0f));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void scanForGamepads() {
|
||||
int[] deviceIds = InputDevice.getDeviceIds();
|
||||
if (deviceIds == null) {
|
||||
return;
|
||||
}
|
||||
for (int i=0; i < deviceIds.length; i++) {
|
||||
InputDevice device = InputDevice.getDevice(deviceIds[i]);
|
||||
if (device == null) {
|
||||
continue;
|
||||
}
|
||||
if ((device.getSources() & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD) {
|
||||
continue;
|
||||
}
|
||||
addGamepad(device);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addGamepad(InputDevice device) {
|
||||
//TODO: when we're using a newer SDK version, use these.
|
||||
//if (Build.VERSION.SDK_INT >= 12) {
|
||||
//int vid = device.getVendorId();
|
||||
//int pid = device.getProductId();
|
||||
//}
|
||||
sPendingGamepads.put(device.getId(), new ArrayList<KeyEvent>());
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createGamepadAddRemoveEvent(device.getId(), true));
|
||||
}
|
||||
|
||||
private static void removeGamepad(int deviceId) {
|
||||
Gamepad gamepad = sGamepads.get(deviceId);
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createGamepadAddRemoveEvent(gamepad.id, false));
|
||||
sGamepads.remove(deviceId);
|
||||
}
|
||||
|
||||
private static void addDeviceListener() {
|
||||
if (Versions.preJB) {
|
||||
// Poll known gamepads to see if they've disappeared.
|
||||
sPollTimer = new Timer();
|
||||
sPollTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (Integer deviceId : sGamepads.keySet()) {
|
||||
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.containsKey(deviceId)) {
|
||||
// Got removed before Goanna's ack reached us.
|
||||
// gamepadAdded will deal with it.
|
||||
sPendingGamepads.remove(deviceId);
|
||||
return;
|
||||
}
|
||||
if (sGamepads.containsKey(deviceId)) {
|
||||
removeGamepad(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceChanged(int deviceId) {
|
||||
}
|
||||
};
|
||||
((InputManager)GoannaAppShell.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)GoannaAppShell.getContext().getSystemService(Context.INPUT_SERVICE)).unregisterInputDeviceListener(sListener);
|
||||
sListener = null;
|
||||
}
|
||||
}
|
||||
@@ -1,463 +0,0 @@
|
||||
#filter substitution
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="@ANDROID_PACKAGE_NAME@"
|
||||
android:installLocation="auto"
|
||||
android:versionCode="@ANDROID_VERSION_CODE@"
|
||||
android:versionName="@MOZ_APP_VERSION@"
|
||||
#ifdef MOZ_ANDROID_SHARED_ID
|
||||
android:sharedUserId="@MOZ_ANDROID_SHARED_ID@"
|
||||
#endif
|
||||
>
|
||||
<uses-sdk android:minSdkVersion="@MOZ_ANDROID_MIN_SDK_VERSION@"
|
||||
#ifdef MOZ_ANDROID_MAX_SDK_VERSION
|
||||
android:maxSdkVersion="@MOZ_ANDROID_MAX_SDK_VERSION@"
|
||||
#endif
|
||||
android:targetSdkVersion="@ANDROID_TARGET_SDK@"/>
|
||||
|
||||
#include ../services/manifests/FxAccountAndroidManifest_permissions.xml.in
|
||||
#include ../services/manifests/HealthReportAndroidManifest_permissions.xml.in
|
||||
#include ../services/manifests/SyncAndroidManifest_permissions.xml.in
|
||||
|
||||
#ifdef MOZ_ANDROID_SEARCH_ACTIVITY
|
||||
#include ../search/manifests/SearchAndroidManifest_permissions.xml.in
|
||||
#endif
|
||||
|
||||
<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.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="com.android.browser.permission.READ_HISTORY_BOOKMARKS"/>
|
||||
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="@ANDROID_PACKAGE_NAME@.permissions.PASSWORD_PROVIDER"/>
|
||||
<uses-permission android:name="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/>
|
||||
<uses-permission android:name="@ANDROID_PACKAGE_NAME@.permissions.FORMHISTORY_PROVIDER"/>
|
||||
#ifdef MOZ_ANDROID_DOWNLOADS_INTEGRATION
|
||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
||||
#endif
|
||||
#ifdef MOZ_WEBSMS_BACKEND
|
||||
<!-- WebSMS -->
|
||||
<uses-permission android:name="android.permission.SEND_SMS"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_SMS"/>
|
||||
<uses-permission android:name="android.permission.READ_SMS"/>
|
||||
#endif
|
||||
|
||||
<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 NIGHTLY_BUILD
|
||||
<!-- Contacts API -->
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
|
||||
#endif
|
||||
|
||||
#ifdef MOZ_ANDROID_BEAM
|
||||
<!-- Android Beam support -->
|
||||
<uses-permission android:name="android.permission.NFC"/>
|
||||
<uses-feature android:name="android.hardware.nfc" android:required="false"/>
|
||||
#endif
|
||||
|
||||
#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" />
|
||||
|
||||
<application android:label="@string/moz_app_displayname"
|
||||
android:icon="@drawable/icon"
|
||||
android:logo="@drawable/logo"
|
||||
android:name="org.mozilla.goanna.GoannaApplication"
|
||||
android:hardwareAccelerated="true"
|
||||
# The preprocessor does not yet support arbitrary parentheses, so this cannot
|
||||
# be parenthesized thus to clarify that the logical AND operator has precedence:
|
||||
# !defined(MOZILLA_OFFICIAL) || (defined(NIGHTLY_BUILD) && defined(MOZ_DEBUG))
|
||||
#if !defined(MOZILLA_OFFICIAL) || defined(NIGHTLY_BUILD) && defined(MOZ_DEBUG)
|
||||
android:debuggable="true">
|
||||
#else
|
||||
android:debuggable="false">
|
||||
#endif
|
||||
|
||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true"/>
|
||||
|
||||
#ifdef MOZ_NATIVE_DEVICES
|
||||
<!-- This resources comes from Google Play Services. Required for casting support. -->
|
||||
<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
|
||||
#endif
|
||||
|
||||
<!-- If the windowSoftInputMode adjust* flag changes below, the
|
||||
setSoftInputMode call in BrowserSearch#onStop must also be updated. -->
|
||||
<activity android:name="org.mozilla.goanna.BrowserApp"
|
||||
android:label="@string/moz_app_displayname"
|
||||
android:taskAffinity="@ANDROID_PACKAGE_NAME@.BROWSER"
|
||||
android:alwaysRetainTaskState="true"
|
||||
android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|locale|layoutDirection"
|
||||
android:windowSoftInputMode="stateUnspecified|adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true"
|
||||
android:theme="@style/Goanna.App">
|
||||
<!-- We export this activity so that it can be launched by explicit
|
||||
intents, in particular homescreen shortcuts. See Bug 1032217.
|
||||
In future we would prefer to move all intent filters off the .App
|
||||
alias and onto BrowserApp so that we can deprecate activities
|
||||
that refer to pre-processed class names. -->
|
||||
</activity>
|
||||
|
||||
<!-- Fennec is shipped as the Android package named
|
||||
org.mozilla.{fennec,firefox,firefox_beta}. The internal Java package
|
||||
hierarchy inside the Android package has both an
|
||||
org.mozilla.{fennec,firefox,firefox_beta} subtree *and* an
|
||||
org.mozilla.goanna subtree. The non-org.mozilla.goanna is deprecated
|
||||
and we would like to get rid of it entirely. Until that happens, we
|
||||
have external consumers (such as intents and bookmarks) of
|
||||
non-org.mozilla.goanna Activity classes, so we define activity aliases
|
||||
for backwards compatibility. -->
|
||||
<activity-alias android:name=".App"
|
||||
android:label="@MOZ_APP_DISPLAYNAME@"
|
||||
android:targetActivity="org.mozilla.goanna.BrowserApp">
|
||||
<!-- android:priority ranges between -1000 and 1000. We never want
|
||||
another activity to usurp the MAIN action, so we ratchet our
|
||||
priority up. -->
|
||||
<intent-filter android:priority="999">
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="com.sec.minimode.icon.portrait.normal"
|
||||
android:resource="@drawable/icon"/>
|
||||
|
||||
<meta-data android:name="com.sec.minimode.icon.landscape.normal"
|
||||
android:resource="@drawable/icon" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="org.mozilla.goanna.ACTION_ALERT_CALLBACK" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="org.mozilla.goanna.GUEST_SESSION_INPROGRESS" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Notification API V2 -->
|
||||
<intent-filter>
|
||||
<action android:name="@ANDROID_PACKAGE_NAME@.helperBroadcastAction" />
|
||||
<data android:scheme="moz-notification" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="org.mozilla.goanna.UPDATE"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Default browser intents -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:scheme="about" />
|
||||
<data android:scheme="javascript" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:mimeType="text/html"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
<data android:mimeType="application/xhtml+xml"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.WEB_SEARCH" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- For XPI installs from websites and the download manager. -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:mimeType="application/x-xpinstall" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- For XPI installs from file: URLs. -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:host="" />
|
||||
<data android:scheme="file" />
|
||||
<data android:pathPattern=".*\\.xpi" />
|
||||
</intent-filter>
|
||||
|
||||
#ifdef MOZ_ANDROID_BEAM
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
#endif
|
||||
|
||||
<meta-data android:name="android.app.searchable"
|
||||
android:resource="@xml/searchable" />
|
||||
|
||||
<!-- For debugging -->
|
||||
<intent-filter>
|
||||
<action android:name="org.mozilla.goanna.DEBUG" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity android:name="org.mozilla.goanna.StartPane"
|
||||
android:theme="@style/GoannaStartPane"
|
||||
android:excludeFromRecents="true"/>
|
||||
|
||||
<activity android:name="org.mozilla.goanna.webapp.Dispatcher"
|
||||
android:noHistory="true" >
|
||||
<intent-filter>
|
||||
<!-- catch links from synthetic apks -->
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/webapp" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver android:name="org.mozilla.goanna.webapp.UninstallListener" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PACKAGE_REMOVED" />
|
||||
<data android:scheme="package" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name="org.mozilla.goanna.webapp.TaskKiller">
|
||||
<intent-filter>
|
||||
<action android:name="org.mozilla.webapp.TASK_REMOVED" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- Activity used for launching non-privileged WebApps via a URL -->
|
||||
<activity android:name="org.mozilla.goanna.Webapp"
|
||||
android:label="@string/webapp_generic_name"
|
||||
android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize"
|
||||
android:windowSoftInputMode="stateUnspecified|adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity="org.mozilla.goanna.WEBAPP"
|
||||
android:process=":@ANDROID_PACKAGE_NAME@.Webapp"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@style/Goanna.App">
|
||||
<!-- We export this activity so that it can be launched by explicit
|
||||
intents, in particular old-style WebApp launching homescreen
|
||||
shortcuts. Such shortcuts were made before the new "synthetic
|
||||
APK" WebApps were deployed. See Bug 1032217. -->
|
||||
</activity>
|
||||
|
||||
<!-- Alias Webapp so we can launch it from the package namespace. Prefer
|
||||
to launch with the fully qualified name "org.mozilla.goanna.Webapp". -->
|
||||
<activity-alias android:name=".Webapp"
|
||||
android:label="@string/webapp_generic_name"
|
||||
android:targetActivity="org.mozilla.goanna.Webapp">
|
||||
<intent-filter>
|
||||
<action android:name="org.mozilla.goanna.WEBAPP" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="org.mozilla.goanna.ACTION_ALERT_CALLBACK" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<!-- Declare a predefined number of Webapp<num> activities. These are
|
||||
used so that each web app can launch in its own process. Keep
|
||||
this number in sync with the total number of web apps handled in
|
||||
WebappAllocator. -->
|
||||
|
||||
#define FRAGMENT WebappManifestFragment.xml.frag.in
|
||||
#include WebappFragmentRepeater.inc
|
||||
|
||||
<!-- Masquerade as the Resolver so that we can be opened from the Marketplace. -->
|
||||
<activity-alias
|
||||
android:name="com.android.internal.app.ResolverActivity"
|
||||
android:targetActivity="org.mozilla.goanna.BrowserApp"
|
||||
android:exported="true" />
|
||||
|
||||
<receiver android:name="org.mozilla.goanna.GoannaUpdateReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="@ANDROID_PACKAGE_NAME@.CHECK_UPDATE_RESULT" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name="org.mozilla.goanna.GoannaMessageReceiver"
|
||||
android:permission="@ANDROID_PACKAGE_NAME@.permissions.PASSWORD_PROVIDER">
|
||||
<intent-filter>
|
||||
<action android:name="org.mozilla.goanna.INIT_PW"></action>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- Catch install referrer so we can do post-install work. -->
|
||||
<receiver android:name="org.mozilla.goanna.distribution.ReferrerReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="com.android.vending.INSTALL_REFERRER" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity android:name="org.mozilla.goanna.Restarter"
|
||||
android:process="@ANDROID_PACKAGE_NAME@Restarter"
|
||||
android:noHistory="true"
|
||||
android:theme="@style/Goanna">
|
||||
<intent-filter>
|
||||
<action android:name="org.mozilla.goanna.restart"/>
|
||||
<action android:name="org.mozilla.goanna.restart_update"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
#include ../services/manifests/FxAccountAndroidManifest_activities.xml.in
|
||||
#include ../services/manifests/HealthReportAndroidManifest_activities.xml.in
|
||||
#include ../services/manifests/SyncAndroidManifest_activities.xml.in
|
||||
#ifdef MOZ_ANDROID_SEARCH_ACTIVITY
|
||||
#include ../search/manifests/SearchAndroidManifest_activities.xml.in
|
||||
#endif
|
||||
|
||||
<activity android:name="org.mozilla.goanna.preferences.GoannaPreferences"
|
||||
android:theme="@style/Goanna.Preferences"
|
||||
android:configChanges="orientation|screenSize|locale|layoutDirection"
|
||||
android:excludeFromRecents="true"/>
|
||||
|
||||
<provider android:name="org.mozilla.goanna.db.BrowserProvider"
|
||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.browser"
|
||||
android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER">
|
||||
|
||||
<path-permission android:pathPrefix="/search_suggest_query"
|
||||
android:readPermission="android.permission.GLOBAL_SEARCH" />
|
||||
|
||||
</provider>
|
||||
|
||||
#ifdef MOZ_ANDROID_SHARE_OVERLAY
|
||||
<!-- Share overlay activity
|
||||
|
||||
Setting launchMode="singleTop" ensures onNewIntent is called when the Activity is
|
||||
reused. Ideally we create a new instance but Android L breaks this (bug 1137928). -->
|
||||
<activity android:name="org.mozilla.goanna.overlays.ui.ShareDialog"
|
||||
android:label="@string/overlay_share_label"
|
||||
android:theme="@style/ShareOverlayActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|mcc|mnc|locale|layoutDirection"
|
||||
android:launchMode="singleTop"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<!-- Service to handle requests from overlays. -->
|
||||
<service android:name="org.mozilla.goanna.overlays.service.OverlayActionService" />
|
||||
#endif
|
||||
<!--
|
||||
Ensure that passwords provider runs in its own process. (Bug 718760.)
|
||||
Process name is per-application to avoid loading CPs from multiple
|
||||
Fennec versions into the same process. (Bug 749727.)
|
||||
Process name is a mangled version to avoid a Talos bug. (Bug 750548.)
|
||||
-->
|
||||
<provider android:name="org.mozilla.goanna.db.PasswordsProvider"
|
||||
android:label="@string/sync_configure_engines_title_passwords"
|
||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.passwords"
|
||||
android:permission="@ANDROID_PACKAGE_NAME@.permissions.PASSWORD_PROVIDER"
|
||||
android:process="@MANGLED_ANDROID_PACKAGE_NAME@.PasswordsProvider"/>
|
||||
|
||||
<provider android:name="org.mozilla.goanna.db.FormHistoryProvider"
|
||||
android:label="@string/sync_configure_engines_title_history"
|
||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.formhistory"
|
||||
android:permission="@ANDROID_PACKAGE_NAME@.permissions.FORMHISTORY_PROVIDER"
|
||||
android:protectionLevel="signature"/>
|
||||
|
||||
<provider android:name="org.mozilla.goanna.GoannaProfilesProvider"
|
||||
android:authorities="@ANDROID_PACKAGE_NAME@.profiles"/>
|
||||
|
||||
<provider android:name="org.mozilla.goanna.db.TabsProvider"
|
||||
android:label="@string/sync_configure_engines_title_tabs"
|
||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.tabs"
|
||||
android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/>
|
||||
|
||||
<provider android:name="org.mozilla.goanna.db.HomeProvider"
|
||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.home"
|
||||
android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/>
|
||||
|
||||
<provider android:name="org.mozilla.goanna.db.ReadingListProvider"
|
||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.readinglist"
|
||||
android:exported="false"
|
||||
android:label="@string/reading_list_title"
|
||||
android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/>
|
||||
|
||||
<provider android:name="org.mozilla.goanna.db.SearchHistoryProvider"
|
||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.searchhistory"
|
||||
android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/>
|
||||
|
||||
<service
|
||||
android:exported="false"
|
||||
android:name="org.mozilla.goanna.updater.UpdateService"
|
||||
android:process="@MANGLED_ANDROID_PACKAGE_NAME@.UpdateService">
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:exported="false"
|
||||
android:name="org.mozilla.goanna.NotificationService">
|
||||
</service>
|
||||
|
||||
|
||||
#include ../services/manifests/FxAccountAndroidManifest_services.xml.in
|
||||
#include ../services/manifests/HealthReportAndroidManifest_services.xml.in
|
||||
#include ../services/manifests/SyncAndroidManifest_services.xml.in
|
||||
#ifdef MOZ_ANDROID_SEARCH_ACTIVITY
|
||||
#include ../search/manifests/SearchAndroidManifest_services.xml.in
|
||||
#endif
|
||||
#ifdef MOZ_ANDROID_MLS_STUMBLER
|
||||
#include ../stumbler/manifests/StumblerManifest_services.xml.in
|
||||
#endif
|
||||
|
||||
</application>
|
||||
|
||||
<permission android:name="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"
|
||||
android:protectionLevel="signature"/>
|
||||
|
||||
<permission android:name="@ANDROID_PACKAGE_NAME@.permissions.PASSWORD_PROVIDER"
|
||||
android:protectionLevel="signature"/>
|
||||
|
||||
<permission android:name="@ANDROID_PACKAGE_NAME@.permissions.FORMHISTORY_PROVIDER"
|
||||
android:protectionLevel="signature"/>
|
||||
|
||||
</manifest>
|
||||
@@ -1,296 +0,0 @@
|
||||
//#filter substitution
|
||||
/* -*- 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.goanna;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
/**
|
||||
* A collection of constants that pertain to the build and runtime state of the
|
||||
* application. Typically these are sourced from build-time definitions (see
|
||||
* Makefile.in). This is a Java-side substitute for nsIXULAppInfo, amongst
|
||||
* other things.
|
||||
*
|
||||
* See also SysInfo.java, which includes some of the values available from
|
||||
* nsSystemInfo inside Goanna.
|
||||
*/
|
||||
// Normally, we'd annotate with @RobocopTarget. Since AppConstants is compiled
|
||||
// before RobocopTarget, we instead add o.m.g.AppConstants directly to the
|
||||
// Proguard configuration.
|
||||
public class AppConstants {
|
||||
public static final String ANDROID_PACKAGE_NAME = "@ANDROID_PACKAGE_NAME@";
|
||||
public static final String MANGLED_ANDROID_PACKAGE_NAME = "@MANGLED_ANDROID_PACKAGE_NAME@";
|
||||
|
||||
public static final String MOZ_ANDROID_SHARED_ACCOUNT_TYPE = "@MOZ_ANDROID_SHARED_ACCOUNT_TYPE@";
|
||||
public static final String MOZ_ANDROID_SHARED_FXACCOUNT_TYPE = "@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@";
|
||||
|
||||
/**
|
||||
* Encapsulates access to compile-time version definitions, allowing
|
||||
* for dead code removal for particular APKs.
|
||||
*/
|
||||
public static final class Versions {
|
||||
public static final int MIN_SDK_VERSION = @MOZ_ANDROID_MIN_SDK_VERSION@;
|
||||
public static final int MAX_SDK_VERSION =
|
||||
//#ifdef MOZ_ANDROID_MAX_SDK_VERSION
|
||||
@MOZ_ANDROID_MAX_SDK_VERSION@;
|
||||
//#else
|
||||
999;
|
||||
//#endif
|
||||
|
||||
/*
|
||||
* The SDK_INT >= N check can only pass if our MAX_SDK_VERSION is
|
||||
* _greater than or equal_ to that number, because otherwise we
|
||||
* won't be installed on the device.
|
||||
*
|
||||
* If MIN_SDK_VERSION is greater than or equal to the number, there
|
||||
* is no need to do the runtime check.
|
||||
*/
|
||||
public static final boolean feature10Plus = MIN_SDK_VERSION >= 10 || (MAX_SDK_VERSION >= 10 && Build.VERSION.SDK_INT >= 10);
|
||||
public static final boolean feature11Plus = MIN_SDK_VERSION >= 11 || (MAX_SDK_VERSION >= 11 && Build.VERSION.SDK_INT >= 11);
|
||||
public static final boolean feature12Plus = MIN_SDK_VERSION >= 12 || (MAX_SDK_VERSION >= 12 && Build.VERSION.SDK_INT >= 12);
|
||||
public static final boolean feature14Plus = MIN_SDK_VERSION >= 14 || (MAX_SDK_VERSION >= 14 && Build.VERSION.SDK_INT >= 14);
|
||||
public static final boolean feature15Plus = MIN_SDK_VERSION >= 15 || (MAX_SDK_VERSION >= 15 && Build.VERSION.SDK_INT >= 15);
|
||||
public static final boolean feature16Plus = MIN_SDK_VERSION >= 16 || (MAX_SDK_VERSION >= 16 && Build.VERSION.SDK_INT >= 16);
|
||||
public static final boolean feature17Plus = MIN_SDK_VERSION >= 17 || (MAX_SDK_VERSION >= 17 && Build.VERSION.SDK_INT >= 17);
|
||||
public static final boolean feature19Plus = MIN_SDK_VERSION >= 19 || (MAX_SDK_VERSION >= 19 && Build.VERSION.SDK_INT >= 19);
|
||||
public static final boolean feature21Plus = MIN_SDK_VERSION >= 21 || (MAX_SDK_VERSION >= 21 && Build.VERSION.SDK_INT >= 21);
|
||||
|
||||
/*
|
||||
* If our MIN_SDK_VERSION is 14 or higher, we must be an ICS device.
|
||||
* If our MAX_SDK_VERSION is lower than ICS, we must not be an ICS device.
|
||||
* Otherwise, we need a range check.
|
||||
*/
|
||||
public static final boolean preLollipop = MAX_SDK_VERSION < 21 || (MIN_SDK_VERSION < 21 && Build.VERSION.SDK_INT < 21);
|
||||
public static final boolean preJBMR2 = MAX_SDK_VERSION < 18 || (MIN_SDK_VERSION < 18 && Build.VERSION.SDK_INT < 18);
|
||||
public static final boolean preJB = MAX_SDK_VERSION < 16 || (MIN_SDK_VERSION < 16 && Build.VERSION.SDK_INT < 16);
|
||||
public static final boolean preICS = MAX_SDK_VERSION < 14 || (MIN_SDK_VERSION < 14 && Build.VERSION.SDK_INT < 14);
|
||||
public static final boolean preHCMR2 = MAX_SDK_VERSION < 13 || (MIN_SDK_VERSION < 13 && Build.VERSION.SDK_INT < 13);
|
||||
public static final boolean preHCMR1 = MAX_SDK_VERSION < 12 || (MIN_SDK_VERSION < 12 && Build.VERSION.SDK_INT < 12);
|
||||
public static final boolean preHC = MAX_SDK_VERSION < 11 || (MIN_SDK_VERSION < 11 && Build.VERSION.SDK_INT < 11);
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the Java class that launches the browser.
|
||||
*/
|
||||
public static final String BROWSER_INTENT_CLASS_NAME = "org.mozilla.goanna.BrowserApp";
|
||||
public static final String SEARCH_INTENT_CLASS_NAME = "org.mozilla.search.SearchActivity";
|
||||
|
||||
public static final String GRE_MILESTONE = "@GRE_MILESTONE@";
|
||||
|
||||
public static final String MOZ_APP_ABI = "@MOZ_APP_ABI@";
|
||||
public static final String MOZ_APP_BASENAME = "@MOZ_APP_BASENAME@";
|
||||
|
||||
// For the benefit of future archaeologists: APP_BUILDID and
|
||||
// MOZ_APP_BUILDID are *exactly* the same.
|
||||
// GRE_BUILDID is exactly the same unless you're running on XULRunner,
|
||||
// which is never the case on Android.
|
||||
public static final String MOZ_APP_BUILDID = "@MOZ_APP_BUILDID@";
|
||||
public static final String MOZ_APP_ID = "@MOZ_APP_ID@";
|
||||
public static final String MOZ_APP_NAME = "@MOZ_APP_NAME@";
|
||||
public static final String MOZ_APP_VENDOR = "@MOZ_APP_VENDOR@";
|
||||
public static final String MOZ_APP_VERSION = "@MOZ_APP_VERSION@";
|
||||
public static final String MOZ_APP_DISPLAYNAME = "@MOZ_APP_DISPLAYNAME@";
|
||||
|
||||
// MOZILLA_VERSION is already quoted when it gets substituted in. If we
|
||||
// add additional quotes we end up with ""x.y"", which is a syntax error.
|
||||
public static final String MOZILLA_VERSION = @MOZILLA_VERSION@;
|
||||
|
||||
public static final String MOZ_MOZILLA_API_KEY = "@MOZ_MOZILLA_API_KEY@";
|
||||
public static final boolean MOZ_STUMBLER_BUILD_TIME_ENABLED =
|
||||
//#ifdef MOZ_ANDROID_MLS_STUMBLER
|
||||
true;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
public static final String MOZ_CHILD_PROCESS_NAME = "@MOZ_CHILD_PROCESS_NAME@";
|
||||
public static final String MOZ_UPDATE_CHANNEL = "@MOZ_UPDATE_CHANNEL@";
|
||||
public static final String OMNIJAR_NAME = "@OMNIJAR_NAME@";
|
||||
public static final String OS_TARGET = "@OS_TARGET@";
|
||||
public static final String TARGET_XPCOM_ABI = @TARGET_XPCOM_ABI@;
|
||||
|
||||
public static final String USER_AGENT_BOT_LIKE = "Redirector/" + AppConstants.MOZ_APP_VERSION +
|
||||
" (Android; rv:" + AppConstants.MOZ_APP_VERSION + ")";
|
||||
|
||||
public static final String USER_AGENT_FENNEC_MOBILE = "Mozilla/5.0 (Android; Mobile; rv:" +
|
||||
AppConstants.MOZ_APP_VERSION + ") Goanna/" +
|
||||
AppConstants.MOZ_APP_VERSION + " Firefox/" +
|
||||
AppConstants.MOZ_APP_VERSION;
|
||||
|
||||
public static final String USER_AGENT_FENNEC_TABLET = "Mozilla/5.0 (Android; Tablet; rv:" +
|
||||
AppConstants.MOZ_APP_VERSION + ") Goanna/" +
|
||||
AppConstants.MOZ_APP_VERSION + " Firefox/" +
|
||||
AppConstants.MOZ_APP_VERSION;
|
||||
|
||||
public static final int MOZ_MIN_CPU_VERSION = @MOZ_MIN_CPU_VERSION@;
|
||||
|
||||
public static final boolean MOZ_ANDROID_ANR_REPORTER =
|
||||
//#ifdef MOZ_ANDROID_ANR_REPORTER
|
||||
true;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
public static final String MOZ_PKG_SPECIAL =
|
||||
//#ifdef MOZ_PKG_SPECIAL
|
||||
"@MOZ_PKG_SPECIAL@";
|
||||
//#else
|
||||
null;
|
||||
//#endif
|
||||
|
||||
/**
|
||||
* Whether this APK was built with constrained resources --
|
||||
* no xhdpi+ images, for example.
|
||||
*/
|
||||
public static final boolean MOZ_ANDROID_RESOURCE_CONSTRAINED =
|
||||
//#ifdef MOZ_ANDROID_RESOURCE_CONSTRAINED
|
||||
true;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
public static final boolean MOZ_SERVICES_HEALTHREPORT =
|
||||
//#ifdef MOZ_SERVICES_HEALTHREPORT
|
||||
true;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
public static final boolean MOZ_ANDROID_READING_LIST_SERVICE =
|
||||
//#ifdef MOZ_ANDROID_READING_LIST_SERVICE
|
||||
true;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
public static final boolean MOZ_TELEMETRY_ON_BY_DEFAULT =
|
||||
//#ifdef MOZ_TELEMETRY_ON_BY_DEFAULT
|
||||
true;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
public static final boolean MOZ_ANDROID_TAB_QUEUE =
|
||||
//#ifdef MOZ_ANDROID_TAB_QUEUE
|
||||
true;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
public static final String TELEMETRY_PREF_NAME =
|
||||
"toolkit.telemetry.enabled";
|
||||
|
||||
public static final boolean MOZ_TELEMETRY_REPORTING =
|
||||
//#ifdef MOZ_TELEMETRY_REPORTING
|
||||
true;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
public static final boolean MOZ_CRASHREPORTER =
|
||||
false;
|
||||
|
||||
public static final boolean MOZ_DATA_REPORTING =
|
||||
//#ifdef MOZ_DATA_REPORTING
|
||||
true;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
public static final boolean MOZ_LOCALE_SWITCHER =
|
||||
//#ifdef MOZ_LOCALE_SWITCHER
|
||||
true;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
public static final boolean MOZ_UPDATER =
|
||||
//#ifdef MOZ_UPDATER
|
||||
true;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
public static final boolean MOZ_WEBSMS_BACKEND =
|
||||
//#ifdef MOZ_WEBSMS_BACKEND
|
||||
true;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
// Android Beam is only supported on API14+, so we don't even bother building
|
||||
// it if this APK doesn't include API14 support.
|
||||
public static final boolean MOZ_ANDROID_BEAM =
|
||||
//#ifdef MOZ_ANDROID_BEAM
|
||||
Versions.feature14Plus;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
public static final boolean MOZ_ANDROID_APZ =
|
||||
//#ifdef MOZ_ANDROID_APZ
|
||||
true;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
// See this wiki page for more details about channel specific build defines:
|
||||
// https://wiki.mozilla.org/Platform/Channel-specific_build_defines
|
||||
public static final boolean RELEASE_BUILD =
|
||||
//#ifdef RELEASE_BUILD
|
||||
true;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
public static final boolean NIGHTLY_BUILD =
|
||||
//#ifdef NIGHTLY_BUILD
|
||||
true;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
public static final boolean DEBUG_BUILD =
|
||||
//#ifdef MOZ_DEBUG
|
||||
true;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
public static final boolean MOZ_MEDIA_PLAYER =
|
||||
//#ifdef MOZ_NATIVE_DEVICES
|
||||
true;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
// Official corresponds, roughly, to whether this build is performed on
|
||||
// Mozilla's continuous integration infrastructure. You should disable
|
||||
// developer-only functionality when this flag is set.
|
||||
public static final boolean MOZILLA_OFFICIAL =
|
||||
//#ifdef MOZILLA_OFFICIAL
|
||||
true;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
public static final boolean ANDROID_DOWNLOADS_INTEGRATION =
|
||||
//#ifdef MOZ_ANDROID_DOWNLOADS_INTEGRATION
|
||||
AppConstants.Versions.feature12Plus;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
public static final boolean MOZ_LINKER_EXTRACT =
|
||||
//#ifdef MOZ_LINKER_EXTRACT
|
||||
true;
|
||||
//#else
|
||||
false;
|
||||
//#endif
|
||||
|
||||
public static final boolean MOZ_DRAGGABLE_URLBAR = false;
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Client for posting notifications in the application.
|
||||
*/
|
||||
public class AppNotificationClient extends NotificationClient {
|
||||
private final Context mContext;
|
||||
|
||||
public AppNotificationClient(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void bind() {
|
||||
super.bind();
|
||||
connectHandler(new NotificationHandler(mContext));
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package org.mozilla.goanna;
|
||||
|
||||
/**
|
||||
* Static helper class to provide debug assertions for Java.
|
||||
*
|
||||
* Used in preference to JSR 41 assertions due to their difficulty of use on Android and their
|
||||
* poor behaviour w.r.t bytecode bloat (and to a lesser extent, runtime performance when disabled)
|
||||
*
|
||||
* Calls to methods in this class will be stripped by Proguard for release builds, so may be used
|
||||
* arbitrarily liberally at zero cost.
|
||||
* Under no circumstances should the argument expressions to methods in this class have side effects
|
||||
* relevant to the correctness of execution of the program. Such side effects shall not be checked
|
||||
* for when stripping assertions.
|
||||
*/
|
||||
public class Assert {
|
||||
// Static helper class.
|
||||
private Assert() {}
|
||||
|
||||
/**
|
||||
* Verify that two objects are equal according to their equals method.
|
||||
*/
|
||||
public static void equal(Object a, Object b) {
|
||||
equal(a, b, "Assertion failure: !" + a + ".equals(" + b + ')');
|
||||
}
|
||||
public static void equal(Object a, Object b, String message) {
|
||||
isTrue(a.equals(b), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that an arbitrary boolean expression is true.
|
||||
*/
|
||||
public static void isTrue(boolean a) {
|
||||
isTrue(a, null);
|
||||
}
|
||||
public static void isTrue(boolean a, String message) {
|
||||
if (!a) {
|
||||
throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that an arbitrary boolean expression is false.
|
||||
*/
|
||||
public static void isFalse(boolean a) {
|
||||
isTrue(a, null);
|
||||
}
|
||||
public static void isFalse(boolean a, String message) {
|
||||
if (a) {
|
||||
throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that a given object is null.
|
||||
*/
|
||||
public static void isNull(Object o) {
|
||||
isNull(o, "Assertion failure: " + o + " must be null!");
|
||||
}
|
||||
public static void isNull(Object o, String message) {
|
||||
isTrue(o == null, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that a given object is non-null.
|
||||
*/
|
||||
public static void isNotNull(Object o) {
|
||||
isNotNull(o, "Assertion failure: " + o + " cannot be null!");
|
||||
}
|
||||
public static void isNotNull(Object o, String message) {
|
||||
isTrue(o != null, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail. Should be used whenever an impossible state is encountered (such as the default branch
|
||||
* of a switch over all possible values of an enum: such an assertion may save future developers
|
||||
* time when they try to add new states)
|
||||
*/
|
||||
public static void fail() {
|
||||
isTrue(false);
|
||||
}
|
||||
public static void fail(String message) {
|
||||
isTrue(false, message);
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.AppConstants.Versions;
|
||||
import org.mozilla.goanna.prompts.PromptService;
|
||||
import org.mozilla.goanna.util.ActivityUtils;
|
||||
import org.mozilla.goanna.util.HardwareUtils;
|
||||
import org.mozilla.goanna.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.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.AbsoluteLayout;
|
||||
|
||||
public class BaseGoannaInterface implements GoannaAppShell.GoannaInterface {
|
||||
// Bug 908744: Implement GoannaEventListener
|
||||
// Bug 908752: Implement SensorEventListener
|
||||
// Bug 908755: Implement LocationListener
|
||||
// Bug 908756: Implement Tabs.OnTabsChangedListener
|
||||
// Bug 908760: Implement GoannaEventResponder
|
||||
|
||||
private final Context mContext;
|
||||
private GoannaProfile mProfile;
|
||||
|
||||
public BaseGoannaInterface(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GoannaProfile getProfile() {
|
||||
// Fall back to default profile if we didn't load a specific one
|
||||
if (mProfile == null) {
|
||||
mProfile = GoannaProfile.get(mContext);
|
||||
}
|
||||
return mProfile;
|
||||
}
|
||||
|
||||
// Bug 908770: Implement this
|
||||
@Override
|
||||
public PromptService getPromptService() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return (Activity)mContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDefaultUAString() {
|
||||
return HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET :
|
||||
AppConstants.USER_AGENT_FENNEC_MOBILE;
|
||||
}
|
||||
|
||||
// Bug 908772: Implement this
|
||||
@Override
|
||||
public LocationListener getLocationListener() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Bug 908773: Implement this
|
||||
@Override
|
||||
public SensorEventListener getSensorEventListener() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Bug 908775: Implement this
|
||||
@Override
|
||||
public void doRestart() {}
|
||||
|
||||
@Override
|
||||
public void setFullScreen(final boolean fullscreen) {
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ActivityUtils.setFullScreen(getActivity(), fullscreen);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Bug 908779: Implement this
|
||||
@Override
|
||||
public void addPluginView(final View view, final RectF rect, final boolean isFullScreen) {}
|
||||
|
||||
// Bug 908781: Implement this
|
||||
@Override
|
||||
public void removePluginView(final View view, final boolean isFullScreen) {}
|
||||
|
||||
// Bug 908783: Implement this
|
||||
@Override
|
||||
public void enableCameraView() {}
|
||||
|
||||
// Bug 908785: Implement this
|
||||
@Override
|
||||
public void disableCameraView() {}
|
||||
|
||||
// Bug 908786: Implement this
|
||||
@Override
|
||||
public void addAppStateListener(GoannaAppShell.AppStateListener listener) {}
|
||||
|
||||
// Bug 908787: Implement this
|
||||
@Override
|
||||
public void removeAppStateListener(GoannaAppShell.AppStateListener listener) {}
|
||||
|
||||
// Bug 908788: Implement this
|
||||
@Override
|
||||
public View getCameraView() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Bug 908789: Implement this
|
||||
@Override
|
||||
public void notifyWakeLockChanged(String topic, String state) {}
|
||||
|
||||
// Bug 908790: Implement this
|
||||
@Override
|
||||
public FormAssistPopup getFormAssistPopup() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areTabsShown() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bug 908791: Implement this
|
||||
@Override
|
||||
public AbsoluteLayout getPluginContainer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyCheckUpdateResult(String result) {
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("Update:CheckResult", result));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasTabsSideBar() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bug 908792: Implement this
|
||||
@Override
|
||||
public void invalidateOptionsMenu() {}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,440 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.goanna.util.GoannaJarReader;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* This class manages persistence, application, and otherwise handling of
|
||||
* user-specified locales.
|
||||
*
|
||||
* Of note:
|
||||
*
|
||||
* * It's a singleton, because its scope extends to that of the application,
|
||||
* and definitionally all changes to the locale of the app must go through
|
||||
* this.
|
||||
* * It's lazy.
|
||||
* * It has ties into the Goanna event system, because it has to tell Goanna when
|
||||
* to switch locale.
|
||||
* * It relies on using the SharedPreferences file owned by the browser (in
|
||||
* Fennec's case, "GoannaApp") for performance.
|
||||
*/
|
||||
public class BrowserLocaleManager implements LocaleManager {
|
||||
private static final String LOG_TAG = "GoannaLocales";
|
||||
|
||||
private static final String EVENT_LOCALE_CHANGED = "Locale:Changed";
|
||||
private static final String PREF_LOCALE = "locale";
|
||||
|
||||
private static final String FALLBACK_LOCALE_TAG = "en-US";
|
||||
|
||||
// These are volatile because we don't impose restrictions
|
||||
// over which thread calls our methods.
|
||||
private volatile Locale currentLocale;
|
||||
private volatile Locale systemLocale = Locale.getDefault();
|
||||
|
||||
private final AtomicBoolean inited = new AtomicBoolean(false);
|
||||
private boolean systemLocaleDidChange;
|
||||
private BroadcastReceiver receiver;
|
||||
|
||||
private static final AtomicReference<LocaleManager> instance = new AtomicReference<LocaleManager>();
|
||||
|
||||
public static LocaleManager getInstance() {
|
||||
LocaleManager localeManager = instance.get();
|
||||
if (localeManager != null) {
|
||||
return localeManager;
|
||||
}
|
||||
|
||||
localeManager = new BrowserLocaleManager();
|
||||
if (instance.compareAndSet(null, localeManager)) {
|
||||
return localeManager;
|
||||
} else {
|
||||
return instance.get();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return AppConstants.MOZ_LOCALE_SWITCHER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that you call this early in your application startup,
|
||||
* and with a context that's sufficiently long-lived (typically
|
||||
* the application context).
|
||||
*
|
||||
* Calling multiple times is harmless.
|
||||
*/
|
||||
@Override
|
||||
public void initialize(final Context context) {
|
||||
if (!inited.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
final Locale current = systemLocale;
|
||||
|
||||
// We don't trust Locale.getDefault() here, because we make a
|
||||
// habit of mutating it! Use the one Android supplies, because
|
||||
// that gets regularly reset.
|
||||
// The default value of systemLocale is fine, because we haven't
|
||||
// yet swizzled Locale during static initialization.
|
||||
systemLocale = context.getResources().getConfiguration().locale;
|
||||
systemLocaleDidChange = true;
|
||||
|
||||
Log.d(LOG_TAG, "System locale changed from " + current + " to " + systemLocale);
|
||||
}
|
||||
};
|
||||
context.registerReceiver(receiver, new IntentFilter(Intent.ACTION_LOCALE_CHANGED));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean systemLocaleDidChange() {
|
||||
return systemLocaleDidChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every time the system gives us a new configuration, it
|
||||
* carries the external locale. Fix it.
|
||||
*/
|
||||
@Override
|
||||
public void correctLocale(Context context, Resources res, Configuration config) {
|
||||
final Locale current = getCurrentLocale(context);
|
||||
if (current == null) {
|
||||
Log.d(LOG_TAG, "No selected locale. No correction needed.");
|
||||
return;
|
||||
}
|
||||
|
||||
// I know it's tempting to short-circuit here if the config seems to be
|
||||
// up-to-date, but the rest is necessary.
|
||||
|
||||
config.locale = current;
|
||||
|
||||
// The following two lines are heavily commented in case someone
|
||||
// decides to chase down performance improvements and decides to
|
||||
// question what's going on here.
|
||||
// Both lines should be cheap, *but*...
|
||||
|
||||
// This is unnecessary for basic string choice, but it almost
|
||||
// certainly comes into play when rendering numbers, deciding on RTL,
|
||||
// etc. Take it out if you can prove that's not the case.
|
||||
Locale.setDefault(current);
|
||||
|
||||
// This seems to be a no-op, but every piece of documentation under the
|
||||
// sun suggests that it's necessary, and it certainly makes sense.
|
||||
res.updateConfiguration(config, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* We can be in one of two states.
|
||||
*
|
||||
* If the user has not explicitly chosen a Firefox-specific locale, we say
|
||||
* we are "mirroring" the system locale.
|
||||
*
|
||||
* When we are not mirroring, system locale changes do not impact Firefox
|
||||
* and are essentially ignored; the user's locale selection is the only
|
||||
* thing we care about, and we actively correct incoming configuration
|
||||
* changes to reflect the user's chosen locale.
|
||||
*
|
||||
* By contrast, when we are mirroring, system locale changes cause Firefox
|
||||
* to reflect the new system locale, as if the user picked the new locale.
|
||||
*
|
||||
* If we're currently mirroring the system locale, this method returns the
|
||||
* supplied configuration's locale, unless the current activity locale is
|
||||
* correct. If we're not currently mirroring, this method updates the
|
||||
* configuration object to match the user's currently selected locale, and
|
||||
* returns that, unless the current activity locale is correct.
|
||||
*
|
||||
* If the current activity locale is correct, returns null.
|
||||
*
|
||||
* The caller is expected to redisplay themselves accordingly.
|
||||
*
|
||||
* This method is intended to be called from inside
|
||||
* <code>onConfigurationChanged(Configuration)</code> as part of a strategy
|
||||
* to detect and either apply or undo system locale changes.
|
||||
*/
|
||||
@Override
|
||||
public Locale onSystemConfigurationChanged(final Context context, final Resources resources, final Configuration configuration, final Locale currentActivityLocale) {
|
||||
if (!isMirroringSystemLocale(context)) {
|
||||
correctLocale(context, resources, configuration);
|
||||
}
|
||||
|
||||
final Locale changed = configuration.locale;
|
||||
if (changed.equals(currentActivityLocale)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Goanna needs to know the OS locale to compute a useful Accept-Language
|
||||
* header. If it changed since last time, send a message to Goanna and
|
||||
* persist the new value. If unchanged, returns immediately.
|
||||
*
|
||||
* @param prefs the SharedPreferences instance to use. Cannot be null.
|
||||
* @param osLocale the new locale instance. Safe if null.
|
||||
*/
|
||||
public static void storeAndNotifyOSLocale(final SharedPreferences prefs,
|
||||
final Locale osLocale) {
|
||||
if (osLocale == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String lastOSLocale = prefs.getString("osLocale", null);
|
||||
final String osLocaleString = osLocale.toString();
|
||||
|
||||
if (osLocaleString.equals(lastOSLocale)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the Java-native form.
|
||||
prefs.edit().putString("osLocale", osLocaleString).apply();
|
||||
|
||||
// The value we send to Goanna should be a language tag, not
|
||||
// a Java locale string.
|
||||
final String osLanguageTag = Locales.getLanguageTag(osLocale);
|
||||
final GoannaEvent localeOSEvent = GoannaEvent.createBroadcastEvent("Locale:OS", osLanguageTag);
|
||||
GoannaAppShell.sendEventToGoanna(localeOSEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAndApplyPersistedLocale(Context context) {
|
||||
initialize(context);
|
||||
|
||||
final long t1 = android.os.SystemClock.uptimeMillis();
|
||||
final String localeCode = getPersistedLocale(context);
|
||||
if (localeCode == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Note that we don't tell Goanna about this. We notify Goanna when the
|
||||
// locale is set, not when we update Java.
|
||||
final String resultant = updateLocale(context, localeCode);
|
||||
|
||||
if (resultant == null) {
|
||||
// Update the configuration anyway.
|
||||
updateConfiguration(context, currentLocale);
|
||||
}
|
||||
|
||||
final long t2 = android.os.SystemClock.uptimeMillis();
|
||||
Log.i(LOG_TAG, "Locale read and update took: " + (t2 - t1) + "ms.");
|
||||
return resultant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set locale if it changed.
|
||||
*
|
||||
* Always persists and notifies Goanna.
|
||||
*/
|
||||
@Override
|
||||
public String setSelectedLocale(Context context, String localeCode) {
|
||||
final String resultant = updateLocale(context, localeCode);
|
||||
|
||||
// We always persist and notify Goanna, even if nothing seemed to
|
||||
// change. This might happen if you're picking a locale that's the same
|
||||
// as the current OS locale. The OS locale might change next time we
|
||||
// launch, and we need the Goanna pref and persisted locale to have been
|
||||
// set by the time that happens.
|
||||
persistLocale(context, localeCode);
|
||||
|
||||
// Tell Goanna.
|
||||
GoannaEvent ev = GoannaEvent.createBroadcastEvent(EVENT_LOCALE_CHANGED, Locales.getLanguageTag(getCurrentLocale(context)));
|
||||
GoannaAppShell.sendEventToGoanna(ev);
|
||||
|
||||
return resultant;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetToSystemLocale(Context context) {
|
||||
// Wipe the pref.
|
||||
final SharedPreferences settings = getSharedPreferences(context);
|
||||
settings.edit().remove(PREF_LOCALE).apply();
|
||||
|
||||
// Apply the system locale.
|
||||
updateLocale(context, systemLocale);
|
||||
|
||||
// Tell Goanna.
|
||||
GoannaEvent ev = GoannaEvent.createBroadcastEvent(EVENT_LOCALE_CHANGED, "");
|
||||
GoannaAppShell.sendEventToGoanna(ev);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is public to allow for an activity to force the
|
||||
* current locale to be applied if necessary (e.g., when
|
||||
* a new activity launches).
|
||||
*/
|
||||
@Override
|
||||
public void updateConfiguration(Context context, Locale locale) {
|
||||
Resources res = context.getResources();
|
||||
Configuration config = res.getConfiguration();
|
||||
|
||||
// We should use setLocale, but it's unexpectedly missing
|
||||
// on real devices.
|
||||
config.locale = locale;
|
||||
res.updateConfiguration(config, null);
|
||||
}
|
||||
|
||||
private SharedPreferences getSharedPreferences(Context context) {
|
||||
return GoannaSharedPrefs.forApp(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the persisted locale in Java format: "en_US".
|
||||
*/
|
||||
private String getPersistedLocale(Context context) {
|
||||
final SharedPreferences settings = getSharedPreferences(context);
|
||||
final String locale = settings.getString(PREF_LOCALE, "");
|
||||
|
||||
if ("".equals(locale)) {
|
||||
return null;
|
||||
}
|
||||
return locale;
|
||||
}
|
||||
|
||||
private void persistLocale(Context context, String localeCode) {
|
||||
final SharedPreferences settings = getSharedPreferences(context);
|
||||
settings.edit().putString(PREF_LOCALE, localeCode).apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Locale getCurrentLocale(Context context) {
|
||||
if (currentLocale != null) {
|
||||
return currentLocale;
|
||||
}
|
||||
|
||||
final String current = getPersistedLocale(context);
|
||||
if (current == null) {
|
||||
return null;
|
||||
}
|
||||
return currentLocale = Locales.parseLocaleCode(current);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the Java locale and the Android configuration.
|
||||
*
|
||||
* Returns the persisted locale if it differed.
|
||||
*
|
||||
* Does not notify Goanna.
|
||||
*
|
||||
* @param localeCode a locale string in Java format: "en_US".
|
||||
* @return if it differed, a locale string in Java format: "en_US".
|
||||
*/
|
||||
private String updateLocale(Context context, String localeCode) {
|
||||
// Fast path.
|
||||
final Locale defaultLocale = Locale.getDefault();
|
||||
if (defaultLocale.toString().equals(localeCode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Locale locale = Locales.parseLocaleCode(localeCode);
|
||||
|
||||
return updateLocale(context, locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the Java locale string: e.g., "en_US".
|
||||
*/
|
||||
private String updateLocale(Context context, final Locale locale) {
|
||||
// Fast path.
|
||||
if (Locale.getDefault().equals(locale)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Locale.setDefault(locale);
|
||||
currentLocale = locale;
|
||||
|
||||
// Update resources.
|
||||
updateConfiguration(context, locale);
|
||||
|
||||
return locale.toString();
|
||||
}
|
||||
|
||||
private boolean isMirroringSystemLocale(final Context context) {
|
||||
return getPersistedLocale(context) == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Examines <code>multilocale.json</code>, returning the included list of
|
||||
* locale codes.
|
||||
*
|
||||
* If <code>multilocale.json</code> is not present, returns
|
||||
* <code>null</code>. In that case, consider {@link #getFallbackLocaleTag()}.
|
||||
*
|
||||
* multilocale.json currently looks like this:
|
||||
*
|
||||
* <code>
|
||||
* {"locales": ["en-US", "be", "ca", "cs", "da", "de", "en-GB",
|
||||
* "en-ZA", "es-AR", "es-ES", "es-MX", "et", "fi",
|
||||
* "fr", "ga-IE", "hu", "id", "it", "ja", "ko",
|
||||
* "lt", "lv", "nb-NO", "nl", "pl", "pt-BR",
|
||||
* "pt-PT", "ro", "ru", "sk", "sl", "sv-SE", "th",
|
||||
* "tr", "uk", "zh-CN", "zh-TW", "en-US"]}
|
||||
* </code>
|
||||
*/
|
||||
public static Collection<String> getPackagedLocaleTags(final Context context) {
|
||||
final String resPath = "res/multilocale.json";
|
||||
final String jarURL = GoannaJarReader.getJarURL(context, resPath);
|
||||
|
||||
final String contents = GoannaJarReader.getText(jarURL);
|
||||
if (contents == null) {
|
||||
// GoannaJarReader logs and swallows exceptions.
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final JSONObject multilocale = new JSONObject(contents);
|
||||
final JSONArray locales = multilocale.getJSONArray("locales");
|
||||
if (locales == null) {
|
||||
Log.e(LOG_TAG, "No 'locales' array in multilocales.json!");
|
||||
return null;
|
||||
}
|
||||
|
||||
final Set<String> out = new HashSet<String>(locales.length());
|
||||
for (int i = 0; i < locales.length(); ++i) {
|
||||
// If any item in the array is invalid, this will throw,
|
||||
// and the entire clause will fail, being caught below
|
||||
// and returning null.
|
||||
out.add(locales.getString(i));
|
||||
}
|
||||
|
||||
return out;
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOG_TAG, "Unable to parse multilocale.json.", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the single default locale baked into this application.
|
||||
* Applicable when there is no multilocale.json present.
|
||||
*/
|
||||
@SuppressWarnings("static-method")
|
||||
public String getFallbackLocaleTag() {
|
||||
return FALLBACK_LOCALE_TAG;
|
||||
}
|
||||
}
|
||||
@@ -1,499 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.mozilla.goanna.util.EventCallback;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONException;
|
||||
|
||||
import com.google.android.gms.cast.Cast.MessageReceivedCallback;
|
||||
import com.google.android.gms.cast.ApplicationMetadata;
|
||||
import com.google.android.gms.cast.Cast;
|
||||
import com.google.android.gms.cast.Cast.ApplicationConnectionResult;
|
||||
import com.google.android.gms.cast.CastDevice;
|
||||
import com.google.android.gms.cast.CastMediaControlIntent;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaMetadata;
|
||||
import com.google.android.gms.cast.MediaStatus;
|
||||
import com.google.android.gms.cast.RemoteMediaPlayer;
|
||||
import com.google.android.gms.cast.RemoteMediaPlayer.MediaChannelResult;
|
||||
import com.google.android.gms.common.ConnectionResult;
|
||||
import com.google.android.gms.common.api.GoogleApiClient;
|
||||
import com.google.android.gms.common.api.ResultCallback;
|
||||
import com.google.android.gms.common.api.Status;
|
||||
import com.google.android.gms.common.GooglePlayServicesUtil;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.media.MediaRouter.RouteInfo;
|
||||
import android.util.Log;
|
||||
|
||||
/* Implementation of GoannaMediaPlayer for talking to ChromeCast devices */
|
||||
class ChromeCast implements GoannaMediaPlayer {
|
||||
private static final boolean SHOW_DEBUG = false;
|
||||
|
||||
static final String MIRROR_RECEIVER_APP_ID = "08FF1091";
|
||||
|
||||
private final Context context;
|
||||
private final RouteInfo route;
|
||||
private GoogleApiClient apiClient;
|
||||
private RemoteMediaPlayer remoteMediaPlayer;
|
||||
private final boolean canMirror;
|
||||
private String mSessionId;
|
||||
private MirrorChannel mMirrorChannel;
|
||||
private boolean mApplicationStarted = false;
|
||||
|
||||
// EventCallback which is actually a GoannaEventCallback is sometimes being invoked more
|
||||
// than once. That causes the IllegalStateException to be thrown. To prevent a crash,
|
||||
// catch the exception and report it as an error to the log.
|
||||
private static void sendSuccess(final EventCallback callback, final String msg) {
|
||||
try {
|
||||
callback.sendSuccess(msg);
|
||||
} catch (final IllegalStateException e) {
|
||||
Log.e(LOGTAG, "Attempting to invoke callback.sendSuccess more than once.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendError(final EventCallback callback, final String msg) {
|
||||
try {
|
||||
callback.sendError(msg);
|
||||
} catch (final IllegalStateException e) {
|
||||
Log.e(LOGTAG, "Attempting to invoke callback.sendError more than once.", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Callback to start playback of a url on a remote device
|
||||
private class VideoPlayCallback implements ResultCallback<ApplicationConnectionResult>,
|
||||
RemoteMediaPlayer.OnStatusUpdatedListener,
|
||||
RemoteMediaPlayer.OnMetadataUpdatedListener {
|
||||
private final String url;
|
||||
private final String type;
|
||||
private final String title;
|
||||
private final EventCallback callback;
|
||||
|
||||
public VideoPlayCallback(String url, String type, String title, EventCallback callback) {
|
||||
this.url = url;
|
||||
this.type = type;
|
||||
this.title = title;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusUpdated() {
|
||||
MediaStatus mediaStatus = remoteMediaPlayer.getMediaStatus();
|
||||
boolean isPlaying = mediaStatus.getPlayerState() == MediaStatus.PLAYER_STATE_PLAYING;
|
||||
|
||||
// TODO: Do we want to shutdown when there are errors?
|
||||
if (mediaStatus.getPlayerState() == MediaStatus.PLAYER_STATE_IDLE &&
|
||||
mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_FINISHED) {
|
||||
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("Casting:Stop", null));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMetadataUpdated() { }
|
||||
|
||||
@Override
|
||||
public void onResult(ApplicationConnectionResult result) {
|
||||
Status status = result.getStatus();
|
||||
debug("ApplicationConnectionResultCallback.onResult: statusCode" + status.getStatusCode());
|
||||
if (status.isSuccess()) {
|
||||
remoteMediaPlayer = new RemoteMediaPlayer();
|
||||
remoteMediaPlayer.setOnStatusUpdatedListener(this);
|
||||
remoteMediaPlayer.setOnMetadataUpdatedListener(this);
|
||||
mSessionId = result.getSessionId();
|
||||
if (!verifySession(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Cast.CastApi.setMessageReceivedCallbacks(apiClient, remoteMediaPlayer.getNamespace(), remoteMediaPlayer);
|
||||
} catch (IOException e) {
|
||||
debug("Exception while creating media channel", e);
|
||||
}
|
||||
|
||||
startPlayback();
|
||||
} else {
|
||||
sendError(callback, status.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void startPlayback() {
|
||||
MediaMetadata mediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
|
||||
mediaMetadata.putString(MediaMetadata.KEY_TITLE, title);
|
||||
MediaInfo mediaInfo = new MediaInfo.Builder(url)
|
||||
.setContentType(type)
|
||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
||||
.setMetadata(mediaMetadata)
|
||||
.build();
|
||||
try {
|
||||
remoteMediaPlayer.load(apiClient, mediaInfo, true).setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
|
||||
@Override
|
||||
public void onResult(MediaChannelResult result) {
|
||||
if (result.getStatus().isSuccess()) {
|
||||
sendSuccess(callback, null);
|
||||
debug("Media loaded successfully");
|
||||
return;
|
||||
}
|
||||
|
||||
debug("Media load failed " + result.getStatus());
|
||||
sendError(callback, result.getStatus().toString());
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
} catch (IllegalStateException e) {
|
||||
debug("Problem occurred with media during loading", e);
|
||||
} catch (Exception e) {
|
||||
debug("Problem opening media during loading", e);
|
||||
}
|
||||
|
||||
sendError(callback, "");
|
||||
}
|
||||
}
|
||||
|
||||
public ChromeCast(Context context, RouteInfo route) {
|
||||
int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(context);
|
||||
if (status != ConnectionResult.SUCCESS) {
|
||||
throw new IllegalStateException("Play services are required for Chromecast support (got status code " + status + ")");
|
||||
}
|
||||
|
||||
this.context = context;
|
||||
this.route = route;
|
||||
this.canMirror = route.supportsControlCategory(CastMediaControlIntent.categoryForCast(MIRROR_RECEIVER_APP_ID));
|
||||
}
|
||||
|
||||
/**
|
||||
* This dumps everything we can find about the device into JSON. This will hopefully make it
|
||||
* easier to filter out duplicate devices from different sources in JS.
|
||||
* Returns null if the device can't be found.
|
||||
*/
|
||||
@Override
|
||||
public JSONObject toJSON() {
|
||||
final JSONObject obj = new JSONObject();
|
||||
try {
|
||||
final CastDevice device = CastDevice.getFromBundle(route.getExtras());
|
||||
if (device == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
obj.put("uuid", route.getId());
|
||||
obj.put("version", device.getDeviceVersion());
|
||||
obj.put("friendlyName", device.getFriendlyName());
|
||||
obj.put("location", device.getIpAddress().toString());
|
||||
obj.put("modelName", device.getModelName());
|
||||
obj.put("mirror", canMirror);
|
||||
// For now we just assume all of these are Google devices
|
||||
obj.put("manufacturer", "Google Inc.");
|
||||
} catch (JSONException ex) {
|
||||
debug("Error building route", ex);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(final String title, final String url, final String type, final EventCallback callback) {
|
||||
final CastDevice device = CastDevice.getFromBundle(route.getExtras());
|
||||
Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(device, new Cast.Listener() {
|
||||
@Override
|
||||
public void onApplicationStatusChanged() { }
|
||||
|
||||
@Override
|
||||
public void onVolumeChanged() { }
|
||||
|
||||
@Override
|
||||
public void onApplicationDisconnected(int errorCode) { }
|
||||
});
|
||||
|
||||
apiClient = new GoogleApiClient.Builder(context)
|
||||
.addApi(Cast.API, apiOptionsBuilder.build())
|
||||
.addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
|
||||
@Override
|
||||
public void onConnected(Bundle connectionHint) {
|
||||
// Sometimes apiClient is null here. See bug 1061032
|
||||
if (apiClient != null && !apiClient.isConnected()) {
|
||||
debug("Connection failed");
|
||||
sendError(callback, "Not connected");
|
||||
return;
|
||||
}
|
||||
|
||||
// Launch the media player app and launch this url once its loaded
|
||||
try {
|
||||
Cast.CastApi.launchApplication(apiClient, CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID, true)
|
||||
.setResultCallback(new VideoPlayCallback(url, type, title, callback));
|
||||
} catch (Exception e) {
|
||||
debug("Failed to launch application", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionSuspended(int cause) {
|
||||
debug("suspended");
|
||||
}
|
||||
}).build();
|
||||
|
||||
apiClient.connect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(final EventCallback callback) {
|
||||
// Nothing to be done here
|
||||
sendSuccess(callback, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(final EventCallback callback) {
|
||||
// Nothing to be done here
|
||||
sendSuccess(callback, null);
|
||||
}
|
||||
|
||||
public boolean verifySession(final EventCallback callback) {
|
||||
String msg = null;
|
||||
if (apiClient == null || !apiClient.isConnected()) {
|
||||
msg = "Not connected";
|
||||
}
|
||||
|
||||
if (mSessionId == null) {
|
||||
msg = "No session";
|
||||
}
|
||||
|
||||
if (msg != null) {
|
||||
debug(msg);
|
||||
if (callback != null) {
|
||||
sendError(callback, msg);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void play(final EventCallback callback) {
|
||||
if (!verifySession(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
remoteMediaPlayer.play(apiClient).setResultCallback(new ResultCallback<MediaChannelResult>() {
|
||||
@Override
|
||||
public void onResult(MediaChannelResult result) {
|
||||
Status status = result.getStatus();
|
||||
if (!status.isSuccess()) {
|
||||
debug("Unable to play: " + status.getStatusCode());
|
||||
sendError(callback, status.toString());
|
||||
} else {
|
||||
sendSuccess(callback, null);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch(IllegalStateException ex) {
|
||||
// The media player may throw if the session has been killed. For now, we're just catching this here.
|
||||
sendError(callback, "Error playing");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause(final EventCallback callback) {
|
||||
if (!verifySession(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
remoteMediaPlayer.pause(apiClient).setResultCallback(new ResultCallback<MediaChannelResult>() {
|
||||
@Override
|
||||
public void onResult(MediaChannelResult result) {
|
||||
Status status = result.getStatus();
|
||||
if (!status.isSuccess()) {
|
||||
debug("Unable to pause: " + status.getStatusCode());
|
||||
sendError(callback, status.toString());
|
||||
} else {
|
||||
sendSuccess(callback, null);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch(IllegalStateException ex) {
|
||||
// The media player may throw if the session has been killed. For now, we're just catching this here.
|
||||
sendError(callback, "Error pausing");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void end(final EventCallback callback) {
|
||||
if (!verifySession(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Cast.CastApi.stopApplication(apiClient).setResultCallback(new ResultCallback<Status>() {
|
||||
@Override
|
||||
public void onResult(Status result) {
|
||||
if (result.isSuccess()) {
|
||||
try {
|
||||
Cast.CastApi.removeMessageReceivedCallbacks(apiClient, remoteMediaPlayer.getNamespace());
|
||||
remoteMediaPlayer = null;
|
||||
mSessionId = null;
|
||||
apiClient.disconnect();
|
||||
apiClient = null;
|
||||
|
||||
if (callback != null) {
|
||||
sendSuccess(callback, null);
|
||||
}
|
||||
|
||||
return;
|
||||
} catch(Exception ex) {
|
||||
debug("Error ending", ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (callback != null) {
|
||||
sendError(callback, result.getStatus().toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch(IllegalStateException ex) {
|
||||
// The media player may throw if the session has been killed. For now, we're just catching this here.
|
||||
sendError(callback, "Error stopping");
|
||||
}
|
||||
}
|
||||
|
||||
class MirrorChannel implements MessageReceivedCallback {
|
||||
/**
|
||||
* @return custom namespace
|
||||
*/
|
||||
public String getNamespace() {
|
||||
return "urn:x-cast:org.mozilla.mirror";
|
||||
}
|
||||
|
||||
/*
|
||||
* Receive message from the receiver app
|
||||
*/
|
||||
@Override
|
||||
public void onMessageReceived(CastDevice castDevice, String namespace,
|
||||
String message) {
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("MediaPlayer:Response", message));
|
||||
}
|
||||
|
||||
public void sendMessage(String message) {
|
||||
if (apiClient != null && mMirrorChannel != null) {
|
||||
try {
|
||||
Cast.CastApi.sendMessage(apiClient, mMirrorChannel.getNamespace(), message)
|
||||
.setResultCallback(
|
||||
new ResultCallback<Status>() {
|
||||
@Override
|
||||
public void onResult(Status result) {
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e(LOGTAG, "Exception while sending message", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private class MirrorCallback implements ResultCallback<ApplicationConnectionResult> {
|
||||
final EventCallback callback;
|
||||
MirrorCallback(final EventCallback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onResult(ApplicationConnectionResult result) {
|
||||
Status status = result.getStatus();
|
||||
if (status.isSuccess()) {
|
||||
ApplicationMetadata applicationMetadata = result.getApplicationMetadata();
|
||||
mSessionId = result.getSessionId();
|
||||
String applicationStatus = result.getApplicationStatus();
|
||||
boolean wasLaunched = result.getWasLaunched();
|
||||
mApplicationStarted = true;
|
||||
|
||||
// Create the custom message
|
||||
// channel
|
||||
mMirrorChannel = new MirrorChannel();
|
||||
try {
|
||||
Cast.CastApi.setMessageReceivedCallbacks(apiClient,
|
||||
mMirrorChannel
|
||||
.getNamespace(),
|
||||
mMirrorChannel);
|
||||
sendSuccess(callback, null);
|
||||
} catch (IOException e) {
|
||||
Log.e(LOGTAG, "Exception while creating channel", e);
|
||||
}
|
||||
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("Casting:Mirror", route.getId()));
|
||||
} else {
|
||||
sendError(callback, status.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void message(String msg, final EventCallback callback) {
|
||||
if (mMirrorChannel != null) {
|
||||
mMirrorChannel.sendMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mirror(final EventCallback callback) {
|
||||
final CastDevice device = CastDevice.getFromBundle(route.getExtras());
|
||||
Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(device, new Cast.Listener() {
|
||||
@Override
|
||||
public void onApplicationStatusChanged() { }
|
||||
|
||||
@Override
|
||||
public void onVolumeChanged() { }
|
||||
|
||||
@Override
|
||||
public void onApplicationDisconnected(int errorCode) { }
|
||||
});
|
||||
|
||||
apiClient = new GoogleApiClient.Builder(context)
|
||||
.addApi(Cast.API, apiOptionsBuilder.build())
|
||||
.addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
|
||||
@Override
|
||||
public void onConnected(Bundle connectionHint) {
|
||||
// Sometimes apiClient is null here. See bug 1061032
|
||||
if (apiClient == null || !apiClient.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Launch the media player app and launch this url once its loaded
|
||||
try {
|
||||
Cast.CastApi.launchApplication(apiClient, MIRROR_RECEIVER_APP_ID, true)
|
||||
.setResultCallback(new MirrorCallback(callback));
|
||||
} catch (Exception e) {
|
||||
debug("Failed to launch application", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionSuspended(int cause) {
|
||||
debug("suspended");
|
||||
}
|
||||
}).build();
|
||||
|
||||
apiClient.connect();
|
||||
}
|
||||
|
||||
private static final String LOGTAG = "GoannaChromeCast";
|
||||
private void debug(String msg, Exception e) {
|
||||
if (SHOW_DEBUG) {
|
||||
Log.e(LOGTAG, msg, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void debug(String msg) {
|
||||
if (SHOW_DEBUG) {
|
||||
Log.d(LOGTAG, msg);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
public interface ContextGetter {
|
||||
Context getContext();
|
||||
SharedPreferences getSharedPreferences();
|
||||
}
|
||||
|
||||
@@ -1,468 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
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 = "GoannaCrashHandler";
|
||||
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
|
||||
*/
|
||||
protected void logException(final Thread thread, final Throwable exc) {
|
||||
try {
|
||||
Log.e(LOGTAG, ">>> REPORTING UNCAUGHT EXCEPTION FROM THREAD "
|
||||
+ thread.getId() + " (\"" + thread.getName() + "\")", exc);
|
||||
|
||||
if (MAIN_THREAD != thread) {
|
||||
Log.e(LOGTAG, "Main thread (" + MAIN_THREAD.getId() + ") stack:");
|
||||
for (StackTraceElement ste : MAIN_THREAD.getStackTrace()) {
|
||||
Log.e(LOGTAG, " " + ste.toString());
|
||||
}
|
||||
}
|
||||
} catch (final Throwable e) {
|
||||
// If something throws here, we want to continue to report the exception,
|
||||
// so we catch all exceptions and ignore them.
|
||||
}
|
||||
}
|
||||
|
||||
private static long getCrashTime() {
|
||||
return System.currentTimeMillis() / 1000;
|
||||
}
|
||||
|
||||
private static long getStartupTime() {
|
||||
// Process start time is also the proc file modified time.
|
||||
final long uptimeMins = (new File("/proc/self/cmdline")).lastModified();
|
||||
if (uptimeMins == 0L) {
|
||||
return getCrashTime();
|
||||
}
|
||||
return uptimeMins / 1000;
|
||||
}
|
||||
|
||||
private static String getJavaPackageName() {
|
||||
return CrashHandler.class.getPackage().getName();
|
||||
}
|
||||
|
||||
protected String getAppPackageName() {
|
||||
final Context context = getAppContext();
|
||||
|
||||
if (context != null) {
|
||||
return context.getPackageName();
|
||||
}
|
||||
|
||||
try {
|
||||
// Package name is also the command line string in most cases.
|
||||
final FileReader reader = new FileReader("/proc/self/cmdline");
|
||||
final char[] buffer = new char[64];
|
||||
try {
|
||||
if (reader.read(buffer) > 0) {
|
||||
// cmdline is delimited by '\0', and we want the first token.
|
||||
final int nul = Arrays.asList(buffer).indexOf('\0');
|
||||
return (new String(buffer, 0, nul < 0 ? buffer.length : nul)).trim();
|
||||
}
|
||||
} finally {
|
||||
reader.close();
|
||||
}
|
||||
|
||||
} catch (final IOException e) {
|
||||
Log.i(LOGTAG, "Error reading package name", e);
|
||||
}
|
||||
|
||||
// Fallback to using CrashHandler's package name.
|
||||
return getJavaPackageName();
|
||||
}
|
||||
|
||||
protected Context getAppContext() {
|
||||
return appContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the crash "extras" to be reported.
|
||||
*
|
||||
* @param thread The exception thread
|
||||
* @param exc An exception
|
||||
* @return "Extras" in the from of a Bundle
|
||||
*/
|
||||
protected Bundle getCrashExtras(final Thread thread, final Throwable exc) {
|
||||
final Context context = getAppContext();
|
||||
final Bundle extras = new Bundle();
|
||||
final String pkgName = getAppPackageName();
|
||||
|
||||
extras.putString("ProductName", pkgName);
|
||||
extras.putLong("CrashTime", getCrashTime());
|
||||
extras.putLong("StartupTime", getStartupTime());
|
||||
|
||||
if (context != null) {
|
||||
final PackageManager pkgMgr = context.getPackageManager();
|
||||
try {
|
||||
final PackageInfo pkgInfo = pkgMgr.getPackageInfo(pkgName, 0);
|
||||
extras.putString("Version", pkgInfo.versionName);
|
||||
extras.putInt("BuildID", pkgInfo.versionCode);
|
||||
extras.putLong("InstallTime", pkgInfo.lastUpdateTime / 1000);
|
||||
} catch (final PackageManager.NameNotFoundException e) {
|
||||
Log.i(LOGTAG, "Error getting package info", e);
|
||||
}
|
||||
}
|
||||
|
||||
extras.putString("JavaStackTrace", getExceptionStackTrace(exc));
|
||||
return extras;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the crash minidump content to be reported.
|
||||
*
|
||||
* @param thread The exception thread
|
||||
* @param exc An exception
|
||||
* @return Minidump content
|
||||
*/
|
||||
protected byte[] getCrashDump(final Thread thread, final Throwable exc) {
|
||||
return new byte[0]; // No minidump.
|
||||
}
|
||||
|
||||
protected static String normalizeUrlString(final String str) {
|
||||
if (str == null) {
|
||||
return "";
|
||||
}
|
||||
return Uri.encode(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the server URL to send the crash report to.
|
||||
*
|
||||
* @param extras The crash extras Bundle
|
||||
*/
|
||||
protected String getServerUrl(final Bundle extras) {
|
||||
return String.format(DEFAULT_SERVER_URL,
|
||||
normalizeUrlString(extras.getString("ProductID")),
|
||||
normalizeUrlString(extras.getString("Version")),
|
||||
normalizeUrlString(extras.getString("BuildID")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch the crash reporter activity that sends the crash report to the server.
|
||||
*
|
||||
* @param dumpFile Path for the minidump file
|
||||
* @param extraFile Path for the crash extra file
|
||||
* @return Whether the crash reporter was successfully launched
|
||||
*/
|
||||
protected boolean launchCrashReporter(final String dumpFile, final String extraFile) {
|
||||
try {
|
||||
final Context context = getAppContext();
|
||||
final String javaPkg = getJavaPackageName();
|
||||
final String pkg = getAppPackageName();
|
||||
final String component = javaPkg + ".CrashReporter";
|
||||
final String action = javaPkg + ".reportCrash";
|
||||
final ProcessBuilder pb;
|
||||
|
||||
if (context != null) {
|
||||
final Intent intent = new Intent(action);
|
||||
intent.setComponent(new ComponentName(pkg, component));
|
||||
intent.putExtra("minidumpPath", dumpFile);
|
||||
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) {
|
||||
// Goanna may pass in null for thread to denote the current thread.
|
||||
thread = Thread.currentThread();
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.unregistered) {
|
||||
// Only process crash ourselves if we have not been unregistered.
|
||||
|
||||
this.crashing = true;
|
||||
exc = getRootException(exc);
|
||||
logException(thread, exc);
|
||||
|
||||
if (reportException(thread, exc)) {
|
||||
// Reporting succeeded; we can terminate our process now.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (systemUncaughtHandler != null) {
|
||||
// Follow the chain of uncaught handlers.
|
||||
systemUncaughtHandler.uncaughtException(thread, exc);
|
||||
}
|
||||
} finally {
|
||||
terminateProcess();
|
||||
}
|
||||
}
|
||||
|
||||
public static CrashHandler createDefaultCrashHandler(final Context context) {
|
||||
return new CrashHandler(context) {
|
||||
@Override
|
||||
protected Bundle getCrashExtras(final Thread thread, final Throwable exc) {
|
||||
final Bundle extras = super.getCrashExtras(thread, exc);
|
||||
|
||||
extras.putString("ProductName", AppConstants.MOZ_APP_BASENAME);
|
||||
extras.putString("ProductID", AppConstants.MOZ_APP_ID);
|
||||
extras.putString("Version", AppConstants.MOZ_APP_VERSION);
|
||||
extras.putString("BuildID", AppConstants.MOZ_APP_BUILDID);
|
||||
extras.putString("Vendor", AppConstants.MOZ_APP_VENDOR);
|
||||
extras.putString("ReleaseChannel", AppConstants.MOZ_UPDATE_CHANNEL);
|
||||
return extras;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean reportException(final Thread thread, final Throwable exc) {
|
||||
if (AppConstants.MOZ_CRASHREPORTER && AppConstants.MOZILLA_OFFICIAL) {
|
||||
// Only use Java crash reporter if enabled on official build.
|
||||
return super.reportException(thread, exc);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.widget.ThemedEditText;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
|
||||
public class CustomEditText extends ThemedEditText {
|
||||
private OnKeyPreImeListener mOnKeyPreImeListener;
|
||||
private OnSelectionChangedListener mOnSelectionChangedListener;
|
||||
private OnWindowFocusChangeListener mOnWindowFocusChangeListener;
|
||||
private int mHighlightColor;
|
||||
|
||||
public CustomEditText(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setPrivateMode(false); // Initialize mHighlightColor.
|
||||
}
|
||||
|
||||
public interface OnKeyPreImeListener {
|
||||
public boolean onKeyPreIme(View v, int keyCode, KeyEvent event);
|
||||
}
|
||||
|
||||
public void setOnKeyPreImeListener(OnKeyPreImeListener listener) {
|
||||
mOnKeyPreImeListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
|
||||
if (mOnKeyPreImeListener != null)
|
||||
return mOnKeyPreImeListener.onKeyPreIme(this, keyCode, event);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public interface OnSelectionChangedListener {
|
||||
public void onSelectionChanged(int selStart, int selEnd);
|
||||
}
|
||||
|
||||
public void setOnSelectionChangedListener(OnSelectionChangedListener listener) {
|
||||
mOnSelectionChangedListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSelectionChanged(int selStart, int selEnd) {
|
||||
if (mOnSelectionChangedListener != null)
|
||||
mOnSelectionChangedListener.onSelectionChanged(selStart, selEnd);
|
||||
|
||||
super.onSelectionChanged(selStart, selEnd);
|
||||
}
|
||||
|
||||
public interface OnWindowFocusChangeListener {
|
||||
public void onWindowFocusChanged(boolean hasFocus);
|
||||
}
|
||||
|
||||
public void setOnWindowFocusChangeListener(OnWindowFocusChangeListener listener) {
|
||||
mOnWindowFocusChangeListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowFocusChanged(boolean hasFocus) {
|
||||
super.onWindowFocusChanged(hasFocus);
|
||||
if (mOnWindowFocusChangeListener != null)
|
||||
mOnWindowFocusChangeListener.onWindowFocusChanged(hasFocus);
|
||||
}
|
||||
|
||||
// Provide a getHighlightColor implementation for API level < 16.
|
||||
@Override
|
||||
public int getHighlightColor() {
|
||||
return mHighlightColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPrivateMode(boolean isPrivate) {
|
||||
super.setPrivateMode(isPrivate);
|
||||
|
||||
mHighlightColor = getContext().getResources().getColor(isPrivate
|
||||
? R.color.url_bar_text_highlight_pb : R.color.url_bar_text_highlight);
|
||||
// android:textColorHighlight cannot support a ColorStateList.
|
||||
setHighlightColor(mHighlightColor);
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.AppConstants.Versions;
|
||||
import org.mozilla.goanna.preferences.GoannaPreferences;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Typeface;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.StyleSpan;
|
||||
|
||||
public class DataReportingNotification {
|
||||
|
||||
private static final String LOGTAG = "DataReportNotification";
|
||||
|
||||
public static final String ALERT_NAME_DATAREPORTING_NOTIFICATION = "datareporting-notification";
|
||||
|
||||
private static final String PREFS_POLICY_NOTIFIED_TIME = "datareporting.policy.dataSubmissionPolicyNotifiedTime";
|
||||
private static final String PREFS_POLICY_VERSION = "datareporting.policy.dataSubmissionPolicyVersion";
|
||||
private static final int DATA_REPORTING_VERSION = 2;
|
||||
|
||||
public static void checkAndNotifyPolicy(Context context) {
|
||||
SharedPreferences dataPrefs = GoannaSharedPrefs.forApp(context);
|
||||
final int currentVersion = dataPrefs.getInt(PREFS_POLICY_VERSION, -1);
|
||||
|
||||
if (currentVersion < 1) {
|
||||
// This is a first run, so notify user about data policy.
|
||||
notifyDataPolicy(context, dataPrefs);
|
||||
|
||||
// If healthreport is enabled, set default preference value.
|
||||
if (AppConstants.MOZ_SERVICES_HEALTHREPORT) {
|
||||
SharedPreferences.Editor editor = dataPrefs.edit();
|
||||
editor.putBoolean(GoannaPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true);
|
||||
editor.apply();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentVersion == 1) {
|
||||
// Redisplay notification only for Beta because version 2 updates Beta policy and update version.
|
||||
if (TextUtils.equals("beta", AppConstants.MOZ_UPDATE_CHANNEL)) {
|
||||
notifyDataPolicy(context, dataPrefs);
|
||||
} else {
|
||||
// Silently update the version.
|
||||
SharedPreferences.Editor editor = dataPrefs.edit();
|
||||
editor.putInt(PREFS_POLICY_VERSION, DATA_REPORTING_VERSION);
|
||||
editor.apply();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentVersion >= DATA_REPORTING_VERSION) {
|
||||
// Do nothing, we're at a current (or future) version.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch a notification of the data policy, and record notification time and version.
|
||||
*/
|
||||
private static void notifyDataPolicy(Context context, SharedPreferences sharedPrefs) {
|
||||
boolean result = false;
|
||||
try {
|
||||
// Launch main App to launch Data choices when notification is clicked.
|
||||
Intent prefIntent = new Intent(GoannaApp.ACTION_LAUNCH_SETTINGS);
|
||||
prefIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.BROWSER_INTENT_CLASS_NAME);
|
||||
|
||||
GoannaPreferences.setResourceToOpen(prefIntent, "preferences_vendor");
|
||||
prefIntent.putExtra(ALERT_NAME_DATAREPORTING_NOTIFICATION, true);
|
||||
|
||||
PendingIntent contentIntent = PendingIntent.getActivity(context, 0, prefIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
final Resources resources = context.getResources();
|
||||
|
||||
// Create and send notification.
|
||||
String notificationTitle = resources.getString(R.string.datareporting_notification_title);
|
||||
String notificationSummary;
|
||||
if (Versions.preJB) {
|
||||
notificationSummary = resources.getString(R.string.datareporting_notification_action);
|
||||
} else {
|
||||
// Display partial version of Big Style notification for supporting devices.
|
||||
notificationSummary = resources.getString(R.string.datareporting_notification_summary);
|
||||
}
|
||||
String notificationAction = resources.getString(R.string.datareporting_notification_action);
|
||||
String notificationBigSummary = resources.getString(R.string.datareporting_notification_summary);
|
||||
|
||||
// Make styled ticker text for display in notification bar.
|
||||
String tickerString = resources.getString(R.string.datareporting_notification_ticker_text);
|
||||
SpannableString tickerText = new SpannableString(tickerString);
|
||||
// Bold the notification title of the ticker text, which is the same string as notificationTitle.
|
||||
tickerText.setSpan(new StyleSpan(Typeface.BOLD), 0, notificationTitle.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
|
||||
Notification notification = new NotificationCompat.Builder(context)
|
||||
.setContentTitle(notificationTitle)
|
||||
.setContentText(notificationSummary)
|
||||
.setSmallIcon(R.drawable.ic_status_logo)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(contentIntent)
|
||||
.setStyle(new NotificationCompat.BigTextStyle()
|
||||
.bigText(notificationBigSummary))
|
||||
.addAction(R.drawable.firefox_settings_alert, notificationAction, contentIntent)
|
||||
.setTicker(tickerText)
|
||||
.build();
|
||||
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
int notificationID = ALERT_NAME_DATAREPORTING_NOTIFICATION.hashCode();
|
||||
notificationManager.notify(notificationID, notification);
|
||||
|
||||
// Record version and notification time.
|
||||
SharedPreferences.Editor editor = sharedPrefs.edit();
|
||||
long now = System.currentTimeMillis();
|
||||
editor.putLong(PREFS_POLICY_NOTIFIED_TIME, now);
|
||||
editor.putInt(PREFS_POLICY_VERSION, DATA_REPORTING_VERSION);
|
||||
editor.apply();
|
||||
result = true;
|
||||
} finally {
|
||||
// We want to track any errors, so record notification outcome.
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.POLICY_NOTIFICATION_SUCCESS, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.goanna.AppConstants.Versions;
|
||||
import org.mozilla.goanna.prompts.PromptInput;
|
||||
import org.mozilla.goanna.util.GoannaEventListener;
|
||||
import org.mozilla.goanna.util.ThreadUtils;
|
||||
import org.mozilla.goanna.widget.ArrowPopup;
|
||||
import org.mozilla.goanna.widget.DoorHanger;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
|
||||
public class DoorHangerPopup extends ArrowPopup
|
||||
implements GoannaEventListener,
|
||||
Tabs.OnTabsChangedListener,
|
||||
DoorHanger.OnButtonClickListener {
|
||||
private static final String LOGTAG = "GoannaDoorHangerPopup";
|
||||
|
||||
// Stores a set of all active DoorHanger notifications. A DoorHanger is
|
||||
// uniquely identified by its tabId and value.
|
||||
private final HashSet<DoorHanger> mDoorHangers;
|
||||
|
||||
// Whether or not the doorhanger popup is disabled.
|
||||
private boolean mDisabled;
|
||||
|
||||
public DoorHangerPopup(Context context) {
|
||||
super(context);
|
||||
|
||||
mDoorHangers = new HashSet<DoorHanger>();
|
||||
|
||||
EventDispatcher.getInstance().registerGoannaThreadListener(this,
|
||||
"Doorhanger:Add",
|
||||
"Doorhanger:Remove");
|
||||
Tabs.registerOnTabsChangedListener(this);
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
EventDispatcher.getInstance().unregisterGoannaThreadListener(this,
|
||||
"Doorhanger:Add",
|
||||
"Doorhanger:Remove");
|
||||
Tabs.unregisterOnTabsChangedListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporarily disables the doorhanger popup. If the popup is disabled,
|
||||
* it will not be shown to the user, but it will continue to process
|
||||
* calls to add/remove doorhanger notifications.
|
||||
*/
|
||||
void disable() {
|
||||
mDisabled = true;
|
||||
updatePopup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-enables the doorhanger popup.
|
||||
*/
|
||||
void enable() {
|
||||
mDisabled = false;
|
||||
updatePopup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(String event, JSONObject goannaObject) {
|
||||
try {
|
||||
if (event.equals("Doorhanger:Add")) {
|
||||
final int tabId = goannaObject.getInt("tabID");
|
||||
final String value = goannaObject.getString("value");
|
||||
final String message = goannaObject.getString("message");
|
||||
final JSONArray buttons = goannaObject.getJSONArray("buttons");
|
||||
final JSONObject options = goannaObject.getJSONObject("options");
|
||||
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
addDoorHanger(tabId, value, message, buttons, options);
|
||||
}
|
||||
});
|
||||
} else if (event.equals("Doorhanger:Remove")) {
|
||||
final int tabId = goannaObject.getInt("tabID");
|
||||
final String value = goannaObject.getString("value");
|
||||
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
DoorHanger doorHanger = getDoorHanger(tabId, value);
|
||||
if (doorHanger == null)
|
||||
return;
|
||||
|
||||
removeDoorHanger(doorHanger);
|
||||
updatePopup();
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
|
||||
}
|
||||
}
|
||||
|
||||
// This callback is automatically executed on the UI thread.
|
||||
@Override
|
||||
public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final Object data) {
|
||||
switch(msg) {
|
||||
case CLOSED:
|
||||
// Remove any doorhangers for a tab when it's closed (make
|
||||
// a temporary set to avoid a ConcurrentModificationException)
|
||||
HashSet<DoorHanger> doorHangersToRemove = new HashSet<DoorHanger>();
|
||||
for (DoorHanger dh : mDoorHangers) {
|
||||
if (dh.getTabId() == tab.getId())
|
||||
doorHangersToRemove.add(dh);
|
||||
}
|
||||
for (DoorHanger dh : doorHangersToRemove) {
|
||||
removeDoorHanger(dh);
|
||||
}
|
||||
break;
|
||||
|
||||
case LOCATION_CHANGE:
|
||||
// Only remove doorhangers if the popup is hidden or if we're navigating to a new URL
|
||||
if (!isShowing() || !data.equals(tab.getURL()))
|
||||
removeTransientDoorHangers(tab.getId());
|
||||
|
||||
// Update the popup if the location change was on the current tab
|
||||
if (Tabs.getInstance().isSelectedTab(tab))
|
||||
updatePopup();
|
||||
break;
|
||||
|
||||
case SELECTED:
|
||||
// Always update the popup when a new tab is selected. This will cover cases
|
||||
// where a different tab was closed, since we always need to select a new tab.
|
||||
updatePopup();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a doorhanger.
|
||||
*
|
||||
* This method must be called on the UI thread.
|
||||
*/
|
||||
void addDoorHanger(final int tabId, final String value, final String message,
|
||||
final JSONArray buttons, final JSONObject options) {
|
||||
// Don't add a doorhanger for a tab that doesn't exist
|
||||
if (Tabs.getInstance().getTab(tabId) == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace the doorhanger if it already exists
|
||||
DoorHanger oldDoorHanger = getDoorHanger(tabId, value);
|
||||
if (oldDoorHanger != null) {
|
||||
removeDoorHanger(oldDoorHanger);
|
||||
}
|
||||
|
||||
if (!mInflated) {
|
||||
init();
|
||||
}
|
||||
|
||||
final DoorHanger newDoorHanger = new DoorHanger(mContext, tabId, value);
|
||||
newDoorHanger.setMessage(message);
|
||||
newDoorHanger.setOptions(options);
|
||||
|
||||
for (int i = 0; i < buttons.length(); i++) {
|
||||
try {
|
||||
JSONObject buttonObject = buttons.getJSONObject(i);
|
||||
String label = buttonObject.getString("label");
|
||||
String tag = String.valueOf(buttonObject.getInt("callback"));
|
||||
newDoorHanger.addButton(label, tag, this);
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Error creating doorhanger button", e);
|
||||
}
|
||||
}
|
||||
|
||||
mDoorHangers.add(newDoorHanger);
|
||||
mContent.addView(newDoorHanger);
|
||||
|
||||
// Only update the popup if we're adding a notification to the selected tab
|
||||
if (tabId == Tabs.getInstance().getSelectedTab().getId())
|
||||
updatePopup();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* DoorHanger.OnButtonClickListener implementation
|
||||
*/
|
||||
@Override
|
||||
public void onButtonClick(DoorHanger dh, String tag) {
|
||||
JSONObject response = new JSONObject();
|
||||
try {
|
||||
response.put("callback", tag);
|
||||
|
||||
CheckBox checkBox = dh.getCheckBox();
|
||||
// If the checkbox is being used, pass its value
|
||||
if (checkBox != null) {
|
||||
response.put("checked", checkBox.isChecked());
|
||||
}
|
||||
|
||||
List<PromptInput> doorHangerInputs = dh.getInputs();
|
||||
if (doorHangerInputs != null) {
|
||||
JSONObject inputs = new JSONObject();
|
||||
for (PromptInput input : doorHangerInputs) {
|
||||
inputs.put(input.getId(), input.getValue());
|
||||
}
|
||||
response.put("inputs", inputs);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Error creating onClick response", e);
|
||||
}
|
||||
|
||||
GoannaEvent e = GoannaEvent.createBroadcastEvent("Doorhanger:Reply", response.toString());
|
||||
GoannaAppShell.sendEventToGoanna(e);
|
||||
removeDoorHanger(dh);
|
||||
updatePopup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a doorhanger.
|
||||
*
|
||||
* This method must be called on the UI thread.
|
||||
*/
|
||||
DoorHanger getDoorHanger(int tabId, String value) {
|
||||
for (DoorHanger dh : mDoorHangers) {
|
||||
if (dh.getTabId() == tabId && dh.getValue().equals(value))
|
||||
return dh;
|
||||
}
|
||||
|
||||
// If there's no doorhanger for the given tabId and value, return null
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a doorhanger.
|
||||
*
|
||||
* This method must be called on the UI thread.
|
||||
*/
|
||||
void removeDoorHanger(final DoorHanger doorHanger) {
|
||||
mDoorHangers.remove(doorHanger);
|
||||
mContent.removeView(doorHanger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes doorhangers for a given tab.
|
||||
*
|
||||
* This method must be called on the UI thread.
|
||||
*/
|
||||
void removeTransientDoorHangers(int tabId) {
|
||||
// Make a temporary set to avoid a ConcurrentModificationException
|
||||
HashSet<DoorHanger> doorHangersToRemove = new HashSet<DoorHanger>();
|
||||
for (DoorHanger dh : mDoorHangers) {
|
||||
// Only remove transient doorhangers for the given tab
|
||||
if (dh.getTabId() == tabId && dh.shouldRemove(isShowing()))
|
||||
doorHangersToRemove.add(dh);
|
||||
}
|
||||
|
||||
for (DoorHanger dh : doorHangersToRemove) {
|
||||
removeDoorHanger(dh);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the popup state.
|
||||
*
|
||||
* This method must be called on the UI thread.
|
||||
*/
|
||||
void updatePopup() {
|
||||
// Bail if the selected tab is null, if there are no active doorhangers,
|
||||
// if we haven't inflated the layout yet (this can happen if updatePopup()
|
||||
// is called before the runnable from addDoorHanger() runs), or if the
|
||||
// doorhanger popup is temporarily disabled.
|
||||
Tab tab = Tabs.getInstance().getSelectedTab();
|
||||
if (tab == null || mDoorHangers.size() == 0 || !mInflated || mDisabled) {
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
// Show doorhangers for the selected tab
|
||||
int tabId = tab.getId();
|
||||
boolean shouldShowPopup = false;
|
||||
for (DoorHanger dh : mDoorHangers) {
|
||||
if (dh.getTabId() == tabId) {
|
||||
dh.setVisibility(View.VISIBLE);
|
||||
shouldShowPopup = true;
|
||||
} else {
|
||||
dh.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
// Dismiss the popup if there are no doorhangers to show for this tab
|
||||
if (!shouldShowPopup) {
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
showDividers();
|
||||
if (isShowing()) {
|
||||
show();
|
||||
return;
|
||||
}
|
||||
|
||||
// Make the popup focusable for accessibility. This gets done here
|
||||
// so the node can be accessibility focused, but on pre-ICS devices this
|
||||
// causes crashes, so it is done after the popup is shown.
|
||||
if (Versions.feature14Plus) {
|
||||
setFocusable(true);
|
||||
}
|
||||
|
||||
show();
|
||||
|
||||
if (Versions.preICS) {
|
||||
// Make the popup focusable for keyboard accessibility.
|
||||
setFocusable(true);
|
||||
}
|
||||
}
|
||||
|
||||
//Show all inter-DoorHanger dividers (ie. Dividers on all visible DoorHangers except the last one)
|
||||
private void showDividers() {
|
||||
int count = mContent.getChildCount();
|
||||
DoorHanger lastVisibleDoorHanger = null;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
DoorHanger dh = (DoorHanger) mContent.getChildAt(i);
|
||||
dh.showDivider();
|
||||
if (dh.getVisibility() == View.VISIBLE) {
|
||||
lastVisibleDoorHanger = dh;
|
||||
}
|
||||
}
|
||||
if (lastVisibleDoorHanger != null) {
|
||||
lastVisibleDoorHanger.hideDivider();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismiss() {
|
||||
// If the popup is focusable while it is hidden, we run into crashes
|
||||
// on pre-ICS devices when the popup gets focus before it is shown.
|
||||
setFocusable(false);
|
||||
super.dismiss();
|
||||
}
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.AppConstants.Versions;
|
||||
import org.mozilla.goanna.util.NativeEventListener;
|
||||
import org.mozilla.goanna.util.NativeJSObject;
|
||||
import org.mozilla.goanna.util.EventCallback;
|
||||
import org.mozilla.goanna.mozglue.generatorannotations.WrapElementForJNI;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.IllegalArgumentException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import android.app.DownloadManager;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.media.MediaScannerConnection;
|
||||
import android.media.MediaScannerConnection.MediaScannerConnectionClient;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
public class DownloadsIntegration implements NativeEventListener
|
||||
{
|
||||
private static final String LOGTAG = "GoannaDownloadsIntegration";
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static final List<String> UNKNOWN_MIME_TYPES = new ArrayList<String>(3) {{
|
||||
add("unknown/unknown"); // This will be used as a default mime type for unknown files
|
||||
add("application/unknown");
|
||||
add("application/octet-stream"); // Github uses this for APK files
|
||||
}};
|
||||
|
||||
private static final String DOWNLOAD_REMOVE = "Download:Remove";
|
||||
|
||||
private DownloadsIntegration() {
|
||||
EventDispatcher.getInstance().registerGoannaThreadListener((NativeEventListener)this, DOWNLOAD_REMOVE);
|
||||
}
|
||||
|
||||
private static DownloadsIntegration sInstance;
|
||||
|
||||
private static class Download {
|
||||
final File file;
|
||||
final long id;
|
||||
|
||||
final private static int UNKNOWN_ID = -1;
|
||||
|
||||
public Download(final String path) {
|
||||
this(path, UNKNOWN_ID);
|
||||
}
|
||||
|
||||
public Download(final String path, final long id) {
|
||||
file = new File(path);
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public static Download fromJSON(final NativeJSObject obj) {
|
||||
final String path = obj.getString("path");
|
||||
return new Download(path);
|
||||
}
|
||||
|
||||
public static Download fromCursor(final Cursor c) {
|
||||
final String path = c.getString(c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME));
|
||||
final long id = c.getLong(c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID));
|
||||
return new Download(path, id);
|
||||
}
|
||||
|
||||
public boolean equals(final Download download) {
|
||||
return file.equals(download.file);
|
||||
}
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
if (sInstance == null) {
|
||||
sInstance = new DownloadsIntegration();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(final String event, final NativeJSObject message,
|
||||
final EventCallback callback) {
|
||||
if (DOWNLOAD_REMOVE.equals(event)) {
|
||||
final Download d = Download.fromJSON(message);
|
||||
removeDownload(d);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean useSystemDownloadManager() {
|
||||
if (!AppConstants.ANDROID_DOWNLOADS_INTEGRATION) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int state = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
|
||||
try {
|
||||
state = GoannaAppShell.getContext().getPackageManager().getApplicationEnabledSetting("com.android.providers.downloads");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Download Manager package does not exist
|
||||
return false;
|
||||
}
|
||||
|
||||
return (PackageManager.COMPONENT_ENABLED_STATE_ENABLED == state ||
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT == state);
|
||||
}
|
||||
|
||||
@WrapElementForJNI
|
||||
public static void scanMedia(final String aFile, String aMimeType) {
|
||||
String mimeType = aMimeType;
|
||||
if (UNKNOWN_MIME_TYPES.contains(mimeType)) {
|
||||
// If this is a generic undefined mimetype, erase it so that we can try to determine
|
||||
// one from the file extension below.
|
||||
mimeType = "";
|
||||
}
|
||||
|
||||
// If the platform didn't give us a mimetype, try to guess one from the filename
|
||||
if (TextUtils.isEmpty(mimeType)) {
|
||||
final int extPosition = aFile.lastIndexOf(".");
|
||||
if (extPosition > 0 && extPosition < aFile.length() - 1) {
|
||||
mimeType = GoannaAppShell.getMimeTypeFromExtension(aFile.substring(extPosition+1));
|
||||
}
|
||||
}
|
||||
|
||||
// addCompletedDownload will throw if it received any null parameters. Use aMimeType or a default
|
||||
// if we still don't have one.
|
||||
if (TextUtils.isEmpty(mimeType)) {
|
||||
if (TextUtils.isEmpty(aMimeType)) {
|
||||
mimeType = UNKNOWN_MIME_TYPES.get(0);
|
||||
} else {
|
||||
mimeType = aMimeType;
|
||||
}
|
||||
}
|
||||
|
||||
if (useSystemDownloadManager()) {
|
||||
final File f = new File(aFile);
|
||||
final DownloadManager dm = (DownloadManager) GoannaAppShell.getContext().getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
dm.addCompletedDownload(f.getName(),
|
||||
f.getName(),
|
||||
true, // Media scanner should scan this
|
||||
mimeType,
|
||||
f.getAbsolutePath(),
|
||||
Math.max(1, f.length()), // Some versions of Android require downloads to be at least length 1
|
||||
false); // Don't show a notification.
|
||||
} else {
|
||||
final Context context = GoannaAppShell.getContext();
|
||||
final GoannaMediaScannerClient client = new GoannaMediaScannerClient(context, aFile, mimeType);
|
||||
client.connect();
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeDownload(final Download download) {
|
||||
if (!useSystemDownloadManager()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final DownloadManager dm = (DownloadManager) GoannaAppShell.getContext().getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
|
||||
Cursor c = null;
|
||||
try {
|
||||
c = dm.query((new DownloadManager.Query()).setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL));
|
||||
if (c == null || !c.moveToFirst()) {
|
||||
return;
|
||||
}
|
||||
|
||||
do {
|
||||
final Download d = Download.fromCursor(c);
|
||||
// Try hard as we can to verify this download is the one we think it is
|
||||
if (download.equals(d)) {
|
||||
dm.remove(d.id);
|
||||
}
|
||||
} while(c.moveToNext());
|
||||
} finally {
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class GoannaMediaScannerClient implements MediaScannerConnectionClient {
|
||||
private final String mFile;
|
||||
private final String mMimeType;
|
||||
private MediaScannerConnection mScanner;
|
||||
|
||||
public GoannaMediaScannerClient(Context context, String file, String mimeType) {
|
||||
mFile = file;
|
||||
mMimeType = mimeType;
|
||||
mScanner = new MediaScannerConnection(context, this);
|
||||
}
|
||||
|
||||
public void connect() {
|
||||
mScanner.connect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaScannerConnected() {
|
||||
mScanner.scanFile(mFile, mMimeType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScanCompleted(String path, Uri uri) {
|
||||
if(path.equals(mFile)) {
|
||||
mScanner.disconnect();
|
||||
mScanner = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
import org.mozilla.goanna.PrefsHelper.PrefHandlerBase;
|
||||
import org.mozilla.goanna.gfx.LayerView;
|
||||
import org.mozilla.goanna.util.ThreadUtils;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
public class DynamicToolbar {
|
||||
private static final String STATE_ENABLED = "dynamic_toolbar";
|
||||
private static final String CHROME_PREF = "browser.chrome.dynamictoolbar";
|
||||
|
||||
// DynamicToolbar is enabled iff prefEnabled is true *and* accessibilityEnabled is false,
|
||||
// so it is disabled by default on startup. We do not enable it until we explicitly get
|
||||
// the pref from Goanna telling us to turn it on.
|
||||
private volatile boolean prefEnabled;
|
||||
private boolean accessibilityEnabled;
|
||||
|
||||
private final int prefObserverId;
|
||||
private final EnumSet<PinReason> pinFlags = EnumSet.noneOf(PinReason.class);
|
||||
private LayerView layerView;
|
||||
private OnEnabledChangedListener enabledChangedListener;
|
||||
|
||||
public enum PinReason {
|
||||
RELAYOUT,
|
||||
ACTION_MODE
|
||||
}
|
||||
|
||||
public enum VisibilityTransition {
|
||||
IMMEDIATE,
|
||||
ANIMATE
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for changes to the dynamic toolbar's enabled state.
|
||||
*/
|
||||
public interface OnEnabledChangedListener {
|
||||
/**
|
||||
* This callback is executed on the UI thread.
|
||||
*/
|
||||
public void onEnabledChanged(boolean enabled);
|
||||
}
|
||||
|
||||
public DynamicToolbar() {
|
||||
// Listen to the dynamic toolbar pref
|
||||
prefObserverId = PrefsHelper.getPref(CHROME_PREF, new PrefHandler());
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
PrefsHelper.removeObserver(prefObserverId);
|
||||
}
|
||||
|
||||
public void setLayerView(LayerView layerView) {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
|
||||
this.layerView = layerView;
|
||||
}
|
||||
|
||||
public void setEnabledChangedListener(OnEnabledChangedListener listener) {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
|
||||
enabledChangedListener = listener;
|
||||
}
|
||||
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
|
||||
outState.putBoolean(STATE_ENABLED, prefEnabled);
|
||||
}
|
||||
|
||||
public void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
prefEnabled = savedInstanceState.getBoolean(STATE_ENABLED);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
|
||||
return prefEnabled && !accessibilityEnabled;
|
||||
}
|
||||
|
||||
public void setAccessibilityEnabled(boolean enabled) {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
|
||||
if (accessibilityEnabled == enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable the dynamic toolbar when accessibility features are enabled,
|
||||
// and re-read the preference when they're disabled.
|
||||
accessibilityEnabled = enabled;
|
||||
if (prefEnabled) {
|
||||
triggerEnabledListener();
|
||||
}
|
||||
}
|
||||
|
||||
public void setVisible(boolean visible, VisibilityTransition transition) {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
|
||||
if (layerView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean immediate = transition == VisibilityTransition.IMMEDIATE;
|
||||
if (visible) {
|
||||
layerView.getLayerMarginsAnimator().showMargins(immediate);
|
||||
} else {
|
||||
layerView.getLayerMarginsAnimator().hideMargins(immediate);
|
||||
}
|
||||
}
|
||||
|
||||
public void setPinned(boolean pinned, PinReason reason) {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
|
||||
if (layerView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pinned) {
|
||||
pinFlags.add(reason);
|
||||
} else {
|
||||
pinFlags.remove(reason);
|
||||
}
|
||||
|
||||
layerView.getLayerMarginsAnimator().setMarginsPinned(!pinFlags.isEmpty());
|
||||
}
|
||||
|
||||
private void triggerEnabledListener() {
|
||||
if (enabledChangedListener != null) {
|
||||
enabledChangedListener.onEnabledChanged(isEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
private class PrefHandler extends PrefHandlerBase {
|
||||
@Override
|
||||
public void prefValue(String pref, boolean value) {
|
||||
if (value == prefEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
prefEnabled = value;
|
||||
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// If accessibility is enabled, the dynamic toolbar is
|
||||
// forced to be off.
|
||||
if (!accessibilityEnabled) {
|
||||
triggerEnabledListener();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isObserver() {
|
||||
// We want to be notified of changes to be able to switch mode
|
||||
// without restarting.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.db.BrowserDB;
|
||||
import org.mozilla.goanna.db.BrowserContract.Bookmarks;
|
||||
import org.mozilla.goanna.util.ThreadUtils;
|
||||
import org.mozilla.goanna.util.UIAsyncTask;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.database.Cursor;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* A dialog that allows editing a bookmarks url, title, or keywords
|
||||
* <p>
|
||||
* Invoked by calling one of the {@link org.mozilla.goanna.EditBookmarkDialog#show(String)}
|
||||
* methods.
|
||||
*/
|
||||
public class EditBookmarkDialog {
|
||||
private final Context mContext;
|
||||
|
||||
public EditBookmarkDialog(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* A private struct to make it easier to pass bookmark data across threads
|
||||
*/
|
||||
private class Bookmark {
|
||||
final int id;
|
||||
final String title;
|
||||
final String url;
|
||||
final String keyword;
|
||||
|
||||
public Bookmark(int aId, String aTitle, String aUrl, String aKeyword) {
|
||||
id = aId;
|
||||
title = aTitle;
|
||||
url = aUrl;
|
||||
keyword = aKeyword;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This text watcher to enable or disable the OK button if the dialog contains
|
||||
* valid information. This class is overridden to do data checking on different fields.
|
||||
* By itself, it always enables the button.
|
||||
*
|
||||
* Callers can also assign a paired partner to the TextWatcher, and callers will check
|
||||
* that both are enabled before enabling the ok button.
|
||||
*/
|
||||
private class EditBookmarkTextWatcher implements TextWatcher {
|
||||
// A stored reference to the dialog containing the text field being watched
|
||||
protected AlertDialog mDialog;
|
||||
|
||||
// A stored text watcher to do the real verification of a field
|
||||
protected EditBookmarkTextWatcher mPairedTextWatcher;
|
||||
|
||||
// Whether or not the ok button should be enabled.
|
||||
protected boolean mEnabled = true;
|
||||
|
||||
public EditBookmarkTextWatcher(AlertDialog aDialog) {
|
||||
mDialog = aDialog;
|
||||
}
|
||||
|
||||
public void setPairedTextWatcher(EditBookmarkTextWatcher aTextWatcher) {
|
||||
mPairedTextWatcher = aTextWatcher;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return mEnabled;
|
||||
}
|
||||
|
||||
// Textwatcher interface
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
// Disable if the we're disabled or the paired partner is disabled
|
||||
boolean enabled = mEnabled && (mPairedTextWatcher == null || mPairedTextWatcher.isEnabled());
|
||||
mDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {}
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* A version of the EditBookmarkTextWatcher for the url field of the dialog.
|
||||
* Only checks if the field is empty or not.
|
||||
*/
|
||||
private class LocationTextWatcher extends EditBookmarkTextWatcher {
|
||||
public LocationTextWatcher(AlertDialog aDialog) {
|
||||
super(aDialog);
|
||||
}
|
||||
|
||||
// Disables the ok button if the location field is empty.
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
mEnabled = (s.toString().trim().length() > 0);
|
||||
super.onTextChanged(s, start, before, count);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A version of the EditBookmarkTextWatcher for the keyword field of the dialog.
|
||||
* Checks if the field has any (non leading or trailing) spaces.
|
||||
*/
|
||||
private class KeywordTextWatcher extends EditBookmarkTextWatcher {
|
||||
public KeywordTextWatcher(AlertDialog aDialog) {
|
||||
super(aDialog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
// Disable if the keyword contains spaces
|
||||
mEnabled = (s.toString().trim().indexOf(' ') == -1);
|
||||
super.onTextChanged(s, start, before, count);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Edit bookmark dialog for a particular url. If the url is bookmarked multiple times
|
||||
* this will just edit the first instance it finds.
|
||||
*
|
||||
* @param url The url of the bookmark to edit. The dialog will look up other information like the id,
|
||||
* current title, or keywords associated with this url. If the url isn't bookmarked, the
|
||||
* dialog will fail silently. If the url is bookmarked multiple times, this will only show
|
||||
* information about the first it finds.
|
||||
*/
|
||||
public void show(final String url) {
|
||||
final ContentResolver cr = mContext.getContentResolver();
|
||||
final BrowserDB db = GoannaProfile.get(mContext).getDB();
|
||||
(new UIAsyncTask.WithoutParams<Bookmark>(ThreadUtils.getBackgroundHandler()) {
|
||||
@Override
|
||||
public Bookmark doInBackground() {
|
||||
final Cursor cursor = db.getBookmarkForUrl(cr, url);
|
||||
if (cursor == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Bookmark bookmark = null;
|
||||
try {
|
||||
cursor.moveToFirst();
|
||||
bookmark = new Bookmark(cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.TITLE)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.URL)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.KEYWORD)));
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute(Bookmark bookmark) {
|
||||
if (bookmark == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
show(bookmark.id, bookmark.title, bookmark.url, bookmark.keyword);
|
||||
}
|
||||
}).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Edit bookmark dialog for a set of data. This will show the dialog whether
|
||||
* a bookmark with this url exists or not, but the results will NOT be saved if the id
|
||||
* is not a valid bookmark id.
|
||||
*
|
||||
* @param id The id of the bookmark to change. If there is no bookmark with this ID, the dialog
|
||||
* will fail silently.
|
||||
* @param title The initial title to show in the dialog
|
||||
* @param url The initial url to show in the dialog
|
||||
* @param keyword The initial keyword to show in the dialog
|
||||
*/
|
||||
public void show(final int id, final String title, final String url, final String keyword) {
|
||||
final Context context = mContext;
|
||||
|
||||
AlertDialog.Builder editPrompt = new AlertDialog.Builder(context);
|
||||
final View editView = LayoutInflater.from(context).inflate(R.layout.bookmark_edit, null);
|
||||
editPrompt.setTitle(R.string.bookmark_edit_title);
|
||||
editPrompt.setView(editView);
|
||||
|
||||
final EditText nameText = ((EditText) editView.findViewById(R.id.edit_bookmark_name));
|
||||
final EditText locationText = ((EditText) editView.findViewById(R.id.edit_bookmark_location));
|
||||
final EditText keywordText = ((EditText) editView.findViewById(R.id.edit_bookmark_keyword));
|
||||
nameText.setText(title);
|
||||
locationText.setText(url);
|
||||
keywordText.setText(keyword);
|
||||
|
||||
final BrowserDB db = GoannaProfile.get(mContext).getDB();
|
||||
editPrompt.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int whichButton) {
|
||||
(new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
|
||||
@Override
|
||||
public Void doInBackground() {
|
||||
String newUrl = locationText.getText().toString().trim();
|
||||
String newKeyword = keywordText.getText().toString().trim();
|
||||
|
||||
db.updateBookmark(context.getContentResolver(), id, newUrl, nameText.getText().toString(), newKeyword);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute(Void result) {
|
||||
Toast.makeText(context, R.string.bookmark_updated, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}).execute();
|
||||
}
|
||||
});
|
||||
|
||||
editPrompt.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int whichButton) {
|
||||
// do nothing
|
||||
}
|
||||
});
|
||||
|
||||
final AlertDialog dialog = editPrompt.create();
|
||||
|
||||
// Create our TextWatchers
|
||||
LocationTextWatcher locationTextWatcher = new LocationTextWatcher(dialog);
|
||||
KeywordTextWatcher keywordTextWatcher = new KeywordTextWatcher(dialog);
|
||||
|
||||
// Cross reference the TextWatchers
|
||||
locationTextWatcher.setPairedTextWatcher(keywordTextWatcher);
|
||||
keywordTextWatcher.setPairedTextWatcher(locationTextWatcher);
|
||||
|
||||
// Add the TextWatcher Listeners
|
||||
locationText.addTextChangedListener(locationTextWatcher);
|
||||
keywordText.addTextChangedListener(keywordTextWatcher);
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.GoannaAppShell;
|
||||
import org.mozilla.goanna.GoannaEvent;
|
||||
import org.mozilla.goanna.mozglue.RobocopTarget;
|
||||
import org.mozilla.goanna.util.EventCallback;
|
||||
import org.mozilla.goanna.util.GoannaEventListener;
|
||||
import org.mozilla.goanna.util.NativeEventListener;
|
||||
import org.mozilla.goanna.util.NativeJSContainer;
|
||||
import org.mozilla.goanna.util.NativeJSObject;
|
||||
import org.mozilla.goanna.util.ThreadUtils;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
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 = "GoannaEventDispatcher";
|
||||
private static final String GUID = "__guid__";
|
||||
private static final String STATUS_ERROR = "error";
|
||||
private static final String STATUS_SUCCESS = "success";
|
||||
|
||||
private static final EventDispatcher INSTANCE = new EventDispatcher();
|
||||
|
||||
/**
|
||||
* The capacity of a HashMap is rounded up to the next power-of-2. Every time the size
|
||||
* of the map goes beyond 75% of the capacity, the map is rehashed. Therefore, to
|
||||
* empirically determine the initial capacity that avoids rehashing, we need to
|
||||
* determine the initial size, divide it by 75%, and round up to the next power-of-2.
|
||||
*/
|
||||
private static final int GECKO_NATIVE_EVENTS_COUNT = 0; // Default for HashMap
|
||||
private static final int GECKO_JSON_EVENTS_COUNT = 256; // Empirically measured
|
||||
|
||||
private final Map<String, List<NativeEventListener>> mGoannaThreadNativeListeners =
|
||||
new HashMap<String, List<NativeEventListener>>(GECKO_NATIVE_EVENTS_COUNT);
|
||||
private final Map<String, List<GoannaEventListener>> mGoannaThreadJSONListeners =
|
||||
new HashMap<String, List<GoannaEventListener>>(GECKO_JSON_EVENTS_COUNT);
|
||||
|
||||
public static EventDispatcher getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private EventDispatcher() {
|
||||
}
|
||||
|
||||
private <T> void registerListener(final Class<? extends List<T>> 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) {
|
||||
listeners = listType.newInstance();
|
||||
listenersMap.put(event, listeners);
|
||||
}
|
||||
if (!AppConstants.RELEASE_BUILD && listeners.contains(listener)) {
|
||||
throw new IllegalStateException("Already registered " + event);
|
||||
}
|
||||
listeners.add(listener);
|
||||
}
|
||||
}
|
||||
} catch (final IllegalAccessException | InstantiationException e) {
|
||||
throw new IllegalArgumentException("Invalid new list type", e);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> void checkNotRegistered(final Map<String, List<T>> listenersMap,
|
||||
final String[] events) {
|
||||
synchronized (listenersMap) {
|
||||
for (final String event: events) {
|
||||
if (listenersMap.get(event) != null) {
|
||||
throw new IllegalStateException(
|
||||
"Already registered " + event + " under a different type");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private <T> void unregisterListener(final Map<String, List<T>> listenersMap,
|
||||
final T listener,
|
||||
final String[] events) {
|
||||
synchronized (listenersMap) {
|
||||
for (final String event : events) {
|
||||
List<T> listeners = listenersMap.get(event);
|
||||
if ((listeners == null ||
|
||||
!listeners.remove(listener)) && !AppConstants.RELEASE_BUILD) {
|
||||
throw new IllegalArgumentException(event + " was not registered");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void registerGoannaThreadListener(final NativeEventListener listener,
|
||||
final String... events) {
|
||||
checkNotRegistered(mGoannaThreadJSONListeners, events);
|
||||
|
||||
// For listeners running on the Goanna 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((Class)CopyOnWriteArrayList.class,
|
||||
mGoannaThreadNativeListeners, listener, events);
|
||||
}
|
||||
|
||||
@Deprecated // Use NativeEventListener instead
|
||||
@SuppressWarnings("unchecked")
|
||||
public void registerGoannaThreadListener(final GoannaEventListener listener,
|
||||
final String... events) {
|
||||
checkNotRegistered(mGoannaThreadNativeListeners, events);
|
||||
|
||||
registerListener((Class)CopyOnWriteArrayList.class,
|
||||
mGoannaThreadJSONListeners, listener, events);
|
||||
}
|
||||
|
||||
public void unregisterGoannaThreadListener(final NativeEventListener listener,
|
||||
final String... events) {
|
||||
unregisterListener(mGoannaThreadNativeListeners, listener, events);
|
||||
}
|
||||
|
||||
@Deprecated // Use NativeEventListener instead
|
||||
public void unregisterGoannaThreadListener(final GoannaEventListener listener,
|
||||
final String... events) {
|
||||
unregisterListener(mGoannaThreadJSONListeners, listener, events);
|
||||
}
|
||||
|
||||
public void dispatchEvent(final NativeJSContainer message) {
|
||||
// First try native listeners.
|
||||
final String type = message.optString("type", null);
|
||||
if (type == null) {
|
||||
Log.e(LOGTAG, "JSON message must have a type property");
|
||||
return;
|
||||
}
|
||||
|
||||
final List<NativeEventListener> listeners;
|
||||
synchronized (mGoannaThreadNativeListeners) {
|
||||
listeners = mGoannaThreadNativeListeners.get(type);
|
||||
}
|
||||
|
||||
final String guid = message.optString(GUID, null);
|
||||
EventCallback callback = null;
|
||||
if (guid != null) {
|
||||
callback = new GoannaEventCallback(guid, type);
|
||||
}
|
||||
|
||||
if (listeners != null) {
|
||||
if (listeners.size() == 0) {
|
||||
Log.w(LOGTAG, "No listeners for " + type);
|
||||
}
|
||||
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 JSON listeners
|
||||
// and return early. This assumption is checked when registering listeners.
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// If we didn't find native listeners, try JSON listeners.
|
||||
dispatchEvent(new JSONObject(message.toString()), callback);
|
||||
} catch (final JSONException e) {
|
||||
Log.e(LOGTAG, "Cannot parse JSON", e);
|
||||
} catch (final UnsupportedOperationException e) {
|
||||
Log.e(LOGTAG, "Cannot convert message to JSON", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void dispatchEvent(final JSONObject message, final EventCallback callback) {
|
||||
// {
|
||||
// "type": "value",
|
||||
// "event_specific": "value",
|
||||
// ...
|
||||
try {
|
||||
final String type = message.getString("type");
|
||||
|
||||
List<GoannaEventListener> listeners;
|
||||
synchronized (mGoannaThreadJSONListeners) {
|
||||
listeners = mGoannaThreadJSONListeners.get(type);
|
||||
}
|
||||
if (listeners == null || listeners.size() == 0) {
|
||||
Log.w(LOGTAG, "No listeners for " + type);
|
||||
|
||||
// If there are no listeners, dispatch an error.
|
||||
if (callback != null) {
|
||||
callback.sendError("No listeners for request");
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (final GoannaEventListener listener : listeners) {
|
||||
listener.handleMessage(type, message);
|
||||
}
|
||||
} catch (final JSONException e) {
|
||||
Log.e(LOGTAG, "handleGoannaMessage throws " + e, e);
|
||||
}
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
@Deprecated
|
||||
public static void sendResponse(JSONObject message, Object response) {
|
||||
sendResponseHelper(STATUS_SUCCESS, message, response);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static void sendError(JSONObject message, Object response) {
|
||||
sendResponseHelper(STATUS_ERROR, message, response);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
private static void sendResponseHelper(String status, JSONObject message, Object response) {
|
||||
try {
|
||||
final String topic = message.getString("type") + ":Response";
|
||||
final JSONObject wrapper = new JSONObject();
|
||||
wrapper.put(GUID, message.getString(GUID));
|
||||
wrapper.put("status", status);
|
||||
wrapper.put("response", response);
|
||||
|
||||
if (ThreadUtils.isOnGoannaThread()) {
|
||||
GoannaAppShell.notifyGoannaObservers(topic, wrapper.toString());
|
||||
} else {
|
||||
GoannaAppShell.sendEventToGoanna(
|
||||
GoannaEvent.createBroadcastEvent(topic, wrapper.toString()));
|
||||
}
|
||||
} catch (final JSONException e) {
|
||||
Log.e(LOGTAG, "Unable to send response", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class GoannaEventCallback implements EventCallback {
|
||||
private final String guid;
|
||||
private final String type;
|
||||
private boolean sent;
|
||||
|
||||
public GoannaEventCallback(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.isOnGoannaThread()) {
|
||||
GoannaAppShell.notifyGoannaObservers(topic, wrapper.toString());
|
||||
} else {
|
||||
GoannaAppShell.sendEventToGoanna(
|
||||
GoannaEvent.createBroadcastEvent(topic, wrapper.toString()));
|
||||
}
|
||||
} catch (final JSONException e) {
|
||||
Log.e(LOGTAG, "Unable to send response for: " + type, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.GoannaAppShell;
|
||||
import org.mozilla.goanna.util.GoannaEventListener;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.MediaStore;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class FilePicker implements GoannaEventListener {
|
||||
private static final String LOGTAG = "GoannaFilePicker";
|
||||
private static FilePicker sFilePicker;
|
||||
private final Context context;
|
||||
|
||||
public interface ResultHandler {
|
||||
public void gotFile(String filename);
|
||||
}
|
||||
|
||||
public static void init(Context context) {
|
||||
if (sFilePicker == null) {
|
||||
sFilePicker = new FilePicker(context.getApplicationContext());
|
||||
}
|
||||
}
|
||||
|
||||
protected FilePicker(Context context) {
|
||||
this.context = context;
|
||||
EventDispatcher.getInstance().registerGoannaThreadListener(this, "FilePicker:Show");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(String event, final JSONObject message) {
|
||||
if (event.equals("FilePicker:Show")) {
|
||||
String mimeType = "*/*";
|
||||
final String mode = message.optString("mode");
|
||||
final int tabId = message.optInt("tabId", -1);
|
||||
final String title = message.optString("title");
|
||||
|
||||
if ("mimeType".equals(mode))
|
||||
mimeType = message.optString("mimeType");
|
||||
else if ("extension".equals(mode))
|
||||
mimeType = GoannaAppShell.getMimeTypeFromExtensions(message.optString("extensions"));
|
||||
|
||||
showFilePickerAsync(title, mimeType, new ResultHandler() {
|
||||
@Override
|
||||
public void gotFile(String filename) {
|
||||
try {
|
||||
message.put("file", filename);
|
||||
} catch (JSONException ex) {
|
||||
Log.i(LOGTAG, "Can't add filename to message " + filename);
|
||||
}
|
||||
|
||||
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent(
|
||||
"FilePicker:Result", message.toString()));
|
||||
}
|
||||
}, tabId);
|
||||
}
|
||||
}
|
||||
|
||||
private void addActivities(Intent intent, HashMap<String, Intent> intents, HashMap<String, Intent> filters) {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
List<ResolveInfo> lri = pm.queryIntentActivities(intent, 0);
|
||||
for (ResolveInfo ri : lri) {
|
||||
ComponentName cn = new ComponentName(ri.activityInfo.applicationInfo.packageName, ri.activityInfo.name);
|
||||
if (filters != null && !filters.containsKey(cn.toString())) {
|
||||
Intent rintent = new Intent(intent);
|
||||
rintent.setComponent(cn);
|
||||
intents.put(cn.toString(), rintent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Intent getIntent(String mimeType) {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType(mimeType);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
return intent;
|
||||
}
|
||||
|
||||
private List<Intent> getIntentsForFilePicker(final String mimeType,
|
||||
final FilePickerResultHandler fileHandler) {
|
||||
// The base intent to use for the file picker. Even if this is an implicit intent, Android will
|
||||
// still show a list of Activities that match this action/type.
|
||||
Intent baseIntent;
|
||||
// A HashMap of Activities the base intent will show in the chooser. This is used
|
||||
// to filter activities from other intents so that we don't show duplicates.
|
||||
HashMap<String, Intent> baseIntents = new HashMap<String, Intent>();
|
||||
// A list of other activities to shwo in the picker (and the intents to launch them).
|
||||
HashMap<String, Intent> intents = new HashMap<String, Intent> ();
|
||||
|
||||
if ("audio/*".equals(mimeType)) {
|
||||
// For audio the only intent is the mimetype
|
||||
baseIntent = getIntent(mimeType);
|
||||
addActivities(baseIntent, baseIntents, null);
|
||||
} else if ("image/*".equals(mimeType)) {
|
||||
// For images the base is a capture intent
|
||||
baseIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
baseIntent.putExtra(MediaStore.EXTRA_OUTPUT,
|
||||
Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
|
||||
fileHandler.generateImageName())));
|
||||
addActivities(baseIntent, baseIntents, null);
|
||||
|
||||
// We also add the mimetype intent
|
||||
addActivities(getIntent(mimeType), intents, baseIntents);
|
||||
} else if ("video/*".equals(mimeType)) {
|
||||
// For videos the base is a capture intent
|
||||
baseIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
|
||||
addActivities(baseIntent, baseIntents, null);
|
||||
|
||||
// We also add the mimetype intent
|
||||
addActivities(getIntent(mimeType), intents, baseIntents);
|
||||
} else {
|
||||
baseIntent = getIntent("*/*");
|
||||
addActivities(baseIntent, baseIntents, null);
|
||||
|
||||
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
intent.putExtra(MediaStore.EXTRA_OUTPUT,
|
||||
Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
|
||||
fileHandler.generateImageName())));
|
||||
addActivities(intent, intents, baseIntents);
|
||||
intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
|
||||
addActivities(intent, intents, baseIntents);
|
||||
}
|
||||
|
||||
// If we didn't find any activities, we fall back to the */* mimetype intent
|
||||
if (baseIntents.size() == 0 && intents.size() == 0) {
|
||||
intents.clear();
|
||||
|
||||
baseIntent = getIntent("*/*");
|
||||
addActivities(baseIntent, baseIntents, null);
|
||||
}
|
||||
|
||||
ArrayList<Intent> vals = new ArrayList<Intent>(intents.values());
|
||||
vals.add(0, baseIntent);
|
||||
return vals;
|
||||
}
|
||||
|
||||
private String getFilePickerTitle(String mimeType) {
|
||||
if (mimeType.equals("audio/*")) {
|
||||
return context.getString(R.string.filepicker_audio_title);
|
||||
} else if (mimeType.equals("image/*")) {
|
||||
return context.getString(R.string.filepicker_image_title);
|
||||
} else if (mimeType.equals("video/*")) {
|
||||
return context.getString(R.string.filepicker_video_title);
|
||||
} else {
|
||||
return context.getString(R.string.filepicker_title);
|
||||
}
|
||||
}
|
||||
|
||||
private interface IntentHandler {
|
||||
public void gotIntent(Intent intent);
|
||||
}
|
||||
|
||||
/* Gets an intent that can open a particular mimetype. Will show a prompt with a list
|
||||
* of Activities that can handle the mietype. Asynchronously calls the handler when
|
||||
* one of the intents is selected. If the caller passes in null for the handler, will still
|
||||
* prompt for the activity, but will throw away the result.
|
||||
*/
|
||||
private void getFilePickerIntentAsync(String title,
|
||||
final String mimeType,
|
||||
final FilePickerResultHandler fileHandler,
|
||||
final IntentHandler handler) {
|
||||
List<Intent> intents = getIntentsForFilePicker(mimeType, fileHandler);
|
||||
|
||||
if (intents.size() == 0) {
|
||||
Log.i(LOGTAG, "no activities for the file picker!");
|
||||
handler.gotIntent(null);
|
||||
return;
|
||||
}
|
||||
|
||||
Intent base = intents.remove(0);
|
||||
|
||||
if (intents.size() == 0) {
|
||||
handler.gotIntent(base);
|
||||
return;
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(title)) {
|
||||
title = getFilePickerTitle(mimeType);
|
||||
}
|
||||
Intent chooser = Intent.createChooser(base, title);
|
||||
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents.toArray(new Parcelable[intents.size()]));
|
||||
handler.gotIntent(chooser);
|
||||
}
|
||||
|
||||
/* Allows the user to pick an activity to load files from using a list prompt. Then opens the activity and
|
||||
* sends the file returned to the passed in handler. If a null handler is passed in, will still
|
||||
* pick and launch the file picker, but will throw away the result.
|
||||
*/
|
||||
protected void showFilePickerAsync(final String title, final String mimeType, final ResultHandler handler, final int tabId) {
|
||||
final FilePickerResultHandler fileHandler = new FilePickerResultHandler(handler, context, tabId);
|
||||
getFilePickerIntentAsync(title, mimeType, fileHandler, new IntentHandler() {
|
||||
@Override
|
||||
public void gotIntent(Intent intent) {
|
||||
if (handler == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (intent == null) {
|
||||
handler.gotFile("");
|
||||
return;
|
||||
}
|
||||
|
||||
ActivityHandlerHelper.startIntent(intent, fileHandler);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.mozilla.goanna.util.ActivityResultHandler;
|
||||
import org.mozilla.goanna.util.ThreadUtils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.Process;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.text.TextUtils;
|
||||
import android.text.format.Time;
|
||||
import android.util.Log;
|
||||
|
||||
class FilePickerResultHandler implements ActivityResultHandler {
|
||||
private static final String LOGTAG = "GoannaFilePickerResultHandler";
|
||||
private static final String UPLOADS_DIR = "uploads";
|
||||
|
||||
private final FilePicker.ResultHandler handler;
|
||||
private final int tabId;
|
||||
private final File cacheDir;
|
||||
|
||||
// this code is really hacky and doesn't belong anywhere so I'm putting it here for now
|
||||
// until I can come up with a better solution.
|
||||
private String mImageName = "";
|
||||
|
||||
/* Use this constructor to asynchronously listen for results */
|
||||
public FilePickerResultHandler(final FilePicker.ResultHandler handler, final Context context, final int tabId) {
|
||||
this.tabId = tabId;
|
||||
this.cacheDir = new File(context.getCacheDir(), UPLOADS_DIR);
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
void sendResult(String res) {
|
||||
if (handler != null) {
|
||||
handler.gotFile(res);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int resultCode, Intent intent) {
|
||||
if (resultCode != Activity.RESULT_OK) {
|
||||
sendResult("");
|
||||
return;
|
||||
}
|
||||
|
||||
// Camera results won't return an Intent. Use the file name we passed to the original intent.
|
||||
if (intent == null) {
|
||||
if (mImageName != null) {
|
||||
File file = new File(Environment.getExternalStorageDirectory(), mImageName);
|
||||
sendResult(file.getAbsolutePath());
|
||||
} else {
|
||||
sendResult("");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Uri uri = intent.getData();
|
||||
if (uri == null) {
|
||||
sendResult("");
|
||||
return;
|
||||
}
|
||||
|
||||
// Some file pickers may return a file uri
|
||||
if ("file".equals(uri.getScheme())) {
|
||||
String path = uri.getPath();
|
||||
sendResult(path == null ? "" : path);
|
||||
return;
|
||||
}
|
||||
|
||||
final FragmentActivity fa = (FragmentActivity) GoannaAppShell.getGoannaInterface().getActivity();
|
||||
final LoaderManager lm = fa.getSupportLoaderManager();
|
||||
// Finally, Video pickers and some file pickers may return a content provider.
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
// Try a query to make sure the expected columns exist
|
||||
final ContentResolver cr = fa.getContentResolver();
|
||||
cursor = cr.query(uri, new String[] { MediaStore.Video.Media.DATA }, null, null, null);
|
||||
|
||||
int index = cursor.getColumnIndex(MediaStore.Video.Media.DATA);
|
||||
if (index >= 0) {
|
||||
lm.initLoader(intent.hashCode(), null, new VideoLoaderCallbacks(uri));
|
||||
return;
|
||||
}
|
||||
} catch(Exception ex) {
|
||||
// We'll try a different loader below
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
lm.initLoader(uri.hashCode(), null, new FileLoaderCallbacks(uri, cacheDir, tabId));
|
||||
}
|
||||
|
||||
public String generateImageName() {
|
||||
Time now = new Time();
|
||||
now.setToNow();
|
||||
mImageName = now.format("%Y-%m-%d %H.%M.%S") + ".jpg";
|
||||
return mImageName;
|
||||
}
|
||||
|
||||
private class VideoLoaderCallbacks implements LoaderCallbacks<Cursor> {
|
||||
final private Uri uri;
|
||||
public VideoLoaderCallbacks(Uri uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
final FragmentActivity fa = (FragmentActivity) GoannaAppShell.getGoannaInterface().getActivity();
|
||||
return new CursorLoader(fa,
|
||||
uri,
|
||||
new String[] { MediaStore.Video.Media.DATA },
|
||||
null, // selection
|
||||
null, // selectionArgs
|
||||
null); // sortOrder
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
|
||||
if (cursor.moveToFirst()) {
|
||||
String res = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA));
|
||||
|
||||
// Some pickers (the KitKat Documents one for instance) won't return a temporary file here.
|
||||
// Fall back to the normal FileLoader if we didn't find anything.
|
||||
if (TextUtils.isEmpty(res)) {
|
||||
tryFileLoaderCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
sendResult(res);
|
||||
} else {
|
||||
tryFileLoaderCallback();
|
||||
}
|
||||
}
|
||||
|
||||
private void tryFileLoaderCallback() {
|
||||
final FragmentActivity fa = (FragmentActivity) GoannaAppShell.getGoannaInterface().getActivity();
|
||||
final LoaderManager lm = fa.getSupportLoaderManager();
|
||||
lm.initLoader(uri.hashCode(), null, new FileLoaderCallbacks(uri, cacheDir, tabId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* This class's only dependency on FilePickerResultHandler is sendResult.
|
||||
*/
|
||||
private class FileLoaderCallbacks implements LoaderCallbacks<Cursor>,
|
||||
Tabs.OnTabsChangedListener {
|
||||
private final Uri uri;
|
||||
private final File cacheDir;
|
||||
private final int tabId;
|
||||
String tempFile;
|
||||
|
||||
public FileLoaderCallbacks(Uri uri, File cacheDir, int tabId) {
|
||||
this.uri = uri;
|
||||
this.cacheDir = cacheDir;
|
||||
this.tabId = tabId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
final FragmentActivity fa = (FragmentActivity) GoannaAppShell.getGoannaInterface().getActivity();
|
||||
return new CursorLoader(fa,
|
||||
uri,
|
||||
new String[] { OpenableColumns.DISPLAY_NAME },
|
||||
null, // selection
|
||||
null, // selectionArgs
|
||||
null); // sortOrder
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
|
||||
if (cursor.moveToFirst()) {
|
||||
String name = cursor.getString(0);
|
||||
// tmp filenames must be at least 3 characters long. Add a prefix to make sure that happens
|
||||
String fileName = "tmp_" + Process.myPid() + "-";
|
||||
String fileExt;
|
||||
int period;
|
||||
|
||||
final FragmentActivity fa = (FragmentActivity) GoannaAppShell.getGoannaInterface().getActivity();
|
||||
final ContentResolver cr = fa.getContentResolver();
|
||||
|
||||
// Generate an extension if we don't already have one
|
||||
if (name == null || (period = name.lastIndexOf('.')) == -1) {
|
||||
String mimeType = cr.getType(uri);
|
||||
fileExt = "." + GoannaAppShell.getExtensionFromMimeType(mimeType);
|
||||
} else {
|
||||
fileExt = name.substring(period);
|
||||
fileName += name.substring(0, period);
|
||||
}
|
||||
|
||||
// Now write the data to the temp file
|
||||
try {
|
||||
cacheDir.mkdir();
|
||||
|
||||
File file = File.createTempFile(fileName, fileExt, cacheDir);
|
||||
FileOutputStream fos = new FileOutputStream(file);
|
||||
InputStream is = cr.openInputStream(uri);
|
||||
byte[] buf = new byte[4096];
|
||||
int len = is.read(buf);
|
||||
while (len != -1) {
|
||||
fos.write(buf, 0, len);
|
||||
len = is.read(buf);
|
||||
}
|
||||
fos.close();
|
||||
|
||||
tempFile = file.getAbsolutePath();
|
||||
sendResult((tempFile == null) ? "" : tempFile);
|
||||
|
||||
if (tabId > -1 && !TextUtils.isEmpty(tempFile)) {
|
||||
Tabs.registerOnTabsChangedListener(this);
|
||||
}
|
||||
} catch(IOException ex) {
|
||||
Log.i(LOGTAG, "Error writing file", ex);
|
||||
}
|
||||
} else {
|
||||
sendResult("");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) { }
|
||||
|
||||
/*Tabs.OnTabsChangedListener*/
|
||||
// This cleans up our temp file. If it doesn't run, we just hope that Android
|
||||
// will eventually does the cleanup for us.
|
||||
@Override
|
||||
public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
|
||||
if ((tab == null) || (tab.getId() != tabId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg == Tabs.TabEvents.LOCATION_CHANGE ||
|
||||
msg == Tabs.TabEvents.CLOSED) {
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
File f = new File(tempFile);
|
||||
f.delete();
|
||||
}
|
||||
});
|
||||
|
||||
// Tabs' listener array is safe to modify during use: its
|
||||
// iteration pattern is based on snapshots.
|
||||
Tabs.unregisterOnTabsChangedListener(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.util.GoannaEventListener;
|
||||
import org.mozilla.goanna.util.GoannaRequest;
|
||||
import org.mozilla.goanna.util.NativeJSObject;
|
||||
import org.mozilla.goanna.util.ThreadUtils;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.CheckedTextView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class FindInPageBar extends LinearLayout implements TextWatcher, View.OnClickListener, GoannaEventListener {
|
||||
private static final String LOGTAG = "GoannaFindInPageBar";
|
||||
private static final String REQUEST_ID = "FindInPageBar";
|
||||
|
||||
// Will be removed by Bug 1113297.
|
||||
private static final boolean MATCH_CASE_ENABLED = AppConstants.NIGHTLY_BUILD;
|
||||
|
||||
private final Context mContext;
|
||||
private CustomEditText mFindText;
|
||||
private CheckedTextView mMatchCase;
|
||||
private TextView mStatusText;
|
||||
private boolean mInflated;
|
||||
|
||||
public FindInPageBar(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mContext = context;
|
||||
setFocusable(true);
|
||||
}
|
||||
|
||||
public void inflateContent() {
|
||||
LayoutInflater inflater = LayoutInflater.from(mContext);
|
||||
View content = inflater.inflate(R.layout.find_in_page_content, this);
|
||||
|
||||
content.findViewById(R.id.find_prev).setOnClickListener(this);
|
||||
content.findViewById(R.id.find_next).setOnClickListener(this);
|
||||
content.findViewById(R.id.find_close).setOnClickListener(this);
|
||||
|
||||
// Capture clicks on the rest of the view to prevent them from
|
||||
// leaking into other views positioned below.
|
||||
content.setOnClickListener(this);
|
||||
|
||||
mFindText = (CustomEditText) content.findViewById(R.id.find_text);
|
||||
mFindText.addTextChangedListener(this);
|
||||
mFindText.setOnKeyPreImeListener(new CustomEditText.OnKeyPreImeListener() {
|
||||
@Override
|
||||
public boolean onKeyPreIme(View v, int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
hide();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
mMatchCase = (CheckedTextView) content.findViewById(R.id.find_matchcase);
|
||||
if (MATCH_CASE_ENABLED) {
|
||||
mMatchCase.setOnClickListener(this);
|
||||
} else {
|
||||
mMatchCase.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
mStatusText = (TextView) content.findViewById(R.id.find_status);
|
||||
|
||||
mInflated = true;
|
||||
EventDispatcher.getInstance().registerGoannaThreadListener(this, "TextSelection:Data");
|
||||
}
|
||||
|
||||
public void show() {
|
||||
if (!mInflated)
|
||||
inflateContent();
|
||||
|
||||
setVisibility(VISIBLE);
|
||||
mFindText.requestFocus();
|
||||
|
||||
// handleMessage() receives response message and determines initial state of softInput
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("TextSelection:Get", REQUEST_ID));
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("FindInPage:Opened", null));
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
// Always clear the Find string, primarily for privacy.
|
||||
mFindText.setText("");
|
||||
|
||||
setVisibility(GONE);
|
||||
getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("FindInPage:Closed", null));
|
||||
}
|
||||
|
||||
private InputMethodManager getInputMethodManager(View view) {
|
||||
Context context = view.getContext();
|
||||
return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
}
|
||||
|
||||
public void onDestroy() {
|
||||
if (!mInflated) {
|
||||
return;
|
||||
}
|
||||
EventDispatcher.getInstance().unregisterGoannaThreadListener(this, "TextSelection:Data");
|
||||
}
|
||||
|
||||
// TextWatcher implementation
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
sendRequestToFinderHelper("FindInPage:Find", s.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// View.OnClickListener implementation
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
final int viewId = v.getId();
|
||||
|
||||
if (viewId == R.id.find_matchcase) {
|
||||
// Toggle matchcase state (color).
|
||||
mMatchCase.toggle();
|
||||
|
||||
// Repeat the find after a matchcase change.
|
||||
sendRequestToFinderHelper("FindInPage:Find", mFindText.getText().toString());
|
||||
getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewId == R.id.find_prev) {
|
||||
sendRequestToFinderHelper("FindInPage:Prev", mFindText.getText().toString());
|
||||
getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewId == R.id.find_next) {
|
||||
sendRequestToFinderHelper("FindInPage:Next", mFindText.getText().toString());
|
||||
getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewId == R.id.find_close) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
// GoannaEventListener implementation
|
||||
|
||||
@Override
|
||||
public void handleMessage(String event, JSONObject message) {
|
||||
if (!event.equals("TextSelection:Data") || !REQUEST_ID.equals(message.optString("requestId"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String text = message.optString("text");
|
||||
|
||||
// Populate an initial find string, virtual keyboard not required.
|
||||
if (!TextUtils.isEmpty(text)) {
|
||||
// Populate initial selection
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mFindText.setText(text);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the virtual keyboard.
|
||||
if (mFindText.hasWindowFocus()) {
|
||||
getInputMethodManager(mFindText).showSoftInput(mFindText, 0);
|
||||
} else {
|
||||
// showSoftInput won't work until after the window is focused.
|
||||
mFindText.setOnWindowFocusChangeListener(new CustomEditText.OnWindowFocusChangeListener() {
|
||||
@Override
|
||||
public void onWindowFocusChanged(boolean hasFocus) {
|
||||
if (!hasFocus)
|
||||
return;
|
||||
|
||||
mFindText.setOnWindowFocusChangeListener(null);
|
||||
getInputMethodManager(mFindText).showSoftInput(mFindText, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request find operation, and update matchCount results (current count and total).
|
||||
*/
|
||||
private void sendRequestToFinderHelper(final String request, final String searchString) {
|
||||
final JSONObject json = new JSONObject();
|
||||
try {
|
||||
json.put("searchString", searchString);
|
||||
json.put("matchCase", mMatchCase.isChecked());
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "JSON error - Error creating JSONObject", e);
|
||||
return;
|
||||
}
|
||||
|
||||
GoannaAppShell.sendRequestToGoanna(new GoannaRequest(request, json) {
|
||||
@Override
|
||||
public void onResponse(NativeJSObject nativeJSObject) {
|
||||
final int total = nativeJSObject.optInt("total", 0);
|
||||
if (total == -1) {
|
||||
final int limit = nativeJSObject.optInt("limit", 0);
|
||||
updateResult(Integer.toString(limit) + "+");
|
||||
} else if (total > 0) {
|
||||
final int current = nativeJSObject.optInt("current", 0);
|
||||
updateResult(Integer.toString(current) + "/" + Integer.toString(total));
|
||||
} else {
|
||||
// We display no match-count information, when there were no
|
||||
// matches found, or if matching has been turned off by setting
|
||||
// pref accessibility.typeaheadfind.matchesCountLimit to 0.
|
||||
updateResult("");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(NativeJSObject error) {
|
||||
// Goanna didn't respond due to state change, javascript error, etc.
|
||||
Log.d(LOGTAG, "No response from Goanna on request to match string: [" +
|
||||
searchString + "]");
|
||||
updateResult("");
|
||||
}
|
||||
|
||||
private void updateResult(final String statusText) {
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mStatusText.setVisibility(statusText.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
mStatusText.setText(statusText);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,447 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.gfx.FloatSize;
|
||||
import org.mozilla.goanna.gfx.ImmutableViewportMetrics;
|
||||
import org.mozilla.goanna.util.GoannaEventListener;
|
||||
import org.mozilla.goanna.util.ThreadUtils;
|
||||
import org.mozilla.goanna.widget.SwipeDismissListViewTouchListener;
|
||||
import org.mozilla.goanna.widget.SwipeDismissListViewTouchListener.OnDismissCallback;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.PointF;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.RelativeLayout.LayoutParams;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
public class FormAssistPopup extends RelativeLayout implements GoannaEventListener {
|
||||
private final Context mContext;
|
||||
private final Animation mAnimation;
|
||||
|
||||
private ListView mAutoCompleteList;
|
||||
private RelativeLayout mValidationMessage;
|
||||
private TextView mValidationMessageText;
|
||||
private ImageView mValidationMessageArrow;
|
||||
private ImageView mValidationMessageArrowInverted;
|
||||
|
||||
private double mX;
|
||||
private double mY;
|
||||
private double mW;
|
||||
private double mH;
|
||||
|
||||
private enum PopupType {
|
||||
AUTOCOMPLETE,
|
||||
VALIDATIONMESSAGE;
|
||||
}
|
||||
private PopupType mPopupType;
|
||||
|
||||
private static final int MAX_VISIBLE_ROWS = 5;
|
||||
|
||||
private static int sAutoCompleteMinWidth;
|
||||
private static int sAutoCompleteRowHeight;
|
||||
private static int sValidationMessageHeight;
|
||||
private static int sValidationTextMarginTop;
|
||||
private static LayoutParams sValidationTextLayoutNormal;
|
||||
private static LayoutParams sValidationTextLayoutInverted;
|
||||
|
||||
private static final String LOGTAG = "GoannaFormAssistPopup";
|
||||
|
||||
// The blocklist is so short that ArrayList is probably cheaper than HashSet.
|
||||
private static final Collection<String> sInputMethodBlocklist = Arrays.asList(
|
||||
InputMethods.METHOD_GOOGLE_JAPANESE_INPUT, // bug 775850
|
||||
InputMethods.METHOD_OPENWNN_PLUS, // bug 768108
|
||||
InputMethods.METHOD_SIMEJI, // bug 768108
|
||||
InputMethods.METHOD_SWYPE, // bug 755909
|
||||
InputMethods.METHOD_SWYPE_BETA // bug 755909
|
||||
);
|
||||
|
||||
public FormAssistPopup(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mContext = context;
|
||||
|
||||
mAnimation = AnimationUtils.loadAnimation(context, R.anim.grow_fade_in);
|
||||
mAnimation.setDuration(75);
|
||||
|
||||
setFocusable(false);
|
||||
|
||||
EventDispatcher.getInstance().registerGoannaThreadListener(this,
|
||||
"FormAssist:AutoComplete",
|
||||
"FormAssist:ValidationMessage",
|
||||
"FormAssist:Hide");
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
EventDispatcher.getInstance().unregisterGoannaThreadListener(this,
|
||||
"FormAssist:AutoComplete",
|
||||
"FormAssist:ValidationMessage",
|
||||
"FormAssist:Hide");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(String event, JSONObject message) {
|
||||
try {
|
||||
if (event.equals("FormAssist:AutoComplete")) {
|
||||
handleAutoCompleteMessage(message);
|
||||
} else if (event.equals("FormAssist:ValidationMessage")) {
|
||||
handleValidationMessage(message);
|
||||
} else if (event.equals("FormAssist:Hide")) {
|
||||
handleHideMessage(message);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleAutoCompleteMessage(JSONObject message) throws JSONException {
|
||||
final JSONArray suggestions = message.getJSONArray("suggestions");
|
||||
final JSONObject rect = message.getJSONObject("rect");
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
showAutoCompleteSuggestions(suggestions, rect);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleValidationMessage(JSONObject message) throws JSONException {
|
||||
final String validationMessage = message.getString("validationMessage");
|
||||
final JSONObject rect = message.getJSONObject("rect");
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
showValidationMessage(validationMessage, rect);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleHideMessage(JSONObject message) {
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showAutoCompleteSuggestions(JSONArray suggestions, JSONObject rect) {
|
||||
if (mAutoCompleteList == null) {
|
||||
LayoutInflater inflater = LayoutInflater.from(mContext);
|
||||
mAutoCompleteList = (ListView) inflater.inflate(R.layout.autocomplete_list, null);
|
||||
|
||||
mAutoCompleteList.setOnItemClickListener(new OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parentView, View view, int position, long id) {
|
||||
// Use the value stored with the autocomplete view, not the label text,
|
||||
// since they can be different.
|
||||
TextView textView = (TextView) view;
|
||||
String value = (String) textView.getTag();
|
||||
broadcastGoannaEvent("FormAssist:AutoComplete", value);
|
||||
hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Create a ListView-specific touch listener. ListViews are given special treatment because
|
||||
// by default they handle touches for their list items... i.e. they're in charge of drawing
|
||||
// the pressed state (the list selector), handling list item clicks, etc.
|
||||
final SwipeDismissListViewTouchListener touchListener = new SwipeDismissListViewTouchListener(mAutoCompleteList, new OnDismissCallback() {
|
||||
@Override
|
||||
public void onDismiss(ListView listView, final int position) {
|
||||
// Use the value stored with the autocomplete view, not the label text,
|
||||
// since they can be different.
|
||||
AutoCompleteListAdapter adapter = (AutoCompleteListAdapter) listView.getAdapter();
|
||||
Pair<String, String> item = adapter.getItem(position);
|
||||
|
||||
// Remove the item from form history.
|
||||
broadcastGoannaEvent("FormAssist:Remove", item.second);
|
||||
|
||||
// Update the list
|
||||
adapter.remove(item);
|
||||
adapter.notifyDataSetChanged();
|
||||
positionAndShowPopup();
|
||||
}
|
||||
});
|
||||
mAutoCompleteList.setOnTouchListener(touchListener);
|
||||
|
||||
// Setting this scroll listener is required to ensure that during ListView scrolling,
|
||||
// we don't look for swipes.
|
||||
mAutoCompleteList.setOnScrollListener(touchListener.makeScrollListener());
|
||||
|
||||
// Setting this recycler listener is required to make sure animated views are reset.
|
||||
mAutoCompleteList.setRecyclerListener(touchListener.makeRecyclerListener());
|
||||
|
||||
addView(mAutoCompleteList);
|
||||
}
|
||||
|
||||
AutoCompleteListAdapter adapter = new AutoCompleteListAdapter(mContext, R.layout.autocomplete_list_item);
|
||||
adapter.populateSuggestionsList(suggestions);
|
||||
mAutoCompleteList.setAdapter(adapter);
|
||||
|
||||
if (setGoannaPositionData(rect, true)) {
|
||||
positionAndShowPopup();
|
||||
}
|
||||
}
|
||||
|
||||
private void showValidationMessage(String validationMessage, JSONObject rect) {
|
||||
if (mValidationMessage == null) {
|
||||
LayoutInflater inflater = LayoutInflater.from(mContext);
|
||||
mValidationMessage = (RelativeLayout) inflater.inflate(R.layout.validation_message, null);
|
||||
|
||||
addView(mValidationMessage);
|
||||
mValidationMessageText = (TextView) mValidationMessage.findViewById(R.id.validation_message_text);
|
||||
|
||||
sValidationTextMarginTop = (int) (mContext.getResources().getDimension(R.dimen.validation_message_margin_top));
|
||||
|
||||
sValidationTextLayoutNormal = new LayoutParams(mValidationMessageText.getLayoutParams());
|
||||
sValidationTextLayoutNormal.setMargins(0, sValidationTextMarginTop, 0, 0);
|
||||
|
||||
sValidationTextLayoutInverted = new LayoutParams((ViewGroup.MarginLayoutParams) sValidationTextLayoutNormal);
|
||||
sValidationTextLayoutInverted.setMargins(0, 0, 0, 0);
|
||||
|
||||
mValidationMessageArrow = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow);
|
||||
mValidationMessageArrowInverted = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow_inverted);
|
||||
}
|
||||
|
||||
mValidationMessageText.setText(validationMessage);
|
||||
|
||||
// We need to set the text as selected for the marquee text to work.
|
||||
mValidationMessageText.setSelected(true);
|
||||
|
||||
if (setGoannaPositionData(rect, false)) {
|
||||
positionAndShowPopup();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean setGoannaPositionData(JSONObject rect, boolean isAutoComplete) {
|
||||
try {
|
||||
mX = rect.getDouble("x");
|
||||
mY = rect.getDouble("y");
|
||||
mW = rect.getDouble("w");
|
||||
mH = rect.getDouble("h");
|
||||
} catch (JSONException e) {
|
||||
// Bail if we can't get the correct dimensions for the popup.
|
||||
Log.e(LOGTAG, "Error getting FormAssistPopup dimensions", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
mPopupType = (isAutoComplete ?
|
||||
PopupType.AUTOCOMPLETE : PopupType.VALIDATIONMESSAGE);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void positionAndShowPopup() {
|
||||
positionAndShowPopup(GoannaAppShell.getLayerView().getViewportMetrics());
|
||||
}
|
||||
|
||||
private void positionAndShowPopup(ImmutableViewportMetrics aMetrics) {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
|
||||
// Don't show the form assist popup when using fullscreen VKB
|
||||
InputMethodManager imm =
|
||||
(InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (imm.isFullscreenMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide/show the appropriate popup contents
|
||||
if (mAutoCompleteList != null) {
|
||||
mAutoCompleteList.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? VISIBLE : GONE);
|
||||
}
|
||||
if (mValidationMessage != null) {
|
||||
mValidationMessage.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? GONE : VISIBLE);
|
||||
}
|
||||
|
||||
if (sAutoCompleteMinWidth == 0) {
|
||||
Resources res = mContext.getResources();
|
||||
sAutoCompleteMinWidth = (int) (res.getDimension(R.dimen.autocomplete_min_width));
|
||||
sAutoCompleteRowHeight = (int) (res.getDimension(R.dimen.autocomplete_row_height));
|
||||
sValidationMessageHeight = (int) (res.getDimension(R.dimen.validation_message_height));
|
||||
}
|
||||
|
||||
float zoom = aMetrics.zoomFactor;
|
||||
PointF offset = aMetrics.getMarginOffset();
|
||||
|
||||
// These values correspond to the input box for which we want to
|
||||
// display the FormAssistPopup.
|
||||
int left = (int) (mX * zoom - aMetrics.viewportRectLeft + offset.x);
|
||||
int top = (int) (mY * zoom - aMetrics.viewportRectTop + offset.y);
|
||||
int width = (int) (mW * zoom);
|
||||
int height = (int) (mH * zoom);
|
||||
|
||||
int popupWidth = LayoutParams.MATCH_PARENT;
|
||||
int popupLeft = left < 0 ? 0 : left;
|
||||
|
||||
FloatSize viewport = aMetrics.getSize();
|
||||
|
||||
// For autocomplete suggestions, if the input is smaller than the screen-width,
|
||||
// shrink the popup's width. Otherwise, keep it as MATCH_PARENT.
|
||||
if ((mPopupType == PopupType.AUTOCOMPLETE) && (left + width) < viewport.width) {
|
||||
popupWidth = left < 0 ? left + width : width;
|
||||
|
||||
// Ensure the popup has a minimum width.
|
||||
if (popupWidth < sAutoCompleteMinWidth) {
|
||||
popupWidth = sAutoCompleteMinWidth;
|
||||
|
||||
// Move the popup to the left if there isn't enough room for it.
|
||||
if ((popupLeft + popupWidth) > viewport.width) {
|
||||
popupLeft = (int) (viewport.width - popupWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int popupHeight;
|
||||
if (mPopupType == PopupType.AUTOCOMPLETE) {
|
||||
// Limit the amount of visible rows.
|
||||
int rows = mAutoCompleteList.getAdapter().getCount();
|
||||
if (rows > MAX_VISIBLE_ROWS) {
|
||||
rows = MAX_VISIBLE_ROWS;
|
||||
}
|
||||
|
||||
popupHeight = sAutoCompleteRowHeight * rows;
|
||||
} else {
|
||||
popupHeight = sValidationMessageHeight;
|
||||
}
|
||||
|
||||
int popupTop = top + height;
|
||||
|
||||
if (mPopupType == PopupType.VALIDATIONMESSAGE) {
|
||||
mValidationMessageText.setLayoutParams(sValidationTextLayoutNormal);
|
||||
mValidationMessageArrow.setVisibility(VISIBLE);
|
||||
mValidationMessageArrowInverted.setVisibility(GONE);
|
||||
}
|
||||
|
||||
// If the popup doesn't fit below the input box, shrink its height, or
|
||||
// see if we can place it above the input instead.
|
||||
if ((popupTop + popupHeight) > viewport.height) {
|
||||
// Find where the maximum space is, and put the popup there.
|
||||
if ((viewport.height - popupTop) > top) {
|
||||
// Shrink the height to fit it below the input box.
|
||||
popupHeight = (int) (viewport.height - popupTop);
|
||||
} else {
|
||||
if (popupHeight < top) {
|
||||
// No shrinking needed to fit on top.
|
||||
popupTop = (top - popupHeight);
|
||||
} else {
|
||||
// Shrink to available space on top.
|
||||
popupTop = 0;
|
||||
popupHeight = top;
|
||||
}
|
||||
|
||||
if (mPopupType == PopupType.VALIDATIONMESSAGE) {
|
||||
mValidationMessageText.setLayoutParams(sValidationTextLayoutInverted);
|
||||
mValidationMessageArrow.setVisibility(GONE);
|
||||
mValidationMessageArrowInverted.setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LayoutParams layoutParams = new LayoutParams(popupWidth, popupHeight);
|
||||
layoutParams.setMargins(popupLeft, popupTop, 0, 0);
|
||||
setLayoutParams(layoutParams);
|
||||
requestLayout();
|
||||
|
||||
if (!isShown()) {
|
||||
setVisibility(VISIBLE);
|
||||
startAnimation(mAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
if (isShown()) {
|
||||
setVisibility(GONE);
|
||||
broadcastGoannaEvent("FormAssist:Hidden", null);
|
||||
}
|
||||
}
|
||||
|
||||
void onInputMethodChanged(String newInputMethod) {
|
||||
boolean blocklisted = sInputMethodBlocklist.contains(newInputMethod);
|
||||
broadcastGoannaEvent("FormAssist:Blocklisted", String.valueOf(blocklisted));
|
||||
}
|
||||
|
||||
void onMetricsChanged(final ImmutableViewportMetrics aMetrics) {
|
||||
if (!isShown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
positionAndShowPopup(aMetrics);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void broadcastGoannaEvent(String eventName, String eventData) {
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent(eventName, eventData));
|
||||
}
|
||||
|
||||
private class AutoCompleteListAdapter extends ArrayAdapter<Pair<String, String>> {
|
||||
private final LayoutInflater mInflater;
|
||||
private final int mTextViewResourceId;
|
||||
|
||||
public AutoCompleteListAdapter(Context context, int textViewResourceId) {
|
||||
super(context, textViewResourceId);
|
||||
|
||||
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
mTextViewResourceId = textViewResourceId;
|
||||
}
|
||||
|
||||
// This method takes an array of autocomplete suggestions with label/value properties
|
||||
// and adds label/value Pair objects to the array that backs the adapter.
|
||||
public void populateSuggestionsList(JSONArray suggestions) {
|
||||
try {
|
||||
for (int i = 0; i < suggestions.length(); i++) {
|
||||
JSONObject suggestion = suggestions.getJSONObject(i);
|
||||
String label = suggestion.getString("label");
|
||||
String value = suggestion.getString("value");
|
||||
add(new Pair<String, String>(label, value));
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "JSONException", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = mInflater.inflate(mTextViewResourceId, null);
|
||||
}
|
||||
|
||||
Pair<String, String> item = getItem(position);
|
||||
TextView itemView = (TextView) convertView;
|
||||
|
||||
// Set the text with the suggestion label
|
||||
itemView.setText(item.first);
|
||||
|
||||
// Set a tag with the suggestion value
|
||||
itemView.setTag(item.second);
|
||||
|
||||
return convertView;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
|
||||
import org.mozilla.goanna.db.BrowserDB;
|
||||
import org.mozilla.goanna.util.ThreadUtils;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
class GlobalHistory {
|
||||
private static final String LOGTAG = "GoannaGlobalHistory";
|
||||
|
||||
private static final String TELEMETRY_HISTOGRAM_ADD = "FENNEC_GLOBALHISTORY_ADD_MS";
|
||||
private static final String TELEMETRY_HISTOGRAM_UPDATE = "FENNEC_GLOBALHISTORY_UPDATE_MS";
|
||||
private static final String TELEMETRY_HISTOGRAM_BUILD_VISITED_LINK = "FENNEC_GLOBALHISTORY_VISITED_BUILD_MS";
|
||||
|
||||
private static final GlobalHistory sInstance = new GlobalHistory();
|
||||
|
||||
static GlobalHistory getInstance() {
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
// this is the delay between receiving a URI check request and processing it.
|
||||
// this allows batching together multiple requests and processing them together,
|
||||
// which is more efficient.
|
||||
private static final long BATCHING_DELAY_MS = 100;
|
||||
|
||||
private final Handler mHandler; // a background thread on which we can process requests
|
||||
|
||||
// Note: These fields are accessed through the NotificationRunnable inner class.
|
||||
final Queue<String> mPendingUris; // URIs that need to be checked
|
||||
SoftReference<Set<String>> mVisitedCache; // cache of the visited URI list
|
||||
boolean mProcessing; // = false // whether or not the runnable is queued/working
|
||||
|
||||
private class NotifierRunnable implements Runnable {
|
||||
private final ContentResolver mContentResolver;
|
||||
private final BrowserDB mDB;
|
||||
|
||||
public NotifierRunnable(final Context context) {
|
||||
mContentResolver = context.getContentResolver();
|
||||
mDB = GoannaProfile.get(context).getDB();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Set<String> visitedSet = mVisitedCache.get();
|
||||
if (visitedSet == null) {
|
||||
// The cache was wiped. Repopulate it.
|
||||
Log.w(LOGTAG, "Rebuilding visited link set...");
|
||||
final long start = SystemClock.uptimeMillis();
|
||||
final Cursor c = mDB.getAllVisitedHistory(mContentResolver);
|
||||
if (c == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
visitedSet = new HashSet<String>();
|
||||
if (c.moveToFirst()) {
|
||||
do {
|
||||
visitedSet.add(c.getString(0));
|
||||
} while (c.moveToNext());
|
||||
}
|
||||
mVisitedCache = new SoftReference<Set<String>>(visitedSet);
|
||||
final long end = SystemClock.uptimeMillis();
|
||||
final long took = end - start;
|
||||
Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_BUILD_VISITED_LINK, (int) Math.min(took, Integer.MAX_VALUE));
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
|
||||
// This runs on the same handler thread as the checkUriVisited code,
|
||||
// so no synchronization is needed.
|
||||
while (true) {
|
||||
final String uri = mPendingUris.poll();
|
||||
if (uri == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (visitedSet.contains(uri)) {
|
||||
GoannaAppShell.notifyUriVisited(uri);
|
||||
}
|
||||
}
|
||||
|
||||
mProcessing = false;
|
||||
}
|
||||
};
|
||||
|
||||
private GlobalHistory() {
|
||||
mHandler = ThreadUtils.getBackgroundHandler();
|
||||
mPendingUris = new LinkedList<String>();
|
||||
mVisitedCache = new SoftReference<Set<String>>(null);
|
||||
}
|
||||
|
||||
public void addToGoannaOnly(String uri) {
|
||||
Set<String> visitedSet = mVisitedCache.get();
|
||||
if (visitedSet != null) {
|
||||
visitedSet.add(uri);
|
||||
}
|
||||
GoannaAppShell.notifyUriVisited(uri);
|
||||
}
|
||||
|
||||
public void add(final Context context, final BrowserDB db, String uri) {
|
||||
ThreadUtils.assertOnBackgroundThread();
|
||||
final long start = SystemClock.uptimeMillis();
|
||||
|
||||
db.updateVisitedHistory(context.getContentResolver(), uri);
|
||||
|
||||
final long end = SystemClock.uptimeMillis();
|
||||
final long took = end - start;
|
||||
Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_ADD, (int) Math.min(took, Integer.MAX_VALUE));
|
||||
addToGoannaOnly(uri);
|
||||
}
|
||||
|
||||
@SuppressWarnings("static-method")
|
||||
public void update(final ContentResolver cr, final BrowserDB db, String uri, String title) {
|
||||
ThreadUtils.assertOnBackgroundThread();
|
||||
final long start = SystemClock.uptimeMillis();
|
||||
|
||||
db.updateHistoryTitle(cr, uri, title);
|
||||
|
||||
final long end = SystemClock.uptimeMillis();
|
||||
final long took = end - start;
|
||||
Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_UPDATE, (int) Math.min(took, Integer.MAX_VALUE));
|
||||
}
|
||||
|
||||
public void checkUriVisited(final String uri) {
|
||||
final NotifierRunnable runnable = new NotifierRunnable(GoannaAppShell.getContext());
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// this runs on the same handler thread as the processing loop,
|
||||
// so no synchronization needed
|
||||
mPendingUris.add(uri);
|
||||
if (mProcessing) {
|
||||
// there's already a runnable queued up or working away, so
|
||||
// no need to post another
|
||||
return;
|
||||
}
|
||||
mProcessing = true;
|
||||
mHandler.postDelayed(runnable, BATCHING_DELAY_MS);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,434 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.goanna.AppConstants.Versions;
|
||||
import org.mozilla.goanna.gfx.LayerView;
|
||||
import org.mozilla.goanna.util.ThreadUtils;
|
||||
import org.mozilla.goanna.util.UIAsyncTask;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.ActivityManager.RunningServiceInfo;
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.view.accessibility.AccessibilityNodeProvider;
|
||||
|
||||
import com.googlecode.eyesfree.braille.selfbraille.SelfBrailleClient;
|
||||
import com.googlecode.eyesfree.braille.selfbraille.WriteData;
|
||||
|
||||
public class GoannaAccessibility {
|
||||
private static final String LOGTAG = "GoannaAccessibility";
|
||||
private static final int VIRTUAL_ENTRY_POINT_BEFORE = 1;
|
||||
private static final int VIRTUAL_CURSOR_PREVIOUS = 2;
|
||||
private static final int VIRTUAL_CURSOR_POSITION = 3;
|
||||
private static final int VIRTUAL_CURSOR_NEXT = 4;
|
||||
private static final int VIRTUAL_ENTRY_POINT_AFTER = 5;
|
||||
|
||||
private static boolean sEnabled;
|
||||
// Used to store the JSON message and populate the event later in the code path.
|
||||
private static JSONObject sEventMessage;
|
||||
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;
|
||||
|
||||
private static final HashSet<String> sServiceWhitelist =
|
||||
new HashSet<String>(Arrays.asList(new String[] {
|
||||
"com.google.android.marvin.talkback.TalkBackService", // Google Talkback screen reader
|
||||
"com.mot.readout.ScreenReader", // Motorola screen reader
|
||||
"info.spielproject.spiel.SpielService", // Spiel screen reader
|
||||
"es.codefactory.android.app.ma.MAAccessibilityService" // Codefactory Mobile Accessibility screen reader
|
||||
}));
|
||||
|
||||
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);
|
||||
if (accessibilityManager.isEnabled()) {
|
||||
ActivityManager activityManager =
|
||||
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
List<RunningServiceInfo> runningServices = activityManager.getRunningServices(Integer.MAX_VALUE);
|
||||
|
||||
for (RunningServiceInfo runningServiceInfo : runningServices) {
|
||||
sEnabled = sServiceWhitelist.contains(runningServiceInfo.service.getClassName());
|
||||
if (sEnabled)
|
||||
break;
|
||||
}
|
||||
if (Versions.feature16Plus && sEnabled && sSelfBrailleClient == null) {
|
||||
sSelfBrailleClient = new SelfBrailleClient(GoannaAppShell.getContext(), false);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
ret.put("enabled", sEnabled);
|
||||
} catch (Exception ex) {
|
||||
Log.e(LOGTAG, "Error building JSON arguments for Accessibility:Settings:", ex);
|
||||
}
|
||||
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("Accessibility:Settings",
|
||||
ret.toString()));
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute(Void args) {
|
||||
boolean isGoannaApp = false;
|
||||
try {
|
||||
isGoannaApp = context instanceof GoannaApp;
|
||||
} catch (NoClassDefFoundError ex) {}
|
||||
if (isGoannaApp) {
|
||||
// Disable the dynamic toolbar when enabling accessibility.
|
||||
// These features tend not to interact well.
|
||||
((GoannaApp) context).setAccessibilityEnabled(sEnabled);
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
private static void populateEventFromJSON (AccessibilityEvent event, JSONObject message) {
|
||||
final JSONArray textArray = message.optJSONArray("text");
|
||||
if (textArray != null) {
|
||||
for (int i = 0; i < textArray.length(); i++)
|
||||
event.getText().add(textArray.optString(i));
|
||||
}
|
||||
|
||||
event.setContentDescription(message.optString("description"));
|
||||
event.setEnabled(message.optBoolean("enabled", true));
|
||||
event.setChecked(message.optBoolean("checked"));
|
||||
event.setPassword(message.optBoolean("password"));
|
||||
event.setAddedCount(message.optInt("addedCount", -1));
|
||||
event.setRemovedCount(message.optInt("removedCount", -1));
|
||||
event.setFromIndex(message.optInt("fromIndex", -1));
|
||||
event.setItemCount(message.optInt("itemCount", -1));
|
||||
event.setCurrentItemIndex(message.optInt("currentItemIndex", -1));
|
||||
event.setBeforeText(message.optString("beforeText"));
|
||||
if (Versions.feature14Plus) {
|
||||
event.setToIndex(message.optInt("toIndex", -1));
|
||||
event.setScrollable(message.optBoolean("scrollable"));
|
||||
event.setScrollX(message.optInt("scrollX", -1));
|
||||
event.setScrollY(message.optInt("scrollY", -1));
|
||||
}
|
||||
if (Versions.feature15Plus) {
|
||||
event.setMaxScrollX(message.optInt("maxScrollX", -1));
|
||||
event.setMaxScrollY(message.optInt("maxScrollY", -1));
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendDirectAccessibilityEvent(int eventType, JSONObject message) {
|
||||
final AccessibilityEvent accEvent = AccessibilityEvent.obtain(eventType);
|
||||
accEvent.setClassName(GoannaAccessibility.class.getName());
|
||||
accEvent.setPackageName(GoannaAppShell.getContext().getPackageName());
|
||||
populateEventFromJSON(accEvent, message);
|
||||
AccessibilityManager accessibilityManager =
|
||||
(AccessibilityManager) GoannaAppShell.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
|
||||
try {
|
||||
accessibilityManager.sendAccessibilityEvent(accEvent);
|
||||
} catch (IllegalStateException e) {
|
||||
// Accessibility is off.
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendAccessibilityEvent (final JSONObject message) {
|
||||
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;
|
||||
}
|
||||
|
||||
final int eventType = message.optInt("eventType", -1);
|
||||
if (eventType < 0) {
|
||||
Log.e(LOGTAG, "No accessibility event type provided");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Versions.preJB) {
|
||||
// Before Jelly Bean we send events directly from here while spoofing the source by setting
|
||||
// the package and class name manually.
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
sendDirectAccessibilityEvent(eventType, message);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// In Jelly Bean we populate an AccessibilityNodeInfo with the minimal amount of data to have
|
||||
// it work with TalkBack.
|
||||
final LayerView view = GoannaAppShell.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"));
|
||||
}
|
||||
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// If this is an accessibility focus, a lot of internal voodoo happens so we perform an
|
||||
// accessibility focus action on the view, and it in turn sends the right events.
|
||||
switch (eventType) {
|
||||
case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
|
||||
sEventMessage = message;
|
||||
view.performAccessibilityAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
|
||||
break;
|
||||
case AccessibilityEvent.TYPE_ANNOUNCEMENT:
|
||||
case AccessibilityEvent.TYPE_VIEW_SCROLLED:
|
||||
sEventMessage = null;
|
||||
final AccessibilityEvent accEvent = AccessibilityEvent.obtain(eventType);
|
||||
view.onInitializeAccessibilityEvent(accEvent);
|
||||
populateEventFromJSON(accEvent, message);
|
||||
view.getParent().requestSendAccessibilityEvent(view, accEvent);
|
||||
break;
|
||||
default:
|
||||
sEventMessage = message;
|
||||
view.sendAccessibilityEvent(eventType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendBrailleText(final View view, final String text, final int selectionStart, final int selectionEnd) {
|
||||
AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(view, VIRTUAL_CURSOR_POSITION);
|
||||
WriteData data = WriteData.forInfo(info);
|
||||
data.setText(text);
|
||||
// Set either the focus blink or the current caret position/selection
|
||||
data.setSelectionStart(selectionStart);
|
||||
data.setSelectionEnd(selectionEnd);
|
||||
sSelfBrailleClient.write(data);
|
||||
}
|
||||
|
||||
public static void setDelegate(LayerView layerview) {
|
||||
// Only use this delegate in Jelly Bean.
|
||||
if (Versions.feature16Plus) {
|
||||
layerview.setAccessibilityDelegate(new GoannaAccessibilityDelegate());
|
||||
layerview.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setAccessibilityStateChangeListener(final Context context) {
|
||||
// The state change listener is only supported on API14+
|
||||
if (Versions.feature14Plus) {
|
||||
AccessibilityManager accessibilityManager =
|
||||
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
|
||||
accessibilityManager.addAccessibilityStateChangeListener(new AccessibilityManager.AccessibilityStateChangeListener() {
|
||||
@Override
|
||||
public void onAccessibilityStateChanged(boolean enabled) {
|
||||
updateAccessibilitySettings(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void onLayerViewFocusChanged(LayerView layerview, boolean gainFocus) {
|
||||
if (sEnabled)
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("Accessibility:Focus",
|
||||
gainFocus ? "true" : "false"));
|
||||
}
|
||||
|
||||
public static class GoannaAccessibilityDelegate extends View.AccessibilityDelegate {
|
||||
AccessibilityNodeProvider mAccessibilityNodeProvider;
|
||||
|
||||
@Override
|
||||
public void onPopulateAccessibilityEvent (View host, AccessibilityEvent event) {
|
||||
super.onPopulateAccessibilityEvent(host, event);
|
||||
if (sEventMessage != null) {
|
||||
populateEventFromJSON(event, sEventMessage);
|
||||
event.setSource(host, sCurrentNode);
|
||||
}
|
||||
// We save the hover enter event so that we could reuse it for a subsequent accessibility focus event.
|
||||
if (event.getEventType() != AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
|
||||
sEventMessage = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccessibilityNodeProvider getAccessibilityNodeProvider(final View host) {
|
||||
if (mAccessibilityNodeProvider == null)
|
||||
// The accessibility node structure for web content consists of 5 LayerView child nodes:
|
||||
// 1. VIRTUAL_ENTRY_POINT_BEFORE: Represents the entry point before the LayerView.
|
||||
// 2. VIRTUAL_CURSOR_PREVIOUS: Represents the virtual cursor position that is previous to the
|
||||
// current one.
|
||||
// 3. VIRTUAL_CURSOR_POSITION: Represents the current position of the virtual cursor.
|
||||
// 4. VIRTUAL_CURSOR_NEXT: Represents the next virtual cursor position.
|
||||
// 5. 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_PREVIOUS);
|
||||
info.addChild(host, VIRTUAL_CURSOR_POSITION);
|
||||
info.addChild(host, VIRTUAL_CURSOR_NEXT);
|
||||
info.addChild(host, VIRTUAL_ENTRY_POINT_AFTER);
|
||||
break;
|
||||
default:
|
||||
info.setParent(host);
|
||||
info.setSource(host, virtualDescendantId);
|
||||
info.setVisibleToUser(host.isShown());
|
||||
info.setPackageName(GoannaAppShell.getContext().getPackageName());
|
||||
info.setClassName(host.getClass().getName());
|
||||
info.setEnabled(true);
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
|
||||
info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER |
|
||||
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD |
|
||||
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 accessibility focus is requested on one of its siblings we move the virtual cursor
|
||||
// either forward or backward depending on which sibling was selected.
|
||||
// When we enter the view forward or backward we just ask Goanna to get focus, keeping the current position.
|
||||
|
||||
switch (virtualViewId) {
|
||||
case VIRTUAL_CURSOR_PREVIOUS:
|
||||
GoannaAppShell.
|
||||
sendEventToGoanna(GoannaEvent.createBroadcastEvent("Accessibility:PreviousObject", null));
|
||||
return true;
|
||||
case VIRTUAL_CURSOR_NEXT:
|
||||
GoannaAppShell.
|
||||
sendEventToGoanna(GoannaEvent.createBroadcastEvent("Accessibility:NextObject", null));
|
||||
return true;
|
||||
case VIRTUAL_ENTRY_POINT_BEFORE:
|
||||
case VIRTUAL_ENTRY_POINT_AFTER:
|
||||
GoannaAppShell.
|
||||
sendEventToGoanna(GoannaEvent.createBroadcastEvent("Accessibility:Focus", "true"));
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (action == AccessibilityNodeInfo.ACTION_CLICK && virtualViewId == VIRTUAL_CURSOR_POSITION) {
|
||||
GoannaAppShell.
|
||||
sendEventToGoanna(GoannaEvent.createBroadcastEvent("Accessibility:ActivateObject", null));
|
||||
return true;
|
||||
} else if (action == AccessibilityNodeInfo.ACTION_LONG_CLICK && virtualViewId == VIRTUAL_CURSOR_POSITION) {
|
||||
GoannaAppShell.
|
||||
sendEventToGoanna(GoannaEvent.createBroadcastEvent("Accessibility:LongPress", null));
|
||||
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
|
||||
int granularity = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
|
||||
if (granularity < 0) {
|
||||
int keyIndex = BRAILLE_CLICK_BASE_INDEX - granularity;
|
||||
JSONObject activationData = new JSONObject();
|
||||
try {
|
||||
activationData.put("keyIndex", keyIndex);
|
||||
} catch (JSONException e) {
|
||||
return true;
|
||||
}
|
||||
GoannaAppShell.
|
||||
sendEventToGoanna(GoannaEvent.createBroadcastEvent("Accessibility:ActivateObject", activationData.toString()));
|
||||
} else {
|
||||
JSONObject movementData = new JSONObject();
|
||||
try {
|
||||
movementData.put("direction", "Next");
|
||||
movementData.put("granularity", granularity);
|
||||
} catch (JSONException e) {
|
||||
return true;
|
||||
}
|
||||
GoannaAppShell.
|
||||
sendEventToGoanna(GoannaEvent.createBroadcastEvent("Accessibility:MoveByGranularity", movementData.toString()));
|
||||
}
|
||||
return true;
|
||||
} else if (action == AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY &&
|
||||
virtualViewId == VIRTUAL_CURSOR_POSITION) {
|
||||
JSONObject movementData = new JSONObject();
|
||||
try {
|
||||
movementData.put("direction", "Previous");
|
||||
movementData.put("granularity", arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT));
|
||||
} catch (JSONException e) {
|
||||
return true;
|
||||
}
|
||||
GoannaAppShell.
|
||||
sendEventToGoanna(GoannaEvent.createBroadcastEvent("Accessibility:MoveByGranularity", movementData.toString()));
|
||||
return true;
|
||||
}
|
||||
return host.performAccessibilityAction(action, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
return mAccessibilityNodeProvider;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
|
||||
public class GoannaActivity extends FragmentActivity implements GoannaActivityStatus {
|
||||
// has this activity recently started another Goanna activity?
|
||||
private boolean mGoannaActivityOpened;
|
||||
|
||||
/**
|
||||
* Display any resources that show strings or encompass locale-specific
|
||||
* representations.
|
||||
*
|
||||
* onLocaleReady must always be called on the UI thread.
|
||||
*/
|
||||
public void onLocaleReady(final String locale) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
|
||||
if (getApplication() instanceof GoannaApplication) {
|
||||
((GoannaApplication) getApplication()).onActivityPause(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (getApplication() instanceof GoannaApplication) {
|
||||
((GoannaApplication) getApplication()).onActivityResume(this);
|
||||
mGoannaActivityOpened = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(android.os.Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (AppConstants.MOZ_ANDROID_ANR_REPORTER) {
|
||||
ANRReporter.register(getApplicationContext());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (AppConstants.MOZ_ANDROID_ANR_REPORTER) {
|
||||
ANRReporter.unregister();
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startActivity(Intent intent) {
|
||||
mGoannaActivityOpened = checkIfGoannaActivity(intent);
|
||||
super.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startActivityForResult(Intent intent, int request) {
|
||||
mGoannaActivityOpened = checkIfGoannaActivity(intent);
|
||||
super.startActivityForResult(intent, request);
|
||||
}
|
||||
|
||||
private static boolean checkIfGoannaActivity(Intent intent) {
|
||||
// Whenever we call our own activity, the component and its package name is set.
|
||||
// If we call an activity from another package, or an open intent (leaving android to resolve)
|
||||
// component has a different package name or it is null.
|
||||
ComponentName component = intent.getComponent();
|
||||
return (component != null &&
|
||||
AppConstants.ANDROID_PACKAGE_NAME.equals(component.getPackageName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGoannaActivityOpened() {
|
||||
return mGoannaActivityOpened;
|
||||
}
|
||||
|
||||
public boolean isApplicationInBackground() {
|
||||
return ((GoannaApplication) getApplication()).isApplicationInBackground();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLowMemory() {
|
||||
MemoryMonitor.getInstance().onLowMemory();
|
||||
super.onLowMemory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrimMemory(int level) {
|
||||
MemoryMonitor.getInstance().onTrimMemory(level);
|
||||
super.onTrimMemory(level);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
public interface GoannaActivityStatus {
|
||||
public boolean isGoannaActivityOpened();
|
||||
public boolean isFinishing(); // typically from android.app.Activity
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,168 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.db.BrowserContract;
|
||||
import org.mozilla.goanna.db.BrowserDB;
|
||||
import org.mozilla.goanna.db.LocalBrowserDB;
|
||||
import org.mozilla.goanna.home.HomePanelsManager;
|
||||
import org.mozilla.goanna.lwt.LightweightTheme;
|
||||
import org.mozilla.goanna.mozglue.GoannaLoader;
|
||||
import org.mozilla.goanna.util.Clipboard;
|
||||
import org.mozilla.goanna.util.HardwareUtils;
|
||||
import org.mozilla.goanna.util.ThreadUtils;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class GoannaApplication extends Application
|
||||
implements ContextGetter {
|
||||
private static final String LOG_TAG = "GoannaApplication";
|
||||
|
||||
private static volatile GoannaApplication instance;
|
||||
|
||||
private boolean mInBackground;
|
||||
private boolean mPausedGoanna;
|
||||
|
||||
private LightweightTheme mLightweightTheme;
|
||||
|
||||
public GoannaApplication() {
|
||||
super();
|
||||
instance = this;
|
||||
}
|
||||
|
||||
public static GoannaApplication get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context getContext() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SharedPreferences getSharedPreferences() {
|
||||
return GoannaSharedPrefs.forApp(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to do locale work here, because we need to intercept
|
||||
* each hit to onConfigurationChanged.
|
||||
*/
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration config) {
|
||||
Log.d(LOG_TAG, "onConfigurationChanged: " + config.locale +
|
||||
", background: " + mInBackground);
|
||||
|
||||
// Do nothing if we're in the background. It'll simply cause a loop
|
||||
// (Bug 936756 Comment 11), and it's not necessary.
|
||||
if (mInBackground) {
|
||||
super.onConfigurationChanged(config);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, correct the locale. This catches some cases that GoannaApp
|
||||
// doesn't get a chance to.
|
||||
try {
|
||||
BrowserLocaleManager.getInstance().correctLocale(this, getResources(), config);
|
||||
} catch (IllegalStateException ex) {
|
||||
// GoannaApp hasn't started, so we have no ContextGetter in BrowserLocaleManager.
|
||||
Log.w(LOG_TAG, "Couldn't correct locale.", ex);
|
||||
}
|
||||
|
||||
super.onConfigurationChanged(config);
|
||||
}
|
||||
|
||||
public void onActivityPause(GoannaActivityStatus activity) {
|
||||
mInBackground = true;
|
||||
|
||||
if ((activity.isFinishing() == false) &&
|
||||
(activity.isGoannaActivityOpened() == false)) {
|
||||
// Notify Goanna that we are pausing; the cache service will be
|
||||
// shutdown, closing the disk cache cleanly. If the android
|
||||
// low memory killer subsequently kills us, the disk cache will
|
||||
// be left in a consistent state, avoiding costly cleanup and
|
||||
// re-creation.
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createAppBackgroundingEvent());
|
||||
mPausedGoanna = true;
|
||||
|
||||
final BrowserDB db = GoannaProfile.get(this).getDB();
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
db.expireHistory(getContentResolver(), BrowserContract.ExpirePriority.NORMAL);
|
||||
}
|
||||
});
|
||||
}
|
||||
GoannaConnectivityReceiver.getInstance().stop();
|
||||
GoannaNetworkManager.getInstance().stop();
|
||||
}
|
||||
|
||||
public void onActivityResume(GoannaActivityStatus activity) {
|
||||
if (mPausedGoanna) {
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createAppForegroundingEvent());
|
||||
mPausedGoanna = false;
|
||||
}
|
||||
|
||||
final Context applicationContext = getApplicationContext();
|
||||
GoannaBatteryManager.getInstance().start(applicationContext);
|
||||
GoannaConnectivityReceiver.getInstance().start(applicationContext);
|
||||
GoannaNetworkManager.getInstance().start(applicationContext);
|
||||
|
||||
mInBackground = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
final Context context = getApplicationContext();
|
||||
HardwareUtils.init(context);
|
||||
Clipboard.init(context);
|
||||
FilePicker.init(context);
|
||||
GoannaLoader.loadMozGlue(context);
|
||||
DownloadsIntegration.init();
|
||||
HomePanelsManager.getInstance().init(context);
|
||||
|
||||
// This getInstance call will force initialization of the NotificationHelper, but does nothing with the result
|
||||
NotificationHelper.getInstance(context).init();
|
||||
|
||||
// Make sure that all browser-ish applications default to the real LocalBrowserDB.
|
||||
// GoannaView consumers use their own Application class, so this doesn't affect them.
|
||||
// WebappImpl overrides this on creation.
|
||||
//
|
||||
// We need to do this before any access to the profile; it controls
|
||||
// which database class is used.
|
||||
//
|
||||
// As such, this needs to occur before the GoannaView in GoannaApp is inflated -- i.e., in the
|
||||
// GoannaApp constructor or earlier -- because GoannaView implicitly accesses the profile. This is earlier!
|
||||
GoannaProfile.setBrowserDBFactory(new BrowserDB.Factory() {
|
||||
@Override
|
||||
public BrowserDB get(String profileName, File profileDir) {
|
||||
// Note that we don't use the profile directory -- we
|
||||
// send operations to the ContentProvider, which does
|
||||
// its own thing.
|
||||
return new LocalBrowserDB(profileName);
|
||||
}
|
||||
});
|
||||
|
||||
super.onCreate();
|
||||
}
|
||||
|
||||
public boolean isApplicationInBackground() {
|
||||
return mInBackground;
|
||||
}
|
||||
|
||||
public LightweightTheme getLightweightTheme() {
|
||||
return mLightweightTheme;
|
||||
}
|
||||
|
||||
public void prepareLightweightTheme() {
|
||||
mLightweightTheme = new LightweightTheme(this);
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
public class GoannaBatteryManager extends BroadcastReceiver {
|
||||
private static final String LOGTAG = "GoannaBatteryManager";
|
||||
|
||||
// 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 GoannaBatteryManager sInstance = new GoannaBatteryManager();
|
||||
|
||||
private final IntentFilter mFilter;
|
||||
private Context mApplicationContext;
|
||||
private boolean mIsEnabled;
|
||||
|
||||
public static GoannaBatteryManager getInstance() {
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
private GoannaBatteryManager() {
|
||||
mFilter = new IntentFilter();
|
||||
mFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
|
||||
}
|
||||
|
||||
public synchronized void start(final Context context) {
|
||||
if (mIsEnabled) {
|
||||
Log.w(LOGTAG, "Already started!");
|
||||
return;
|
||||
}
|
||||
|
||||
mApplicationContext = context.getApplicationContext();
|
||||
// registerReceiver will return null if registering fails.
|
||||
if (mApplicationContext.registerReceiver(this, mFilter) == null) {
|
||||
Log.e(LOGTAG, "Registering receiver failed");
|
||||
} else {
|
||||
mIsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
if (!mIsEnabled) {
|
||||
Log.w(LOGTAG, "Already stopped!");
|
||||
return;
|
||||
}
|
||||
|
||||
mApplicationContext.unregisterReceiver(this);
|
||||
mApplicationContext = null;
|
||||
mIsEnabled = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (!intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) {
|
||||
Log.e(LOGTAG, "Got an unexpected intent!");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean previousCharging = isCharging();
|
||||
double previousLevel = getLevel();
|
||||
|
||||
// NOTE: it might not be common (in 2012) but technically, Android can run
|
||||
// on a device that has no battery so we want to make sure it's not the case
|
||||
// before bothering checking for battery state.
|
||||
// However, the Galaxy Nexus phone advertises itself as battery-less which
|
||||
// force us to special-case the logic.
|
||||
// See the Google bug: https://code.google.com/p/android/issues/detail?id=22035
|
||||
if (intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false) ||
|
||||
Build.MODEL.equals("Galaxy Nexus")) {
|
||||
int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
|
||||
if (plugged == -1) {
|
||||
sCharging = kDefaultCharging;
|
||||
Log.e(LOGTAG, "Failed to get the plugged status!");
|
||||
} else {
|
||||
// Likely, if plugged > 0, it's likely plugged and charging but the doc
|
||||
// isn't clear about that.
|
||||
sCharging = plugged != 0;
|
||||
}
|
||||
|
||||
if (sCharging != previousCharging) {
|
||||
sRemainingTime = kUnknownRemainingTime;
|
||||
// The new remaining time is going to take some time to show up but
|
||||
// it's the best way to show a not too wrong value.
|
||||
sLastLevelChange = 0;
|
||||
}
|
||||
|
||||
// We need two doubles because sLevel is a double.
|
||||
double current = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
|
||||
double max = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
|
||||
if (current == -1 || max == -1) {
|
||||
Log.e(LOGTAG, "Failed to get battery level!");
|
||||
sLevel = kDefaultLevel;
|
||||
} else {
|
||||
sLevel = current / max;
|
||||
}
|
||||
|
||||
if (sLevel == 1.0 && sCharging) {
|
||||
sRemainingTime = kDefaultRemainingTime;
|
||||
} else if (sLevel != previousLevel) {
|
||||
// Estimate remaining time.
|
||||
if (sLastLevelChange != 0) {
|
||||
// Use elapsedRealtime() because we want to track time across device sleeps.
|
||||
long currentTime = SystemClock.elapsedRealtime();
|
||||
long dt = (currentTime - sLastLevelChange) / 1000;
|
||||
double dLevel = sLevel - previousLevel;
|
||||
|
||||
if (sCharging) {
|
||||
if (dLevel < 0) {
|
||||
Log.w(LOGTAG, "When charging, level should increase!");
|
||||
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())) {
|
||||
GoannaAppShell.notifyBatteryChange(getLevel(), isCharging(), getRemainingTime());
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isCharging() {
|
||||
return sCharging;
|
||||
}
|
||||
|
||||
public static double getLevel() {
|
||||
return sLevel;
|
||||
}
|
||||
|
||||
public static double getRemainingTime() {
|
||||
return sRemainingTime;
|
||||
}
|
||||
|
||||
public static void enableNotifications() {
|
||||
sNotificationsEnabled = true;
|
||||
}
|
||||
|
||||
public static void disableNotifications() {
|
||||
sNotificationsEnabled = false;
|
||||
}
|
||||
|
||||
public static double[] getCurrentInformation() {
|
||||
return new double[] { getLevel(), isCharging() ? 1.0 : 0.0, getRemainingTime() };
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.util.Log;
|
||||
|
||||
public class GoannaConnectivityReceiver extends BroadcastReceiver {
|
||||
/*
|
||||
* Keep the below constants in sync with
|
||||
* http://mxr.mozilla.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
|
||||
*/
|
||||
private static final String LINK_DATA_UP = "up";
|
||||
private static final String LINK_DATA_DOWN = "down";
|
||||
private static final String LINK_DATA_CHANGED = "changed";
|
||||
private static final String LINK_DATA_UNKNOWN = "unknown";
|
||||
|
||||
private static final String LOGTAG = "GoannaConnectivityReceiver";
|
||||
|
||||
private static final GoannaConnectivityReceiver sInstance = new GoannaConnectivityReceiver();
|
||||
|
||||
private final IntentFilter mFilter;
|
||||
private Context mApplicationContext;
|
||||
private boolean mIsEnabled;
|
||||
|
||||
public static GoannaConnectivityReceiver getInstance() {
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
private GoannaConnectivityReceiver() {
|
||||
mFilter = new IntentFilter();
|
||||
mFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
|
||||
}
|
||||
|
||||
public synchronized void start(Context context) {
|
||||
if (mIsEnabled) {
|
||||
Log.w(LOGTAG, "Already started!");
|
||||
return;
|
||||
}
|
||||
|
||||
mApplicationContext = context.getApplicationContext();
|
||||
|
||||
// registerReceiver will return null if registering fails.
|
||||
if (mApplicationContext.registerReceiver(this, mFilter) == null) {
|
||||
Log.e(LOGTAG, "Registering receiver failed");
|
||||
} else {
|
||||
mIsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
if (!mIsEnabled) {
|
||||
Log.w(LOGTAG, "Already stopped!");
|
||||
return;
|
||||
}
|
||||
|
||||
mApplicationContext.unregisterReceiver(this);
|
||||
mApplicationContext = null;
|
||||
mIsEnabled = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
ConnectivityManager cm = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo info = cm.getActiveNetworkInfo();
|
||||
|
||||
final String status;
|
||||
if (info == null) {
|
||||
status = LINK_DATA_UNKNOWN;
|
||||
} else if (!info.isConnected()) {
|
||||
status = LINK_DATA_DOWN;
|
||||
} else {
|
||||
status = LINK_DATA_UP;
|
||||
}
|
||||
|
||||
if (GoannaThread.checkLaunchState(GoannaThread.LaunchState.GoannaRunning)) {
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createNetworkLinkChangeEvent(status));
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createNetworkLinkChangeEvent(LINK_DATA_CHANGED));
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.text.Editable;
|
||||
|
||||
/**
|
||||
* Interface for the IC thread.
|
||||
*/
|
||||
interface GoannaEditableClient {
|
||||
void sendEvent(GoannaEvent event);
|
||||
Editable getEditable();
|
||||
void setUpdateGoanna(boolean update, boolean force);
|
||||
void setSuppressKeyUp(boolean suppress);
|
||||
Handler getInputConnectionHandler();
|
||||
boolean setInputConnectionHandler(Handler handler);
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
/**
|
||||
* Interface for the Editable to listen on the Goanna thread, as well as for the IC thread to listen
|
||||
* to the Editable.
|
||||
*/
|
||||
interface GoannaEditableListener {
|
||||
// IME notification type for notifyIME(), corresponding to NotificationToIME enum in Goanna
|
||||
int NOTIFY_IME_OPEN_VKB = -2;
|
||||
int NOTIFY_IME_REPLY_EVENT = -1;
|
||||
int NOTIFY_IME_OF_FOCUS = 1;
|
||||
int NOTIFY_IME_OF_BLUR = 2;
|
||||
int NOTIFY_IME_TO_COMMIT_COMPOSITION = 8;
|
||||
int NOTIFY_IME_TO_CANCEL_COMPOSITION = 9;
|
||||
// IME enabled state for notifyIMEContext()
|
||||
int IME_STATE_DISABLED = 0;
|
||||
int IME_STATE_ENABLED = 1;
|
||||
int IME_STATE_PASSWORD = 2;
|
||||
int IME_STATE_PLUGIN = 3;
|
||||
|
||||
void notifyIME(int type);
|
||||
void notifyIMEContext(int state, String typeHint, String modeHint, String actionHint);
|
||||
void onSelectionChange(int start, int end);
|
||||
void onTextChange(CharSequence text, int start, int oldEnd, int newEnd);
|
||||
}
|
||||
@@ -1,850 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
|
||||
import org.mozilla.goanna.AppConstants.Versions;
|
||||
import org.mozilla.goanna.gfx.DisplayPortMetrics;
|
||||
import org.mozilla.goanna.gfx.ImmutableViewportMetrics;
|
||||
|
||||
import android.graphics.Point;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.Rect;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorManager;
|
||||
import android.location.Address;
|
||||
import android.location.Location;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import org.mozilla.goanna.mozglue.JNITarget;
|
||||
import org.mozilla.goanna.mozglue.RobocopTarget;
|
||||
|
||||
/**
|
||||
* We're not allowed to hold on to most events given to us
|
||||
* so we save the parts of the events we want to use in GoannaEvent.
|
||||
* Fields have different meanings depending on the event type.
|
||||
*/
|
||||
@JNITarget
|
||||
public class GoannaEvent {
|
||||
private static final String LOGTAG = "GoannaEvent";
|
||||
|
||||
private static final int EVENT_FACTORY_SIZE = 5;
|
||||
|
||||
// Maybe we're probably better to just make mType non final, and just store GoannaEvents in here...
|
||||
private static final SparseArray<ArrayBlockingQueue<GoannaEvent>> mEvents = new SparseArray<ArrayBlockingQueue<GoannaEvent>>();
|
||||
|
||||
public static GoannaEvent get(NativeGoannaEvent type) {
|
||||
synchronized (mEvents) {
|
||||
ArrayBlockingQueue<GoannaEvent> events = mEvents.get(type.value);
|
||||
if (events != null && events.size() > 0) {
|
||||
return events.poll();
|
||||
}
|
||||
}
|
||||
|
||||
return new GoannaEvent(type);
|
||||
}
|
||||
|
||||
public void recycle() {
|
||||
synchronized (mEvents) {
|
||||
ArrayBlockingQueue<GoannaEvent> events = mEvents.get(mType);
|
||||
if (events == null) {
|
||||
events = new ArrayBlockingQueue<GoannaEvent>(EVENT_FACTORY_SIZE);
|
||||
mEvents.put(mType, events);
|
||||
}
|
||||
|
||||
events.offer(this);
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure to keep these values in sync with the enum in
|
||||
// AndroidGoannaEvent in widget/android/AndroidJavaWrappers.h
|
||||
@JNITarget
|
||||
private enum NativeGoannaEvent {
|
||||
NATIVE_POKE(0),
|
||||
KEY_EVENT(1),
|
||||
MOTION_EVENT(2),
|
||||
SENSOR_EVENT(3),
|
||||
PROCESS_OBJECT(4),
|
||||
LOCATION_EVENT(5),
|
||||
IME_EVENT(6),
|
||||
SIZE_CHANGED(8),
|
||||
APP_BACKGROUNDING(9),
|
||||
APP_FOREGROUNDING(10),
|
||||
LOAD_URI(12),
|
||||
NOOP(15),
|
||||
BROADCAST(19),
|
||||
VIEWPORT(20),
|
||||
VISITED(21),
|
||||
NETWORK_CHANGED(22),
|
||||
THUMBNAIL(25),
|
||||
SCREENORIENTATION_CHANGED(27),
|
||||
COMPOSITOR_CREATE(28),
|
||||
COMPOSITOR_PAUSE(29),
|
||||
COMPOSITOR_RESUME(30),
|
||||
NATIVE_GESTURE_EVENT(31),
|
||||
IME_KEY_EVENT(32),
|
||||
CALL_OBSERVER(33),
|
||||
REMOVE_OBSERVER(34),
|
||||
LOW_MEMORY(35),
|
||||
NETWORK_LINK_CHANGE(36),
|
||||
TELEMETRY_HISTOGRAM_ADD(37),
|
||||
PREFERENCES_OBSERVE(39),
|
||||
PREFERENCES_GET(40),
|
||||
PREFERENCES_REMOVE_OBSERVERS(41),
|
||||
TELEMETRY_UI_SESSION_START(42),
|
||||
TELEMETRY_UI_SESSION_STOP(43),
|
||||
TELEMETRY_UI_EVENT(44),
|
||||
GAMEPAD_ADDREMOVE(45),
|
||||
GAMEPAD_DATA(46),
|
||||
LONG_PRESS(47),
|
||||
ZOOMEDVIEW(48);
|
||||
|
||||
public final int value;
|
||||
|
||||
private NativeGoannaEvent(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Encapsulation of common IME actions.
|
||||
@JNITarget
|
||||
public enum ImeAction {
|
||||
IME_SYNCHRONIZE(0),
|
||||
IME_REPLACE_TEXT(1),
|
||||
IME_SET_SELECTION(2),
|
||||
IME_ADD_COMPOSITION_RANGE(3),
|
||||
IME_UPDATE_COMPOSITION(4),
|
||||
IME_REMOVE_COMPOSITION(5),
|
||||
IME_ACKNOWLEDGE_FOCUS(6),
|
||||
IME_COMPOSE_TEXT(7);
|
||||
|
||||
public final int value;
|
||||
|
||||
private ImeAction(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
public static final int IME_RANGE_CARETPOSITION = 1;
|
||||
public static final int IME_RANGE_RAWINPUT = 2;
|
||||
public static final int IME_RANGE_SELECTEDRAWTEXT = 3;
|
||||
public static final int IME_RANGE_CONVERTEDTEXT = 4;
|
||||
public static final int IME_RANGE_SELECTEDCONVERTEDTEXT = 5;
|
||||
|
||||
public static final int IME_RANGE_LINE_NONE = 0;
|
||||
public static final int IME_RANGE_LINE_DOTTED = 1;
|
||||
public static final int IME_RANGE_LINE_DASHED = 2;
|
||||
public static final int IME_RANGE_LINE_SOLID = 3;
|
||||
public static final int IME_RANGE_LINE_DOUBLE = 4;
|
||||
public static final int IME_RANGE_LINE_WAVY = 5;
|
||||
|
||||
public static final int IME_RANGE_UNDERLINE = 1;
|
||||
public static final int IME_RANGE_FORECOLOR = 2;
|
||||
public static final int IME_RANGE_BACKCOLOR = 4;
|
||||
public static final int IME_RANGE_LINECOLOR = 8;
|
||||
|
||||
public static final int ACTION_MAGNIFY_START = 11;
|
||||
public static final int ACTION_MAGNIFY = 12;
|
||||
public static final int ACTION_MAGNIFY_END = 13;
|
||||
|
||||
public static final int ACTION_GAMEPAD_ADDED = 1;
|
||||
public static final int ACTION_GAMEPAD_REMOVED = 2;
|
||||
|
||||
public static final int ACTION_GAMEPAD_BUTTON = 1;
|
||||
public static final int ACTION_GAMEPAD_AXES = 2;
|
||||
|
||||
public static final int ACTION_OBJECT_LAYER_CLIENT = 1;
|
||||
|
||||
private final int mType;
|
||||
private int mAction;
|
||||
private boolean mAckNeeded;
|
||||
private long mTime;
|
||||
private Point[] mPoints;
|
||||
private int[] mPointIndicies;
|
||||
private int mPointerIndex; // index of the point that has changed
|
||||
private float[] mOrientations;
|
||||
private float[] mPressures;
|
||||
private int[] mToolTypes;
|
||||
private Point[] mPointRadii;
|
||||
private Rect mRect;
|
||||
private double mX;
|
||||
private double mY;
|
||||
private double mZ;
|
||||
|
||||
private int mMetaState;
|
||||
private int mFlags;
|
||||
private int mKeyCode;
|
||||
private int mScanCode;
|
||||
private int mUnicodeChar;
|
||||
private int mBaseUnicodeChar; // mUnicodeChar without meta states applied
|
||||
private int mDOMPrintableKeyValue;
|
||||
private int mRepeatCount;
|
||||
private int mCount;
|
||||
private int mStart;
|
||||
private int mEnd;
|
||||
private String mCharacters;
|
||||
private String mCharactersExtra;
|
||||
private String mData;
|
||||
private int mRangeType;
|
||||
private int mRangeStyles;
|
||||
private int mRangeLineStyle;
|
||||
private boolean mRangeBoldLine;
|
||||
private int mRangeForeColor;
|
||||
private int mRangeBackColor;
|
||||
private int mRangeLineColor;
|
||||
private Location mLocation;
|
||||
private Address mAddress;
|
||||
|
||||
private int mConnectionType;
|
||||
private boolean mIsWifi;
|
||||
private int mDHCPGateway;
|
||||
|
||||
private int mNativeWindow;
|
||||
|
||||
private short mScreenOrientation;
|
||||
|
||||
private ByteBuffer mBuffer;
|
||||
|
||||
private int mWidth;
|
||||
private int mHeight;
|
||||
|
||||
private int mID;
|
||||
private int mGamepadButton;
|
||||
private boolean mGamepadButtonPressed;
|
||||
private float mGamepadButtonValue;
|
||||
private float[] mGamepadValues;
|
||||
|
||||
private String[] mPrefNames;
|
||||
|
||||
private Object mObject;
|
||||
|
||||
private GoannaEvent(NativeGoannaEvent event) {
|
||||
mType = event.value;
|
||||
}
|
||||
|
||||
public static GoannaEvent createAppBackgroundingEvent() {
|
||||
return GoannaEvent.get(NativeGoannaEvent.APP_BACKGROUNDING);
|
||||
}
|
||||
|
||||
public static GoannaEvent createAppForegroundingEvent() {
|
||||
return GoannaEvent.get(NativeGoannaEvent.APP_FOREGROUNDING);
|
||||
}
|
||||
|
||||
public static GoannaEvent createNoOpEvent() {
|
||||
return GoannaEvent.get(NativeGoannaEvent.NOOP);
|
||||
}
|
||||
|
||||
public static GoannaEvent createKeyEvent(KeyEvent k, int metaState) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.KEY_EVENT);
|
||||
event.initKeyEvent(k, metaState);
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createCompositorCreateEvent(int width, int height) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.COMPOSITOR_CREATE);
|
||||
event.mWidth = width;
|
||||
event.mHeight = height;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createCompositorPauseEvent() {
|
||||
return GoannaEvent.get(NativeGoannaEvent.COMPOSITOR_PAUSE);
|
||||
}
|
||||
|
||||
public static GoannaEvent createCompositorResumeEvent() {
|
||||
return GoannaEvent.get(NativeGoannaEvent.COMPOSITOR_RESUME);
|
||||
}
|
||||
|
||||
private void initKeyEvent(KeyEvent k, int metaState) {
|
||||
mAction = k.getAction();
|
||||
mTime = k.getEventTime();
|
||||
// Normally we expect k.getMetaState() to reflect the current meta-state; however,
|
||||
// some software-generated key events may not have k.getMetaState() set, e.g. key
|
||||
// events from Swype. Therefore, it's necessary to combine the key's meta-states
|
||||
// with the meta-states that we keep separately in KeyListener
|
||||
mMetaState = k.getMetaState() | metaState;
|
||||
mFlags = k.getFlags();
|
||||
mKeyCode = k.getKeyCode();
|
||||
mScanCode = k.getScanCode();
|
||||
mUnicodeChar = k.getUnicodeChar(mMetaState);
|
||||
// e.g. for Ctrl+A, Android returns 0 for mUnicodeChar,
|
||||
// but Goanna expects 'a', so we return that in mBaseUnicodeChar
|
||||
mBaseUnicodeChar = k.getUnicodeChar(0);
|
||||
mRepeatCount = k.getRepeatCount();
|
||||
mCharacters = k.getCharacters();
|
||||
if (mUnicodeChar >= ' ') {
|
||||
mDOMPrintableKeyValue = mUnicodeChar;
|
||||
} else {
|
||||
int unmodifiedMetaState =
|
||||
mMetaState & ~(KeyEvent.META_ALT_MASK |
|
||||
KeyEvent.META_CTRL_MASK |
|
||||
KeyEvent.META_META_MASK);
|
||||
if (unmodifiedMetaState != mMetaState) {
|
||||
mDOMPrintableKeyValue = k.getUnicodeChar(unmodifiedMetaState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is a replacement for the the KeyEvent.isGamepadButton method to be
|
||||
* compatible with Build.VERSION.SDK_INT < 12. This is an implementation of the
|
||||
* same method isGamepadButton available after SDK 12.
|
||||
* @param keyCode int with the key code (Android key constant from KeyEvent).
|
||||
* @return True if the keycode is a gamepad button, such as {@link #KEYCODE_BUTTON_A}.
|
||||
*/
|
||||
private static boolean isGamepadButton(int keyCode) {
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_BUTTON_A:
|
||||
case KeyEvent.KEYCODE_BUTTON_B:
|
||||
case KeyEvent.KEYCODE_BUTTON_C:
|
||||
case KeyEvent.KEYCODE_BUTTON_X:
|
||||
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||
case KeyEvent.KEYCODE_BUTTON_Z:
|
||||
case KeyEvent.KEYCODE_BUTTON_L1:
|
||||
case KeyEvent.KEYCODE_BUTTON_R1:
|
||||
case KeyEvent.KEYCODE_BUTTON_L2:
|
||||
case KeyEvent.KEYCODE_BUTTON_R2:
|
||||
case KeyEvent.KEYCODE_BUTTON_THUMBL:
|
||||
case KeyEvent.KEYCODE_BUTTON_THUMBR:
|
||||
case KeyEvent.KEYCODE_BUTTON_START:
|
||||
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
||||
case KeyEvent.KEYCODE_BUTTON_MODE:
|
||||
case KeyEvent.KEYCODE_BUTTON_1:
|
||||
case KeyEvent.KEYCODE_BUTTON_2:
|
||||
case KeyEvent.KEYCODE_BUTTON_3:
|
||||
case KeyEvent.KEYCODE_BUTTON_4:
|
||||
case KeyEvent.KEYCODE_BUTTON_5:
|
||||
case KeyEvent.KEYCODE_BUTTON_6:
|
||||
case KeyEvent.KEYCODE_BUTTON_7:
|
||||
case KeyEvent.KEYCODE_BUTTON_8:
|
||||
case KeyEvent.KEYCODE_BUTTON_9:
|
||||
case KeyEvent.KEYCODE_BUTTON_10:
|
||||
case KeyEvent.KEYCODE_BUTTON_11:
|
||||
case KeyEvent.KEYCODE_BUTTON_12:
|
||||
case KeyEvent.KEYCODE_BUTTON_13:
|
||||
case KeyEvent.KEYCODE_BUTTON_14:
|
||||
case KeyEvent.KEYCODE_BUTTON_15:
|
||||
case KeyEvent.KEYCODE_BUTTON_16:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static GoannaEvent createNativeGestureEvent(int action, PointF pt, double size) {
|
||||
try {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.NATIVE_GESTURE_EVENT);
|
||||
event.mAction = action;
|
||||
event.mCount = 1;
|
||||
event.mPoints = new Point[1];
|
||||
|
||||
PointF goannaPoint = new PointF(pt.x, pt.y);
|
||||
goannaPoint = GoannaAppShell.getLayerView().convertViewPointToLayerPoint(goannaPoint);
|
||||
|
||||
if (goannaPoint == null) {
|
||||
// This could happen if Goanna isn't ready yet.
|
||||
return null;
|
||||
}
|
||||
|
||||
event.mPoints[0] = new Point(Math.round(goannaPoint.x), Math.round(goannaPoint.y));
|
||||
|
||||
event.mX = size;
|
||||
event.mTime = System.currentTimeMillis();
|
||||
return event;
|
||||
} catch (Exception e) {
|
||||
// This can happen if Goanna isn't ready yet
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a GoannaEvent that contains the data from the MotionEvent.
|
||||
* The keepInViewCoordinates parameter can be set to false to convert from the Java
|
||||
* coordinate system (device pixels relative to the LayerView) to a coordinate system
|
||||
* relative to goanna's coordinate system (CSS pixels relative to goanna scroll position).
|
||||
*/
|
||||
public static GoannaEvent createMotionEvent(MotionEvent m, boolean keepInViewCoordinates) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.MOTION_EVENT);
|
||||
event.initMotionEvent(m, keepInViewCoordinates);
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a GoannaEvent that contains the data from the LongPressEvent, to be
|
||||
* dispatched in CSS pixels relative to goanna's scroll position.
|
||||
*/
|
||||
public static GoannaEvent createLongPressEvent(MotionEvent m) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.LONG_PRESS);
|
||||
event.initMotionEvent(m, false);
|
||||
return event;
|
||||
}
|
||||
|
||||
private void initMotionEvent(MotionEvent m, boolean keepInViewCoordinates) {
|
||||
mAction = m.getActionMasked();
|
||||
mTime = (System.currentTimeMillis() - SystemClock.elapsedRealtime()) + m.getEventTime();
|
||||
mMetaState = m.getMetaState();
|
||||
|
||||
switch (mAction) {
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
case MotionEvent.ACTION_HOVER_ENTER:
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
case MotionEvent.ACTION_HOVER_EXIT: {
|
||||
mCount = m.getPointerCount();
|
||||
mPoints = new Point[mCount];
|
||||
mPointIndicies = new int[mCount];
|
||||
mOrientations = new float[mCount];
|
||||
mPressures = new float[mCount];
|
||||
mToolTypes = new int[mCount];
|
||||
mPointRadii = new Point[mCount];
|
||||
mPointerIndex = m.getActionIndex();
|
||||
for (int i = 0; i < mCount; i++) {
|
||||
addMotionPoint(i, i, m, keepInViewCoordinates);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
mCount = 0;
|
||||
mPointerIndex = -1;
|
||||
mPoints = new Point[mCount];
|
||||
mPointIndicies = new int[mCount];
|
||||
mOrientations = new float[mCount];
|
||||
mPressures = new float[mCount];
|
||||
mToolTypes = new int[mCount];
|
||||
mPointRadii = new Point[mCount];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addMotionPoint(int index, int eventIndex, MotionEvent event, boolean keepInViewCoordinates) {
|
||||
try {
|
||||
PointF goannaPoint = new PointF(event.getX(eventIndex), event.getY(eventIndex));
|
||||
if (!keepInViewCoordinates) {
|
||||
goannaPoint = GoannaAppShell.getLayerView().convertViewPointToLayerPoint(goannaPoint);
|
||||
}
|
||||
|
||||
mPoints[index] = new Point(Math.round(goannaPoint.x), Math.round(goannaPoint.y));
|
||||
mPointIndicies[index] = event.getPointerId(eventIndex);
|
||||
|
||||
double radians = event.getOrientation(eventIndex);
|
||||
mOrientations[index] = (float) Math.toDegrees(radians);
|
||||
// w3c touchevents spec does not allow orientations == 90
|
||||
// this shifts it to -90, which will be shifted to zero below
|
||||
if (mOrientations[index] == 90)
|
||||
mOrientations[index] = -90;
|
||||
|
||||
// w3c touchevent radius are given by an orientation between 0 and 90
|
||||
// the radius is found by removing the orientation and measuring the x and y
|
||||
// radius of the resulting ellipse
|
||||
// for android orientations >= 0 and < 90, the major axis should correspond to
|
||||
// just reporting the y radius as the major one, and x as minor
|
||||
// however, for a radius < 0, we have to shift the orientation by adding 90, and
|
||||
// reverse which radius is major and minor
|
||||
if (mOrientations[index] < 0) {
|
||||
mOrientations[index] += 90;
|
||||
mPointRadii[index] = new Point((int)event.getToolMajor(eventIndex)/2,
|
||||
(int)event.getToolMinor(eventIndex)/2);
|
||||
} else {
|
||||
mPointRadii[index] = new Point((int)event.getToolMinor(eventIndex)/2,
|
||||
(int)event.getToolMajor(eventIndex)/2);
|
||||
}
|
||||
|
||||
if (!keepInViewCoordinates) {
|
||||
// If we are converting to goanna CSS pixels, then we should adjust the
|
||||
// radii as well
|
||||
float zoom = GoannaAppShell.getLayerView().getViewportMetrics().zoomFactor;
|
||||
mPointRadii[index].x /= zoom;
|
||||
mPointRadii[index].y /= zoom;
|
||||
}
|
||||
mPressures[index] = event.getPressure(eventIndex);
|
||||
if (Versions.feature14Plus) {
|
||||
mToolTypes[index] = event.getToolType(index);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Log.e(LOGTAG, "Error creating motion point " + index, ex);
|
||||
mPointRadii[index] = new Point(0, 0);
|
||||
mPoints[index] = new Point(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static int HalSensorAccuracyFor(int androidAccuracy) {
|
||||
switch (androidAccuracy) {
|
||||
case SensorManager.SENSOR_STATUS_UNRELIABLE:
|
||||
return GoannaHalDefines.SENSOR_ACCURACY_UNRELIABLE;
|
||||
case SensorManager.SENSOR_STATUS_ACCURACY_LOW:
|
||||
return GoannaHalDefines.SENSOR_ACCURACY_LOW;
|
||||
case SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM:
|
||||
return GoannaHalDefines.SENSOR_ACCURACY_MED;
|
||||
case SensorManager.SENSOR_STATUS_ACCURACY_HIGH:
|
||||
return GoannaHalDefines.SENSOR_ACCURACY_HIGH;
|
||||
}
|
||||
return GoannaHalDefines.SENSOR_ACCURACY_UNKNOWN;
|
||||
}
|
||||
|
||||
public static GoannaEvent createSensorEvent(SensorEvent s) {
|
||||
int sensor_type = s.sensor.getType();
|
||||
GoannaEvent event = null;
|
||||
|
||||
switch(sensor_type) {
|
||||
|
||||
case Sensor.TYPE_ACCELEROMETER:
|
||||
event = GoannaEvent.get(NativeGoannaEvent.SENSOR_EVENT);
|
||||
event.mFlags = GoannaHalDefines.SENSOR_ACCELERATION;
|
||||
event.mMetaState = HalSensorAccuracyFor(s.accuracy);
|
||||
event.mX = s.values[0];
|
||||
event.mY = s.values[1];
|
||||
event.mZ = s.values[2];
|
||||
break;
|
||||
|
||||
case 10 /* Requires API Level 9, so just use the raw value - Sensor.TYPE_LINEAR_ACCELEROMETER*/ :
|
||||
event = GoannaEvent.get(NativeGoannaEvent.SENSOR_EVENT);
|
||||
event.mFlags = GoannaHalDefines.SENSOR_LINEAR_ACCELERATION;
|
||||
event.mMetaState = HalSensorAccuracyFor(s.accuracy);
|
||||
event.mX = s.values[0];
|
||||
event.mY = s.values[1];
|
||||
event.mZ = s.values[2];
|
||||
break;
|
||||
|
||||
case Sensor.TYPE_ORIENTATION:
|
||||
event = GoannaEvent.get(NativeGoannaEvent.SENSOR_EVENT);
|
||||
event.mFlags = GoannaHalDefines.SENSOR_ORIENTATION;
|
||||
event.mMetaState = HalSensorAccuracyFor(s.accuracy);
|
||||
event.mX = s.values[0];
|
||||
event.mY = s.values[1];
|
||||
event.mZ = s.values[2];
|
||||
break;
|
||||
|
||||
case Sensor.TYPE_GYROSCOPE:
|
||||
event = GoannaEvent.get(NativeGoannaEvent.SENSOR_EVENT);
|
||||
event.mFlags = GoannaHalDefines.SENSOR_GYROSCOPE;
|
||||
event.mMetaState = HalSensorAccuracyFor(s.accuracy);
|
||||
event.mX = Math.toDegrees(s.values[0]);
|
||||
event.mY = Math.toDegrees(s.values[1]);
|
||||
event.mZ = Math.toDegrees(s.values[2]);
|
||||
break;
|
||||
|
||||
case Sensor.TYPE_PROXIMITY:
|
||||
event = GoannaEvent.get(NativeGoannaEvent.SENSOR_EVENT);
|
||||
event.mFlags = GoannaHalDefines.SENSOR_PROXIMITY;
|
||||
event.mMetaState = HalSensorAccuracyFor(s.accuracy);
|
||||
event.mX = s.values[0];
|
||||
event.mY = 0;
|
||||
event.mZ = s.sensor.getMaximumRange();
|
||||
break;
|
||||
|
||||
case Sensor.TYPE_LIGHT:
|
||||
event = GoannaEvent.get(NativeGoannaEvent.SENSOR_EVENT);
|
||||
event.mFlags = GoannaHalDefines.SENSOR_LIGHT;
|
||||
event.mMetaState = HalSensorAccuracyFor(s.accuracy);
|
||||
event.mX = s.values[0];
|
||||
break;
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createObjectEvent(final int action, final Object object) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.PROCESS_OBJECT);
|
||||
event.mAction = action;
|
||||
event.mObject = object;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createLocationEvent(Location l) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.LOCATION_EVENT);
|
||||
event.mLocation = l;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createIMEEvent(ImeAction action) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.IME_EVENT);
|
||||
event.mAction = action.value;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createIMEKeyEvent(KeyEvent k) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.IME_KEY_EVENT);
|
||||
event.initKeyEvent(k, 0);
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createIMEReplaceEvent(int start, int end, String text) {
|
||||
return createIMETextEvent(false, start, end, text);
|
||||
}
|
||||
|
||||
public static GoannaEvent createIMEComposeEvent(int start, int end, String text) {
|
||||
return createIMETextEvent(true, start, end, text);
|
||||
}
|
||||
|
||||
private static GoannaEvent createIMETextEvent(boolean compose, int start, int end, String text) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.IME_EVENT);
|
||||
event.mAction = (compose ? ImeAction.IME_COMPOSE_TEXT : ImeAction.IME_REPLACE_TEXT).value;
|
||||
event.mStart = start;
|
||||
event.mEnd = end;
|
||||
event.mCharacters = text;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createIMESelectEvent(int start, int end) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.IME_EVENT);
|
||||
event.mAction = ImeAction.IME_SET_SELECTION.value;
|
||||
event.mStart = start;
|
||||
event.mEnd = end;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createIMECompositionEvent(int start, int end) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.IME_EVENT);
|
||||
event.mAction = ImeAction.IME_UPDATE_COMPOSITION.value;
|
||||
event.mStart = start;
|
||||
event.mEnd = end;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createIMERangeEvent(int start,
|
||||
int end, int rangeType,
|
||||
int rangeStyles,
|
||||
int rangeLineStyle,
|
||||
boolean rangeBoldLine,
|
||||
int rangeForeColor,
|
||||
int rangeBackColor,
|
||||
int rangeLineColor) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.IME_EVENT);
|
||||
event.mAction = ImeAction.IME_ADD_COMPOSITION_RANGE.value;
|
||||
event.mStart = start;
|
||||
event.mEnd = end;
|
||||
event.mRangeType = rangeType;
|
||||
event.mRangeStyles = rangeStyles;
|
||||
event.mRangeLineStyle = rangeLineStyle;
|
||||
event.mRangeBoldLine = rangeBoldLine;
|
||||
event.mRangeForeColor = rangeForeColor;
|
||||
event.mRangeBackColor = rangeBackColor;
|
||||
event.mRangeLineColor = rangeLineColor;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createSizeChangedEvent(int w, int h, int screenw, int screenh) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.SIZE_CHANGED);
|
||||
event.mPoints = new Point[2];
|
||||
event.mPoints[0] = new Point(w, h);
|
||||
event.mPoints[1] = new Point(screenw, screenh);
|
||||
return event;
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
public static GoannaEvent createBroadcastEvent(String subject, String data) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.BROADCAST);
|
||||
event.mCharacters = subject;
|
||||
event.mCharactersExtra = data;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createViewportEvent(ImmutableViewportMetrics metrics, DisplayPortMetrics displayPort) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.VIEWPORT);
|
||||
event.mCharacters = "Viewport:Change";
|
||||
StringBuilder sb = new StringBuilder(256);
|
||||
sb.append("{ \"x\" : ").append(metrics.viewportRectLeft)
|
||||
.append(", \"y\" : ").append(metrics.viewportRectTop)
|
||||
.append(", \"zoom\" : ").append(metrics.zoomFactor)
|
||||
.append(", \"fixedMarginLeft\" : ").append(metrics.marginLeft)
|
||||
.append(", \"fixedMarginTop\" : ").append(metrics.marginTop)
|
||||
.append(", \"fixedMarginRight\" : ").append(metrics.marginRight)
|
||||
.append(", \"fixedMarginBottom\" : ").append(metrics.marginBottom)
|
||||
.append(", \"displayPort\" :").append(displayPort.toJSON())
|
||||
.append('}');
|
||||
event.mCharactersExtra = sb.toString();
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createURILoadEvent(String uri) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.LOAD_URI);
|
||||
event.mCharacters = uri;
|
||||
event.mCharactersExtra = "";
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createBookmarkLoadEvent(String uri) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.LOAD_URI);
|
||||
event.mCharacters = uri;
|
||||
event.mCharactersExtra = "-bookmark";
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createVisitedEvent(String data) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.VISITED);
|
||||
event.mCharacters = data;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createNetworkEvent(int connectionType, boolean isWifi, int DHCPGateway) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.NETWORK_CHANGED);
|
||||
event.mConnectionType = connectionType;
|
||||
event.mIsWifi = isWifi;
|
||||
event.mDHCPGateway = DHCPGateway;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createThumbnailEvent(int tabId, int bufw, int bufh, ByteBuffer buffer) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.THUMBNAIL);
|
||||
event.mPoints = new Point[1];
|
||||
event.mPoints[0] = new Point(bufw, bufh);
|
||||
event.mMetaState = tabId;
|
||||
event.mBuffer = buffer;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createZoomedViewEvent(int tabId, int x, int y, int bufw, int bufh, float scaleFactor, ByteBuffer buffer) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.ZOOMEDVIEW);
|
||||
event.mPoints = new Point[2];
|
||||
event.mPoints[0] = new Point(x, y);
|
||||
event.mPoints[1] = new Point(bufw, bufh);
|
||||
event.mX = (double) scaleFactor;
|
||||
event.mMetaState = tabId;
|
||||
event.mBuffer = buffer;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createScreenOrientationEvent(short aScreenOrientation) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.SCREENORIENTATION_CHANGED);
|
||||
event.mScreenOrientation = aScreenOrientation;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createCallObserverEvent(String observerKey, String topic, String data) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.CALL_OBSERVER);
|
||||
event.mCharacters = observerKey;
|
||||
event.mCharactersExtra = topic;
|
||||
event.mData = data;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createRemoveObserverEvent(String observerKey) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.REMOVE_OBSERVER);
|
||||
event.mCharacters = observerKey;
|
||||
return event;
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
public static GoannaEvent createPreferencesObserveEvent(int requestId, String[] prefNames) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.PREFERENCES_OBSERVE);
|
||||
event.mCount = requestId;
|
||||
event.mPrefNames = prefNames;
|
||||
return event;
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
public static GoannaEvent createPreferencesGetEvent(int requestId, String[] prefNames) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.PREFERENCES_GET);
|
||||
event.mCount = requestId;
|
||||
event.mPrefNames = prefNames;
|
||||
return event;
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
public static GoannaEvent createPreferencesRemoveObserversEvent(int requestId) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.PREFERENCES_REMOVE_OBSERVERS);
|
||||
event.mCount = requestId;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createLowMemoryEvent(int level) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.LOW_MEMORY);
|
||||
event.mMetaState = level;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createNetworkLinkChangeEvent(String status) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.NETWORK_LINK_CHANGE);
|
||||
event.mCharacters = status;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createTelemetryHistogramAddEvent(String histogram,
|
||||
int value) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.TELEMETRY_HISTOGRAM_ADD);
|
||||
event.mCharacters = histogram;
|
||||
event.mCount = value;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createTelemetryUISessionStartEvent(String session, long timestamp) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.TELEMETRY_UI_SESSION_START);
|
||||
event.mCharacters = session;
|
||||
event.mTime = timestamp;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createTelemetryUISessionStopEvent(String session, String reason, long timestamp) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.TELEMETRY_UI_SESSION_STOP);
|
||||
event.mCharacters = session;
|
||||
event.mCharactersExtra = reason;
|
||||
event.mTime = timestamp;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createTelemetryUIEvent(String action, String method, long timestamp, String extras) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.TELEMETRY_UI_EVENT);
|
||||
event.mData = action;
|
||||
event.mCharacters = method;
|
||||
event.mCharactersExtra = extras;
|
||||
event.mTime = timestamp;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createGamepadAddRemoveEvent(int id, boolean added) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.GAMEPAD_ADDREMOVE);
|
||||
event.mID = id;
|
||||
event.mAction = added ? ACTION_GAMEPAD_ADDED : ACTION_GAMEPAD_REMOVED;
|
||||
return event;
|
||||
}
|
||||
|
||||
private static int boolArrayToBitfield(boolean[] array) {
|
||||
int bits = 0;
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
if (array[i]) {
|
||||
bits |= 1<<i;
|
||||
}
|
||||
}
|
||||
return bits;
|
||||
}
|
||||
|
||||
public static GoannaEvent createGamepadButtonEvent(int id,
|
||||
int which,
|
||||
boolean pressed,
|
||||
float value) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.GAMEPAD_DATA);
|
||||
event.mID = id;
|
||||
event.mAction = ACTION_GAMEPAD_BUTTON;
|
||||
event.mGamepadButton = which;
|
||||
event.mGamepadButtonPressed = pressed;
|
||||
event.mGamepadButtonValue = value;
|
||||
return event;
|
||||
}
|
||||
|
||||
public static GoannaEvent createGamepadAxisEvent(int id, boolean[] valid,
|
||||
float[] values) {
|
||||
GoannaEvent event = GoannaEvent.get(NativeGoannaEvent.GAMEPAD_DATA);
|
||||
event.mID = id;
|
||||
event.mAction = ACTION_GAMEPAD_AXES;
|
||||
event.mFlags = boolArrayToBitfield(valid);
|
||||
event.mCount = values.length;
|
||||
event.mGamepadValues = values;
|
||||
return event;
|
||||
}
|
||||
|
||||
public void setAckNeeded(boolean ackNeeded) {
|
||||
mAckNeeded = ackNeeded;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
public class GoannaHalDefines
|
||||
{
|
||||
/*
|
||||
* Keep these values consistent with |SensorType| in Hal.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_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
@@ -1,218 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import org.mozilla.goanna.mozglue.generatorannotations.WrapElementForJNI;
|
||||
|
||||
import java.lang.Thread;
|
||||
import java.util.Set;
|
||||
|
||||
public class GoannaJavaSampler {
|
||||
private static final String LOGTAG = "JavaSampler";
|
||||
private static Thread sSamplingThread;
|
||||
private static SamplingThread sSamplingRunnable;
|
||||
private static Thread sMainThread;
|
||||
private static volatile boolean sLibsLoaded;
|
||||
|
||||
// Use the same timer primitive as the profiler
|
||||
// to get a perfect sample syncing.
|
||||
private static native double getProfilerTime();
|
||||
|
||||
private static class Sample {
|
||||
public Frame[] mFrames;
|
||||
public double mTime;
|
||||
public long mJavaTime; // non-zero if Android system time is used
|
||||
public Sample(StackTraceElement[] aStack) {
|
||||
mFrames = new Frame[aStack.length];
|
||||
if (sLibsLoaded) {
|
||||
mTime = getProfilerTime();
|
||||
}
|
||||
if (mTime == 0.0d) {
|
||||
// getProfilerTime is not available yet; either libs are not loaded,
|
||||
// or profiling hasn't started on the Goanna side yet
|
||||
mJavaTime = SystemClock.elapsedRealtime();
|
||||
}
|
||||
for (int i = 0; i < aStack.length; i++) {
|
||||
mFrames[aStack.length - 1 - i] = new Frame();
|
||||
mFrames[aStack.length - 1 - i].fileName = aStack[i].getFileName();
|
||||
mFrames[aStack.length - 1 - i].lineNo = aStack[i].getLineNumber();
|
||||
mFrames[aStack.length - 1 - i].methodName = aStack[i].getMethodName();
|
||||
mFrames[aStack.length - 1 - i].className = aStack[i].getClassName();
|
||||
}
|
||||
}
|
||||
}
|
||||
private static class Frame {
|
||||
public String fileName;
|
||||
public int lineNo;
|
||||
public String methodName;
|
||||
public String className;
|
||||
}
|
||||
|
||||
private static class SamplingThread implements Runnable {
|
||||
private final int mInterval;
|
||||
private final int mSampleCount;
|
||||
|
||||
private boolean mPauseSampler;
|
||||
private boolean mStopSampler;
|
||||
|
||||
private final SparseArray<Sample[]> mSamples = new SparseArray<Sample[]>();
|
||||
private int mSamplePos;
|
||||
|
||||
public SamplingThread(final int aInterval, final int aSampleCount) {
|
||||
// If we sample faster then 10ms we get to many missed samples
|
||||
mInterval = Math.max(10, aInterval);
|
||||
mSampleCount = aSampleCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (GoannaJavaSampler.class) {
|
||||
mSamples.put(0, new Sample[mSampleCount]);
|
||||
mSamplePos = 0;
|
||||
|
||||
// Find the main thread
|
||||
Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
|
||||
for (Thread t : threadSet) {
|
||||
if (t.getName().compareToIgnoreCase("main") == 0) {
|
||||
sMainThread = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (sMainThread == null) {
|
||||
Log.e(LOGTAG, "Main thread not found");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
Thread.sleep(mInterval);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
synchronized (GoannaJavaSampler.class) {
|
||||
if (!mPauseSampler) {
|
||||
StackTraceElement[] bt = sMainThread.getStackTrace();
|
||||
mSamples.get(0)[mSamplePos] = new Sample(bt);
|
||||
mSamplePos = (mSamplePos+1) % mSamples.get(0).length;
|
||||
}
|
||||
if (mStopSampler) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Sample getSample(int aThreadId, int aSampleId) {
|
||||
if (aThreadId < mSamples.size() && aSampleId < mSamples.get(aThreadId).length &&
|
||||
mSamples.get(aThreadId)[aSampleId] != null) {
|
||||
int startPos = 0;
|
||||
if (mSamples.get(aThreadId)[mSamplePos] != null) {
|
||||
startPos = mSamplePos;
|
||||
}
|
||||
int readPos = (startPos + aSampleId) % mSamples.get(aThreadId).length;
|
||||
return mSamples.get(aThreadId)[readPos];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@WrapElementForJNI(allowMultithread = true, stubName = "GetThreadNameJavaProfilingWrapper")
|
||||
public synchronized static String getThreadName(int aThreadId) {
|
||||
if (aThreadId == 0 && sMainThread != null) {
|
||||
return sMainThread.getName();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private synchronized static Sample getSample(int aThreadId, int aSampleId) {
|
||||
return sSamplingRunnable.getSample(aThreadId, aSampleId);
|
||||
}
|
||||
|
||||
@WrapElementForJNI(allowMultithread = true, stubName = "GetSampleTimeJavaProfiling")
|
||||
public synchronized static double getSampleTime(int aThreadId, int aSampleId) {
|
||||
Sample sample = getSample(aThreadId, aSampleId);
|
||||
if (sample != null) {
|
||||
if (sample.mJavaTime != 0) {
|
||||
return (sample.mJavaTime -
|
||||
SystemClock.elapsedRealtime()) + getProfilerTime();
|
||||
}
|
||||
System.out.println("Sample: " + sample.mTime);
|
||||
return sample.mTime;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@WrapElementForJNI(allowMultithread = true, stubName = "GetFrameNameJavaProfilingWrapper")
|
||||
public synchronized static String getFrameName(int aThreadId, int aSampleId, int aFrameId) {
|
||||
Sample sample = getSample(aThreadId, aSampleId);
|
||||
if (sample != null && aFrameId < sample.mFrames.length) {
|
||||
Frame frame = sample.mFrames[aFrameId];
|
||||
if (frame == null) {
|
||||
return null;
|
||||
}
|
||||
return frame.className + "." + frame.methodName + "()";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@WrapElementForJNI(allowMultithread = true, stubName = "StartJavaProfiling")
|
||||
public static void start(int aInterval, int aSamples) {
|
||||
synchronized (GoannaJavaSampler.class) {
|
||||
if (sSamplingRunnable != null) {
|
||||
return;
|
||||
}
|
||||
sSamplingRunnable = new SamplingThread(aInterval, aSamples);
|
||||
sSamplingThread = new Thread(sSamplingRunnable, "Java Sampler");
|
||||
sSamplingThread.start();
|
||||
}
|
||||
}
|
||||
|
||||
@WrapElementForJNI(allowMultithread = true, stubName = "PauseJavaProfiling")
|
||||
public static void pause() {
|
||||
synchronized (GoannaJavaSampler.class) {
|
||||
sSamplingRunnable.mPauseSampler = true;
|
||||
}
|
||||
}
|
||||
|
||||
@WrapElementForJNI(allowMultithread = true, stubName = "UnpauseJavaProfiling")
|
||||
public static void unpause() {
|
||||
synchronized (GoannaJavaSampler.class) {
|
||||
sSamplingRunnable.mPauseSampler = false;
|
||||
}
|
||||
}
|
||||
|
||||
@WrapElementForJNI(allowMultithread = true, stubName = "StopJavaProfiling")
|
||||
public static void stop() {
|
||||
synchronized (GoannaJavaSampler.class) {
|
||||
if (sSamplingThread == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
sSamplingRunnable.mStopSampler = true;
|
||||
try {
|
||||
sSamplingThread.join();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
sSamplingThread = null;
|
||||
sSamplingRunnable = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setLibsLoaded() {
|
||||
sLibsLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.goanna.util.EventCallback;
|
||||
|
||||
/**
|
||||
* Wrapper for MediaRouter types supported by Android, such as Chromecast, Miracast, etc.
|
||||
*/
|
||||
interface GoannaMediaPlayer {
|
||||
/**
|
||||
* Can return null.
|
||||
*/
|
||||
JSONObject toJSON();
|
||||
void load(String title, String url, String type, EventCallback callback);
|
||||
void play(EventCallback callback);
|
||||
void pause(EventCallback callback);
|
||||
void stop(EventCallback callback);
|
||||
void start(EventCallback callback);
|
||||
void end(EventCallback callback);
|
||||
void mirror(EventCallback callback);
|
||||
void message(String message, EventCallback callback);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
public class GoannaMessageReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
final String action = intent.getAction();
|
||||
if (GoannaApp.ACTION_INIT_PW.equals(action)) {
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("Passwords:Init", null));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.mozglue.JNITarget;
|
||||
import org.mozilla.goanna.util.NativeEventListener;
|
||||
import org.mozilla.goanna.util.NativeJSObject;
|
||||
import org.mozilla.goanna.util.EventCallback;
|
||||
|
||||
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.NetworkInfo;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
|
||||
/*
|
||||
* A part of the work of GoannaNetworkManager is to give an general connection
|
||||
* type based on the current connection. According to spec of NetworkInformation
|
||||
* 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 the information of
|
||||
* exact network type, especially the cellular network type.
|
||||
*
|
||||
* Current connection is firstly obtained from Android's ConnectivityManager,
|
||||
* which is represented by the constant, and then will be mapped into the
|
||||
* connection type defined in Network Information API version 3.
|
||||
*/
|
||||
|
||||
public class GoannaNetworkManager extends BroadcastReceiver implements NativeEventListener {
|
||||
private static final String LOGTAG = "GoannaNetworkManager";
|
||||
|
||||
private static GoannaNetworkManager sInstance;
|
||||
|
||||
public static void destroy() {
|
||||
if (sInstance != null) {
|
||||
sInstance.onDestroy();
|
||||
sInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Connection Type defined in Network Information API v3.
|
||||
private enum ConnectionType {
|
||||
CELLULAR(0),
|
||||
BLUETOOTH(1),
|
||||
ETHERNET(2),
|
||||
WIFI(3),
|
||||
OTHER(4),
|
||||
NONE(5);
|
||||
|
||||
public final int value;
|
||||
|
||||
private ConnectionType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
private enum InfoType {
|
||||
MCC,
|
||||
MNC
|
||||
}
|
||||
|
||||
private GoannaNetworkManager() {
|
||||
EventDispatcher.getInstance().registerGoannaThreadListener(this, "Wifi:Enable");
|
||||
}
|
||||
|
||||
private void onDestroy() {
|
||||
EventDispatcher.getInstance().unregisterGoannaThreadListener(this, "Wifi:Enable");
|
||||
}
|
||||
|
||||
private volatile ConnectionType mConnectionType = ConnectionType.NONE;
|
||||
private final IntentFilter mNetworkFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
|
||||
|
||||
// Whether the manager should be listening to Network Information changes.
|
||||
private boolean mShouldBeListening;
|
||||
|
||||
// Whether the manager should notify Goanna that a change in Network
|
||||
// Information happened.
|
||||
private boolean mShouldNotify;
|
||||
|
||||
// The application context used for registering receivers, so
|
||||
// we can unregister them again later.
|
||||
private volatile Context mApplicationContext;
|
||||
private boolean mIsListening;
|
||||
|
||||
public static GoannaNetworkManager getInstance() {
|
||||
if (sInstance == null) {
|
||||
sInstance = new GoannaNetworkManager();
|
||||
}
|
||||
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context aContext, Intent aIntent) {
|
||||
updateConnectionType();
|
||||
}
|
||||
|
||||
public void start(final Context context) {
|
||||
// Note that this initialization clause only runs once.
|
||||
mApplicationContext = context.getApplicationContext();
|
||||
if (mConnectionType == ConnectionType.NONE) {
|
||||
mConnectionType = getConnectionType();
|
||||
}
|
||||
|
||||
mShouldBeListening = true;
|
||||
updateConnectionType();
|
||||
|
||||
if (mShouldNotify) {
|
||||
startListening();
|
||||
}
|
||||
}
|
||||
|
||||
private void startListening() {
|
||||
if (mIsListening) {
|
||||
Log.w(LOGTAG, "Already started!");
|
||||
return;
|
||||
}
|
||||
|
||||
final Context appContext = mApplicationContext;
|
||||
if (appContext == null) {
|
||||
Log.w(LOGTAG, "Not registering receiver: no context!");
|
||||
return;
|
||||
}
|
||||
|
||||
// registerReceiver will return null if registering fails.
|
||||
if (appContext.registerReceiver(this, mNetworkFilter) == null) {
|
||||
Log.e(LOGTAG, "Registering receiver failed");
|
||||
} else {
|
||||
mIsListening = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
mShouldBeListening = false;
|
||||
|
||||
if (mShouldNotify) {
|
||||
stopListening();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(final String event, final NativeJSObject message,
|
||||
final EventCallback callback) {
|
||||
if (event.equals("Wifi:Enable")) {
|
||||
final WifiManager mgr = (WifiManager) mApplicationContext.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);
|
||||
mApplicationContext.startActivity(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void stopListening() {
|
||||
if (null == mApplicationContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mIsListening) {
|
||||
Log.w(LOGTAG, "Already stopped!");
|
||||
return;
|
||||
}
|
||||
|
||||
mApplicationContext.unregisterReceiver(this);
|
||||
mIsListening = false;
|
||||
}
|
||||
|
||||
private int wifiDhcpGatewayAddress() {
|
||||
if (mConnectionType != ConnectionType.WIFI) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (null == mApplicationContext) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
WifiManager mgr = (WifiManager) mApplicationContext.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;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateConnectionType() {
|
||||
final ConnectionType previousConnectionType = mConnectionType;
|
||||
final ConnectionType newConnectionType = getConnectionType();
|
||||
if (newConnectionType == previousConnectionType) {
|
||||
return;
|
||||
}
|
||||
|
||||
mConnectionType = newConnectionType;
|
||||
|
||||
if (!mShouldNotify) {
|
||||
return;
|
||||
}
|
||||
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createNetworkEvent(
|
||||
newConnectionType.value,
|
||||
newConnectionType == ConnectionType.WIFI,
|
||||
wifiDhcpGatewayAddress()));
|
||||
}
|
||||
|
||||
public double[] getCurrentInformation() {
|
||||
final ConnectionType connectionType = mConnectionType;
|
||||
return new double[] { connectionType.value,
|
||||
connectionType == ConnectionType.WIFI ? 1.0 : 0.0,
|
||||
wifiDhcpGatewayAddress() };
|
||||
}
|
||||
|
||||
public void enableNotifications() {
|
||||
// We set mShouldNotify *after* calling updateConnectionType() to make sure we
|
||||
// don't notify an eventual change in mConnectionType.
|
||||
mConnectionType = ConnectionType.NONE; // force a notification
|
||||
updateConnectionType();
|
||||
mShouldNotify = true;
|
||||
|
||||
if (mShouldBeListening) {
|
||||
startListening();
|
||||
}
|
||||
}
|
||||
|
||||
public void disableNotifications() {
|
||||
mShouldNotify = false;
|
||||
|
||||
if (mShouldBeListening) {
|
||||
stopListening();
|
||||
}
|
||||
}
|
||||
|
||||
private ConnectionType getConnectionType() {
|
||||
final Context appContext = mApplicationContext;
|
||||
|
||||
if (null == appContext) {
|
||||
return ConnectionType.NONE;
|
||||
}
|
||||
|
||||
ConnectivityManager cm = (ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (cm == null) {
|
||||
Log.e(LOGTAG, "Connectivity service does not exist");
|
||||
return ConnectionType.NONE;
|
||||
}
|
||||
|
||||
NetworkInfo ni = null;
|
||||
try {
|
||||
ni = cm.getActiveNetworkInfo();
|
||||
} catch (SecurityException se) {} // if we don't have the permission, fall through to null check
|
||||
|
||||
if (ni == null) {
|
||||
return ConnectionType.NONE;
|
||||
}
|
||||
|
||||
switch (ni.getType()) {
|
||||
case ConnectivityManager.TYPE_BLUETOOTH:
|
||||
return ConnectionType.BLUETOOTH;
|
||||
case ConnectivityManager.TYPE_ETHERNET:
|
||||
return ConnectionType.ETHERNET;
|
||||
case ConnectivityManager.TYPE_MOBILE:
|
||||
case ConnectivityManager.TYPE_WIMAX:
|
||||
return ConnectionType.CELLULAR;
|
||||
case ConnectivityManager.TYPE_WIFI:
|
||||
return ConnectionType.WIFI;
|
||||
default:
|
||||
Log.w(LOGTAG, "Ignoring the current network type.");
|
||||
return ConnectionType.OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
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 GoannaAppShell has been
|
||||
* initialized: they depend on access to the context.
|
||||
*/
|
||||
@JNITarget
|
||||
public static int getMCC() {
|
||||
return getNetworkOperator(InfoType.MCC, GoannaAppShell.getContext().getApplicationContext());
|
||||
}
|
||||
|
||||
@JNITarget
|
||||
public static int getMNC() {
|
||||
return getNetworkOperator(InfoType.MNC, GoannaAppShell.getContext().getApplicationContext());
|
||||
}
|
||||
}
|
||||
@@ -1,888 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Hashtable;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.mozilla.goanna.GoannaProfileDirectories.NoMozillaDirectoryException;
|
||||
import org.mozilla.goanna.GoannaProfileDirectories.NoSuchProfileException;
|
||||
import org.mozilla.goanna.db.BrowserDB;
|
||||
import org.mozilla.goanna.db.LocalBrowserDB;
|
||||
import org.mozilla.goanna.db.StubBrowserDB;
|
||||
import org.mozilla.goanna.distribution.Distribution;
|
||||
import org.mozilla.goanna.mozglue.ContextUtils;
|
||||
import org.mozilla.goanna.mozglue.RobocopTarget;
|
||||
import org.mozilla.goanna.firstrun.FirstrunPane;
|
||||
import org.mozilla.goanna.util.INIParser;
|
||||
import org.mozilla.goanna.util.INISection;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
public final class GoannaProfile {
|
||||
private static final String LOGTAG = "GoannaProfile";
|
||||
|
||||
// Only tests should need to do this.
|
||||
// We can default this to AppConstants.RELEASE_BUILD once we fix Bug 1069687.
|
||||
private static volatile boolean sAcceptDirectoryChanges = true;
|
||||
|
||||
@RobocopTarget
|
||||
public static void enableDirectoryChanges() {
|
||||
Log.w(LOGTAG, "Directory changes should only be enabled for tests. And even then it's a bad idea.");
|
||||
sAcceptDirectoryChanges = true;
|
||||
}
|
||||
|
||||
// Used to "lock" the guest profile, so that we'll always restart in it
|
||||
private static final String LOCK_FILE_NAME = ".active_lock";
|
||||
public static final String DEFAULT_PROFILE = "default";
|
||||
public static final String GUEST_PROFILE = "guest";
|
||||
|
||||
private static final HashMap<String, GoannaProfile> sProfileCache = new HashMap<String, GoannaProfile>();
|
||||
private static String sDefaultProfileName;
|
||||
|
||||
// Caches the guest profile dir.
|
||||
private static File sGuestDir;
|
||||
private static GoannaProfile sGuestProfile;
|
||||
|
||||
public static boolean sIsUsingCustomProfile;
|
||||
|
||||
private final String mName;
|
||||
private final File mMozillaDir;
|
||||
private final boolean mIsWebAppProfile;
|
||||
private final Context mApplicationContext;
|
||||
|
||||
private final BrowserDB mDB;
|
||||
|
||||
/**
|
||||
* Access to this member should be synchronized to avoid
|
||||
* races during creation -- particularly between getDir and GoannaView#init.
|
||||
*
|
||||
* Not final because this is lazily computed.
|
||||
*/
|
||||
private File mProfileDir;
|
||||
|
||||
// Caches whether or not a profile is "locked".
|
||||
// Only used by the guest profile to determine if it should be reused or
|
||||
// deleted on startup.
|
||||
// These are volatile for an incremental improvement in thread safety,
|
||||
// but this is not a complete solution for concurrency.
|
||||
private volatile LockState mLocked = LockState.UNDEFINED;
|
||||
private volatile boolean mInGuestMode;
|
||||
|
||||
// Constants to cache whether or not a profile is "locked".
|
||||
private enum LockState {
|
||||
LOCKED,
|
||||
UNLOCKED,
|
||||
UNDEFINED
|
||||
};
|
||||
|
||||
/**
|
||||
* Warning: has a side-effect of setting sIsUsingCustomProfile.
|
||||
* Can return null.
|
||||
*/
|
||||
public static GoannaProfile getFromArgs(final Context context, final String args) {
|
||||
if (args == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String profileName = null;
|
||||
String profilePath = null;
|
||||
if (args.contains("-P")) {
|
||||
final Pattern p = Pattern.compile("(?:-P\\s*)(\\w*)(\\s*)");
|
||||
final Matcher m = p.matcher(args);
|
||||
if (m.find()) {
|
||||
profileName = m.group(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (args.contains("-profile")) {
|
||||
final Pattern p = Pattern.compile("(?:-profile\\s*)(\\S*)(\\s*)");
|
||||
final Matcher m = p.matcher(args);
|
||||
if (m.find()) {
|
||||
profilePath = m.group(1);
|
||||
}
|
||||
|
||||
if (profileName == null) {
|
||||
profileName = GoannaProfile.DEFAULT_PROFILE;
|
||||
}
|
||||
|
||||
GoannaProfile.sIsUsingCustomProfile = true;
|
||||
}
|
||||
|
||||
if (profileName == null && profilePath == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return GoannaProfile.get(context, profileName, profilePath);
|
||||
}
|
||||
|
||||
public static GoannaProfile get(Context context) {
|
||||
boolean isGoannaApp = false;
|
||||
try {
|
||||
isGoannaApp = context instanceof GoannaApp;
|
||||
} catch (NoClassDefFoundError ex) {}
|
||||
|
||||
if (isGoannaApp) {
|
||||
// Check for a cached profile on this context already
|
||||
// TODO: We should not be caching profile information on the Activity context
|
||||
final GoannaApp goannaApp = (GoannaApp) context;
|
||||
if (goannaApp.mProfile != null) {
|
||||
return goannaApp.mProfile;
|
||||
}
|
||||
}
|
||||
|
||||
final String args;
|
||||
if (context instanceof Activity) {
|
||||
args = ContextUtils.getStringExtra(((Activity) context).getIntent(), "args");
|
||||
} else {
|
||||
args = null;
|
||||
}
|
||||
|
||||
if (GuestSession.shouldUse(context, args)) {
|
||||
final GoannaProfile p = GoannaProfile.getOrCreateGuestProfile(context);
|
||||
if (isGoannaApp) {
|
||||
((GoannaApp) context).mProfile = p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
final GoannaProfile fromArgs = GoannaProfile.getFromArgs(context, args);
|
||||
if (fromArgs != null) {
|
||||
if (isGoannaApp) {
|
||||
((GoannaApp) context).mProfile = fromArgs;
|
||||
}
|
||||
return fromArgs;
|
||||
}
|
||||
|
||||
if (isGoannaApp) {
|
||||
final GoannaApp goannaApp = (GoannaApp) context;
|
||||
String defaultProfileName;
|
||||
try {
|
||||
defaultProfileName = goannaApp.getDefaultProfileName();
|
||||
} catch (NoMozillaDirectoryException e) {
|
||||
// If this failed, we're screwed. But there are so many callers that
|
||||
// we'll just throw a RuntimeException.
|
||||
Log.wtf(LOGTAG, "Unable to get default profile name.", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
// Otherwise, get the default profile for the Activity.
|
||||
return get(context, defaultProfileName);
|
||||
}
|
||||
|
||||
return get(context, "");
|
||||
}
|
||||
|
||||
public static GoannaProfile get(Context context, String profileName) {
|
||||
synchronized (sProfileCache) {
|
||||
GoannaProfile profile = sProfileCache.get(profileName);
|
||||
if (profile != null)
|
||||
return profile;
|
||||
}
|
||||
return get(context, profileName, (File)null);
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
public static GoannaProfile get(Context context, String profileName, String profilePath) {
|
||||
File dir = null;
|
||||
if (!TextUtils.isEmpty(profilePath)) {
|
||||
dir = new File(profilePath);
|
||||
if (!dir.exists() || !dir.isDirectory()) {
|
||||
Log.w(LOGTAG, "requested profile directory missing: " + profilePath);
|
||||
}
|
||||
}
|
||||
return get(context, profileName, dir);
|
||||
}
|
||||
|
||||
// Extension hook.
|
||||
private static volatile BrowserDB.Factory sDBFactory;
|
||||
public static void setBrowserDBFactory(BrowserDB.Factory factory) {
|
||||
sDBFactory = factory;
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
public static GoannaProfile get(Context context, String profileName, File profileDir) {
|
||||
if (sDBFactory == null) {
|
||||
// We do this so that GoannaView consumers don't need to know anything about BrowserDB.
|
||||
// It's a bit of a broken abstraction, but very tightly coupled, so we work around it
|
||||
// for now. We can't just have GoannaView set this, because then it would collide in
|
||||
// Fennec's use of GoannaView.
|
||||
// We should never see this in Fennec itself, because GoannaApplication sets the factory
|
||||
// in onCreate.
|
||||
Log.d(LOGTAG, "Defaulting to StubBrowserDB.");
|
||||
sDBFactory = StubBrowserDB.getFactory();
|
||||
}
|
||||
return GoannaProfile.get(context, profileName, profileDir, sDBFactory);
|
||||
}
|
||||
|
||||
// Note that the profile cache respects only the profile name!
|
||||
// If the directory changes, the returned GoannaProfile instance will be mutated.
|
||||
// If the factory differs, it will be *ignored*.
|
||||
public static GoannaProfile get(Context context, String profileName, File profileDir, BrowserDB.Factory dbFactory) {
|
||||
Log.v(LOGTAG, "Fetching profile: '" + profileName + "', '" + profileDir + "'");
|
||||
if (context == null) {
|
||||
throw new IllegalArgumentException("context must be non-null");
|
||||
}
|
||||
|
||||
// If no profile was passed in, look for the default profile listed in profiles.ini.
|
||||
// If that doesn't exist, look for a profile called 'default'.
|
||||
if (TextUtils.isEmpty(profileName) && profileDir == null) {
|
||||
try {
|
||||
profileName = GoannaProfile.getDefaultProfileName(context);
|
||||
} catch (NoMozillaDirectoryException e) {
|
||||
// We're unable to do anything sane here.
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Actually try to look up the profile.
|
||||
synchronized (sProfileCache) {
|
||||
GoannaProfile profile = sProfileCache.get(profileName);
|
||||
if (profile == null) {
|
||||
try {
|
||||
profile = new GoannaProfile(context, profileName, profileDir, dbFactory);
|
||||
} catch (NoMozillaDirectoryException e) {
|
||||
// We're unable to do anything sane here.
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
sProfileCache.put(profileName, profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
if (profileDir == null) {
|
||||
// Fine.
|
||||
return profile;
|
||||
}
|
||||
|
||||
if (profile.getDir().equals(profileDir)) {
|
||||
// Great! We're consistent.
|
||||
return profile;
|
||||
}
|
||||
|
||||
if (sAcceptDirectoryChanges) {
|
||||
if (AppConstants.RELEASE_BUILD) {
|
||||
Log.e(LOGTAG, "Release build trying to switch out profile dir. This is an error, but let's do what we can.");
|
||||
}
|
||||
profile.setDir(profileDir);
|
||||
return profile;
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Refusing to reuse profile with a different directory.");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean removeProfile(Context context, String profileName) {
|
||||
if (profileName == null) {
|
||||
Log.w(LOGTAG, "Unable to remove profile: null profile name.");
|
||||
return false;
|
||||
}
|
||||
|
||||
final GoannaProfile profile = get(context, profileName);
|
||||
if (profile == null) {
|
||||
return false;
|
||||
}
|
||||
final boolean success = profile.remove();
|
||||
|
||||
if (success) {
|
||||
// Clear all shared prefs for the given profile.
|
||||
GoannaSharedPrefs.forProfileName(context, profileName)
|
||||
.edit().clear().apply();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
// Only public for access from tests.
|
||||
@RobocopTarget
|
||||
public static GoannaProfile createGuestProfile(Context context) {
|
||||
try {
|
||||
// We need to force the creation of a new guest profile if we want it outside of the normal profile path,
|
||||
// otherwise GoannaProfile.getDir will try to be smart and build it for us in the normal profiles dir.
|
||||
getGuestDir(context).mkdir();
|
||||
GoannaProfile profile = getGuestProfile(context);
|
||||
|
||||
// If we're creating this guest session over the keyguard, don't lock it.
|
||||
// This will force the guest session to exit if the user unlocks their phone
|
||||
// and starts Fennec.
|
||||
profile.lock();
|
||||
|
||||
/*
|
||||
* Now do the things that createProfileDirectory normally does --
|
||||
* right now that's kicking off DB init.
|
||||
*/
|
||||
profile.enqueueInitialization(profile.getDir());
|
||||
|
||||
return profile;
|
||||
} catch (Exception ex) {
|
||||
Log.e(LOGTAG, "Error creating guest profile", ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void leaveGuestSession(Context context) {
|
||||
GoannaProfile profile = getGuestProfile(context);
|
||||
if (profile != null) {
|
||||
profile.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private static File getGuestDir(Context context) {
|
||||
if (sGuestDir == null) {
|
||||
sGuestDir = context.getFileStreamPath("guest");
|
||||
}
|
||||
return sGuestDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs IO. Be careful of using this on the main thread.
|
||||
*/
|
||||
public static GoannaProfile getOrCreateGuestProfile(Context context) {
|
||||
GoannaProfile p = getGuestProfile(context);
|
||||
if (p == null) {
|
||||
return createGuestProfile(context);
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
public static GoannaProfile getGuestProfile(Context context) {
|
||||
if (sGuestProfile == null) {
|
||||
File guestDir = getGuestDir(context);
|
||||
if (guestDir.exists()) {
|
||||
sGuestProfile = get(context, GUEST_PROFILE, guestDir);
|
||||
sGuestProfile.mInGuestMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
return sGuestProfile;
|
||||
}
|
||||
|
||||
public static boolean maybeCleanupGuestProfile(final Context context) {
|
||||
final GoannaProfile profile = getGuestProfile(context);
|
||||
if (profile == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!profile.locked()) {
|
||||
profile.mInGuestMode = false;
|
||||
|
||||
// If the guest dir exists, but it's unlocked, delete it
|
||||
removeGuestProfile(context);
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void removeGuestProfile(Context context) {
|
||||
boolean success = false;
|
||||
try {
|
||||
File guestDir = getGuestDir(context);
|
||||
if (guestDir.exists()) {
|
||||
success = delete(guestDir);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Log.e(LOGTAG, "Error removing guest profile", ex);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
// Clear all shared prefs for the guest profile.
|
||||
GoannaSharedPrefs.forProfileName(context, GUEST_PROFILE)
|
||||
.edit().clear().apply();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean delete(File file) throws IOException {
|
||||
// Try to do a quick initial delete
|
||||
if (file.delete())
|
||||
return true;
|
||||
|
||||
if (file.isDirectory()) {
|
||||
// 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);
|
||||
delete(fileDelete);
|
||||
}
|
||||
}
|
||||
|
||||
// Even if this is a dir, it should now be empty and delete should work
|
||||
return file.delete();
|
||||
}
|
||||
|
||||
private GoannaProfile(Context context, String profileName, File profileDir, BrowserDB.Factory dbFactory) throws NoMozillaDirectoryException {
|
||||
if (TextUtils.isEmpty(profileName)) {
|
||||
throw new IllegalArgumentException("Unable to create GoannaProfile for empty profile name.");
|
||||
}
|
||||
|
||||
mApplicationContext = context.getApplicationContext();
|
||||
mName = profileName;
|
||||
mIsWebAppProfile = profileName.startsWith("webapp");
|
||||
mMozillaDir = GoannaProfileDirectories.getMozillaDirectory(context);
|
||||
|
||||
// This apes the behavior of setDir.
|
||||
if (profileDir != null && profileDir.exists() && profileDir.isDirectory()) {
|
||||
mProfileDir = profileDir;
|
||||
}
|
||||
|
||||
// N.B., mProfileDir can be null at this point.
|
||||
mDB = dbFactory.get(profileName, mProfileDir);
|
||||
}
|
||||
|
||||
public BrowserDB getDB() {
|
||||
return mDB;
|
||||
}
|
||||
|
||||
// Warning, Changing the lock file state from outside apis will cause this to become out of sync
|
||||
public boolean locked() {
|
||||
if (mLocked != LockState.UNDEFINED) {
|
||||
return mLocked == LockState.LOCKED;
|
||||
}
|
||||
|
||||
boolean profileExists;
|
||||
synchronized (this) {
|
||||
profileExists = mProfileDir != null && mProfileDir.exists();
|
||||
}
|
||||
|
||||
// Don't use getDir() as it will create a dir if none exists.
|
||||
if (profileExists) {
|
||||
File lockFile = new File(mProfileDir, LOCK_FILE_NAME);
|
||||
boolean res = lockFile.exists();
|
||||
mLocked = res ? LockState.LOCKED : LockState.UNLOCKED;
|
||||
} else {
|
||||
mLocked = LockState.UNLOCKED;
|
||||
}
|
||||
|
||||
return mLocked == LockState.LOCKED;
|
||||
}
|
||||
|
||||
public boolean lock() {
|
||||
try {
|
||||
// If this dir doesn't exist getDir will create it for us
|
||||
final File lockFile = new File(getDir(), LOCK_FILE_NAME);
|
||||
final boolean result = lockFile.createNewFile();
|
||||
if (lockFile.exists()) {
|
||||
mLocked = LockState.LOCKED;
|
||||
} else {
|
||||
mLocked = LockState.UNLOCKED;
|
||||
}
|
||||
return result;
|
||||
} catch(IOException ex) {
|
||||
Log.e(LOGTAG, "Error locking profile", ex);
|
||||
}
|
||||
mLocked = LockState.UNLOCKED;
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean unlock() {
|
||||
final File profileDir;
|
||||
synchronized (this) {
|
||||
// Don't use getDir() as it will create a dir.
|
||||
profileDir = mProfileDir;
|
||||
}
|
||||
|
||||
if (profileDir == null || !profileDir.exists()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
final File lockFile = new File(profileDir, LOCK_FILE_NAME);
|
||||
if (!lockFile.exists()) {
|
||||
mLocked = LockState.UNLOCKED;
|
||||
return true;
|
||||
}
|
||||
|
||||
final boolean result = delete(lockFile);
|
||||
if (result) {
|
||||
mLocked = LockState.UNLOCKED;
|
||||
} else {
|
||||
mLocked = LockState.LOCKED;
|
||||
}
|
||||
return result;
|
||||
} catch(IOException ex) {
|
||||
Log.e(LOGTAG, "Error unlocking profile", ex);
|
||||
}
|
||||
|
||||
mLocked = LockState.LOCKED;
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean inGuestMode() {
|
||||
return mInGuestMode;
|
||||
}
|
||||
|
||||
private void setDir(File dir) {
|
||||
if (dir != null && dir.exists() && dir.isDirectory()) {
|
||||
synchronized (this) {
|
||||
mProfileDir = dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
public synchronized File getDir() {
|
||||
forceCreate();
|
||||
return mProfileDir;
|
||||
}
|
||||
|
||||
public synchronized GoannaProfile forceCreate() {
|
||||
if (mProfileDir != null) {
|
||||
return this;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if a profile with this name already exists.
|
||||
try {
|
||||
mProfileDir = findProfileDir();
|
||||
Log.d(LOGTAG, "Found profile dir.");
|
||||
} catch (NoSuchProfileException noSuchProfile) {
|
||||
// If it doesn't exist, create it.
|
||||
mProfileDir = createProfileDir();
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
Log.e(LOGTAG, "Error getting profile dir", ioe);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public File getFile(String aFile) {
|
||||
File f = getDir();
|
||||
if (f == null)
|
||||
return null;
|
||||
|
||||
return new File(f, aFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the session file to the backup session file.
|
||||
*
|
||||
* sessionstore.js should hold the current session, and sessionstore.bak
|
||||
* should hold the previous session (where it is used to read the "tabs
|
||||
* from last time"). Normally, sessionstore.js is moved to sessionstore.bak
|
||||
* on a clean quit, but this doesn't happen if Fennec crashed. Thus, this
|
||||
* method should be called after a crash so sessionstore.bak correctly
|
||||
* holds the previous session.
|
||||
*/
|
||||
public void moveSessionFile() {
|
||||
File sessionFile = getFile("sessionstore.js");
|
||||
if (sessionFile != null && sessionFile.exists()) {
|
||||
File sessionFileBackup = getFile("sessionstore.bak");
|
||||
sessionFile.renameTo(sessionFileBackup);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string from a session file.
|
||||
*
|
||||
* The session can either be read from sessionstore.js or sessionstore.bak.
|
||||
* In general, sessionstore.js holds the current session, and
|
||||
* sessionstore.bak holds the previous session.
|
||||
*
|
||||
* @param readBackup if true, the session is read from sessionstore.bak;
|
||||
* otherwise, the session is read from sessionstore.js
|
||||
*
|
||||
* @return the session string
|
||||
*/
|
||||
public String readSessionFile(boolean readBackup) {
|
||||
File sessionFile = getFile(readBackup ? "sessionstore.bak" : "sessionstore.js");
|
||||
|
||||
try {
|
||||
if (sessionFile != null && sessionFile.exists()) {
|
||||
return readFile(sessionFile);
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
Log.e(LOGTAG, "Unable to read session file", ioe);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String readFile(String filename) throws IOException {
|
||||
File dir = getDir();
|
||||
if (dir == null) {
|
||||
throw new IOException("No profile directory found");
|
||||
}
|
||||
File target = new File(dir, filename);
|
||||
return readFile(target);
|
||||
}
|
||||
|
||||
private String readFile(File target) throws IOException {
|
||||
FileReader fr = new FileReader(target);
|
||||
try {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
char[] buf = new char[8192];
|
||||
int read = fr.read(buf);
|
||||
while (read >= 0) {
|
||||
sb.append(buf, 0, read);
|
||||
read = fr.read(buf);
|
||||
}
|
||||
return sb.toString();
|
||||
} finally {
|
||||
fr.close();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean remove() {
|
||||
try {
|
||||
synchronized (this) {
|
||||
final File dir = getDir();
|
||||
if (dir.exists()) {
|
||||
delete(dir);
|
||||
}
|
||||
|
||||
try {
|
||||
mProfileDir = findProfileDir();
|
||||
} catch (NoSuchProfileException noSuchProfile) {
|
||||
// If the profile doesn't exist, there's nothing left for us to do.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final INIParser parser = GoannaProfileDirectories.getProfilesINI(mMozillaDir);
|
||||
final Hashtable<String, INISection> sections = parser.getSections();
|
||||
for (Enumeration<INISection> e = sections.elements(); e.hasMoreElements();) {
|
||||
final INISection section = e.nextElement();
|
||||
String name = section.getStringProperty("Name");
|
||||
|
||||
if (name == null || !name.equals(mName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (section.getName().startsWith("Profile")) {
|
||||
// ok, we have stupid Profile#-named things. Rename backwards.
|
||||
try {
|
||||
int sectionNumber = Integer.parseInt(section.getName().substring("Profile".length()));
|
||||
String curSection = "Profile" + sectionNumber;
|
||||
String nextSection = "Profile" + (sectionNumber+1);
|
||||
|
||||
sections.remove(curSection);
|
||||
|
||||
while (sections.containsKey(nextSection)) {
|
||||
parser.renameSection(nextSection, curSection);
|
||||
sectionNumber++;
|
||||
|
||||
curSection = nextSection;
|
||||
nextSection = "Profile" + (sectionNumber+1);
|
||||
}
|
||||
} catch (NumberFormatException nex) {
|
||||
// uhm, malformed Profile thing; we can't do much.
|
||||
Log.e(LOGTAG, "Malformed section name in profiles.ini: " + section.getName());
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// this really shouldn't be the case, but handle it anyway
|
||||
parser.removeSection(mName);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
parser.write();
|
||||
return true;
|
||||
} catch (IOException ex) {
|
||||
Log.w(LOGTAG, "Failed to remove profile.", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the default profile name for this application, or
|
||||
* {@link GoannaProfile#DEFAULT_PROFILE} if none could be found.
|
||||
*
|
||||
* @throws NoMozillaDirectoryException
|
||||
* if the Mozilla directory did not exist and could not be
|
||||
* created.
|
||||
*/
|
||||
public static String getDefaultProfileName(final Context context) throws NoMozillaDirectoryException {
|
||||
// Have we read the default profile from the INI already?
|
||||
// Changing the default profile requires a restart, so we don't
|
||||
// need to worry about runtime changes.
|
||||
if (sDefaultProfileName != null) {
|
||||
return sDefaultProfileName;
|
||||
}
|
||||
|
||||
final String profileName = GoannaProfileDirectories.findDefaultProfileName(context);
|
||||
if (profileName == null) {
|
||||
// Note that we don't persist this back to profiles.ini.
|
||||
sDefaultProfileName = DEFAULT_PROFILE;
|
||||
return DEFAULT_PROFILE;
|
||||
}
|
||||
|
||||
sDefaultProfileName = profileName;
|
||||
return sDefaultProfileName;
|
||||
}
|
||||
|
||||
private File findProfileDir() throws NoSuchProfileException {
|
||||
return GoannaProfileDirectories.findProfileDir(mMozillaDir, mName);
|
||||
}
|
||||
|
||||
private File createProfileDir() throws IOException {
|
||||
INIParser parser = GoannaProfileDirectories.getProfilesINI(mMozillaDir);
|
||||
|
||||
// Salt the name of our requested profile
|
||||
String saltedName = GoannaProfileDirectories.saltProfileName(mName);
|
||||
File profileDir = new File(mMozillaDir, saltedName);
|
||||
while (profileDir.exists()) {
|
||||
saltedName = GoannaProfileDirectories.saltProfileName(mName);
|
||||
profileDir = new File(mMozillaDir, saltedName);
|
||||
}
|
||||
|
||||
// Attempt to create the salted profile dir
|
||||
if (!profileDir.mkdirs()) {
|
||||
throw new IOException("Unable to create profile.");
|
||||
}
|
||||
Log.d(LOGTAG, "Created new profile dir.");
|
||||
|
||||
// Now update profiles.ini
|
||||
// If this is the first time its created, we also add a General section
|
||||
// look for the first profile number that isn't taken yet
|
||||
int profileNum = 0;
|
||||
boolean isDefaultSet = false;
|
||||
INISection profileSection;
|
||||
while ((profileSection = parser.getSection("Profile" + profileNum)) != null) {
|
||||
profileNum++;
|
||||
if (profileSection.getProperty("Default") != null) {
|
||||
isDefaultSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
profileSection = new INISection("Profile" + profileNum);
|
||||
profileSection.setProperty("Name", mName);
|
||||
profileSection.setProperty("IsRelative", 1);
|
||||
profileSection.setProperty("Path", saltedName);
|
||||
|
||||
if (parser.getSection("General") == null) {
|
||||
INISection generalSection = new INISection("General");
|
||||
generalSection.setProperty("StartWithLastProfile", 1);
|
||||
parser.addSection(generalSection);
|
||||
}
|
||||
|
||||
if (!isDefaultSet && !mIsWebAppProfile) {
|
||||
// only set as default if this is the first non-webapp
|
||||
// profile we're creating
|
||||
profileSection.setProperty("Default", 1);
|
||||
|
||||
// We have no intention of stopping this session. The FIRSTRUN session
|
||||
// ends when the browsing session/activity has ended. All events
|
||||
// during firstrun will be tagged as FIRSTRUN.
|
||||
Telemetry.startUISession(TelemetryContract.Session.FIRSTRUN);
|
||||
}
|
||||
|
||||
parser.addSection(profileSection);
|
||||
parser.write();
|
||||
|
||||
// Trigger init for non-webapp profiles.
|
||||
if (!mIsWebAppProfile) {
|
||||
enqueueInitialization(profileDir);
|
||||
}
|
||||
|
||||
// Write out profile creation time, mirroring the logic in nsToolkitProfileService.
|
||||
try {
|
||||
FileOutputStream stream = new FileOutputStream(profileDir.getAbsolutePath() + File.separator + "times.json");
|
||||
OutputStreamWriter writer = new OutputStreamWriter(stream, Charset.forName("UTF-8"));
|
||||
try {
|
||||
writer.append("{\"created\": " + System.currentTimeMillis() + "}\n");
|
||||
} finally {
|
||||
writer.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Best-effort.
|
||||
Log.w(LOGTAG, "Couldn't write times.json.", e);
|
||||
}
|
||||
|
||||
// Initialize pref flag for displaying the start pane for a new non-webapp profile.
|
||||
if (!mIsWebAppProfile) {
|
||||
final SharedPreferences prefs = GoannaSharedPrefs.forProfile(mApplicationContext);
|
||||
prefs.edit().putBoolean(FirstrunPane.PREF_FIRSTRUN_ENABLED, true).apply();
|
||||
}
|
||||
|
||||
return profileDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called once, immediately before creation of the profile
|
||||
* directory completes.
|
||||
*
|
||||
* It queues up work to be done in the background to prepare the profile,
|
||||
* such as adding default bookmarks.
|
||||
*
|
||||
* This is public for use *from tests only*!
|
||||
*/
|
||||
@RobocopTarget
|
||||
public void enqueueInitialization(final File profileDir) {
|
||||
Log.i(LOGTAG, "Enqueuing profile init.");
|
||||
final Context context = mApplicationContext;
|
||||
|
||||
// Add everything when we're done loading the distribution.
|
||||
final Distribution distribution = Distribution.getInstance(context);
|
||||
distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
|
||||
@Override
|
||||
public void distributionNotFound() {
|
||||
this.distributionFound(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void distributionFound(Distribution distribution) {
|
||||
Log.d(LOGTAG, "Running post-distribution task: bookmarks.");
|
||||
|
||||
final ContentResolver cr = context.getContentResolver();
|
||||
|
||||
// Because we are running in the background, we want to synchronize on the
|
||||
// GoannaProfile instance so that we don't race with main thread operations
|
||||
// such as locking/unlocking/removing the profile.
|
||||
synchronized (GoannaProfile.this) {
|
||||
// Skip initialization if the profile directory has been removed.
|
||||
if (!profileDir.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We pass the number of added bookmarks to ensure that the
|
||||
// indices of the distribution and default bookmarks are
|
||||
// contiguous. Because there are always at least as many
|
||||
// bookmarks as there are favicons, we can also guarantee that
|
||||
// the favicon IDs won't overlap.
|
||||
final LocalBrowserDB db = new LocalBrowserDB(getName());
|
||||
final int offset = distribution == null ? 0 : db.addDistributionBookmarks(cr, distribution, 0);
|
||||
db.addDefaultBookmarks(context, cr, offset);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void distributionArrivedLate(Distribution distribution) {
|
||||
Log.d(LOGTAG, "Running late distribution task: bookmarks.");
|
||||
// Recover as best we can.
|
||||
synchronized (GoannaProfile.this) {
|
||||
// Skip initialization if the profile directory has been removed.
|
||||
if (!profileDir.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final LocalBrowserDB db = new LocalBrowserDB(getName());
|
||||
// We assume we've been called very soon after startup, and so our offset
|
||||
// into "Mobile Bookmarks" is the number of bookmarks in the DB.
|
||||
final ContentResolver cr = context.getContentResolver();
|
||||
final int offset = db.getCount(cr, "bookmarks");
|
||||
db.addDistributionBookmarks(cr, distribution, offset);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.mozilla.goanna.mozglue.RobocopTarget;
|
||||
import org.mozilla.goanna.util.INIParser;
|
||||
import org.mozilla.goanna.util.INISection;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* <code>GoannaProfileDirectories</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 GoannaProfileDirectories {
|
||||
@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 GoannaProfile} 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 GoannaProfile} 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 = GoannaProfileDirectories.getProfilesINI(getMozillaDirectory(context));
|
||||
|
||||
for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
|
||||
final INISection section = e.nextElement();
|
||||
if (section.getIntProperty("Default") == 1) {
|
||||
return section.getStringProperty("Name");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static Map<String, String> getDefaultProfile(final File mozillaDir) {
|
||||
return getMatchingProfiles(mozillaDir, sectionIsDefault, true);
|
||||
}
|
||||
|
||||
static Map<String, String> getProfilesNamed(final File mozillaDir, final String name) {
|
||||
final INISectionPredicate predicate = new INISectionPredicate() {
|
||||
@Override
|
||||
public boolean matches(final INISection section) {
|
||||
return name.equals(section.getStringProperty("Name"));
|
||||
}
|
||||
};
|
||||
return getMatchingProfiles(mozillaDir, predicate, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link GoannaProfileDirectories#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 = GoannaProfileDirectories.getProfilesINI(mozillaDir);
|
||||
|
||||
for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
|
||||
final INISection section = e.nextElement();
|
||||
if (predicate == null || predicate.matches(section)) {
|
||||
final String name = section.getStringProperty("Name");
|
||||
final String pathString = section.getStringProperty("Path");
|
||||
final boolean isRelative = section.getIntProperty("IsRelative") == 1;
|
||||
final File path = isRelative ? new File(mozillaDir, pathString) : new File(pathString);
|
||||
result.put(name, path.getAbsolutePath());
|
||||
|
||||
if (stopOnSuccess) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static File findProfileDir(final File mozillaDir, final String profileName) throws NoSuchProfileException {
|
||||
// Open profiles.ini to find the correct path.
|
||||
final INIParser parser = GoannaProfileDirectories.getProfilesINI(mozillaDir);
|
||||
|
||||
for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
|
||||
final INISection section = e.nextElement();
|
||||
final String name = section.getStringProperty("Name");
|
||||
if (name != null && name.equals(profileName)) {
|
||||
if (section.getIntProperty("IsRelative") == 1) {
|
||||
return new File(mozillaDir, section.getStringProperty("Path"));
|
||||
}
|
||||
return new File(section.getStringProperty("Path"));
|
||||
}
|
||||
}
|
||||
|
||||
throw new NoSuchProfileException("No profile " + profileName);
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.mozilla.goanna.GoannaProfileDirectories.NoMozillaDirectoryException;
|
||||
import org.mozilla.goanna.db.BrowserContract;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* This is not a per-profile provider. This provider allows read-only,
|
||||
* restricted access to certain attributes of Fennec profiles.
|
||||
*/
|
||||
public class GoannaProfilesProvider extends ContentProvider {
|
||||
private static final String LOG_TAG = "GoannaProfilesProvider";
|
||||
|
||||
private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
|
||||
private static final int PROFILES = 100;
|
||||
private static final int PROFILES_NAME = 101;
|
||||
private static final int PROFILES_DEFAULT = 200;
|
||||
|
||||
private static final String[] DEFAULT_ARGS = {
|
||||
BrowserContract.Profiles.NAME,
|
||||
BrowserContract.Profiles.PATH,
|
||||
};
|
||||
|
||||
static {
|
||||
URI_MATCHER.addURI(BrowserContract.PROFILES_AUTHORITY, "profiles", PROFILES);
|
||||
URI_MATCHER.addURI(BrowserContract.PROFILES_AUTHORITY, "profiles/*", PROFILES_NAME);
|
||||
URI_MATCHER.addURI(BrowserContract.PROFILES_AUTHORITY, "default", PROFILES_DEFAULT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
// Successfully loaded.
|
||||
return true;
|
||||
}
|
||||
|
||||
private String[] profileValues(final String name, final String path, int len, int nameIndex, int pathIndex) {
|
||||
final String[] values = new String[len];
|
||||
if (nameIndex >= 0) {
|
||||
values[nameIndex] = name;
|
||||
}
|
||||
if (pathIndex >= 0) {
|
||||
values[pathIndex] = path;
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
protected void addRowForProfile(final MatrixCursor cursor, final int len, final int nameIndex, final int pathIndex, final String name, final String path) {
|
||||
if (path == null || name == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
cursor.addRow(profileValues(name, path, len, nameIndex, pathIndex));
|
||||
}
|
||||
|
||||
protected Cursor getCursorForProfiles(final String[] args, Map<String, String> profiles) {
|
||||
// Compute the projection.
|
||||
int nameIndex = -1;
|
||||
int pathIndex = -1;
|
||||
for (int i = 0; i < args.length; ++i) {
|
||||
if (BrowserContract.Profiles.NAME.equals(args[i])) {
|
||||
nameIndex = i;
|
||||
} else if (BrowserContract.Profiles.PATH.equals(args[i])) {
|
||||
pathIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
final MatrixCursor cursor = new MatrixCursor(args);
|
||||
for (Entry<String, String> entry : profiles.entrySet()) {
|
||||
addRowForProfile(cursor, args.length, nameIndex, pathIndex, entry.getKey(), entry.getValue());
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection,
|
||||
String[] selectionArgs, String sortOrder) {
|
||||
|
||||
final String[] args = (projection == null) ? DEFAULT_ARGS : projection;
|
||||
|
||||
final File mozillaDir;
|
||||
try {
|
||||
mozillaDir = GoannaProfileDirectories.getMozillaDirectory(getContext());
|
||||
} catch (NoMozillaDirectoryException e) {
|
||||
Log.d(LOG_TAG, "No Mozilla directory; cannot query for profiles. Assuming there are none.");
|
||||
return new MatrixCursor(projection);
|
||||
}
|
||||
|
||||
final Map<String, String> matchingProfiles;
|
||||
|
||||
final int match = URI_MATCHER.match(uri);
|
||||
switch (match) {
|
||||
case PROFILES:
|
||||
// Return all profiles.
|
||||
matchingProfiles = GoannaProfileDirectories.getAllProfiles(mozillaDir);
|
||||
break;
|
||||
case PROFILES_NAME:
|
||||
// Return data about the specified profile.
|
||||
final String name = uri.getLastPathSegment();
|
||||
matchingProfiles = GoannaProfileDirectories.getProfilesNamed(mozillaDir,
|
||||
name);
|
||||
break;
|
||||
case PROFILES_DEFAULT:
|
||||
matchingProfiles = GoannaProfileDirectories.getDefaultProfile(mozillaDir);
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unknown query URI " + uri);
|
||||
}
|
||||
|
||||
return getCursorForProfiles(args, matchingProfiles);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
throw new IllegalStateException("Inserts not supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
||||
throw new IllegalStateException("Deletes not supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection,
|
||||
String[] selectionArgs) {
|
||||
throw new IllegalStateException("Updates not supported.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,384 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
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 GoannaScreenOrientation {
|
||||
private static final String LOGTAG = "GoannaScreenOrientation";
|
||||
|
||||
// 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 GoannaScreenOrientation 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 Goanna 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 GoannaScreenOrientation() {
|
||||
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 GoannaScreenOrientation getInstance() {
|
||||
if (sInstance == null) {
|
||||
sInstance = new GoannaScreenOrientation();
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
/*
|
||||
* Enable Goanna screen orientation events on update.
|
||||
*/
|
||||
public void enableNotifications() {
|
||||
update();
|
||||
mShouldNotify = true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Disable Goanna screen orientation events on update.
|
||||
*/
|
||||
public void disableNotifications() {
|
||||
mShouldNotify = false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Update screen orientation.
|
||||
* Retrieve orientation and rotation via GoannaAppShell.
|
||||
*
|
||||
* @return Whether the screen orientation has changed.
|
||||
*/
|
||||
public boolean update() {
|
||||
Activity activity = GoannaAppShell.getGoannaInterface().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 GoannaAppShell.
|
||||
*
|
||||
* @param aAndroidOrientation
|
||||
* Android screen orientation from Configuration.orientation.
|
||||
*
|
||||
* @return Whether the screen orientation has changed.
|
||||
*/
|
||||
public boolean update(int aAndroidOrientation) {
|
||||
return update(getScreenOrientation(aAndroidOrientation, getRotation()));
|
||||
}
|
||||
|
||||
/*
|
||||
* Update screen orientation given the screen orientation.
|
||||
*
|
||||
* @param aScreenOrientation
|
||||
* Goanna 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) {
|
||||
// Goanna 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;
|
||||
}
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createScreenOrientationEvent(aScreenOrientation.value));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* @return The Android orientation (Configuration.orientation).
|
||||
*/
|
||||
public int getAndroidOrientation() {
|
||||
return screenOrientationToAndroidOrientation(getScreenOrientation());
|
||||
}
|
||||
|
||||
/*
|
||||
* @return The Goanna screen orientation derived from Android orientation and
|
||||
* rotation.
|
||||
*/
|
||||
public ScreenOrientation getScreenOrientation() {
|
||||
return mScreenOrientation;
|
||||
}
|
||||
|
||||
/*
|
||||
* Lock screen orientation given the Goanna screen orientation.
|
||||
*
|
||||
* @param aGoannaOrientation
|
||||
* The Goanna orientation provided.
|
||||
*/
|
||||
public void lock(int aGoannaOrientation) {
|
||||
lock(ScreenOrientation.get(aGoannaOrientation));
|
||||
}
|
||||
|
||||
/*
|
||||
* Lock screen orientation given the Goanna screen orientation.
|
||||
* Retrieve rotation via GoannaAppShell.
|
||||
*
|
||||
* @param aScreenOrientation
|
||||
* Goanna 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();
|
||||
}
|
||||
|
||||
/*
|
||||
* Set the given requested orientation for the current activity.
|
||||
* This is essentially an unlock without an update.
|
||||
*
|
||||
* @param aScreenOrientation
|
||||
* Goanna screen orientation.
|
||||
*
|
||||
* @return Whether the requested orientation was set. This can only fail if
|
||||
* the current activity cannot be retrieved vie GoannaAppShell.
|
||||
*
|
||||
*/
|
||||
private boolean setRequestedOrientation(ScreenOrientation aScreenOrientation) {
|
||||
int activityOrientation = screenOrientationToActivityInfoOrientation(aScreenOrientation);
|
||||
Activity activity = GoannaAppShell.getGoannaInterface().getActivity();
|
||||
if (activity == null) {
|
||||
Log.w(LOGTAG, "setRequestOrientation: failed to get activity");
|
||||
}
|
||||
if (activity.getRequestedOrientation() == activityOrientation) {
|
||||
return false;
|
||||
}
|
||||
activity.setRequestedOrientation(activityOrientation);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Combine the Android orientation and rotation to the Goanna orientation.
|
||||
*
|
||||
* @param aAndroidOrientation
|
||||
* Android orientation from Configuration.orientation.
|
||||
* @param aRotation
|
||||
* Device rotation from Display.getRotation().
|
||||
*
|
||||
* @return Goanna 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 from Display.getRotation().
|
||||
*/
|
||||
private int getRotation() {
|
||||
Activity activity = GoannaAppShell.getGoannaInterface().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 Goanna 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 Goanna 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 Goanna screen orientation to Android orientation.
|
||||
*
|
||||
* @param aScreenOrientation
|
||||
* Goanna 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 Goanna 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
|
||||
* Goanna 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.mozilla.goanna.mozglue.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 GoannaSharedPrefs} provides scoped SharedPreferences instances.
|
||||
* You should use this API instead of using Context.getSharedPreferences()
|
||||
* directly. There are three methods to get scoped SharedPreferences instances:
|
||||
*
|
||||
* forApp()
|
||||
* Use it for app-wide, cross-profile pref keys.
|
||||
* 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 GoannaSharedPrefs} 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
|
||||
*/
|
||||
@RobocopTarget
|
||||
public final class GoannaSharedPrefs {
|
||||
private static final String LOGTAG = "GoannaSharedPrefs";
|
||||
|
||||
// Increment it to trigger a new migration
|
||||
public static final int PREFS_VERSION = 1;
|
||||
|
||||
// Name for app-scoped prefs
|
||||
public static final String APP_PREFS_NAME = "GoannaApp";
|
||||
|
||||
// Used when fetching profile-scoped prefs.
|
||||
public static final String PROFILE_PREFS_NAME_PREFIX = "GoannaProfile-";
|
||||
|
||||
// The prefs key that holds the current migration
|
||||
private static final String PREFS_VERSION_KEY = "goanna_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"
|
||||
};
|
||||
|
||||
// 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 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 = GoannaProfile.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 = GoannaProfile.getDefaultProfileName(context);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to get default profile name for migration");
|
||||
}
|
||||
|
||||
final Editor profileEditor = forProfileName(context, defaultProfileName, 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Update prefs version accordingly.
|
||||
appEditor.putInt(PREFS_VERSION_KEY, PREFS_VERSION);
|
||||
|
||||
appEditor.apply();
|
||||
profileEditor.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();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,209 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.mozglue.GoannaLoader;
|
||||
import org.mozilla.goanna.mozglue.RobocopTarget;
|
||||
import org.mozilla.goanna.util.GoannaEventListener;
|
||||
import org.mozilla.goanna.util.ThreadUtils;
|
||||
|
||||
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.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public class GoannaThread extends Thread implements GoannaEventListener {
|
||||
private static final String LOGTAG = "GoannaThread";
|
||||
|
||||
@RobocopTarget
|
||||
public enum LaunchState {
|
||||
Launching,
|
||||
WaitForDebugger,
|
||||
Launched,
|
||||
GoannaRunning,
|
||||
GoannaExiting,
|
||||
GoannaExited
|
||||
}
|
||||
|
||||
private static final AtomicReference<LaunchState> sLaunchState =
|
||||
new AtomicReference<LaunchState>(LaunchState.Launching);
|
||||
|
||||
private static GoannaThread sGoannaThread;
|
||||
|
||||
private final String mArgs;
|
||||
private final String mAction;
|
||||
private final String mUri;
|
||||
|
||||
public static boolean ensureInit() {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
if (isCreated())
|
||||
return false;
|
||||
sGoannaThread = new GoannaThread(sArgs, sAction, sUri);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static String sArgs;
|
||||
public static String sAction;
|
||||
public static String sUri;
|
||||
|
||||
public static void setArgs(String args) {
|
||||
sArgs = args;
|
||||
}
|
||||
|
||||
public static void setAction(String action) {
|
||||
sAction = action;
|
||||
}
|
||||
|
||||
public static void setUri(String uri) {
|
||||
sUri = uri;
|
||||
}
|
||||
|
||||
GoannaThread(String args, String action, String uri) {
|
||||
mArgs = args;
|
||||
mAction = action;
|
||||
mUri = uri;
|
||||
setName("Goanna");
|
||||
EventDispatcher.getInstance().registerGoannaThreadListener(this, "Goanna:Ready");
|
||||
}
|
||||
|
||||
public static boolean isCreated() {
|
||||
return sGoannaThread != null;
|
||||
}
|
||||
|
||||
public static void createAndStart() {
|
||||
if (ensureInit())
|
||||
sGoannaThread.start();
|
||||
}
|
||||
|
||||
private String initGoannaEnvironment() {
|
||||
final Locale locale = Locale.getDefault();
|
||||
|
||||
final Context context = GoannaAppShell.getContext();
|
||||
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 resourcePath = "";
|
||||
String[] pluginDirs = null;
|
||||
try {
|
||||
pluginDirs = GoannaAppShell.getPluginDirectories();
|
||||
} catch (Exception e) {
|
||||
Log.w(LOGTAG, "Caught exception getting plugin dirs.", e);
|
||||
}
|
||||
|
||||
resourcePath = context.getPackageResourcePath();
|
||||
GoannaLoader.setupGoannaEnvironment(context, pluginDirs, context.getFilesDir().getPath());
|
||||
|
||||
GoannaLoader.loadSQLiteLibs(context, resourcePath);
|
||||
GoannaLoader.loadNSSLibs(context, resourcePath);
|
||||
GoannaLoader.loadGoannaLibs(context, resourcePath);
|
||||
GoannaJavaSampler.setLibsLoaded();
|
||||
|
||||
return resourcePath;
|
||||
}
|
||||
|
||||
private String getTypeFromAction(String action) {
|
||||
if (GoannaApp.ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
|
||||
return "-bookmark";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String addCustomProfileArg(String args) {
|
||||
String profileArg = "";
|
||||
String guestArg = "";
|
||||
if (GoannaAppShell.getGoannaInterface() != null) {
|
||||
final GoannaProfile profile = GoannaAppShell.getGoannaInterface().getProfile();
|
||||
|
||||
if (profile.inGuestMode()) {
|
||||
try {
|
||||
profileArg = " -profile " + profile.getDir().getCanonicalPath();
|
||||
} catch (final IOException ioe) {
|
||||
Log.e(LOGTAG, "error getting guest profile path", ioe);
|
||||
}
|
||||
|
||||
if (args == null || !args.contains(BrowserApp.GUEST_BROWSING_ARG)) {
|
||||
guestArg = " " + BrowserApp.GUEST_BROWSING_ARG;
|
||||
}
|
||||
} else if (!GoannaProfile.sIsUsingCustomProfile) {
|
||||
// If nothing was passed in the intent, make sure the default profile exists and
|
||||
// force Goanna to use the default profile for this activity
|
||||
profileArg = " -P " + profile.forceCreate().getName();
|
||||
}
|
||||
}
|
||||
|
||||
return (args != null ? args : "") + profileArg + guestArg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Looper.prepare();
|
||||
ThreadUtils.sGoannaThread = this;
|
||||
ThreadUtils.sGoannaHandler = new Handler();
|
||||
ThreadUtils.sGoannaQueue = Looper.myQueue();
|
||||
|
||||
String path = initGoannaEnvironment();
|
||||
|
||||
// This can only happen after the call to initGoannaEnvironment
|
||||
// above, because otherwise the JNI code hasn't been loaded yet.
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override public void run() {
|
||||
GoannaAppShell.registerJavaUiThread();
|
||||
}
|
||||
});
|
||||
|
||||
Log.w(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - runGoanna");
|
||||
|
||||
String args = addCustomProfileArg(mArgs);
|
||||
String type = getTypeFromAction(mAction);
|
||||
|
||||
if (!AppConstants.MOZILLA_OFFICIAL) {
|
||||
Log.i(LOGTAG, "RunGoanna - args = " + args);
|
||||
}
|
||||
// and then fire us up
|
||||
GoannaAppShell.runGoanna(path, args, mUri, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(String event, JSONObject message) {
|
||||
if ("Goanna:Ready".equals(event)) {
|
||||
EventDispatcher.getInstance().unregisterGoannaThreadListener(this, event);
|
||||
setLaunchState(LaunchState.GoannaRunning);
|
||||
GoannaAppShell.sendPendingEventsToGoanna();
|
||||
}
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
public static boolean checkLaunchState(LaunchState checkState) {
|
||||
return sLaunchState.get() == checkState;
|
||||
}
|
||||
|
||||
static void setLaunchState(LaunchState setState) {
|
||||
sLaunchState.set(setState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the launch state to <code>setState</code> and return true if the current launch
|
||||
* state is <code>checkState</code>; otherwise do nothing and return false.
|
||||
*/
|
||||
static boolean checkAndSetLaunchState(LaunchState checkState, LaunchState setState) {
|
||||
return sLaunchState.compareAndSet(checkState, setState);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.updater.UpdateServiceHelper;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
public class GoannaUpdateReceiver extends BroadcastReceiver
|
||||
{
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (UpdateServiceHelper.ACTION_CHECK_UPDATE_RESULT.equals(intent.getAction())) {
|
||||
String result = intent.getStringExtra("result");
|
||||
if (GoannaAppShell.getGoannaInterface() != null && result != null) {
|
||||
GoannaAppShell.getGoannaInterface().notifyCheckUpdateResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,695 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.goanna.gfx.LayerView;
|
||||
import org.mozilla.goanna.mozglue.GoannaLoader;
|
||||
import org.mozilla.goanna.util.Clipboard;
|
||||
import org.mozilla.goanna.util.EventCallback;
|
||||
import org.mozilla.goanna.util.GoannaEventListener;
|
||||
import org.mozilla.goanna.util.HardwareUtils;
|
||||
import org.mozilla.goanna.util.NativeEventListener;
|
||||
import org.mozilla.goanna.util.NativeJSObject;
|
||||
import org.mozilla.goanna.util.ThreadUtils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Bundle;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
public class GoannaView extends LayerView
|
||||
implements ContextGetter {
|
||||
|
||||
private static final String DEFAULT_SHARED_PREFERENCES_FILE = "GoannaView";
|
||||
private static final String LOGTAG = "GoannaView";
|
||||
|
||||
private ChromeDelegate mChromeDelegate;
|
||||
private ContentDelegate mContentDelegate;
|
||||
|
||||
private final GoannaEventListener mGoannaEventListener = new GoannaEventListener() {
|
||||
@Override
|
||||
public void handleMessage(final String event, final JSONObject message) {
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (event.equals("Goanna: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) {
|
||||
GoannaAccessibility.sendAccessibilityEvent(message);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOGTAG, "handleMessage threw for " + event, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private final NativeEventListener mNativeEventListener = new NativeEventListener() {
|
||||
@Override
|
||||
public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) {
|
||||
try {
|
||||
if ("Accessibility:Ready".equals(event)) {
|
||||
GoannaAccessibility.updateAccessibilitySettings(getContext());
|
||||
} else if ("GoannaView:Message".equals(event)) {
|
||||
// We need to pull out the bundle while on the Goanna 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public GoannaView(Context context) {
|
||||
super(context);
|
||||
init(context, null, true);
|
||||
}
|
||||
|
||||
public GoannaView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GoannaView);
|
||||
String url = a.getString(R.styleable.GoannaView_url);
|
||||
boolean doInit = a.getBoolean(R.styleable.GoannaView_doinit, true);
|
||||
a.recycle();
|
||||
init(context, url, doInit);
|
||||
}
|
||||
|
||||
private void init(Context context, String url, boolean doInit) {
|
||||
// Perform common initialization for Fennec/GoannaView.
|
||||
GoannaAppShell.setLayerView(this);
|
||||
|
||||
// TODO: Fennec currently takes care of its own initialization, so this
|
||||
// flag is a hack used in Fennec to prevent GoannaView initialization.
|
||||
// This should go away once Fennec also uses GoannaView for
|
||||
// initialization.
|
||||
if (!doInit)
|
||||
return;
|
||||
|
||||
// If running outside of a GoannaActivity (eg, from a library project),
|
||||
// load the native code and disable content providers
|
||||
boolean isGoannaActivity = false;
|
||||
try {
|
||||
isGoannaActivity = context instanceof GoannaActivity;
|
||||
} catch (NoClassDefFoundError ex) {}
|
||||
|
||||
if (!isGoannaActivity) {
|
||||
// Set the GoannaInterface if the context is an activity and the GoannaInterface
|
||||
// has not already been set
|
||||
if (context instanceof Activity && getGoannaInterface() == null) {
|
||||
setGoannaInterface(new BaseGoannaInterface(context));
|
||||
}
|
||||
|
||||
Clipboard.init(context);
|
||||
HardwareUtils.init(context);
|
||||
|
||||
// If you want to use GoannaNetworkManager, start it.
|
||||
|
||||
GoannaLoader.loadMozGlue(context);
|
||||
|
||||
final GoannaProfile profile = GoannaProfile.get(context);
|
||||
}
|
||||
|
||||
if (url != null) {
|
||||
GoannaThread.setUri(url);
|
||||
GoannaThread.setAction(Intent.ACTION_VIEW);
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createURILoadEvent(url));
|
||||
}
|
||||
GoannaAppShell.setContextGetter(this);
|
||||
if (context instanceof Activity) {
|
||||
Tabs tabs = Tabs.getInstance();
|
||||
tabs.attachToContext(context);
|
||||
}
|
||||
|
||||
EventDispatcher.getInstance().registerGoannaThreadListener(mGoannaEventListener,
|
||||
"Goanna:Ready",
|
||||
"Accessibility:Event",
|
||||
"Content:StateChange",
|
||||
"Content:LoadError",
|
||||
"Content:PageShow",
|
||||
"DOMTitleChanged",
|
||||
"Link:Favicon",
|
||||
"Prompt:Show",
|
||||
"Prompt:ShowTop");
|
||||
|
||||
EventDispatcher.getInstance().registerGoannaThreadListener(mNativeEventListener,
|
||||
"Accessibility:Ready",
|
||||
"GoannaView:Message");
|
||||
|
||||
initializeView(EventDispatcher.getInstance());
|
||||
|
||||
if (GoannaThread.checkAndSetLaunchState(GoannaThread.LaunchState.Launching, GoannaThread.LaunchState.Launched)) {
|
||||
// This is the first launch, so finish initialization and go.
|
||||
GoannaProfile profile = GoannaProfile.get(context).forceCreate();
|
||||
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createObjectEvent(
|
||||
GoannaEvent.ACTION_OBJECT_LAYER_CLIENT, getLayerClientObject()));
|
||||
GoannaThread.createAndStart();
|
||||
} else if(GoannaThread.checkLaunchState(GoannaThread.LaunchState.GoannaRunning)) {
|
||||
// If Goanna is already running, that means the Activity was
|
||||
// destroyed, so we need to re-attach Goanna to this GoannaView.
|
||||
connectToGoanna();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Browser to the GoannaView container.
|
||||
* @param url The URL resource to load into the new Browser.
|
||||
*/
|
||||
public Browser addBrowser(String url) {
|
||||
Tab tab = Tabs.getInstance().loadUrl(url, Tabs.LOADURL_NEW_TAB);
|
||||
if (tab != null) {
|
||||
return new Browser(tab.getId());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a Browser from the GoannaView container.
|
||||
* @param browser The Browser to remove.
|
||||
*/
|
||||
public void removeBrowser(Browser browser) {
|
||||
Tab tab = Tabs.getInstance().getTab(browser.getId());
|
||||
if (tab != null) {
|
||||
Tabs.getInstance().closeTab(tab);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active/visible Browser.
|
||||
* @param browser The Browser to make selected.
|
||||
*/
|
||||
public void setCurrentBrowser(Browser browser) {
|
||||
Tab tab = Tabs.getInstance().getTab(browser.getId());
|
||||
if (tab != null) {
|
||||
Tabs.getInstance().selectTab(tab.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active/visible Browser.
|
||||
* @return The current selected Browser.
|
||||
*/
|
||||
public Browser getCurrentBrowser() {
|
||||
Tab tab = Tabs.getInstance().getSelectedTab();
|
||||
if (tab != null) {
|
||||
return new Browser(tab.getId());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of current Browsers in the GoannaView container.
|
||||
* @return An unmodifiable List of Browser objects.
|
||||
*/
|
||||
public List<Browser> getBrowsers() {
|
||||
ArrayList<Browser> browsers = new ArrayList<Browser>();
|
||||
Iterable<Tab> tabs = Tabs.getInstance().getTabsInOrder();
|
||||
for (Tab tab : tabs) {
|
||||
browsers.add(new Browser(tab.getId()));
|
||||
}
|
||||
return Collections.unmodifiableList(browsers);
|
||||
}
|
||||
|
||||
public void importScript(final String url) {
|
||||
if (url.startsWith("resource://android/assets/")) {
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("GoannaView:ImportScript", url));
|
||||
return;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Must import script from 'resources://android/assets/' location.");
|
||||
}
|
||||
|
||||
private void connectToGoanna() {
|
||||
GoannaThread.setLaunchState(GoannaThread.LaunchState.GoannaRunning);
|
||||
Tab selectedTab = Tabs.getInstance().getSelectedTab();
|
||||
if (selectedTab != null)
|
||||
Tabs.getInstance().notifyListeners(selectedTab, Tabs.TabEvents.SELECTED);
|
||||
goannaConnected();
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("Viewport:Flush", null));
|
||||
}
|
||||
|
||||
private void handleReady(final JSONObject message) {
|
||||
connectToGoanna();
|
||||
|
||||
if (mChromeDelegate != null) {
|
||||
mChromeDelegate.onReady(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleStateChange(final JSONObject message) throws JSONException {
|
||||
int state = message.getInt("state");
|
||||
if ((state & GoannaAppShell.WPL_STATE_IS_NETWORK) != 0) {
|
||||
if ((state & GoannaAppShell.WPL_STATE_START) != 0) {
|
||||
if (mContentDelegate != null) {
|
||||
int id = message.getInt("tabID");
|
||||
mContentDelegate.onPageStart(this, new Browser(id), message.getString("uri"));
|
||||
}
|
||||
} else if ((state & GoannaAppShell.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(GoannaView.this, new Browser(id), false);
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePageShow(final JSONObject message) throws JSONException {
|
||||
if (mContentDelegate != null) {
|
||||
int id = message.getInt("tabID");
|
||||
mContentDelegate.onPageShow(GoannaView.this, new Browser(id));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTitleChanged(final JSONObject message) throws JSONException {
|
||||
if (mContentDelegate != null) {
|
||||
int id = message.getInt("tabID");
|
||||
mContentDelegate.onReceivedTitle(GoannaView.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(GoannaView.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(GoannaView.this, null, text, new PromptResult(message));
|
||||
} else if ("confirm".equals(hint)) {
|
||||
String text = message.optString("text");
|
||||
mChromeDelegate.onConfirm(GoannaView.this, null, text, new PromptResult(message));
|
||||
} else if ("prompt".equals(hint)) {
|
||||
String text = message.optString("text");
|
||||
String defaultValue = message.optString("textbox0");
|
||||
mChromeDelegate.onPrompt(GoannaView.this, null, text, defaultValue, new PromptResult(message));
|
||||
} else if ("remotedebug".equals(hint)) {
|
||||
mChromeDelegate.onDebugRequest(GoannaView.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(GoannaView.this, data, result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the chrome callback handler.
|
||||
* This will replace the current handler.
|
||||
* @param chrome An implementation of GoannaViewChrome.
|
||||
*/
|
||||
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 setGoannaInterface(final BaseGoannaInterface goannaInterface) {
|
||||
GoannaAppShell.setGoannaInterface(goannaInterface);
|
||||
}
|
||||
|
||||
public static GoannaAppShell.GoannaInterface getGoannaInterface() {
|
||||
return GoannaAppShell.getGoannaInterface();
|
||||
}
|
||||
|
||||
protected String getSharedPreferencesFile() {
|
||||
return DEFAULT_SHARED_PREFERENCES_FILE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SharedPreferences getSharedPreferences() {
|
||||
return getContext().getSharedPreferences(getSharedPreferencesFile(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for a browser in the GoannaView container. Associated with a browser
|
||||
* element in the Goanna 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 Goanna 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);
|
||||
}
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("Tab:Load", args.toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the current URL resource into the Browser. The URL is force loaded from the
|
||||
* network and is not pulled from cache.
|
||||
*/
|
||||
public void reload() {
|
||||
Tab tab = Tabs.getInstance().getTab(mId);
|
||||
if (tab != null) {
|
||||
tab.doReload();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the current loading operation.
|
||||
*/
|
||||
public void stop() {
|
||||
Tab tab = Tabs.getInstance().getTab(mId);
|
||||
if (tab != null) {
|
||||
tab.doStop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the Browser has session history and can go back to a
|
||||
* previous page.
|
||||
* @return A boolean flag indicating if previous session exists.
|
||||
* This method will likely be removed and replaced by a callback in GoannaViewContent
|
||||
*/
|
||||
public boolean canGoBack() {
|
||||
Tab tab = Tabs.getInstance().getTab(mId);
|
||||
if (tab != null) {
|
||||
return tab.canDoBack();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move backward in the session history, if that's possible.
|
||||
*/
|
||||
public void goBack() {
|
||||
Tab tab = Tabs.getInstance().getTab(mId);
|
||||
if (tab != null) {
|
||||
tab.doBack();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the Browser has session history and can go forward to a
|
||||
* new page.
|
||||
* @return A boolean flag indicating if forward session exists.
|
||||
* This method will likely be removed and replaced by a callback in GoannaViewContent
|
||||
*/
|
||||
public boolean canGoForward() {
|
||||
Tab tab = Tabs.getInstance().getTab(mId);
|
||||
if (tab != null) {
|
||||
return tab.canDoForward();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward in the session history, if that's possible.
|
||||
*/
|
||||
public void goForward() {
|
||||
Tab tab = Tabs.getInstance().getTab(mId);
|
||||
if (tab != null) {
|
||||
tab.doForward();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Provides a means for the client to indicate whether a JavaScript
|
||||
* dialog request should proceed. An instance of this class is passed to
|
||||
* various GoannaViewChrome 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 GoannaViewChrome.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 Goanna is ready to handle requests.
|
||||
* @param view The GoannaView that initiated the callback.
|
||||
*/
|
||||
public void onReady(GoannaView view);
|
||||
|
||||
/**
|
||||
* Tell the host application to display an alert dialog.
|
||||
* @param view The GoannaView 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(GoannaView view, GoannaView.Browser browser, String message, GoannaView.PromptResult result);
|
||||
|
||||
/**
|
||||
* Tell the host application to display a confirmation dialog.
|
||||
* @param view The GoannaView 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(GoannaView view, GoannaView.Browser browser, String message, GoannaView.PromptResult result);
|
||||
|
||||
/**
|
||||
* Tell the host application to display an input prompt dialog.
|
||||
* @param view The GoannaView 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(GoannaView view, GoannaView.Browser browser, String message, String defaultValue, GoannaView.PromptResult result);
|
||||
|
||||
/**
|
||||
* Tell the host application to display a remote debugging request dialog.
|
||||
* @param view The GoannaView that initiated the callback.
|
||||
* @param result A PromptResult used to send back the result without blocking.
|
||||
* Defaults to cancel requests.
|
||||
*/
|
||||
public void onDebugRequest(GoannaView view, GoannaView.PromptResult result);
|
||||
|
||||
/**
|
||||
* Receive a message from an imported script.
|
||||
* @param view The GoannaView 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(GoannaView view, Bundle data, GoannaView.MessageResult result);
|
||||
}
|
||||
|
||||
public interface ContentDelegate {
|
||||
/**
|
||||
* A Browser has started loading content from the network.
|
||||
* @param view The GoannaView that initiated the callback.
|
||||
* @param browser The Browser that is loading the content.
|
||||
* @param url The resource being loaded.
|
||||
*/
|
||||
public void onPageStart(GoannaView view, GoannaView.Browser browser, String url);
|
||||
|
||||
/**
|
||||
* A Browser has finished loading content from the network.
|
||||
* @param view The GoannaView 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(GoannaView view, GoannaView.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 GoannaView that initiated the callback.
|
||||
* @param browser The Browser that is showing the content.
|
||||
*/
|
||||
public void onPageShow(GoannaView view, GoannaView.Browser browser);
|
||||
|
||||
/**
|
||||
* A page title was discovered in the content or updated after the content
|
||||
* loaded.
|
||||
* @param view The GoannaView that initiated the callback.
|
||||
* @param browser The Browser that is showing the content.
|
||||
* @param title The title sent from the content.
|
||||
*/
|
||||
public void onReceivedTitle(GoannaView view, GoannaView.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 GoannaView 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(GoannaView view, GoannaView.Browser browser, String url, int size);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
public class GoannaViewChrome implements GoannaView.ChromeDelegate {
|
||||
/**
|
||||
* Tell the host application that Goanna is ready to handle requests.
|
||||
* @param view The GoannaView that initiated the callback.
|
||||
*/
|
||||
@Override
|
||||
public void onReady(GoannaView view) {}
|
||||
|
||||
/**
|
||||
* Tell the host application to display an alert dialog.
|
||||
* @param view The GoannaView 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(GoannaView view, GoannaView.Browser browser, String message, GoannaView.PromptResult result) {
|
||||
result.cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the host application to display a confirmation dialog.
|
||||
* @param view The GoannaView 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(GoannaView view, GoannaView.Browser browser, String message, GoannaView.PromptResult result) {
|
||||
result.cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the host application to display an input prompt dialog.
|
||||
* @param view The GoannaView 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(GoannaView view, GoannaView.Browser browser, String message, String defaultValue, GoannaView.PromptResult result) {
|
||||
result.cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the host application to display a remote debugging request dialog.
|
||||
* @param view The GoannaView that initiated the callback.
|
||||
* @param result A PromptResult used to send back the result without blocking.
|
||||
* Defaults to cancel requests.
|
||||
*/
|
||||
@Override
|
||||
public void onDebugRequest(GoannaView view, GoannaView.PromptResult result) {
|
||||
result.cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive a message from an imported script.
|
||||
* @param view The GoannaView 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(GoannaView view, Bundle data, GoannaView.MessageResult result) {
|
||||
if (result != null) {
|
||||
result.failure(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
public class GoannaViewContent implements GoannaView.ContentDelegate {
|
||||
/**
|
||||
* A Browser has started loading content from the network.
|
||||
* @param view The GoannaView that initiated the callback.
|
||||
* @param browser The Browser that is loading the content.
|
||||
* @param url The resource being loaded.
|
||||
*/
|
||||
@Override
|
||||
public void onPageStart(GoannaView view, GoannaView.Browser browser, String url) {}
|
||||
|
||||
/**
|
||||
* A Browser has finished loading content from the network.
|
||||
* @param view The GoannaView 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(GoannaView view, GoannaView.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 GoannaView that initiated the callback.
|
||||
* @param browser The Browser that is showing the content.
|
||||
*/
|
||||
@Override
|
||||
public void onPageShow(GoannaView view, GoannaView.Browser browser) {}
|
||||
|
||||
/**
|
||||
* A page title was discovered in the content or updated after the content
|
||||
* loaded.
|
||||
* @param view The GoannaView 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(GoannaView view, GoannaView.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 GoannaView 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(GoannaView view, GoannaView.Browser browser, String url, int size) {}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import android.app.KeyguardManager;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
// Utility methods for entering/exiting guest mode.
|
||||
public class GuestSession {
|
||||
public static final String NOTIFICATION_INTENT = "org.mozilla.goanna.GUEST_SESSION_INPROGRESS";
|
||||
private static final String LOGTAG = "GoannaGuestSession";
|
||||
|
||||
/* Returns true if you should be in guest mode. This can be because a secure keyguard
|
||||
* is locked, or because the user has explicitly started guest mode via a dialog. If the
|
||||
* user has explicitly started Fennec in guest mode, this will return true until they
|
||||
* explicitly exit it.
|
||||
*/
|
||||
public static boolean shouldUse(final Context context, final String args) {
|
||||
// Did the command line args request guest mode?
|
||||
if (args != null && args.contains(BrowserApp.GUEST_BROWSING_ARG)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise, is there a locked guest mode profile?
|
||||
final GoannaProfile profile = GoannaProfile.getGuestProfile(context);
|
||||
if (profile == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return profile.locked();
|
||||
}
|
||||
|
||||
private static PendingIntent getNotificationIntent(Context context) {
|
||||
Intent intent = new Intent(NOTIFICATION_INTENT);
|
||||
intent.setClass(context, BrowserApp.class);
|
||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
public static void showNotification(Context context) {
|
||||
final NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
|
||||
final Resources res = context.getResources();
|
||||
builder.setContentTitle(res.getString(R.string.guest_browsing_notification_title))
|
||||
.setContentText(res.getString(R.string.guest_browsing_notification_text))
|
||||
.setSmallIcon(R.drawable.alert_guest)
|
||||
.setOngoing(true)
|
||||
.setContentIntent(getNotificationIntent(context));
|
||||
|
||||
final NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
manager.notify(R.id.guestNotification, builder.build());
|
||||
}
|
||||
|
||||
public static void hideNotification(Context context) {
|
||||
final NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
manager.cancel(R.id.guestNotification);
|
||||
}
|
||||
|
||||
public static void handleIntent(BrowserApp context, Intent intent) {
|
||||
context.showGuestModeDialog(BrowserApp.GuestModeDialog.LEAVING);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.mozilla.goanna.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);
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.util.ActivityResultHandler;
|
||||
import org.mozilla.goanna.util.GoannaEventListener;
|
||||
import org.mozilla.goanna.util.JSONUtils;
|
||||
import org.mozilla.goanna.util.WebActivityMapper;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public final class IntentHelper implements GoannaEventListener {
|
||||
private static final String LOGTAG = "GoannaIntentHelper";
|
||||
private static final String[] EVENTS = {
|
||||
"Intent:GetHandlers",
|
||||
"Intent:Open",
|
||||
"Intent:OpenForResult",
|
||||
"WebActivity:Open"
|
||||
};
|
||||
private static IntentHelper instance;
|
||||
|
||||
private final Activity activity;
|
||||
|
||||
private IntentHelper(Activity activity) {
|
||||
this.activity = activity;
|
||||
EventDispatcher.getInstance().registerGoannaThreadListener(this, EVENTS);
|
||||
}
|
||||
|
||||
public static IntentHelper init(Activity activity) {
|
||||
if (instance == null) {
|
||||
instance = new IntentHelper(activity);
|
||||
} else {
|
||||
Log.w(LOGTAG, "IntentHelper.init() called twice, ignoring.");
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static void destroy() {
|
||||
if (instance != null) {
|
||||
EventDispatcher.getInstance().unregisterGoannaThreadListener(instance, EVENTS);
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(String event, JSONObject message) {
|
||||
try {
|
||||
if (event.equals("Intent:GetHandlers")) {
|
||||
getHandlers(message);
|
||||
} else if (event.equals("Intent:Open")) {
|
||||
open(message);
|
||||
} else if (event.equals("Intent:OpenForResult")) {
|
||||
openForResult(message);
|
||||
} else if (event.equals("WebActivity:Open")) {
|
||||
openWebActivity(message);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void getHandlers(JSONObject message) throws JSONException {
|
||||
final Intent intent = GoannaAppShell.getOpenURIIntent(activity,
|
||||
message.optString("url"),
|
||||
message.optString("mime"),
|
||||
message.optString("action"),
|
||||
message.optString("title"));
|
||||
final List<String> appList = Arrays.asList(GoannaAppShell.getHandlersForIntent(intent));
|
||||
|
||||
final JSONObject response = new JSONObject();
|
||||
response.put("apps", new JSONArray(appList));
|
||||
EventDispatcher.sendResponse(message, response);
|
||||
}
|
||||
|
||||
private void open(JSONObject message) throws JSONException {
|
||||
GoannaAppShell.openUriExternal(message.optString("url"),
|
||||
message.optString("mime"),
|
||||
message.optString("packageName"),
|
||||
message.optString("className"),
|
||||
message.optString("action"),
|
||||
message.optString("title"));
|
||||
}
|
||||
|
||||
private void openForResult(final JSONObject message) throws JSONException {
|
||||
Intent intent = GoannaAppShell.getOpenURIIntent(activity,
|
||||
message.optString("url"),
|
||||
message.optString("mime"),
|
||||
message.optString("action"),
|
||||
message.optString("title"));
|
||||
intent.setClassName(message.optString("packageName"), message.optString("className"));
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
ActivityHandlerHelper.startIntentForActivity(activity, intent, new ResultHandler(message));
|
||||
}
|
||||
|
||||
private void openWebActivity(JSONObject message) throws JSONException {
|
||||
final Intent intent = WebActivityMapper.getIntentForWebActivity(message.getJSONObject("activity"));
|
||||
ActivityHandlerHelper.startIntentForActivity(activity, intent, new ResultHandler(message));
|
||||
}
|
||||
|
||||
private static class ResultHandler implements ActivityResultHandler {
|
||||
private final JSONObject message;
|
||||
|
||||
public ResultHandler(JSONObject message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int resultCode, Intent data) {
|
||||
JSONObject response = new JSONObject();
|
||||
|
||||
try {
|
||||
if (data != null) {
|
||||
response.put("extras", JSONUtils.bundleToJSON(data.getExtras()));
|
||||
response.put("uri", data.getData().toString());
|
||||
}
|
||||
|
||||
response.put("resultCode", resultCode);
|
||||
} catch (JSONException e) {
|
||||
Log.w(LOGTAG, "Error building JSON response.", e);
|
||||
}
|
||||
|
||||
EventDispatcher.sendResponse(message, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.util.GoannaEventListener;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
import dalvik.system.DexClassLoader;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* The manager for addon-provided Java code.
|
||||
*
|
||||
* Java code in addons can be loaded using the Dex:Load message, and unloaded
|
||||
* via the Dex:Unload message. Addon classes loaded are checked for a constructor
|
||||
* that takes a Map<String, Handler.Callback>. If such a constructor
|
||||
* exists, it is called and the objects populated into the map by the constructor
|
||||
* are registered as event listeners. If no such constructor exists, the default
|
||||
* constructor is invoked instead.
|
||||
*
|
||||
* Note: The Map and Handler.Callback classes were used in this API definition
|
||||
* rather than defining a custom class. This was done explicitly so that the
|
||||
* addon code can be compiled against the android.jar provided in the Android
|
||||
* SDK, rather than having to be compiled against Fennec source code.
|
||||
*
|
||||
* The Handler.Callback instances provided (as described above) are invoked with
|
||||
* Message objects when the corresponding events are dispatched. The Bundle
|
||||
* object attached to the Message will contain the "primitive" values from the
|
||||
* JSON of the event. ("primitive" includes bool/int/long/double/String). If
|
||||
* the addon callback wishes to synchronously return a value back to the event
|
||||
* dispatcher, they can do so by inserting the response string into the bundle
|
||||
* under the key "response".
|
||||
*/
|
||||
class JavaAddonManager implements GoannaEventListener {
|
||||
private static final String LOGTAG = "GoannaJavaAddonManager";
|
||||
|
||||
private static JavaAddonManager sInstance;
|
||||
|
||||
private final EventDispatcher mDispatcher;
|
||||
private final Map<String, Map<String, GoannaEventListener>> mAddonCallbacks;
|
||||
|
||||
private Context mApplicationContext;
|
||||
|
||||
public static JavaAddonManager getInstance() {
|
||||
if (sInstance == null) {
|
||||
sInstance = new JavaAddonManager();
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
private JavaAddonManager() {
|
||||
mDispatcher = EventDispatcher.getInstance();
|
||||
mAddonCallbacks = new HashMap<String, Map<String, GoannaEventListener>>();
|
||||
}
|
||||
|
||||
void init(Context applicationContext) {
|
||||
if (mApplicationContext != null) {
|
||||
// we've already done this registration. don't do it again
|
||||
return;
|
||||
}
|
||||
mApplicationContext = applicationContext;
|
||||
mDispatcher.registerGoannaThreadListener(this,
|
||||
"Dex:Load",
|
||||
"Dex:Unload");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(String event, JSONObject message) {
|
||||
try {
|
||||
if (event.equals("Dex:Load")) {
|
||||
String zipFile = message.getString("zipfile");
|
||||
String implClass = message.getString("impl");
|
||||
Log.d(LOGTAG, "Attempting to load classes.dex file from " + zipFile + " and instantiate " + implClass);
|
||||
try {
|
||||
File tmpDir = mApplicationContext.getDir("dex", 0);
|
||||
DexClassLoader loader = new DexClassLoader(zipFile, tmpDir.getAbsolutePath(), null, mApplicationContext.getClassLoader());
|
||||
Class<?> c = loader.loadClass(implClass);
|
||||
try {
|
||||
Constructor<?> constructor = c.getDeclaredConstructor(Map.class);
|
||||
Map<String, Handler.Callback> callbacks = new HashMap<String, Handler.Callback>();
|
||||
constructor.newInstance(callbacks);
|
||||
registerCallbacks(zipFile, callbacks);
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
Log.d(LOGTAG, "Did not find constructor with parameters Map<String, Handler.Callback>. Falling back to default constructor...");
|
||||
// fallback for instances with no constructor that takes a Map<String, Handler.Callback>
|
||||
c.newInstance();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOGTAG, "Unable to load dex successfully", e);
|
||||
}
|
||||
} else if (event.equals("Dex:Unload")) {
|
||||
String zipFile = message.getString("zipfile");
|
||||
unregisterCallbacks(zipFile);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Exception handling message [" + event + "]:", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerCallbacks(String zipFile, Map<String, Handler.Callback> callbacks) {
|
||||
Map<String, GoannaEventListener> addonCallbacks = mAddonCallbacks.get(zipFile);
|
||||
if (addonCallbacks != null) {
|
||||
Log.w(LOGTAG, "Found pre-existing callbacks for zipfile [" + zipFile + "]; aborting re-registration!");
|
||||
return;
|
||||
}
|
||||
addonCallbacks = new HashMap<String, GoannaEventListener>();
|
||||
for (String event : callbacks.keySet()) {
|
||||
CallbackWrapper wrapper = new CallbackWrapper(callbacks.get(event));
|
||||
mDispatcher.registerGoannaThreadListener(wrapper, event);
|
||||
addonCallbacks.put(event, wrapper);
|
||||
}
|
||||
mAddonCallbacks.put(zipFile, addonCallbacks);
|
||||
}
|
||||
|
||||
private void unregisterCallbacks(String zipFile) {
|
||||
Map<String, GoannaEventListener> callbacks = mAddonCallbacks.remove(zipFile);
|
||||
if (callbacks == null) {
|
||||
Log.w(LOGTAG, "Attempting to unregister callbacks from zipfile [" + zipFile + "] which has no callbacks registered.");
|
||||
return;
|
||||
}
|
||||
for (String event : callbacks.keySet()) {
|
||||
mDispatcher.unregisterGoannaThreadListener(callbacks.get(event), event);
|
||||
}
|
||||
}
|
||||
|
||||
private static class CallbackWrapper implements GoannaEventListener {
|
||||
private final Handler.Callback mDelegate;
|
||||
private Bundle mBundle;
|
||||
|
||||
CallbackWrapper(Handler.Callback delegate) {
|
||||
mDelegate = delegate;
|
||||
}
|
||||
|
||||
private Bundle jsonToBundle(JSONObject json) {
|
||||
// XXX right now we only support primitive types;
|
||||
// we don't recurse down into JSONArray or JSONObject instances
|
||||
Bundle b = new Bundle();
|
||||
for (Iterator<?> keys = json.keys(); keys.hasNext(); ) {
|
||||
try {
|
||||
String key = (String)keys.next();
|
||||
Object value = json.get(key);
|
||||
if (value instanceof Integer) {
|
||||
b.putInt(key, (Integer)value);
|
||||
} else if (value instanceof String) {
|
||||
b.putString(key, (String)value);
|
||||
} else if (value instanceof Boolean) {
|
||||
b.putBoolean(key, (Boolean)value);
|
||||
} else if (value instanceof Long) {
|
||||
b.putLong(key, (Long)value);
|
||||
} else if (value instanceof Double) {
|
||||
b.putDouble(key, (Double)value);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.d(LOGTAG, "Error during JSON->bundle conversion", e);
|
||||
}
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(String event, JSONObject json) {
|
||||
try {
|
||||
if (mBundle != null) {
|
||||
Log.w(LOGTAG, "Event [" + event + "] handler is re-entrant; response messages may be lost");
|
||||
}
|
||||
mBundle = jsonToBundle(json);
|
||||
Message msg = new Message();
|
||||
msg.setData(mBundle);
|
||||
mDelegate.handleMessage(msg);
|
||||
|
||||
JSONObject obj = new JSONObject();
|
||||
obj.put("response", mBundle.getString("response"));
|
||||
EventDispatcher.sendResponse(json, obj);
|
||||
mBundle = null;
|
||||
} catch (Exception e) {
|
||||
Log.e(LOGTAG, "Caught exception thrown from wrapped addon message handler", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
public interface LayoutInterceptor {
|
||||
public void onLayout();
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
|
||||
/**
|
||||
* Implement this interface to provide Fennec's locale switching functionality.
|
||||
*
|
||||
* The LocaleManager is responsible for persisting and applying selected locales,
|
||||
* and correcting configurations after Android has changed them.
|
||||
*/
|
||||
public interface LocaleManager {
|
||||
void initialize(Context context);
|
||||
|
||||
/**
|
||||
* @return true if locale switching is enabled.
|
||||
*/
|
||||
boolean isEnabled();
|
||||
Locale getCurrentLocale(Context context);
|
||||
String getAndApplyPersistedLocale(Context context);
|
||||
void correctLocale(Context context, Resources resources, Configuration newConfig);
|
||||
void updateConfiguration(Context context, Locale locale);
|
||||
String setSelectedLocale(Context context, String localeCode);
|
||||
boolean systemLocaleDidChange();
|
||||
void resetToSystemLocale(Context context);
|
||||
|
||||
/**
|
||||
* Call this in your onConfigurationChanged handler. This method is expected
|
||||
* to do the appropriate thing: if the user has selected a locale, it
|
||||
* corrects the incoming configuration; if not, it signals the new locale to
|
||||
* use.
|
||||
*/
|
||||
Locale onSystemConfigurationChanged(Context context, Resources resources, Configuration configuration, Locale currentActivityLocale);
|
||||
String getFallbackLocaleTag();
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.mozilla.goanna.BrowserLocaleManager;
|
||||
import org.mozilla.goanna.LocaleManager;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.StrictMode;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
|
||||
/**
|
||||
* This is a helper class to do typical locale switching operations without
|
||||
* hitting StrictMode errors or adding boilerplate to common activity
|
||||
* subclasses.
|
||||
*
|
||||
* Either call {@link Locales#initializeLocale(Context)} in your
|
||||
* <code>onCreate</code> method, or inherit from
|
||||
* <code>LocaleAwareFragmentActivity</code> or <code>LocaleAwareActivity</code>.
|
||||
*/
|
||||
public class Locales {
|
||||
public static void initializeLocale(Context context) {
|
||||
final LocaleManager localeManager = BrowserLocaleManager.getInstance();
|
||||
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
|
||||
StrictMode.allowThreadDiskWrites();
|
||||
try {
|
||||
localeManager.getAndApplyPersistedLocale(context);
|
||||
} finally {
|
||||
StrictMode.setThreadPolicy(savedPolicy);
|
||||
}
|
||||
}
|
||||
|
||||
public static class LocaleAwareFragmentActivity extends FragmentActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
Locales.initializeLocale(getApplicationContext());
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
}
|
||||
|
||||
public static class LocaleAwareActivity extends Activity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
Locales.initializeLocale(getApplicationContext());
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sometimes we want just the language for a locale, not the entire language
|
||||
* tag. But Java's .getLanguage method is wrong.
|
||||
*
|
||||
* This method is equivalent to the first part of
|
||||
* {@link Locales#getLanguageTag(Locale)}.
|
||||
*
|
||||
* @return a language string, such as "he" for the Hebrew locales.
|
||||
*/
|
||||
public static String getLanguage(final Locale locale) {
|
||||
// Can, but should never be, an empty string.
|
||||
final String language = locale.getLanguage();
|
||||
|
||||
// Modernize certain language codes.
|
||||
if (language.equals("iw")) {
|
||||
return "he";
|
||||
}
|
||||
|
||||
if (language.equals("in")) {
|
||||
return "id";
|
||||
}
|
||||
|
||||
if (language.equals("ji")) {
|
||||
return "yi";
|
||||
}
|
||||
|
||||
return language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Goanna uses locale codes like "es-ES", whereas a Java {@link Locale}
|
||||
* stringifies as "es_ES".
|
||||
*
|
||||
* This method approximates the Java 7 method
|
||||
* <code>Locale#toLanguageTag()</code>.
|
||||
*
|
||||
* @return a locale string suitable for passing to Goanna.
|
||||
*/
|
||||
public static String getLanguageTag(final Locale locale) {
|
||||
// If this were Java 7:
|
||||
// return locale.toLanguageTag();
|
||||
|
||||
final String language = getLanguage(locale);
|
||||
final String country = locale.getCountry(); // Can be an empty string.
|
||||
if (country.equals("")) {
|
||||
return language;
|
||||
}
|
||||
return language + "-" + country;
|
||||
}
|
||||
|
||||
public static Locale parseLocaleCode(final String localeCode) {
|
||||
int index;
|
||||
if ((index = localeCode.indexOf('-')) != -1 ||
|
||||
(index = localeCode.indexOf('_')) != -1) {
|
||||
final String langCode = localeCode.substring(0, index);
|
||||
final String countryCode = localeCode.substring(index + 1);
|
||||
return new Locale(langCode, countryCode);
|
||||
}
|
||||
|
||||
return new Locale(localeCode);
|
||||
}
|
||||
}
|
||||
@@ -1,484 +0,0 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DIST_FILES := \
|
||||
package-name.txt.in \
|
||||
$(NULL)
|
||||
|
||||
ifneq (,$(findstring -march=armv7,$(OS_CFLAGS)))
|
||||
MIN_CPU_VERSION=7
|
||||
else
|
||||
MIN_CPU_VERSION=5
|
||||
endif
|
||||
|
||||
MOZ_APP_BUILDID=$(shell cat $(DEPTH)/config/buildid)
|
||||
|
||||
# See Bug 1137586 for more details on version code computation.
|
||||
ifeq (,$(ANDROID_VERSION_CODE))
|
||||
ifeq ($(CPU_ARCH),arm)
|
||||
# Increment by MIN_SDK_VERSION -- this adds 9 to every build ID as a minimum.
|
||||
# Our split APK starts at 11.
|
||||
ANDROID_VERSION_CODE=$(shell echo $$((`cat $(DEPTH)/config/buildid | cut -c1-10` + $(MOZ_ANDROID_MIN_SDK_VERSION) + 0)))
|
||||
else # not ARM, so x86.
|
||||
# Increment the version code by 3 for x86 builds so they are offered to x86 phones that have ARM emulators,
|
||||
# beating the 2-point advantage that the v11+ ARMv7 APK has.
|
||||
# If we change our splits in the future, we'll need to do this further still.
|
||||
ANDROID_VERSION_CODE=$(shell echo $$((`cat $(DEPTH)/config/buildid | cut -c1-10` + $(MOZ_ANDROID_MIN_SDK_VERSION) + 3)))
|
||||
endif
|
||||
endif
|
||||
|
||||
UA_BUILDID=$(shell echo $(ANDROID_VERSION_CODE) | cut -c1-8)
|
||||
|
||||
DEFINES += \
|
||||
-DANDROID_VERSION_CODE=$(ANDROID_VERSION_CODE) \
|
||||
-DMOZ_ANDROID_SHARED_ID="$(MOZ_ANDROID_SHARED_ID)" \
|
||||
-DMOZ_ANDROID_SHARED_ACCOUNT_TYPE="$(MOZ_ANDROID_SHARED_ACCOUNT_TYPE)" \
|
||||
-DMOZ_ANDROID_SHARED_FXACCOUNT_TYPE="$(MOZ_ANDROID_SHARED_FXACCOUNT_TYPE)" \
|
||||
-DMOZ_APP_BUILDID=$(MOZ_APP_BUILDID) \
|
||||
-DUA_BUILDID=$(UA_BUILDID) \
|
||||
$(NULL)
|
||||
|
||||
GARBAGE += \
|
||||
AndroidManifest.xml \
|
||||
WebappManifestFragment.xml.frag \
|
||||
classes.dex \
|
||||
goanna.ap_ \
|
||||
res/values/strings.xml \
|
||||
res/raw/browsersearch.json \
|
||||
res/raw/suggestedsites.json \
|
||||
.aapt.deps \
|
||||
fennec_ids.txt \
|
||||
javah.out \
|
||||
jni-stubs.inc \
|
||||
GeneratedJNIWrappers.cpp \
|
||||
GeneratedJNIWrappers.h \
|
||||
$(NULL)
|
||||
|
||||
GARBAGE_DIRS += classes db jars res sync services generated
|
||||
|
||||
# The bootclasspath is functionally identical to the classpath, but allows the
|
||||
# classes given to redefine classes in core packages, such as java.lang.
|
||||
# android.jar is here as it provides Android's definition of the Java Standard
|
||||
# Library. The compatability lib here tweaks a few of the core classes to paint
|
||||
# over changes in behaviour between versions.
|
||||
JAVA_BOOTCLASSPATH := \
|
||||
$(ANDROID_SDK)/android.jar \
|
||||
$(ANDROID_COMPAT_LIB) \
|
||||
$(NULL)
|
||||
|
||||
JAVA_BOOTCLASSPATH := $(subst $(NULL) ,:,$(strip $(JAVA_BOOTCLASSPATH)))
|
||||
|
||||
# If native devices are enabled, add Google Play Services and some of the v7
|
||||
# compat libraries.
|
||||
ifdef MOZ_NATIVE_DEVICES
|
||||
JAVA_CLASSPATH += \
|
||||
$(GOOGLE_PLAY_SERVICES_LIB) \
|
||||
$(ANDROID_MEDIAROUTER_LIB) \
|
||||
$(ANDROID_APPCOMPAT_LIB) \
|
||||
$(NULL)
|
||||
endif
|
||||
|
||||
JAVA_CLASSPATH := $(subst $(NULL) ,:,$(strip $(JAVA_CLASSPATH)))
|
||||
|
||||
# Library jars that we're bundling: these are subject to Proguard before inclusion
|
||||
# into classes.dex.
|
||||
java_bundled_libs := \
|
||||
$(ANDROID_COMPAT_LIB) \
|
||||
$(NULL)
|
||||
|
||||
ifdef MOZ_NATIVE_DEVICES
|
||||
java_bundled_libs += \
|
||||
$(GOOGLE_PLAY_SERVICES_LIB) \
|
||||
$(ANDROID_MEDIAROUTER_LIB) \
|
||||
$(ANDROID_APPCOMPAT_LIB) \
|
||||
$(NULL)
|
||||
endif
|
||||
|
||||
java_bundled_libs := $(subst $(NULL) ,:,$(strip $(java_bundled_libs)))
|
||||
|
||||
# All the jars we're compiling from source. (not to be confused with
|
||||
# java_bundled_libs, which holds the jars which we're including as binaries).
|
||||
ALL_JARS = \
|
||||
constants.jar \
|
||||
goanna-R.jar \
|
||||
goanna-browser.jar \
|
||||
goanna-mozglue.jar \
|
||||
goanna-thirdparty.jar \
|
||||
goanna-util.jar \
|
||||
sync-thirdparty.jar \
|
||||
$(NULL)
|
||||
|
||||
ifdef MOZ_WEBRTC
|
||||
ALL_JARS += webrtc.jar
|
||||
endif
|
||||
|
||||
ifdef MOZ_ANDROID_SEARCH_ACTIVITY
|
||||
ALL_JARS += search-activity.jar
|
||||
endif
|
||||
|
||||
ifdef MOZ_ANDROID_MLS_STUMBLER
|
||||
extra_packages += org.mozilla.mozstumbler
|
||||
ALL_JARS += ../stumbler/stumbler.jar
|
||||
generated/org/mozilla/mozstumbler/R.java: .aapt.deps ;
|
||||
endif
|
||||
|
||||
# The list of jars in Java classpath notation (colon-separated).
|
||||
all_jars_classpath := $(subst $(NULL) ,:,$(strip $(ALL_JARS)))
|
||||
|
||||
include $(topsrcdir)/config/config.mk
|
||||
|
||||
library_jars := \
|
||||
$(ANDROID_SDK)/android.jar \
|
||||
$(NULL)
|
||||
|
||||
library_jars := $(subst $(NULL) ,:,$(strip $(library_jars)))
|
||||
|
||||
classes.dex: .proguard.deps
|
||||
$(REPORT_BUILD)
|
||||
$(DX) --dex --output=classes.dex jars-proguarded
|
||||
|
||||
ifdef MOZ_DISABLE_PROGUARD
|
||||
PROGUARD_PASSES=0
|
||||
else
|
||||
ifdef MOZ_DEBUG
|
||||
PROGUARD_PASSES=1
|
||||
else
|
||||
ifndef MOZILLA_OFFICIAL
|
||||
PROGUARD_PASSES=1
|
||||
else
|
||||
PROGUARD_PASSES=6
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
proguard_config_dir=$(topsrcdir)/mobile/android/config/proguard
|
||||
|
||||
# This stanza ensures that the set of GoannaView classes does not depend on too
|
||||
# much of Fennec, where "too much" is defined as the set of potentially
|
||||
# non-GoannaView classes that GoannaView already depended on at a certain point in
|
||||
# time. The idea is to set a high-water mark that is not to be crossed.
|
||||
classycle_jar := $(topsrcdir)/mobile/android/build/classycle/classycle-1.4.1.jar
|
||||
.goannaview.deps: goannaview.ddf $(classycle_jar) $(ALL_JARS)
|
||||
java -cp $(classycle_jar) \
|
||||
classycle.dependency.DependencyChecker \
|
||||
-mergeInnerClasses \
|
||||
-dependencies=@$< \
|
||||
$(ALL_JARS)
|
||||
@$(TOUCH) $@
|
||||
|
||||
# First, we delete debugging information from libraries. Having line-number
|
||||
# information for libraries for which we lack the source isn't useful, so this
|
||||
# saves us a bit of space. Importantly, Proguard has a bug causing it to
|
||||
# sometimes corrupt this information if present (which it does for some of the
|
||||
# included libraries). This corruption prevents dex from completing, so we need
|
||||
# to get rid of it. This prevents us from seeing line numbers in stack traces
|
||||
# for stack frames inside libraries.
|
||||
#
|
||||
# This step can occur much earlier than the main Proguard pass: it needs only
|
||||
# goanna-R.jar to have been compiled (as that's where the library R.java files
|
||||
# end up), but it does block the main Proguard pass.
|
||||
.bundled.proguard.deps: goanna-R.jar $(proguard_config_dir)/strip-libs.cfg
|
||||
$(REPORT_BUILD)
|
||||
@$(TOUCH) $@
|
||||
java \
|
||||
-Xmx512m -Xms128m \
|
||||
-jar $(ANDROID_SDK_ROOT)/tools/proguard/lib/proguard.jar \
|
||||
@$(proguard_config_dir)/strip-libs.cfg \
|
||||
-injars $(subst ::,:,$(java_bundled_libs))\
|
||||
-outjars bundled-jars-nodebug \
|
||||
-libraryjars $(library_jars):goanna-R.jar
|
||||
|
||||
# We touch the target file before invoking Proguard so that Proguard's
|
||||
# outputs are fresher than the target, preventing a subsequent
|
||||
# invocation from thinking Proguard's outputs are stale. This is safe
|
||||
# because Make removes the target file if any recipe command fails.
|
||||
.proguard.deps: .goannaview.deps .bundled.proguard.deps $(ALL_JARS) $(proguard_config_dir)/proguard.cfg
|
||||
$(REPORT_BUILD)
|
||||
@$(TOUCH) $@
|
||||
java \
|
||||
-Xmx512m -Xms128m \
|
||||
-jar $(ANDROID_SDK_ROOT)/tools/proguard/lib/proguard.jar \
|
||||
@$(proguard_config_dir)/proguard.cfg \
|
||||
-optimizationpasses $(PROGUARD_PASSES) \
|
||||
-injars $(subst ::,:,$(all_jars_classpath)):bundled-jars-nodebug \
|
||||
-outjars jars-proguarded \
|
||||
-libraryjars $(library_jars)
|
||||
|
||||
CLASSES_WITH_JNI= \
|
||||
org.mozilla.goanna.ANRReporter \
|
||||
org.mozilla.goanna.GoannaAppShell \
|
||||
org.mozilla.goanna.GoannaJavaSampler \
|
||||
org.mozilla.goanna.gfx.NativePanZoomController \
|
||||
org.mozilla.goanna.util.NativeJSContainer \
|
||||
org.mozilla.goanna.util.NativeJSObject \
|
||||
$(NULL)
|
||||
|
||||
ifdef MOZ_WEBSMS_BACKEND
|
||||
# Note: if you are building with MOZ_WEBSMS_BACKEND turned on, then
|
||||
# you will get a build error because the generated jni-stubs.inc will
|
||||
# be different than the one checked in (i.e. it will have the sms-related
|
||||
# JNI stubs as well). Just copy the generated file to mozglue/android/
|
||||
# like the error message says and rebuild. All should be well after that.
|
||||
CLASSES_WITH_JNI += org.mozilla.goanna.GoannaSmsManager
|
||||
endif
|
||||
|
||||
jni-stubs.inc: goanna-browser.jar goanna-mozglue.jar goanna-util.jar sync-thirdparty.jar
|
||||
$(JAVAH) -o javah.out -bootclasspath $(JAVA_BOOTCLASSPATH) -classpath $(subst $(NULL) $(NULL),:,$^) $(CLASSES_WITH_JNI)
|
||||
$(PYTHON) $(topsrcdir)/mobile/android/base/jni-generator.py javah.out $@
|
||||
|
||||
ANNOTATION_PROCESSOR_JAR_FILES := $(DEPTH)/build/annotationProcessors/annotationProcessors.jar
|
||||
|
||||
GeneratedJNIWrappers.cpp: $(ANNOTATION_PROCESSOR_JAR_FILES)
|
||||
GeneratedJNIWrappers.cpp: $(ALL_JARS)
|
||||
$(JAVA) -classpath goanna-mozglue.jar:$(JAVA_BOOTCLASSPATH):$(JAVA_CLASSPATH):$(ANNOTATION_PROCESSOR_JAR_FILES) org.mozilla.goanna.annotationProcessors.AnnotationProcessor $(ALL_JARS)
|
||||
|
||||
manifest := \
|
||||
AndroidManifest.xml.in \
|
||||
WebappManifestFragment.xml.frag.in \
|
||||
$(NULL)
|
||||
|
||||
PP_TARGETS += manifest
|
||||
|
||||
# Certain source files need to be preprocessed. This special rule
|
||||
# generates these files into generated/org/mozilla/goanna for
|
||||
# consumption by the build system and IDEs.
|
||||
|
||||
# The list in moz.build looks like
|
||||
# 'preprocessed/org/mozilla/goanna/AppConstants.java'. The list in
|
||||
# constants_PP_JAVAFILES looks like
|
||||
# 'generated/preprocessed/org/mozilla/goanna/AppConstants.java'. We
|
||||
# need to write AppConstants.java.in to
|
||||
# generated/preprocessed/org/mozilla/goanna.
|
||||
preprocessed := $(addsuffix .in,$(subst generated/preprocessed/org/mozilla/goanna/,,$(filter generated/preprocessed/org/mozilla/goanna/%,$(constants_PP_JAVAFILES))))
|
||||
|
||||
preprocessed_PATH := generated/preprocessed/org/mozilla/goanna
|
||||
preprocessed_KEEP_PATH := 1
|
||||
preprocessed_FLAGS := --marker='//\\\#'
|
||||
|
||||
PP_TARGETS += preprocessed
|
||||
|
||||
include $(topsrcdir)/config/rules.mk
|
||||
|
||||
not_android_res_files := \
|
||||
*.mkdir.done* \
|
||||
*.DS_Store* \
|
||||
*\#* \
|
||||
*.rej \
|
||||
*.orig \
|
||||
$(NULL)
|
||||
|
||||
# This uses the fact that Android resource directories list all
|
||||
# resource files one subdirectory below the parent resource directory.
|
||||
android_res_files := $(filter-out $(not_android_res_files),$(wildcard $(addsuffix /*,$(wildcard $(addsuffix /*,$(ANDROID_RES_DIRS))))))
|
||||
|
||||
$(ANDROID_GENERATED_RESFILES): $(call mkdir_deps,$(sort $(dir $(ANDROID_GENERATED_RESFILES))))
|
||||
|
||||
# [Comment 1/3] We don't have correct dependencies for strings.xml at
|
||||
# this point, so we always recursively invoke the submake to check the
|
||||
# dependencies. Sigh. And, with multilocale builds, there will be
|
||||
# multiple strings.xml files, and we need to rebuild goanna.ap_ if any
|
||||
# of them change. But! mobile/android/base/locales does not have
|
||||
# enough information to actually build res/values/strings.xml during a
|
||||
# language repack. So rather than adding rules into the main
|
||||
# makefile, and trying to work around the lack of information, we
|
||||
# force a rebuild of goanna.ap_ during packaging. See below.
|
||||
|
||||
# Since the sub-Make is forced, it doesn't matter that we touch the
|
||||
# target file before the command. If in the future we stop forcing
|
||||
# the sub-Make, touching the target file first is better, because the
|
||||
# sub-Make outputs will be fresher than the target, and not require
|
||||
# rebuilding. This is all safe because Make removes the target file
|
||||
# if any recipe command fails. It is crucial that the sub-Make touch
|
||||
# the target files (those depending on .locales.deps) only when there
|
||||
# contents have changed; otherwise, this will force rebuild them as
|
||||
# part of every build.
|
||||
.locales.deps: FORCE
|
||||
$(TOUCH) $@
|
||||
$(MAKE) -C locales
|
||||
|
||||
|
||||
# This .deps pattern saves an invocation of the sub-Make: the single
|
||||
# invocation generates strings.xml, browsersearch.json, and
|
||||
# suggestedsites.json. The trailing semi-colon defines an empty
|
||||
# recipe: defining no recipe at all causes Make to treat the target
|
||||
# differently, in a way that defeats our dependencies.
|
||||
res/values/strings.xml: .locales.deps ;
|
||||
res/raw/browsersearch.json: .locales.deps ;
|
||||
res/raw/suggestedsites.json: .locales.deps ;
|
||||
|
||||
all_resources = \
|
||||
$(CURDIR)/AndroidManifest.xml \
|
||||
$(CURDIR)/WebappManifestFragment.xml.frag \
|
||||
$(android_res_files) \
|
||||
$(ANDROID_GENERATED_RESFILES) \
|
||||
$(NULL)
|
||||
|
||||
# For GoannaView, we want a zip of an Android res/ directory that
|
||||
# merges the contents of all the ANDROID_RES_DIRS. The inner res/
|
||||
# directory must have the Android resource two-layer hierarchy.
|
||||
|
||||
# The following helper zips files in a directory into a zip file while
|
||||
# maintaining the directory structure rooted below the directory.
|
||||
# (adding or creating said file as appropriate). For example, if the
|
||||
# dir contains dir/subdir/file, calling with directory dir would
|
||||
# create a zip containing subdir/file. Note: the trailing newline is
|
||||
# necessary.
|
||||
|
||||
# $(1): zip file to add to (or create).
|
||||
# $(2): directory to zip contents of.
|
||||
define zip_directory_with_relative_paths
|
||||
cd $(2) && zip -q $(1) -r * -x $(subst *,\\*,$(not_android_res_files))
|
||||
|
||||
endef
|
||||
|
||||
# We delete the archive before updating so that resources removed from
|
||||
# the filesystem are removed from the archive.
|
||||
goannaview_resources.zip: $(all_resources) $(GLOBAL_DEPS)
|
||||
$(REPORT_BUILD)
|
||||
$(RM) -rf $@
|
||||
$(foreach dir,$(ANDROID_RES_DIRS),$(call zip_directory_with_relative_paths,$(CURDIR)/$@,$(dir)))
|
||||
|
||||
# All of generated/org/mozilla/goanna/R.java, goanna.ap_, and R.txt are
|
||||
# produced by aapt; this saves aapt invocations. The trailing
|
||||
# semi-colon defines an empty recipe; defining no recipe at all causes
|
||||
# Make to treat the target differently, in a way that defeats our
|
||||
# dependencies.
|
||||
|
||||
generated/org/mozilla/goanna/R.java: .aapt.deps ;
|
||||
|
||||
# If native devices are enabled, add Google Play Services, build their resources
|
||||
generated/android/support/v7/appcompat/R.java: .aapt.deps ;
|
||||
generated/android/support/v7/mediarouter/R.java: .aapt.deps ;
|
||||
generated/com/google/android/gms/R.java: .aapt.deps ;
|
||||
|
||||
ifdef MOZ_NATIVE_DEVICES
|
||||
extra_packages += android.support.v7.appcompat
|
||||
extra_res_dirs += $(ANDROID_APPCOMPAT_RES)
|
||||
|
||||
extra_packages += android.support.v7.mediarouter
|
||||
extra_res_dirs += $(ANDROID_MEDIAROUTER_RES)
|
||||
|
||||
extra_packages += com.google.android.gms
|
||||
extra_res_dirs += $(GOOGLE_PLAY_SERVICES_RES)
|
||||
endif
|
||||
|
||||
goanna.ap_: .aapt.deps ;
|
||||
R.txt: .aapt.deps ;
|
||||
|
||||
# [Comment 2/3] This tom-foolery provides a target that forces a
|
||||
# rebuild of goanna.ap_. This is used during packaging to ensure that
|
||||
# resources are fresh. The alternative would be complicated; see
|
||||
# [Comment 1/3].
|
||||
|
||||
goanna-nodeps/R.java: .aapt.nodeps ;
|
||||
goanna-nodeps.ap_: .aapt.nodeps ;
|
||||
goanna-nodeps/R.txt: .aapt.nodeps ;
|
||||
|
||||
# This ignores the default set of resources ignored by aapt, plus
|
||||
# files starting with '#'. (Emacs produces temp files named #temp#.)
|
||||
# This doesn't actually set the environment variable; it's used as a
|
||||
# parameter in the aapt invocation below. Consider updating
|
||||
# not_android_res_files as well.
|
||||
|
||||
ANDROID_AAPT_IGNORE := !.svn:!.git:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*.scc:*~:\#*:*.rej:*.orig
|
||||
|
||||
extra_packages := $(subst $(NULL) ,:,$(strip $(extra_packages)))
|
||||
|
||||
# 1: target file.
|
||||
# 2: dependencies.
|
||||
# 3: name of ap_ file to write.
|
||||
# 4: directory to write R.java into.
|
||||
# 5: directory to write R.txt into.
|
||||
# We touch the target file before invoking aapt so that aapt's outputs
|
||||
# are fresher than the target, preventing a subsequent invocation from
|
||||
# thinking aapt's outputs are stale. This is safe because Make
|
||||
# removes the target file if any recipe command fails.
|
||||
|
||||
CONSTRAINED_AAPT_CONFIGURATIONS := mdpi,hdpi
|
||||
|
||||
define aapt_command
|
||||
$(1): $$(call mkdir_deps,$(filter-out ./,$(dir $(3) $(4) $(5)))) $(2)
|
||||
@$$(TOUCH) $$@
|
||||
$$(AAPT) package -f -m \
|
||||
-M AndroidManifest.xml \
|
||||
-I $(ANDROID_SDK)/android.jar \
|
||||
$(if $(MOZ_ANDROID_MAX_SDK_VERSION),--max-res-version $(MOZ_ANDROID_MAX_SDK_VERSION),) \
|
||||
--auto-add-overlay \
|
||||
$$(addprefix -S ,$$(ANDROID_RES_DIRS)) \
|
||||
$(if $(extra_res_dirs),$$(addprefix -S ,$$(extra_res_dirs)),) \
|
||||
$(if $(extra_packages),--extra-packages $$(extra_packages),) \
|
||||
--custom-package org.mozilla.goanna \
|
||||
--non-constant-id \
|
||||
-F $(3) \
|
||||
-J $(4) \
|
||||
--output-text-symbols $(5) \
|
||||
$(if $(MOZ_ANDROID_RESOURCE_CONSTRAINED),-c $(CONSTRAINED_AAPT_CONFIGURATIONS),) \
|
||||
--ignore-assets "$$(ANDROID_AAPT_IGNORE)"
|
||||
endef
|
||||
|
||||
# [Comment 3/3] The first of these rules is used during regular
|
||||
# builds. The second writes an ap_ file that is only used during
|
||||
# packaging. It doesn't write the normal ap_, or R.java, since we
|
||||
# don't want the packaging step to write anything that would make a
|
||||
# further no-op build do work. See also
|
||||
# toolkit/mozapps/installer/packager.mk.
|
||||
|
||||
# .aapt.deps: $(all_resources)
|
||||
$(eval $(call aapt_command,.aapt.deps,$(all_resources),goanna.ap_,generated/,./))
|
||||
|
||||
# .aapt.nodeps: $(CURDIR)/AndroidManifest.xml FORCE
|
||||
$(eval $(call aapt_command,.aapt.nodeps,$(CURDIR)/AndroidManifest.xml FORCE,goanna-nodeps.ap_,goanna-nodeps/,goanna-nodeps/))
|
||||
|
||||
fennec_ids.txt: generated/org/mozilla/goanna/R.java fennec-ids-generator.py
|
||||
$(PYTHON) $(topsrcdir)/mobile/android/base/fennec-ids-generator.py -i $< -o $@
|
||||
|
||||
# Override the Java settings with some specific android settings
|
||||
include $(topsrcdir)/config/android-common.mk
|
||||
|
||||
update-generated-wrappers:
|
||||
@mv $(topsrcdir)/widget/android/GeneratedJNIWrappers.cpp $(topsrcdir)/widget/android/GeneratedJNIWrappers.cpp.old
|
||||
@mv $(topsrcdir)/widget/android/GeneratedJNIWrappers.h $(topsrcdir)/widget/android/GeneratedJNIWrappers.h.old
|
||||
@echo old GeneratedJNIWrappers.cpp/h moved to GeneratedJNIWrappers.cpp/h.old
|
||||
@cp $(CURDIR)/jni-stubs.inc $(topsrcdir)/mozglue/android
|
||||
@cp $(CURDIR)/GeneratedJNIWrappers.* $(topsrcdir)/widget/android
|
||||
@echo Updated GeneratedJNIWrappers
|
||||
|
||||
.PHONY: update-generated-wrappers
|
||||
|
||||
# This target is only used by IDE integrations. It rebuilds resources
|
||||
# that end up in omni.ja, does most of the packaging step, and then
|
||||
# updates omni.ja in place. If you're not using an IDE, you should be
|
||||
# using |mach build mobile/android && mach package|.
|
||||
$(abspath $(DIST)/fennec/$(OMNIJAR_NAME)): FORCE
|
||||
$(REPORT_BUILD)
|
||||
$(MAKE) -C ../locales
|
||||
$(MAKE) -C ../chrome
|
||||
$(MAKE) -C ../components
|
||||
$(MAKE) -C ../modules
|
||||
$(MAKE) -C ../app
|
||||
$(MAKE) -C ../themes/core
|
||||
$(MAKE) -C ../installer stage-package
|
||||
rsync --update $(DIST)/fennec/$(notdir $(OMNIJAR_NAME)) $@
|
||||
$(RM) $(DIST)/fennec/$(notdir $(OMNIJAR_NAME))
|
||||
|
||||
# Targets built very early during a Gradle build.
|
||||
gradle-targets: .aapt.deps
|
||||
|
||||
gradle-omnijar: $(abspath $(DIST)/fennec/$(OMNIJAR_NAME))
|
||||
|
||||
.PHONY: gradle-targets gradle-omnijar
|
||||
|
||||
libs:: goannaview_resources.zip classes.dex jni-stubs.inc GeneratedJNIWrappers.cpp fennec_ids.txt
|
||||
$(INSTALL) goannaview_resources.zip $(FINAL_TARGET)
|
||||
$(INSTALL) classes.dex $(FINAL_TARGET)
|
||||
@(diff jni-stubs.inc $(topsrcdir)/mozglue/android/jni-stubs.inc >/dev/null && diff GeneratedJNIWrappers.cpp $(topsrcdir)/widget/android/GeneratedJNIWrappers.cpp >/dev/null) || \
|
||||
(echo '*****************************************************' && \
|
||||
echo '*** Error: The generated JNI code has changed ***' && \
|
||||
echo '* To update generated code in the tree, please run *' && \
|
||||
echo && \
|
||||
echo ' make -C $(CURDIR) update-generated-wrappers' && \
|
||||
echo && \
|
||||
echo '* Repeat the build, and check in any changes. *' && \
|
||||
echo '*****************************************************' && \
|
||||
exit 1)
|
||||
@@ -1,120 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.util.GoannaEventListener;
|
||||
import org.mozilla.goanna.util.ThreadUtils;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class MediaCastingBar extends RelativeLayout implements View.OnClickListener, GoannaEventListener {
|
||||
private static final String LOGTAG = "GoannaMediaCastingBar";
|
||||
|
||||
private TextView mCastingTo;
|
||||
private ImageButton mMediaPlay;
|
||||
private ImageButton mMediaPause;
|
||||
private ImageButton mMediaStop;
|
||||
|
||||
private boolean mInflated;
|
||||
|
||||
public MediaCastingBar(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
EventDispatcher.getInstance().registerGoannaThreadListener(this,
|
||||
"Casting:Started",
|
||||
"Casting:Stopped");
|
||||
}
|
||||
|
||||
public void inflateContent() {
|
||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
View content = inflater.inflate(R.layout.media_casting, this);
|
||||
|
||||
mMediaPlay = (ImageButton) content.findViewById(R.id.media_play);
|
||||
mMediaPlay.setOnClickListener(this);
|
||||
mMediaPause = (ImageButton) content.findViewById(R.id.media_pause);
|
||||
mMediaPause.setOnClickListener(this);
|
||||
mMediaStop = (ImageButton) content.findViewById(R.id.media_stop);
|
||||
mMediaStop.setOnClickListener(this);
|
||||
|
||||
mCastingTo = (TextView) content.findViewById(R.id.media_sending_to);
|
||||
|
||||
// Capture clicks on the rest of the view to prevent them from
|
||||
// leaking into other views positioned below.
|
||||
content.setOnClickListener(this);
|
||||
|
||||
mInflated = true;
|
||||
}
|
||||
|
||||
public void show() {
|
||||
if (!mInflated)
|
||||
inflateContent();
|
||||
|
||||
setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
setVisibility(GONE);
|
||||
}
|
||||
|
||||
public void onDestroy() {
|
||||
EventDispatcher.getInstance().unregisterGoannaThreadListener(this,
|
||||
"Casting:Started",
|
||||
"Casting:Stopped");
|
||||
}
|
||||
|
||||
// View.OnClickListener implementation
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
final int viewId = v.getId();
|
||||
|
||||
if (viewId == R.id.media_play) {
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("Casting:Play", ""));
|
||||
mMediaPlay.setVisibility(GONE);
|
||||
mMediaPause.setVisibility(VISIBLE);
|
||||
} else if (viewId == R.id.media_pause) {
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("Casting:Pause", ""));
|
||||
mMediaPause.setVisibility(GONE);
|
||||
mMediaPlay.setVisibility(VISIBLE);
|
||||
} else if (viewId == R.id.media_stop) {
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("Casting:Stop", ""));
|
||||
}
|
||||
}
|
||||
|
||||
// GoannaEventListener implementation
|
||||
@Override
|
||||
public void handleMessage(final String event, final JSONObject message) {
|
||||
final String device = message.optString("device");
|
||||
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (event.equals("Casting:Started")) {
|
||||
show();
|
||||
if (!TextUtils.isEmpty(device)) {
|
||||
mCastingTo.setText(device);
|
||||
} else {
|
||||
// Should not happen
|
||||
mCastingTo.setText("");
|
||||
Log.d(LOGTAG, "Device name is empty.");
|
||||
}
|
||||
mMediaPlay.setVisibility(GONE);
|
||||
mMediaPause.setVisibility(VISIBLE);
|
||||
} else if (event.equals("Casting:Stopped")) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.media.MediaControlIntent;
|
||||
import android.support.v7.media.MediaRouteSelector;
|
||||
import android.support.v7.media.MediaRouter;
|
||||
import android.support.v7.media.MediaRouter.RouteInfo;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.gms.cast.CastMediaControlIntent;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.goanna.mozglue.JNITarget;
|
||||
import org.mozilla.goanna.util.EventCallback;
|
||||
import org.mozilla.goanna.util.NativeEventListener;
|
||||
import org.mozilla.goanna.util.NativeJSObject;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
/* Manages a list of GoannaMediaPlayers methods (i.e. Chromecast/Miracast). Routes messages
|
||||
* from Goanna to the correct caster based on the id of the display
|
||||
*/
|
||||
public class MediaPlayerManager extends Fragment implements NativeEventListener {
|
||||
/**
|
||||
* Create a new instance of DetailsFragment, initialized to
|
||||
* show the text at 'index'.
|
||||
*/
|
||||
@JNITarget
|
||||
public static MediaPlayerManager newInstance() {
|
||||
return new MediaPlayerManager();
|
||||
}
|
||||
|
||||
private static final String LOGTAG = "GoannaMediaPlayerManager";
|
||||
|
||||
@JNITarget
|
||||
public static final String MEDIA_PLAYER_TAG = "MPManagerFragment";
|
||||
|
||||
private static final boolean SHOW_DEBUG = false;
|
||||
// Simplified debugging interfaces
|
||||
private static void debug(String msg, Exception e) {
|
||||
if (SHOW_DEBUG) {
|
||||
Log.e(LOGTAG, msg, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void debug(String msg) {
|
||||
if (SHOW_DEBUG) {
|
||||
Log.d(LOGTAG, msg);
|
||||
}
|
||||
}
|
||||
|
||||
private MediaRouter mediaRouter = null;
|
||||
private final Map<String, GoannaMediaPlayer> displays = new HashMap<String, GoannaMediaPlayer>();
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
EventDispatcher.getInstance().registerGoannaThreadListener(this,
|
||||
"MediaPlayer:Load",
|
||||
"MediaPlayer:Start",
|
||||
"MediaPlayer:Stop",
|
||||
"MediaPlayer:Play",
|
||||
"MediaPlayer:Pause",
|
||||
"MediaPlayer:End",
|
||||
"MediaPlayer:Mirror",
|
||||
"MediaPlayer:Message");
|
||||
}
|
||||
|
||||
@Override
|
||||
@JNITarget
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
EventDispatcher.getInstance().unregisterGoannaThreadListener(this,
|
||||
"MediaPlayer:Load",
|
||||
"MediaPlayer:Start",
|
||||
"MediaPlayer:Stop",
|
||||
"MediaPlayer:Play",
|
||||
"MediaPlayer:Pause",
|
||||
"MediaPlayer:End",
|
||||
"MediaPlayer:Mirror",
|
||||
"MediaPlayer:Message");
|
||||
}
|
||||
|
||||
// GoannaEventListener implementation
|
||||
@Override
|
||||
public void handleMessage(String event, final NativeJSObject message, final EventCallback callback) {
|
||||
debug(event);
|
||||
|
||||
final GoannaMediaPlayer display = displays.get(message.getString("id"));
|
||||
if (display == null) {
|
||||
Log.e(LOGTAG, "Couldn't find a display for this id: " + message.getString("id") + " for message: " + event);
|
||||
if (callback != null) {
|
||||
callback.sendError(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ("MediaPlayer:Play".equals(event)) {
|
||||
display.play(callback);
|
||||
} else if ("MediaPlayer:Start".equals(event)) {
|
||||
display.start(callback);
|
||||
} else if ("MediaPlayer:Stop".equals(event)) {
|
||||
display.stop(callback);
|
||||
} else if ("MediaPlayer:Pause".equals(event)) {
|
||||
display.pause(callback);
|
||||
} else if ("MediaPlayer:End".equals(event)) {
|
||||
display.end(callback);
|
||||
} else if ("MediaPlayer:Mirror".equals(event)) {
|
||||
display.mirror(callback);
|
||||
} else if ("MediaPlayer:Message".equals(event) && message.has("data")) {
|
||||
display.message(message.getString("data"), callback);
|
||||
} else if ("MediaPlayer:Load".equals(event)) {
|
||||
final String url = message.optString("source", "");
|
||||
final String type = message.optString("type", "video/mp4");
|
||||
final String title = message.optString("title", "");
|
||||
display.load(title, url, type, callback);
|
||||
}
|
||||
}
|
||||
|
||||
private final MediaRouter.Callback callback =
|
||||
new MediaRouter.Callback() {
|
||||
@Override
|
||||
public void onRouteRemoved(MediaRouter router, RouteInfo route) {
|
||||
debug("onRouteRemoved: route=" + route);
|
||||
displays.remove(route.getId());
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent(
|
||||
"MediaPlayer:Removed", route.getId()));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo route) {
|
||||
}
|
||||
|
||||
// These methods aren't used by the support version Media Router
|
||||
@SuppressWarnings("unused")
|
||||
public void onRouteUnselected(MediaRouter router, int type, RouteInfo route) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo route) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) {
|
||||
debug("onRouteAdded: route=" + route);
|
||||
final GoannaMediaPlayer display = getMediaPlayerForRoute(route);
|
||||
saveAndNotifyOfDisplay("MediaPlayer:Added", route, display);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) {
|
||||
debug("onRouteChanged: route=" + route);
|
||||
final GoannaMediaPlayer display = displays.get(route.getId());
|
||||
saveAndNotifyOfDisplay("MediaPlayer:Changed", route, display);
|
||||
}
|
||||
|
||||
private void saveAndNotifyOfDisplay(final String eventName,
|
||||
MediaRouter.RouteInfo route, final GoannaMediaPlayer display) {
|
||||
if (display == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final JSONObject json = display.toJSON();
|
||||
if (json == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
displays.put(route.getId(), display);
|
||||
final GoannaEvent event = GoannaEvent.createBroadcastEvent(eventName, json.toString());
|
||||
GoannaAppShell.sendEventToGoanna(event);
|
||||
}
|
||||
};
|
||||
|
||||
private GoannaMediaPlayer getMediaPlayerForRoute(MediaRouter.RouteInfo route) {
|
||||
try {
|
||||
if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
|
||||
return new ChromeCast(getActivity(), route);
|
||||
}
|
||||
} catch(Exception ex) {
|
||||
debug("Error handling presentation", ex);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
mediaRouter.removeCallback(callback);
|
||||
mediaRouter = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
// The mediaRouter shouldn't exist here, but this is a nice safety check.
|
||||
if (mediaRouter != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mediaRouter = MediaRouter.getInstance(getActivity());
|
||||
final MediaRouteSelector selectorBuilder = new MediaRouteSelector.Builder()
|
||||
.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)
|
||||
.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
|
||||
.addControlCategory(CastMediaControlIntent.categoryForCast(ChromeCast.MIRROR_RECEIVER_APP_ID))
|
||||
.build();
|
||||
mediaRouter.addCallback(selectorBuilder, callback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
|
||||
}
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.AppConstants.Versions;
|
||||
import org.mozilla.goanna.db.BrowserDB;
|
||||
import org.mozilla.goanna.db.BrowserContract;
|
||||
import org.mozilla.goanna.favicons.Favicons;
|
||||
import org.mozilla.goanna.util.ThreadUtils;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentCallbacks2;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* This is a utility class to keep track of how much memory and disk-space pressure
|
||||
* the system is under. It receives input from GoannaActivity via the onLowMemory() and
|
||||
* onTrimMemory() functions, and also listens for some system intents related to
|
||||
* disk-space notifications. Internally it will track how much memory and disk pressure
|
||||
* the system is under, and perform various actions to help alleviate the pressure.
|
||||
*
|
||||
* Note that since there is no notification for when the system has lots of free memory
|
||||
* again, this class also assumes that, over time, the system will free up memory. This
|
||||
* assumption is implemented using a timer that slowly lowers the internal memory
|
||||
* pressure state if no new low-memory notifications are received.
|
||||
*
|
||||
* Synchronization note: MemoryMonitor contains an inner class PressureDecrementer. Both
|
||||
* of these classes may be accessed from various threads, and have both been designed to
|
||||
* be thread-safe. In terms of lock ordering, code holding the PressureDecrementer lock
|
||||
* is allowed to pick up the MemoryMonitor lock, but not vice-versa.
|
||||
*/
|
||||
class MemoryMonitor extends BroadcastReceiver {
|
||||
private static final String LOGTAG = "GoannaMemoryMonitor";
|
||||
private static final String ACTION_MEMORY_DUMP = "org.mozilla.goanna.MEMORY_DUMP";
|
||||
private static final String ACTION_FORCE_PRESSURE = "org.mozilla.goanna.FORCE_MEMORY_PRESSURE";
|
||||
|
||||
// Memory pressure levels. Keep these in sync with those in AndroidJavaWrappers.h
|
||||
private static final int MEMORY_PRESSURE_NONE = 0;
|
||||
private static final int MEMORY_PRESSURE_CLEANUP = 1;
|
||||
private static final int MEMORY_PRESSURE_LOW = 2;
|
||||
private static final int MEMORY_PRESSURE_MEDIUM = 3;
|
||||
private static final int MEMORY_PRESSURE_HIGH = 4;
|
||||
|
||||
private static final MemoryMonitor sInstance = new MemoryMonitor();
|
||||
|
||||
static MemoryMonitor getInstance() {
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
private final PressureDecrementer mPressureDecrementer;
|
||||
private int mMemoryPressure; // Synchronized access only.
|
||||
private volatile boolean mStoragePressure; // Accessed via UI thread intent, background runnables.
|
||||
private boolean mInited;
|
||||
|
||||
private MemoryMonitor() {
|
||||
mPressureDecrementer = new PressureDecrementer();
|
||||
mMemoryPressure = MEMORY_PRESSURE_NONE;
|
||||
}
|
||||
|
||||
public void init(final Context context) {
|
||||
if (mInited) {
|
||||
return;
|
||||
}
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
|
||||
filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
|
||||
filter.addAction(ACTION_MEMORY_DUMP);
|
||||
filter.addAction(ACTION_FORCE_PRESSURE);
|
||||
context.getApplicationContext().registerReceiver(this, filter);
|
||||
mInited = true;
|
||||
}
|
||||
|
||||
public void onLowMemory() {
|
||||
Log.d(LOGTAG, "onLowMemory() notification received");
|
||||
if (increaseMemoryPressure(MEMORY_PRESSURE_HIGH)) {
|
||||
// We need to wait on Goanna here, because if we haven't reduced
|
||||
// memory usage enough when we return from this, Android will kill us.
|
||||
GoannaAppShell.sendEventToGoannaSync(GoannaEvent.createNoOpEvent());
|
||||
}
|
||||
}
|
||||
|
||||
public void onTrimMemory(int level) {
|
||||
Log.d(LOGTAG, "onTrimMemory() notification received with level " + level);
|
||||
if (Versions.preICS) {
|
||||
// This won't even get called pre-ICS.
|
||||
return;
|
||||
}
|
||||
|
||||
if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE) {
|
||||
increaseMemoryPressure(MEMORY_PRESSURE_HIGH);
|
||||
} else if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
|
||||
increaseMemoryPressure(MEMORY_PRESSURE_MEDIUM);
|
||||
} else if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
|
||||
// includes TRIM_MEMORY_BACKGROUND
|
||||
increaseMemoryPressure(MEMORY_PRESSURE_CLEANUP);
|
||||
} else {
|
||||
// levels down here mean goanna is the foreground process so we
|
||||
// should be less aggressive with wiping memory as it may impact
|
||||
// user experience.
|
||||
increaseMemoryPressure(MEMORY_PRESSURE_LOW);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(intent.getAction())) {
|
||||
Log.d(LOGTAG, "Device storage is low");
|
||||
mStoragePressure = true;
|
||||
ThreadUtils.postToBackgroundThread(new StorageReducer(context));
|
||||
} else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction())) {
|
||||
Log.d(LOGTAG, "Device storage is ok");
|
||||
mStoragePressure = false;
|
||||
} else if (ACTION_MEMORY_DUMP.equals(intent.getAction())) {
|
||||
String label = intent.getStringExtra("label");
|
||||
if (label == null) {
|
||||
label = "default";
|
||||
}
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("Memory:Dump", label));
|
||||
} else if (ACTION_FORCE_PRESSURE.equals(intent.getAction())) {
|
||||
increaseMemoryPressure(MEMORY_PRESSURE_HIGH);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean increaseMemoryPressure(int level) {
|
||||
int oldLevel;
|
||||
synchronized (this) {
|
||||
// bump up our level if we're not already higher
|
||||
if (mMemoryPressure > level) {
|
||||
return false;
|
||||
}
|
||||
oldLevel = mMemoryPressure;
|
||||
mMemoryPressure = level;
|
||||
}
|
||||
|
||||
// since we don't get notifications for when memory pressure is off,
|
||||
// we schedule our own timer to slowly back off the memory pressure level.
|
||||
// note that this will reset the time to next decrement if the decrementer
|
||||
// is already running, which is the desired behaviour because we just got
|
||||
// a new low-mem notification.
|
||||
mPressureDecrementer.start();
|
||||
|
||||
if (oldLevel == level) {
|
||||
// if we're not going to a higher level we probably don't
|
||||
// need to run another round of the same memory reductions
|
||||
// we did on the last memory pressure increase.
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO hook in memory-reduction stuff for different levels here
|
||||
if (level >= MEMORY_PRESSURE_MEDIUM) {
|
||||
//Only send medium or higher events because that's all that is used right now
|
||||
if (GoannaThread.checkLaunchState(GoannaThread.LaunchState.GoannaRunning)) {
|
||||
GoannaAppShell.dispatchMemoryPressure();
|
||||
}
|
||||
|
||||
Favicons.clearMemCache();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Thread-safe due to mStoragePressure's volatility.
|
||||
*/
|
||||
boolean isUnderStoragePressure() {
|
||||
return mStoragePressure;
|
||||
}
|
||||
|
||||
private boolean decreaseMemoryPressure() {
|
||||
int newLevel;
|
||||
synchronized (this) {
|
||||
if (mMemoryPressure <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
newLevel = --mMemoryPressure;
|
||||
}
|
||||
Log.d(LOGTAG, "Decreased memory pressure to " + newLevel);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
class PressureDecrementer implements Runnable {
|
||||
private static final int DECREMENT_DELAY = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
private boolean mPosted;
|
||||
|
||||
synchronized void start() {
|
||||
if (mPosted) {
|
||||
// cancel the old one before scheduling a new one
|
||||
ThreadUtils.getBackgroundHandler().removeCallbacks(this);
|
||||
}
|
||||
ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY);
|
||||
mPosted = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void run() {
|
||||
if (!decreaseMemoryPressure()) {
|
||||
// done decrementing, bail out
|
||||
mPosted = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// need to keep decrementing
|
||||
ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
private static class StorageReducer implements Runnable {
|
||||
private final Context mContext;
|
||||
private final BrowserDB mDB;
|
||||
|
||||
public StorageReducer(final Context context) {
|
||||
this.mContext = context;
|
||||
// Since this may be called while Fennec is in the background, we don't want to risk accidentally
|
||||
// using the wrong context. If the profile we get is a guest profile, use the default profile instead.
|
||||
GoannaProfile profile = GoannaProfile.get(mContext);
|
||||
if (profile.inGuestMode()) {
|
||||
// If it was the guest profile, switch to the default one.
|
||||
profile = GoannaProfile.get(mContext, GoannaProfile.DEFAULT_PROFILE);
|
||||
}
|
||||
|
||||
mDB = profile.getDB();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// this might get run right on startup, if so wait 10 seconds and try again
|
||||
if (!GoannaThread.checkLaunchState(GoannaThread.LaunchState.GoannaRunning)) {
|
||||
ThreadUtils.getBackgroundHandler().postDelayed(this, 10000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MemoryMonitor.getInstance().isUnderStoragePressure()) {
|
||||
// Pressure is off, so we can abort.
|
||||
return;
|
||||
}
|
||||
|
||||
final ContentResolver cr = mContext.getContentResolver();
|
||||
mDB.expireHistory(cr, BrowserContract.ExpirePriority.AGGRESSIVE);
|
||||
mDB.removeThumbnails(cr);
|
||||
|
||||
// TODO: drop or shrink disk caches
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
public interface MotionEventInterceptor {
|
||||
public boolean onInterceptMotionEvent(View view, MotionEvent event);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.mozglue.GoannaLoader;
|
||||
|
||||
import android.content.Context;
|
||||
import org.mozilla.goanna.mozglue.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();
|
||||
GoannaLoader.loadNSSLibs(context, resourcePath);
|
||||
|
||||
String path = GoannaProfile.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();
|
||||
GoannaLoader.loadNSSLibs(context, resourcePath);
|
||||
|
||||
return nativeEncrypt(profilePath, aValue);
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
static public String decrypt(Context context, String aValue)
|
||||
throws Exception {
|
||||
String resourcePath = context.getPackageResourcePath();
|
||||
GoannaLoader.loadNSSLibs(context, resourcePath);
|
||||
|
||||
String path = GoannaProfile.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();
|
||||
GoannaLoader.loadNSSLibs(context, resourcePath);
|
||||
|
||||
return nativeDecrypt(profilePath, aValue);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.mozilla.goanna.mozglue.RobocopTarget;
|
||||
import org.mozilla.goanna.util.HardwareUtils;
|
||||
|
||||
@RobocopTarget
|
||||
public class NewTabletUI {
|
||||
public static boolean isEnabled(final Context context) {
|
||||
return HardwareUtils.isTablet();
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Client for posting notifications through a NotificationHandler.
|
||||
*/
|
||||
public abstract class NotificationClient {
|
||||
private static final String LOGTAG = "GoannaNotificationClient";
|
||||
|
||||
private volatile NotificationHandler mHandler;
|
||||
private boolean mReady;
|
||||
private final LinkedList<Runnable> mTaskQueue = new LinkedList<Runnable>();
|
||||
private final ConcurrentHashMap<Integer, UpdateRunnable> mUpdatesMap =
|
||||
new ConcurrentHashMap<Integer, UpdateRunnable>();
|
||||
|
||||
/**
|
||||
* Runnable that is reused between update notifications.
|
||||
*
|
||||
* Updates happen frequently, so reusing Runnables prevents frequent dynamic allocation.
|
||||
*/
|
||||
private class UpdateRunnable implements Runnable {
|
||||
private long mProgress;
|
||||
private long mProgressMax;
|
||||
private String mAlertText;
|
||||
final private int mNotificationID;
|
||||
|
||||
public UpdateRunnable(int notificationID) {
|
||||
mNotificationID = notificationID;
|
||||
}
|
||||
|
||||
public synchronized boolean updateProgress(long progress, long progressMax, String alertText) {
|
||||
if (progress == mProgress
|
||||
&& mProgressMax == progressMax
|
||||
&& TextUtils.equals(mAlertText, alertText)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mProgress = progress;
|
||||
mProgressMax = progressMax;
|
||||
mAlertText = alertText;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
long progress;
|
||||
long progressMax;
|
||||
String alertText;
|
||||
|
||||
synchronized (this) {
|
||||
progress = mProgress;
|
||||
progressMax = mProgressMax;
|
||||
alertText = mAlertText;
|
||||
}
|
||||
|
||||
mHandler.update(mNotificationID, progress, progressMax, alertText);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a notification.
|
||||
*
|
||||
* @see NotificationHandler#add(int, String, String, String, PendingIntent, PendingIntent)
|
||||
*/
|
||||
public synchronized void add(final int notificationID, final String aImageUrl,
|
||||
final String aAlertTitle, final String aAlertText, final PendingIntent contentIntent) {
|
||||
mTaskQueue.add(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mHandler.add(notificationID, aImageUrl, aAlertTitle, aAlertText, contentIntent);
|
||||
}
|
||||
});
|
||||
notify();
|
||||
|
||||
if (!mReady) {
|
||||
bind();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a notification.
|
||||
*
|
||||
* @see NotificationHandler#add(int, Notification)
|
||||
*/
|
||||
public synchronized void add(final int notificationID, final Notification notification) {
|
||||
mTaskQueue.add(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mHandler.add(notificationID, notification);
|
||||
}
|
||||
});
|
||||
notify();
|
||||
|
||||
if (!mReady) {
|
||||
bind();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a notification.
|
||||
*
|
||||
* @see NotificationHandler#update(int, long, long, String)
|
||||
*/
|
||||
public void update(final int notificationID, final long aProgress, final long aProgressMax,
|
||||
final String aAlertText) {
|
||||
UpdateRunnable runnable = mUpdatesMap.get(notificationID);
|
||||
|
||||
if (runnable == null) {
|
||||
runnable = new UpdateRunnable(notificationID);
|
||||
mUpdatesMap.put(notificationID, runnable);
|
||||
}
|
||||
|
||||
// If we've already posted an update with these values, there's no
|
||||
// need to do it again.
|
||||
if (!runnable.updateProgress(aProgress, aProgressMax, aAlertText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
if (mReady) {
|
||||
mTaskQueue.add(runnable);
|
||||
notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a notification.
|
||||
*
|
||||
* @see NotificationHandler#remove(int)
|
||||
*/
|
||||
public synchronized void remove(final int notificationID) {
|
||||
mTaskQueue.add(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mHandler.remove(notificationID);
|
||||
mUpdatesMap.remove(notificationID);
|
||||
}
|
||||
});
|
||||
|
||||
// If mReady == false, we haven't added any notifications yet. That can happen if Fennec is being
|
||||
// started in response to clicking a notification. Call bind() to ensure the task we posted above is run.
|
||||
if (!mReady) {
|
||||
bind();
|
||||
}
|
||||
|
||||
notify();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a notification is showing progress.
|
||||
*
|
||||
* @see NotificationHandler#isProgressStyle(int)
|
||||
*/
|
||||
public boolean isOngoing(int notificationID) {
|
||||
final NotificationHandler handler = mHandler;
|
||||
return handler != null && handler.isOngoing(notificationID);
|
||||
}
|
||||
|
||||
protected void bind() {
|
||||
mReady = true;
|
||||
}
|
||||
|
||||
protected void unbind() {
|
||||
mReady = false;
|
||||
mUpdatesMap.clear();
|
||||
}
|
||||
|
||||
protected void connectHandler(NotificationHandler handler) {
|
||||
mHandler = handler;
|
||||
new Thread(new NotificationRunnable()).start();
|
||||
}
|
||||
|
||||
private class NotificationRunnable implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
Runnable r;
|
||||
try {
|
||||
while (true) {
|
||||
// Synchronize polls to prevent tasks from being added to the queue
|
||||
// during the isDone check.
|
||||
synchronized (NotificationClient.this) {
|
||||
r = mTaskQueue.poll();
|
||||
while (r == null) {
|
||||
if (mHandler.isDone()) {
|
||||
unbind();
|
||||
return;
|
||||
}
|
||||
NotificationClient.this.wait();
|
||||
r = mTaskQueue.poll();
|
||||
}
|
||||
}
|
||||
r.run();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(LOGTAG, "Notification task queue processing interrupted", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.gfx.BitmapUtils;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class NotificationHandler {
|
||||
private final ConcurrentHashMap<Integer, Notification>
|
||||
mNotifications = new ConcurrentHashMap<Integer, Notification>();
|
||||
private final Context mContext;
|
||||
private final NotificationManager mNotificationManager;
|
||||
|
||||
/**
|
||||
* Notification associated with this service's foreground state.
|
||||
*
|
||||
* {@link android.app.Service#startForeground(int, android.app.Notification)}
|
||||
* associates the foreground with exactly one notification from the service.
|
||||
* To keep Fennec alive during downloads (and to make sure it can be killed
|
||||
* once downloads are complete), we make sure that the foreground is always
|
||||
* associated with an active progress notification if and only if at least
|
||||
* one download is in progress.
|
||||
*/
|
||||
private Notification mForegroundNotification;
|
||||
private int mForegroundNotificationId;
|
||||
|
||||
public NotificationHandler(Context context) {
|
||||
mContext = context;
|
||||
mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a notification.
|
||||
*
|
||||
* @param notificationID the unique ID of the notification
|
||||
* @param aImageUrl URL of the image to use
|
||||
* @param aAlertTitle title of the notification
|
||||
* @param aAlertText text of the notification
|
||||
* @param contentIntent Intent used when the notification is clicked
|
||||
* @param clearIntent Intent used when the notification is removed
|
||||
*/
|
||||
public void add(int notificationID, String aImageUrl, String aAlertTitle,
|
||||
String aAlertText, PendingIntent contentIntent) {
|
||||
// Remove the old notification with the same ID, if any
|
||||
remove(notificationID);
|
||||
|
||||
Uri imageUri = Uri.parse(aImageUrl);
|
||||
int icon = BitmapUtils.getResource(imageUri, R.drawable.ic_status_logo);
|
||||
final AlertNotification notification = new AlertNotification(mContext, notificationID,
|
||||
icon, aAlertTitle, aAlertText, System.currentTimeMillis(), imageUri);
|
||||
|
||||
notification.setLatestEventInfo(mContext, aAlertTitle, aAlertText, contentIntent);
|
||||
|
||||
mNotificationManager.notify(notificationID, notification);
|
||||
mNotifications.put(notificationID, notification);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a notification.
|
||||
*
|
||||
* @param id the unique ID of the notification
|
||||
* @param aNotification the Notification to add
|
||||
*/
|
||||
public void add(int id, Notification notification) {
|
||||
mNotificationManager.notify(id, notification);
|
||||
mNotifications.put(id, notification);
|
||||
|
||||
if (mForegroundNotification == null && isOngoing(notification)) {
|
||||
setForegroundNotification(id, notification);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a notification.
|
||||
*
|
||||
* @param notificationID ID of existing notification
|
||||
* @param aProgress progress of item being updated
|
||||
* @param aProgressMax max progress of item being updated
|
||||
* @param aAlertText text of the notification
|
||||
*/
|
||||
public void update(int notificationID, long aProgress, long aProgressMax, String aAlertText) {
|
||||
final Notification notification = mNotifications.get(notificationID);
|
||||
if (notification == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (notification instanceof AlertNotification) {
|
||||
AlertNotification alert = (AlertNotification)notification;
|
||||
alert.updateProgress(aAlertText, aProgress, aProgressMax);
|
||||
}
|
||||
|
||||
if (mForegroundNotification == null && isOngoing(notification)) {
|
||||
setForegroundNotification(notificationID, notification);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a notification.
|
||||
*
|
||||
* @param notificationID ID of existing notification
|
||||
*/
|
||||
public void remove(int notificationID) {
|
||||
final Notification notification = mNotifications.remove(notificationID);
|
||||
if (notification != null) {
|
||||
updateForegroundNotification(notificationID, notification);
|
||||
}
|
||||
mNotificationManager.cancel(notificationID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the service is done.
|
||||
*
|
||||
* The service is considered finished when all notifications have been
|
||||
* removed.
|
||||
*
|
||||
* @return whether all notifications have been removed
|
||||
*/
|
||||
public boolean isDone() {
|
||||
return mNotifications.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a notification should hold a foreground service to keep Goanna alive
|
||||
*
|
||||
* @param notificationID the id of the notification to check
|
||||
* @return whether the notification is ongoing
|
||||
*/
|
||||
public boolean isOngoing(int notificationID) {
|
||||
final Notification notification = mNotifications.get(notificationID);
|
||||
return isOngoing(notification);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a notification should hold a foreground service to keep Goanna alive
|
||||
*
|
||||
* @param notification the notification to check
|
||||
* @return whether the notification is ongoing
|
||||
*/
|
||||
public boolean isOngoing(Notification notification) {
|
||||
if (notification != null && (isProgressStyle(notification) || ((notification.flags & Notification.FLAG_ONGOING_EVENT) > 0))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to determines whether a notification is an AlertNotification that is showing progress
|
||||
* This method will be deprecated when AlertNotifications are removed (bug 893289).
|
||||
*
|
||||
* @param notification the notification to check
|
||||
* @return whether the notification is an AlertNotification showing progress.
|
||||
*/
|
||||
private boolean isProgressStyle(Notification notification) {
|
||||
if (notification instanceof AlertNotification) {
|
||||
return ((AlertNotification)notification).isProgressStyle();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void setForegroundNotification(int id, Notification notification) {
|
||||
mForegroundNotificationId = id;
|
||||
mForegroundNotification = notification;
|
||||
}
|
||||
|
||||
private void updateForegroundNotification(int oldId, Notification oldNotification) {
|
||||
if (mForegroundNotificationId == oldId) {
|
||||
// If we're removing the notification associated with the
|
||||
// foreground, we need to pick another active notification to act
|
||||
// as the foreground notification.
|
||||
Notification foregroundNotification = null;
|
||||
int foregroundId = 0;
|
||||
for (final Integer id : mNotifications.keySet()) {
|
||||
final Notification notification = mNotifications.get(id);
|
||||
if (isOngoing(notification)) {
|
||||
foregroundNotification = notification;
|
||||
foregroundId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
setForegroundNotification(foregroundId, foregroundNotification);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,377 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.goanna.gfx.BitmapUtils;
|
||||
import org.mozilla.goanna.mozglue.ContextUtils.SafeIntent;
|
||||
import org.mozilla.goanna.util.GoannaEventListener;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.util.Log;
|
||||
|
||||
public final class NotificationHelper implements GoannaEventListener {
|
||||
public static final String HELPER_BROADCAST_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".helperBroadcastAction";
|
||||
|
||||
public static final String NOTIFICATION_ID = "NotificationHelper_ID";
|
||||
private static final String LOGTAG = "GoannaNotificationHelper";
|
||||
private static final String HELPER_NOTIFICATION = "helperNotif";
|
||||
|
||||
// Attributes mandatory to be used while sending a notification from js.
|
||||
private static final String TITLE_ATTR = "title";
|
||||
private static final String TEXT_ATTR = "text";
|
||||
private static final String ID_ATTR = "id";
|
||||
private static final String SMALLICON_ATTR = "smallIcon";
|
||||
|
||||
// Attributes that can be used while sending a notification from js.
|
||||
private static final String PROGRESS_VALUE_ATTR = "progress_value";
|
||||
private static final String PROGRESS_MAX_ATTR = "progress_max";
|
||||
private static final String PROGRESS_INDETERMINATE_ATTR = "progress_indeterminate";
|
||||
private static final String LIGHT_ATTR = "light";
|
||||
private static final String ONGOING_ATTR = "ongoing";
|
||||
private static final String WHEN_ATTR = "when";
|
||||
private static final String PRIORITY_ATTR = "priority";
|
||||
private static final String LARGE_ICON_ATTR = "largeIcon";
|
||||
private static final String EVENT_TYPE_ATTR = "eventType";
|
||||
private static final String ACTIONS_ATTR = "actions";
|
||||
private static final String ACTION_ID_ATTR = "buttonId";
|
||||
private static final String ACTION_TITLE_ATTR = "title";
|
||||
private static final String ACTION_ICON_ATTR = "icon";
|
||||
private static final String PERSISTENT_ATTR = "persistent";
|
||||
private static final String HANDLER_ATTR = "handlerKey";
|
||||
private static final String COOKIE_ATTR = "cookie";
|
||||
|
||||
private static final String NOTIFICATION_SCHEME = "moz-notification";
|
||||
|
||||
private static final String BUTTON_EVENT = "notification-button-clicked";
|
||||
private static final String CLICK_EVENT = "notification-clicked";
|
||||
private static final String CLEARED_EVENT = "notification-cleared";
|
||||
private static final String CLOSED_EVENT = "notification-closed";
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
// Holds a list of notifications that should be cleared if the Fennec Activity is shut down.
|
||||
// Will not include ongoing or persistent notifications that are tied to Goanna's lifecycle.
|
||||
private HashMap<String, String> mClearableNotifications;
|
||||
|
||||
private boolean mInitialized;
|
||||
private static NotificationHelper sInstance;
|
||||
|
||||
private NotificationHelper(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
public void init() {
|
||||
mClearableNotifications = new HashMap<String, String>();
|
||||
EventDispatcher.getInstance().registerGoannaThreadListener(this,
|
||||
"Notification:Show",
|
||||
"Notification:Hide");
|
||||
mInitialized = true;
|
||||
}
|
||||
|
||||
public static NotificationHelper getInstance(Context context) {
|
||||
// If someone else created this singleton, but didn't initialize it, something has gone wrong.
|
||||
if (sInstance != null && !sInstance.mInitialized) {
|
||||
throw new IllegalStateException("NotificationHelper was created by someone else but not initialized");
|
||||
}
|
||||
|
||||
if (sInstance == null) {
|
||||
sInstance = new NotificationHelper(context.getApplicationContext());
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(String event, JSONObject message) {
|
||||
if (event.equals("Notification:Show")) {
|
||||
showNotification(message);
|
||||
} else if (event.equals("Notification:Hide")) {
|
||||
hideNotification(message);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isHelperIntent(Intent i) {
|
||||
return i.getBooleanExtra(HELPER_NOTIFICATION, false);
|
||||
}
|
||||
|
||||
public void handleNotificationIntent(SafeIntent i) {
|
||||
final Uri data = i.getData();
|
||||
if (data == null) {
|
||||
Log.e(LOGTAG, "handleNotificationEvent: empty data");
|
||||
return;
|
||||
}
|
||||
final String id = data.getQueryParameter(ID_ATTR);
|
||||
final String notificationType = data.getQueryParameter(EVENT_TYPE_ATTR);
|
||||
if (id == null || notificationType == null) {
|
||||
Log.e(LOGTAG, "handleNotificationEvent: invalid intent parameters");
|
||||
return;
|
||||
}
|
||||
|
||||
// In case the user swiped out the notification, we empty the id set.
|
||||
if (CLEARED_EVENT.equals(notificationType)) {
|
||||
mClearableNotifications.remove(id);
|
||||
// If Goanna isn't running, we throw away events where the notification was cancelled.
|
||||
// i.e. Don't bug the user if they're just closing a bunch of notifications.
|
||||
if (!GoannaThread.checkLaunchState(GoannaThread.LaunchState.GoannaRunning)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
JSONObject args = new JSONObject();
|
||||
|
||||
// The handler and cookie parameters are optional.
|
||||
final String handler = data.getQueryParameter(HANDLER_ATTR);
|
||||
final String cookie = i.getStringExtra(COOKIE_ATTR);
|
||||
|
||||
try {
|
||||
args.put(ID_ATTR, id);
|
||||
args.put(EVENT_TYPE_ATTR, notificationType);
|
||||
args.put(HANDLER_ATTR, handler);
|
||||
args.put(COOKIE_ATTR, cookie);
|
||||
|
||||
if (BUTTON_EVENT.equals(notificationType)) {
|
||||
final String actionName = data.getQueryParameter(ACTION_ID_ATTR);
|
||||
args.put(ACTION_ID_ATTR, actionName);
|
||||
}
|
||||
|
||||
Log.i(LOGTAG, "Send " + args.toString());
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("Notification:Event", args.toString()));
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Error building JSON notification arguments.", e);
|
||||
}
|
||||
|
||||
// If the notification was clicked, we are closing it. This must be executed after
|
||||
// sending the event to js side because when the notification is canceled no event can be
|
||||
// handled.
|
||||
if (CLICK_EVENT.equals(notificationType) && !i.getBooleanExtra(ONGOING_ATTR, false)) {
|
||||
hideNotification(id, handler, cookie);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Uri.Builder getNotificationBuilder(JSONObject message, String type) {
|
||||
Uri.Builder b = new Uri.Builder();
|
||||
b.scheme(NOTIFICATION_SCHEME).appendQueryParameter(EVENT_TYPE_ATTR, type);
|
||||
|
||||
try {
|
||||
final String id = message.getString(ID_ATTR);
|
||||
b.appendQueryParameter(ID_ATTR, id);
|
||||
} catch (JSONException ex) {
|
||||
Log.i(LOGTAG, "buildNotificationPendingIntent, error parsing", ex);
|
||||
}
|
||||
|
||||
try {
|
||||
final String id = message.getString(HANDLER_ATTR);
|
||||
b.appendQueryParameter(HANDLER_ATTR, id);
|
||||
} catch (JSONException ex) {
|
||||
Log.i(LOGTAG, "Notification doesn't have a handler");
|
||||
}
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
private Intent buildNotificationIntent(JSONObject message, Uri.Builder builder) {
|
||||
Intent notificationIntent = new Intent(HELPER_BROADCAST_ACTION);
|
||||
final boolean ongoing = message.optBoolean(ONGOING_ATTR);
|
||||
notificationIntent.putExtra(ONGOING_ATTR, ongoing);
|
||||
|
||||
final Uri dataUri = builder.build();
|
||||
notificationIntent.setData(dataUri);
|
||||
notificationIntent.putExtra(HELPER_NOTIFICATION, true);
|
||||
notificationIntent.putExtra(COOKIE_ATTR, message.optString(COOKIE_ATTR));
|
||||
notificationIntent.setClass(mContext, GoannaAppShell.getGoannaInterface().getActivity().getClass());
|
||||
return notificationIntent;
|
||||
}
|
||||
|
||||
private PendingIntent buildNotificationPendingIntent(JSONObject message, String type) {
|
||||
Uri.Builder builder = getNotificationBuilder(message, type);
|
||||
final Intent notificationIntent = buildNotificationIntent(message, builder);
|
||||
PendingIntent pi = PendingIntent.getActivity(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
return pi;
|
||||
}
|
||||
|
||||
private PendingIntent buildButtonClickPendingIntent(JSONObject message, JSONObject action) {
|
||||
Uri.Builder builder = getNotificationBuilder(message, BUTTON_EVENT);
|
||||
try {
|
||||
// Action name must be in query uri, otherwise buttons pending intents
|
||||
// would be collapsed.
|
||||
if(action.has(ACTION_ID_ATTR)) {
|
||||
builder.appendQueryParameter(ACTION_ID_ATTR, action.getString(ACTION_ID_ATTR));
|
||||
} else {
|
||||
Log.i(LOGTAG, "button event with no name");
|
||||
}
|
||||
} catch (JSONException ex) {
|
||||
Log.i(LOGTAG, "buildNotificationPendingIntent, error parsing", ex);
|
||||
}
|
||||
final Intent notificationIntent = buildNotificationIntent(message, builder);
|
||||
PendingIntent res = PendingIntent.getActivity(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
return res;
|
||||
}
|
||||
|
||||
private void showNotification(JSONObject message) {
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext);
|
||||
|
||||
// These attributes are required
|
||||
final String id;
|
||||
try {
|
||||
builder.setContentTitle(message.getString(TITLE_ATTR));
|
||||
builder.setContentText(message.getString(TEXT_ATTR));
|
||||
id = message.getString(ID_ATTR);
|
||||
} catch (JSONException ex) {
|
||||
Log.i(LOGTAG, "Error parsing", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
Uri imageUri = Uri.parse(message.optString(SMALLICON_ATTR));
|
||||
builder.setSmallIcon(BitmapUtils.getResource(imageUri, R.drawable.ic_status_logo));
|
||||
|
||||
JSONArray light = message.optJSONArray(LIGHT_ATTR);
|
||||
if (light != null && light.length() == 3) {
|
||||
try {
|
||||
builder.setLights(light.getInt(0),
|
||||
light.getInt(1),
|
||||
light.getInt(2));
|
||||
} catch (JSONException ex) {
|
||||
Log.i(LOGTAG, "Error parsing", ex);
|
||||
}
|
||||
}
|
||||
|
||||
boolean ongoing = message.optBoolean(ONGOING_ATTR);
|
||||
builder.setOngoing(ongoing);
|
||||
|
||||
if (message.has(WHEN_ATTR)) {
|
||||
long when = message.optLong(WHEN_ATTR);
|
||||
builder.setWhen(when);
|
||||
}
|
||||
|
||||
if (message.has(PRIORITY_ATTR)) {
|
||||
int priority = message.optInt(PRIORITY_ATTR);
|
||||
builder.setPriority(priority);
|
||||
}
|
||||
|
||||
if (message.has(LARGE_ICON_ATTR)) {
|
||||
Bitmap b = BitmapUtils.getBitmapFromDataURI(message.optString(LARGE_ICON_ATTR));
|
||||
builder.setLargeIcon(b);
|
||||
}
|
||||
|
||||
if (message.has(PROGRESS_VALUE_ATTR) &&
|
||||
message.has(PROGRESS_MAX_ATTR) &&
|
||||
message.has(PROGRESS_INDETERMINATE_ATTR)) {
|
||||
try {
|
||||
final int progress = message.getInt(PROGRESS_VALUE_ATTR);
|
||||
final int progressMax = message.getInt(PROGRESS_MAX_ATTR);
|
||||
final boolean progressIndeterminate = message.getBoolean(PROGRESS_INDETERMINATE_ATTR);
|
||||
builder.setProgress(progressMax, progress, progressIndeterminate);
|
||||
} catch (JSONException ex) {
|
||||
Log.i(LOGTAG, "Error parsing", ex);
|
||||
}
|
||||
}
|
||||
|
||||
JSONArray actions = message.optJSONArray(ACTIONS_ATTR);
|
||||
if (actions != null) {
|
||||
try {
|
||||
for (int i = 0; i < actions.length(); i++) {
|
||||
JSONObject action = actions.getJSONObject(i);
|
||||
final PendingIntent pending = buildButtonClickPendingIntent(message, action);
|
||||
final String actionTitle = action.getString(ACTION_TITLE_ATTR);
|
||||
final Uri actionImage = Uri.parse(action.optString(ACTION_ICON_ATTR));
|
||||
builder.addAction(BitmapUtils.getResource(actionImage, R.drawable.ic_status_logo),
|
||||
actionTitle,
|
||||
pending);
|
||||
}
|
||||
} catch (JSONException ex) {
|
||||
Log.i(LOGTAG, "Error parsing", ex);
|
||||
}
|
||||
}
|
||||
|
||||
PendingIntent pi = buildNotificationPendingIntent(message, CLICK_EVENT);
|
||||
builder.setContentIntent(pi);
|
||||
PendingIntent deletePendingIntent = buildNotificationPendingIntent(message, CLEARED_EVENT);
|
||||
builder.setDeleteIntent(deletePendingIntent);
|
||||
|
||||
GoannaAppShell.notificationClient.add(id.hashCode(), builder.build());
|
||||
|
||||
boolean persistent = message.optBoolean(PERSISTENT_ATTR);
|
||||
// We add only not persistent notifications to the list since we want to purge only
|
||||
// them when goannaapp is destroyed.
|
||||
if (!persistent && !mClearableNotifications.containsKey(id)) {
|
||||
mClearableNotifications.put(id, message.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void hideNotification(JSONObject message) {
|
||||
final String id;
|
||||
final String handler;
|
||||
final String cookie;
|
||||
try {
|
||||
id = message.getString("id");
|
||||
handler = message.optString("handlerKey");
|
||||
cookie = message.optString("cookie");
|
||||
} catch (JSONException ex) {
|
||||
Log.i(LOGTAG, "Error parsing", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
hideNotification(id, handler, cookie);
|
||||
}
|
||||
|
||||
private void sendNotificationWasClosed(String id, String handlerKey, String cookie) {
|
||||
final JSONObject args = new JSONObject();
|
||||
try {
|
||||
args.put(ID_ATTR, id);
|
||||
args.put(HANDLER_ATTR, handlerKey);
|
||||
args.put(COOKIE_ATTR, cookie);
|
||||
args.put(EVENT_TYPE_ATTR, CLOSED_EVENT);
|
||||
Log.i(LOGTAG, "Send " + args.toString());
|
||||
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("Notification:Event", args.toString()));
|
||||
} catch (JSONException ex) {
|
||||
Log.e(LOGTAG, "sendNotificationWasClosed: error building JSON notification arguments.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void closeNotification(String id, String handlerKey, String cookie) {
|
||||
GoannaAppShell.notificationClient.remove(id.hashCode());
|
||||
sendNotificationWasClosed(id, handlerKey, cookie);
|
||||
}
|
||||
|
||||
public void hideNotification(String id, String handlerKey, String cookie) {
|
||||
mClearableNotifications.remove(id);
|
||||
closeNotification(id, handlerKey, cookie);
|
||||
}
|
||||
|
||||
private void clearAll() {
|
||||
for (Iterator<String> i = mClearableNotifications.keySet().iterator(); i.hasNext();) {
|
||||
final String id = i.next();
|
||||
final String json = mClearableNotifications.get(id);
|
||||
i.remove();
|
||||
|
||||
JSONObject obj;
|
||||
try {
|
||||
obj = new JSONObject(json);
|
||||
} catch(JSONException ex) {
|
||||
obj = new JSONObject();
|
||||
}
|
||||
|
||||
closeNotification(id, obj.optString(HANDLER_ATTR), obj.optString(COOKIE_ATTR));
|
||||
}
|
||||
}
|
||||
|
||||
public static void destroy() {
|
||||
if (sInstance != null) {
|
||||
sInstance.clearAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
|
||||
public class NotificationService extends Service {
|
||||
private final IBinder mBinder = new NotificationBinder();
|
||||
private NotificationHandler mHandler;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// This has to be initialized in onCreate in order to ensure that the NotificationHandler can
|
||||
// access the NotificationManager service.
|
||||
mHandler = new NotificationHandler(this) {
|
||||
@Override
|
||||
protected void setForegroundNotification(int id, Notification notification) {
|
||||
super.setForegroundNotification(id, notification);
|
||||
|
||||
if (notification == null) {
|
||||
stopForeground(true);
|
||||
} else {
|
||||
startForeground(id, notification);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public class NotificationBinder extends Binder {
|
||||
NotificationService getService() {
|
||||
// Return this instance of NotificationService so clients can call public methods
|
||||
return NotificationService.this;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
public NotificationHandler getNotificationHandler() {
|
||||
return mHandler;
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.background.common.GlobalConstants;
|
||||
import org.mozilla.goanna.EventDispatcher;
|
||||
import org.mozilla.goanna.util.GoannaEventListener;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Helper class to send Android Ordered Broadcasts.
|
||||
*/
|
||||
public final class OrderedBroadcastHelper
|
||||
implements GoannaEventListener
|
||||
{
|
||||
public static final String LOGTAG = "GoannaOrdBroadcast";
|
||||
|
||||
public static final String SEND_EVENT = "OrderedBroadcast:Send";
|
||||
|
||||
protected final Context mContext;
|
||||
|
||||
public OrderedBroadcastHelper(Context context) {
|
||||
mContext = context;
|
||||
|
||||
EventDispatcher dispatcher = EventDispatcher.getInstance();
|
||||
if (dispatcher == null) {
|
||||
Log.e(LOGTAG, "Goanna event dispatcher must not be null", new RuntimeException());
|
||||
return;
|
||||
}
|
||||
dispatcher.registerGoannaThreadListener(this, SEND_EVENT);
|
||||
}
|
||||
|
||||
public synchronized void uninit() {
|
||||
EventDispatcher dispatcher = EventDispatcher.getInstance();
|
||||
if (dispatcher == null) {
|
||||
Log.e(LOGTAG, "Goanna event dispatcher must not be null", new RuntimeException());
|
||||
return;
|
||||
}
|
||||
dispatcher.unregisterGoannaThreadListener(this, SEND_EVENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(String event, JSONObject message) {
|
||||
if (!SEND_EVENT.equals(event)) {
|
||||
Log.e(LOGTAG, "OrderedBroadcastHelper got unexpected message " + event);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final String action = message.getString("action");
|
||||
if (action == null) {
|
||||
Log.e(LOGTAG, "action must not be null");
|
||||
return;
|
||||
}
|
||||
|
||||
final String responseEvent = message.getString("responseEvent");
|
||||
if (responseEvent == null) {
|
||||
Log.e(LOGTAG, "responseEvent must not be null");
|
||||
return;
|
||||
}
|
||||
|
||||
// It's fine if the caller-provided token is missing or null.
|
||||
final JSONObject token = (message.has("token") && !message.isNull("token")) ?
|
||||
message.getJSONObject("token") : null;
|
||||
|
||||
// A missing (undefined) permission means the intent will be limited
|
||||
// to the current package. A null means no permission, so any
|
||||
// package can receive the intent.
|
||||
final String permission = message.has("permission") ?
|
||||
(message.isNull("permission") ? null : message.getString("permission")) :
|
||||
GlobalConstants.PER_ANDROID_PACKAGE_PERMISSION;
|
||||
|
||||
final BroadcastReceiver resultReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
int code = getResultCode();
|
||||
|
||||
if (code == Activity.RESULT_OK) {
|
||||
String data = getResultData();
|
||||
|
||||
JSONObject res = new JSONObject();
|
||||
try {
|
||||
res.put("action", action);
|
||||
res.put("token", token);
|
||||
res.put("data", data);
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Got exception in onReceive handling action " + action, e);
|
||||
return;
|
||||
}
|
||||
|
||||
GoannaEvent event = GoannaEvent.createBroadcastEvent(responseEvent, res.toString());
|
||||
GoannaAppShell.sendEventToGoanna(event);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Intent intent = new Intent(action);
|
||||
// OrderedBroadcast.jsm adds its callback ID to the caller's token;
|
||||
// this unwraps that wrapping.
|
||||
if (token != null && token.has("data")) {
|
||||
intent.putExtra("token", token.getString("data"));
|
||||
}
|
||||
|
||||
mContext.sendOrderedBroadcast(intent,
|
||||
permission,
|
||||
resultReceiver,
|
||||
null,
|
||||
Activity.RESULT_OK,
|
||||
null,
|
||||
null);
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Got exception in handleMessage handling event " + event, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.widget.ViewDragHelper;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
/* Outerlayout is the container layout of all the main views. It allows mainlayout to be dragged while targeting
|
||||
the toolbar and it's responsible for handling the dragprocess. It relies on ViewDragHelper to ease the drag process.
|
||||
*/
|
||||
public class OuterLayout extends RelativeLayout {
|
||||
private final double AUTO_OPEN_SPEED_LIMIT = 800.0;
|
||||
private ViewDragHelper mDragHelper;
|
||||
private int mDraggingBorder;
|
||||
private int mDragRange;
|
||||
private boolean mIsOpen = false;
|
||||
private int mDraggingState = ViewDragHelper.STATE_IDLE;
|
||||
private DragCallback mDragCallback;
|
||||
|
||||
public static interface DragCallback {
|
||||
public void startDrag(boolean wasOpen);
|
||||
public void stopDrag(boolean stoppingToOpen);
|
||||
public int getDragRange();
|
||||
public int getOrderedChildIndex(int index);
|
||||
public boolean canDrag(MotionEvent event);
|
||||
public boolean canInterceptEventWhileOpen(MotionEvent event);
|
||||
public void onDragProgress(float progress);
|
||||
public View getViewToDrag();
|
||||
public int getLowerLimit();
|
||||
}
|
||||
|
||||
private class DragHelperCallback extends ViewDragHelper.Callback {
|
||||
@Override
|
||||
public void onViewDragStateChanged(int newState) {
|
||||
if (newState == mDraggingState) { // no change
|
||||
return;
|
||||
}
|
||||
|
||||
// if the view stopped moving.
|
||||
if ((mDraggingState == ViewDragHelper.STATE_DRAGGING || mDraggingState == ViewDragHelper.STATE_SETTLING) &&
|
||||
newState == ViewDragHelper.STATE_IDLE) {
|
||||
|
||||
final float rangeToCheck = mDragRange;
|
||||
final float lowerLimit = mDragCallback.getLowerLimit();
|
||||
if (mDraggingBorder == lowerLimit) {
|
||||
mIsOpen = false;
|
||||
mDragCallback.onDragProgress(0);
|
||||
} else if (mDraggingBorder == rangeToCheck) {
|
||||
mIsOpen = true;
|
||||
mDragCallback.onDragProgress(1);
|
||||
}
|
||||
mDragCallback.stopDrag(mIsOpen);
|
||||
}
|
||||
|
||||
// The view was previuosly moving.
|
||||
if (newState == ViewDragHelper.STATE_DRAGGING && !isMoving()) {
|
||||
mDragCallback.startDrag(mIsOpen);
|
||||
updateRanges();
|
||||
}
|
||||
|
||||
mDraggingState = newState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
|
||||
mDraggingBorder = top;
|
||||
final float progress = Math.min(1, ((float) top) / mDragRange);
|
||||
mDragCallback.onDragProgress(progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewVerticalDragRange(View child) {
|
||||
return mDragRange;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrderedChildIndex(int index) {
|
||||
return mDragCallback.getOrderedChildIndex(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryCaptureView(View view, int i) {
|
||||
return (view.getId() == mDragCallback.getViewToDrag().getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int clampViewPositionVertical(View child, int top, int dy) {
|
||||
return top;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewReleased(View releasedChild, float xvel, float yvel) {
|
||||
final float rangeToCheck = mDragRange;
|
||||
final float speedToCheck = yvel;
|
||||
|
||||
if (mDraggingBorder == mDragCallback.getLowerLimit()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mDraggingBorder == rangeToCheck) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean settleToOpen = false;
|
||||
// Speed has priority over position.
|
||||
if (speedToCheck > AUTO_OPEN_SPEED_LIMIT) {
|
||||
settleToOpen = true;
|
||||
} else if (speedToCheck < -AUTO_OPEN_SPEED_LIMIT) {
|
||||
settleToOpen = false;
|
||||
} else if (mDraggingBorder > rangeToCheck / 2) {
|
||||
settleToOpen = true;
|
||||
} else if (mDraggingBorder < rangeToCheck / 2) {
|
||||
settleToOpen = false;
|
||||
}
|
||||
|
||||
final int settleDestX;
|
||||
final int settleDestY;
|
||||
if (settleToOpen) {
|
||||
settleDestX = 0;
|
||||
settleDestY = mDragRange;
|
||||
} else {
|
||||
settleDestX = 0;
|
||||
settleDestY = mDragCallback.getLowerLimit();
|
||||
}
|
||||
|
||||
if(mDragHelper.settleCapturedViewAt(settleDestX, settleDestY)) {
|
||||
ViewCompat.postInvalidateOnAnimation(OuterLayout.this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public OuterLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
private void updateRanges() {
|
||||
// Need to wait for the tabs to show in order to fetch the right sizes.
|
||||
mDragRange = mDragCallback.getDragRange() + mDragCallback.getLowerLimit();
|
||||
}
|
||||
|
||||
private void updateOrientation() {
|
||||
mDragHelper.setEdgeTrackingEnabled(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
|
||||
mIsOpen = false;
|
||||
super.onFinishInflate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent event) {
|
||||
if (mDragCallback.canDrag(event)) {
|
||||
if (mDragHelper.shouldInterceptTouchEvent(event)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Because while open the target layout is translated and draghelper does not catch it.
|
||||
if (mIsOpen && mDragCallback.canInterceptEventWhileOpen(event)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
// touch events can be passed to the helper if we target the toolbar or we are already dragging.
|
||||
if (mDragCallback.canDrag(event) || mDraggingState == ViewDragHelper.STATE_DRAGGING) {
|
||||
mDragHelper.processTouchEvent(event);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
// The first time fennec is started, tabs might not have been created while we drag. In that case we need
|
||||
// an arbitrary range to start dragging that will be updated as soon as the tabs are created.
|
||||
|
||||
if (mDragRange == 0) {
|
||||
mDragRange = h / 2;
|
||||
}
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void computeScroll() { // needed for automatic settling.
|
||||
if (mDragHelper.continueSettling(true)) {
|
||||
ViewCompat.postInvalidateOnAnimation(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To be called when closing the tabs from outside (i.e. when touching the main layout).
|
||||
*/
|
||||
public void setClosed() {
|
||||
mIsOpen = false;
|
||||
mDragHelper.abort();
|
||||
}
|
||||
|
||||
/**
|
||||
* To be called when opening the tabs from outside (i.e. when clicking on the tabs button).
|
||||
*/
|
||||
public void setOpen() {
|
||||
mIsOpen = true;
|
||||
mDragHelper.abort();
|
||||
}
|
||||
|
||||
public void setDraggableCallback(DragCallback dragCallback) {
|
||||
mDragCallback = dragCallback;
|
||||
updateOrientation();
|
||||
}
|
||||
|
||||
// If a change happens while we are dragging, we abort the dragging and set to open state.
|
||||
public void reset() {
|
||||
updateOrientation();
|
||||
if (isMoving()) {
|
||||
mDragHelper.abort();
|
||||
if (mDragCallback != null) {
|
||||
mDragCallback.stopDrag(false);
|
||||
mDragCallback.onDragProgress(0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateDragHelperParameters() {
|
||||
mDragRange = mDragCallback.getDragRange() + mDragCallback.getLowerLimit();
|
||||
updateOrientation();
|
||||
}
|
||||
|
||||
public boolean isMoving() {
|
||||
return (mDraggingState == ViewDragHelper.STATE_DRAGGING ||
|
||||
mDraggingState == ViewDragHelper.STATE_SETTLING);
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return mIsOpen;
|
||||
}
|
||||
|
||||
public View findTopChildUnder(MotionEvent event) {
|
||||
return mDragHelper.findTopChildUnder((int) event.getX(), (int) event.getY());
|
||||
}
|
||||
|
||||
public void restoreTargetViewPosition() {
|
||||
mDragCallback.getViewToDrag().offsetTopAndBottom(mDraggingBorder);
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.util.GoannaEventListener;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Helper class to get/set goanna prefs.
|
||||
*/
|
||||
public final class PrefsHelper {
|
||||
private static final String LOGTAG = "GoannaPrefsHelper";
|
||||
|
||||
private static boolean sRegistered;
|
||||
private static int sUniqueRequestId = 1;
|
||||
static final SparseArray<PrefHandler> sCallbacks = new SparseArray<PrefHandler>();
|
||||
|
||||
public static int getPref(String prefName, PrefHandler callback) {
|
||||
return getPrefsInternal(new String[] { prefName }, callback);
|
||||
}
|
||||
|
||||
public static int getPrefs(String[] prefNames, PrefHandler callback) {
|
||||
return getPrefsInternal(prefNames, callback);
|
||||
}
|
||||
|
||||
public static int getPrefs(ArrayList<String> prefNames, PrefHandler callback) {
|
||||
return getPrefsInternal(prefNames.toArray(new String[prefNames.size()]), callback);
|
||||
}
|
||||
|
||||
private static int getPrefsInternal(String[] prefNames, PrefHandler callback) {
|
||||
int requestId;
|
||||
synchronized (PrefsHelper.class) {
|
||||
ensureRegistered();
|
||||
|
||||
requestId = sUniqueRequestId++;
|
||||
sCallbacks.put(requestId, callback);
|
||||
}
|
||||
|
||||
GoannaEvent event;
|
||||
if (callback.isObserver()) {
|
||||
event = GoannaEvent.createPreferencesObserveEvent(requestId, prefNames);
|
||||
} else {
|
||||
event = GoannaEvent.createPreferencesGetEvent(requestId, prefNames);
|
||||
}
|
||||
GoannaAppShell.sendEventToGoanna(event);
|
||||
|
||||
return requestId;
|
||||
}
|
||||
|
||||
private static void ensureRegistered() {
|
||||
if (sRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
GoannaEventListener listener = new GoannaEventListener() {
|
||||
@Override
|
||||
public void handleMessage(String event, JSONObject message) {
|
||||
try {
|
||||
PrefHandler callback;
|
||||
synchronized (PrefsHelper.class) {
|
||||
try {
|
||||
int requestId = message.getInt("requestId");
|
||||
callback = sCallbacks.get(requestId);
|
||||
if (callback != null && !callback.isObserver()) {
|
||||
sCallbacks.delete(requestId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
callback = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (callback == null) {
|
||||
Log.d(LOGTAG, "Preferences:Data message had an unknown requestId; ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
JSONArray jsonPrefs = message.getJSONArray("preferences");
|
||||
for (int i = 0; i < jsonPrefs.length(); i++) {
|
||||
JSONObject pref = jsonPrefs.getJSONObject(i);
|
||||
String name = pref.getString("name");
|
||||
String type = pref.getString("type");
|
||||
try {
|
||||
if ("bool".equals(type)) {
|
||||
callback.prefValue(name, pref.getBoolean("value"));
|
||||
} else if ("int".equals(type)) {
|
||||
callback.prefValue(name, pref.getInt("value"));
|
||||
} else if ("string".equals(type)) {
|
||||
callback.prefValue(name, pref.getString("value"));
|
||||
} else {
|
||||
Log.e(LOGTAG, "Unknown pref value type [" + type + "] for pref [" + name + "]");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOGTAG, "Handler for preference [" + name + "] threw exception", e);
|
||||
}
|
||||
}
|
||||
callback.finish();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOGTAG, "Error handling Preferences:Data message", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
EventDispatcher.getInstance().registerGoannaThreadListener(listener, "Preferences:Data");
|
||||
sRegistered = true;
|
||||
}
|
||||
|
||||
public static void setPref(String pref, Object value) {
|
||||
if (pref == null || pref.length() == 0) {
|
||||
throw new IllegalArgumentException("Pref name must be non-empty");
|
||||
}
|
||||
|
||||
try {
|
||||
JSONObject jsonPref = new JSONObject();
|
||||
jsonPref.put("name", pref);
|
||||
if (value instanceof Boolean) {
|
||||
jsonPref.put("type", "bool");
|
||||
jsonPref.put("value", ((Boolean)value).booleanValue());
|
||||
} else if (value instanceof Integer) {
|
||||
jsonPref.put("type", "int");
|
||||
jsonPref.put("value", ((Integer)value).intValue());
|
||||
} else {
|
||||
jsonPref.put("type", "string");
|
||||
jsonPref.put("value", String.valueOf(value));
|
||||
}
|
||||
|
||||
GoannaEvent event = GoannaEvent.createBroadcastEvent("Preferences:Set", jsonPref.toString());
|
||||
GoannaAppShell.sendEventToGoanna(event);
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Error setting pref [" + pref + "]", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeObserver(int requestId) {
|
||||
if (requestId < 0) {
|
||||
throw new IllegalArgumentException("Invalid request ID");
|
||||
}
|
||||
|
||||
synchronized (PrefsHelper.class) {
|
||||
PrefHandler callback = sCallbacks.get(requestId);
|
||||
sCallbacks.delete(requestId);
|
||||
|
||||
if (callback == null) {
|
||||
Log.e(LOGTAG, "Unknown request ID " + requestId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
GoannaEvent event = GoannaEvent.createBroadcastEvent("Preferences:RemoveObserver",
|
||||
Integer.toString(requestId));
|
||||
GoannaAppShell.sendEventToGoanna(event);
|
||||
}
|
||||
|
||||
public interface PrefHandler {
|
||||
void prefValue(String pref, boolean value);
|
||||
void prefValue(String pref, int value);
|
||||
void prefValue(String pref, String value);
|
||||
boolean isObserver();
|
||||
void finish();
|
||||
}
|
||||
|
||||
public static abstract class PrefHandlerBase implements PrefHandler {
|
||||
@Override
|
||||
public void prefValue(String pref, boolean value) {
|
||||
Log.w(LOGTAG, "Unhandled boolean value for pref [" + pref + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prefValue(String pref, int value) {
|
||||
Log.w(LOGTAG, "Unhandled int value for pref [" + pref + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prefValue(String pref, String value) {
|
||||
Log.w(LOGTAG, "Unhandled String value for pref [" + pref + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isObserver() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.mozilla.goanna.db.BrowserDB;
|
||||
|
||||
public class PrivateTab extends Tab {
|
||||
public PrivateTab(Context context, int id, String url, boolean external, int parentId, String title) {
|
||||
super(context, id, url, external, parentId, title);
|
||||
|
||||
// Init background to background_private to ensure flicker-free
|
||||
// private tab creation. Page loads will reset it to white as expected.
|
||||
final int bgColor = context.getResources().getColor(R.color.background_private);
|
||||
setBackgroundColor(bgColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveThumbnailToDB(final BrowserDB db) {}
|
||||
|
||||
@Override
|
||||
public boolean isPrivate() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.mozilla.goanna.util.StringUtils;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
public class ReaderModeUtils {
|
||||
private static final String LOGTAG = "ReaderModeUtils";
|
||||
|
||||
public static String getUrlFromAboutReader(String aboutReaderUrl) {
|
||||
return StringUtils.getQueryParameter(aboutReaderUrl, "url");
|
||||
}
|
||||
|
||||
public static boolean isEnteringReaderMode(String currentUrl, String newUrl) {
|
||||
if (currentUrl == null || newUrl == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!AboutPages.isAboutReader(newUrl)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String urlFromAboutReader = getUrlFromAboutReader(newUrl);
|
||||
if (urlFromAboutReader == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return urlFromAboutReader.equals(currentUrl);
|
||||
}
|
||||
|
||||
public static String getAboutReaderForUrl(String url) {
|
||||
return getAboutReaderForUrl(url, -1);
|
||||
}
|
||||
|
||||
public static String getAboutReaderForUrl(String url, int tabId) {
|
||||
String aboutReaderUrl = AboutPages.READER + "?url=" + Uri.encode(url);
|
||||
|
||||
if (tabId >= 0) {
|
||||
aboutReaderUrl += "&tabId=" + tabId;
|
||||
}
|
||||
|
||||
return aboutReaderUrl;
|
||||
}
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.goanna.db.BrowserContract.ReadingListItems;
|
||||
import org.mozilla.goanna.db.BrowserDB;
|
||||
import org.mozilla.goanna.db.DBUtils;
|
||||
import org.mozilla.goanna.db.ReadingListAccessor;
|
||||
import org.mozilla.goanna.favicons.Favicons;
|
||||
import org.mozilla.goanna.mozglue.RobocopTarget;
|
||||
import org.mozilla.goanna.util.EventCallback;
|
||||
import org.mozilla.goanna.util.NativeEventListener;
|
||||
import org.mozilla.goanna.util.NativeJSObject;
|
||||
import org.mozilla.goanna.util.ThreadUtils;
|
||||
import org.mozilla.goanna.util.UIAsyncTask;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
public final class ReadingListHelper implements NativeEventListener {
|
||||
private static final String LOGTAG = "GoannaReadingListHelper";
|
||||
|
||||
protected final Context context;
|
||||
private final BrowserDB db;
|
||||
private final ReadingListAccessor readingListAccessor;
|
||||
private final ContentObserver contentObserver;
|
||||
|
||||
volatile boolean fetchInBackground = true;
|
||||
|
||||
public ReadingListHelper(Context context, GoannaProfile profile) {
|
||||
this.context = context;
|
||||
this.db = profile.getDB();
|
||||
this.readingListAccessor = db.getReadingListAccessor();
|
||||
|
||||
EventDispatcher.getInstance().registerGoannaThreadListener((NativeEventListener) this,
|
||||
"Reader:AddToList", "Reader:UpdateList", "Reader:FaviconRequest", "Reader:ListStatusRequest", "Reader:RemoveFromList");
|
||||
|
||||
|
||||
contentObserver = new ContentObserver(null) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
if (fetchInBackground) {
|
||||
fetchContent();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.readingListAccessor.registerContentObserver(context, contentObserver);
|
||||
}
|
||||
|
||||
public void uninit() {
|
||||
EventDispatcher.getInstance().unregisterGoannaThreadListener((NativeEventListener) this,
|
||||
"Reader:AddToList", "Reader:UpdateList", "Reader:FaviconRequest", "Reader:ListStatusRequest", "Reader:RemoveFromList");
|
||||
|
||||
context.getContentResolver().unregisterContentObserver(contentObserver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(final String event, final NativeJSObject message,
|
||||
final EventCallback callback) {
|
||||
switch(event) {
|
||||
case "Reader:AddToList": {
|
||||
handleAddToList(callback, message);
|
||||
break;
|
||||
}
|
||||
case "Reader:UpdateList": {
|
||||
handleUpdateList(message);
|
||||
break;
|
||||
}
|
||||
case "Reader:FaviconRequest": {
|
||||
handleReaderModeFaviconRequest(callback, message.getString("url"));
|
||||
break;
|
||||
}
|
||||
case "Reader:RemoveFromList": {
|
||||
handleRemoveFromList(message.getString("url"));
|
||||
break;
|
||||
}
|
||||
case "Reader:ListStatusRequest": {
|
||||
handleReadingListStatusRequest(callback, message.getString("url"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A page can be added to the ReadingList by long-tap of the page-action
|
||||
* icon, or by tapping the readinglist-add icon in the ReaderMode banner.
|
||||
*
|
||||
* This method will only add new items, not update existing items.
|
||||
*/
|
||||
private void handleAddToList(final EventCallback callback, final NativeJSObject message) {
|
||||
final ContentResolver cr = context.getContentResolver();
|
||||
final String url = message.getString("url");
|
||||
|
||||
// We can't access a NativeJSObject from the background thread, so we need to get the
|
||||
// values here, even if we may not use them to insert an item into the DB.
|
||||
final ContentValues values = getContentValues(message);
|
||||
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (readingListAccessor.isReadingListItem(cr, url)) {
|
||||
showToast(R.string.reading_list_duplicate, Toast.LENGTH_SHORT);
|
||||
callback.sendError("URL already in reading list: " + url);
|
||||
} else {
|
||||
readingListAccessor.addReadingListItem(cr, values);
|
||||
showToast(R.string.reading_list_added, Toast.LENGTH_SHORT);
|
||||
callback.sendSuccess(url);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a reading list item with new meta data.
|
||||
*/
|
||||
private void handleUpdateList(final NativeJSObject message) {
|
||||
final ContentResolver cr = context.getContentResolver();
|
||||
final ContentValues values = getContentValues(message);
|
||||
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
readingListAccessor.updateReadingListItem(cr, values);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates reading list item content values from JS message.
|
||||
*/
|
||||
private ContentValues getContentValues(NativeJSObject message) {
|
||||
final ContentValues values = new ContentValues();
|
||||
if (message.has("id")) {
|
||||
values.put(ReadingListItems._ID, message.getInt("id"));
|
||||
}
|
||||
|
||||
// url is actually required...
|
||||
String url = null;
|
||||
if (message.has("url")) {
|
||||
url = message.getString("url");
|
||||
values.put(ReadingListItems.URL, url);
|
||||
}
|
||||
|
||||
String title = null;
|
||||
if (message.has("title")) {
|
||||
title = message.getString("title");
|
||||
values.put(ReadingListItems.TITLE, title);
|
||||
}
|
||||
|
||||
// TODO: message actually has "length", but that's no use for us. See Bug 1127451.
|
||||
if (message.has("word_count")) {
|
||||
values.put(ReadingListItems.WORD_COUNT, message.getInt("word_count"));
|
||||
}
|
||||
|
||||
if (message.has("excerpt")) {
|
||||
values.put(ReadingListItems.EXCERPT, message.getString("excerpt"));
|
||||
}
|
||||
|
||||
if (message.has("status")) {
|
||||
final int status = message.getInt("status");
|
||||
values.put(ReadingListItems.CONTENT_STATUS, status);
|
||||
if (status == ReadingListItems.STATUS_FETCHED_ARTICLE) {
|
||||
if (message.has("resolved_title")) {
|
||||
values.put(ReadingListItems.RESOLVED_TITLE, message.getString("resolved_title"));
|
||||
} else {
|
||||
if (title != null) {
|
||||
values.put(ReadingListItems.RESOLVED_TITLE, title);
|
||||
}
|
||||
}
|
||||
if (message.has("resolved_url")) {
|
||||
values.put(ReadingListItems.RESOLVED_URL, message.getString("resolved_url"));
|
||||
} else {
|
||||
if (url != null) {
|
||||
values.put(ReadingListItems.RESOLVED_URL, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Goanna (ReaderMode) requests the page favicon to append to the
|
||||
* document head for display.
|
||||
*/
|
||||
private void handleReaderModeFaviconRequest(final EventCallback callback, final String url) {
|
||||
(new UIAsyncTask.WithoutParams<String>(ThreadUtils.getBackgroundHandler()) {
|
||||
@Override
|
||||
public String doInBackground() {
|
||||
return Favicons.getFaviconURLForPageURL(db, context.getContentResolver(), url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute(String faviconUrl) {
|
||||
JSONObject args = new JSONObject();
|
||||
if (faviconUrl != null) {
|
||||
try {
|
||||
args.put("url", url);
|
||||
args.put("faviconUrl", faviconUrl);
|
||||
} catch (JSONException e) {
|
||||
Log.w(LOGTAG, "Error building JSON favicon arguments.", e);
|
||||
}
|
||||
}
|
||||
callback.sendSuccess(args.toString());
|
||||
}
|
||||
}).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* A page can be removed from the ReadingList by panel context menu,
|
||||
* or by tapping the readinglist-remove icon in the ReaderMode banner.
|
||||
*/
|
||||
private void handleRemoveFromList(final String url) {
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
readingListAccessor.removeReadingListItemWithURL(context.getContentResolver(), url);
|
||||
showToast(R.string.reading_list_removed, Toast.LENGTH_SHORT);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Goanna (ReaderMode) requests the page ReadingList status, to display
|
||||
* the proper ReaderMode banner icon (readinglist-add / readinglist-remove).
|
||||
*/
|
||||
private void handleReadingListStatusRequest(final EventCallback callback, final String url) {
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final int inReadingList = readingListAccessor.isReadingListItem(context.getContentResolver(), url) ? 1 : 0;
|
||||
|
||||
final JSONObject json = new JSONObject();
|
||||
try {
|
||||
json.put("url", url);
|
||||
json.put("inReadingList", inReadingList);
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "JSON error - failed to return inReadingList status", e);
|
||||
}
|
||||
|
||||
// Return the json object to fulfill the promise.
|
||||
callback.sendSuccess(json.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show various status toasts.
|
||||
*/
|
||||
private void showToast(final int resId, final int duration) {
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(context, resId, duration).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void fetchContent() {
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final Cursor c = readingListAccessor.getReadingListUnfetched(context.getContentResolver());
|
||||
try {
|
||||
while (c.moveToNext()) {
|
||||
JSONObject json = new JSONObject();
|
||||
try {
|
||||
json.put("id", c.getInt(c.getColumnIndexOrThrow(ReadingListItems._ID)));
|
||||
json.put("url", c.getString(c.getColumnIndexOrThrow(ReadingListItems.URL)));
|
||||
GoannaAppShell.sendEventToGoanna(
|
||||
GoannaEvent.createBroadcastEvent("Reader:FetchContent", json.toString()));
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Failed to fetch reading list content for item");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
/**
|
||||
* Test code will want to disable background fetches to avoid upsetting
|
||||
* the test harness. Call this by accessing the instance from BrowserApp.
|
||||
*/
|
||||
public void disableBackgroundFetches() {
|
||||
fetchInBackground = false;
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.goanna;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.mozilla.goanna.db.RemoteClient;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.AlertDialog.Builder;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.util.SparseBooleanArray;
|
||||
|
||||
/**
|
||||
* A dialog fragment that displays a list of remote clients.
|
||||
* <p>
|
||||
* The dialog allows both single (one tap) and multiple (checkbox) selection.
|
||||
* The dialog's results are communicated via the {@link RemoteClientsListener}
|
||||
* interface. Either the dialog fragment's <i>target fragment</i> (see
|
||||
* {@link Fragment#setTargetFragment(Fragment, int)}), or the containing
|
||||
* <i>activity</i>, must implement that interface. See
|
||||
* {@link #notifyListener(List)} for details.
|
||||
*/
|
||||
public class RemoteClientsDialogFragment extends DialogFragment {
|
||||
private static final String KEY_TITLE = "title";
|
||||
private static final String KEY_CHOICE_MODE = "choice_mode";
|
||||
private static final String KEY_POSITIVE_BUTTON_TEXT = "positive_button_text";
|
||||
private static final String KEY_CLIENTS = "clients";
|
||||
|
||||
public interface RemoteClientsListener {
|
||||
// Always called on the main UI thread.
|
||||
public void onClients(List<RemoteClient> clients);
|
||||
}
|
||||
|
||||
public enum ChoiceMode {
|
||||
SINGLE,
|
||||
MULTIPLE,
|
||||
}
|
||||
|
||||
public static RemoteClientsDialogFragment newInstance(String title, String positiveButtonText, ChoiceMode choiceMode, ArrayList<RemoteClient> clients) {
|
||||
final RemoteClientsDialogFragment dialog = new RemoteClientsDialogFragment();
|
||||
final Bundle args = new Bundle();
|
||||
args.putString(KEY_TITLE, title);
|
||||
args.putString(KEY_POSITIVE_BUTTON_TEXT, positiveButtonText);
|
||||
args.putInt(KEY_CHOICE_MODE, choiceMode.ordinal());
|
||||
args.putParcelableArrayList(KEY_CLIENTS, clients);
|
||||
dialog.setArguments(args);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public RemoteClientsDialogFragment() {
|
||||
// Empty constructor is required for DialogFragment.
|
||||
}
|
||||
|
||||
protected void notifyListener(List<RemoteClient> clients) {
|
||||
RemoteClientsListener listener;
|
||||
try {
|
||||
listener = (RemoteClientsListener) getTargetFragment();
|
||||
} catch (ClassCastException e) {
|
||||
try {
|
||||
listener = (RemoteClientsListener) getActivity();
|
||||
} catch (ClassCastException f) {
|
||||
throw new ClassCastException(getTargetFragment() + " or " + getActivity()
|
||||
+ " must implement RemoteClientsListener");
|
||||
}
|
||||
}
|
||||
listener.onClients(clients);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final String title = getArguments().getString(KEY_TITLE);
|
||||
final String positiveButtonText = getArguments().getString(KEY_POSITIVE_BUTTON_TEXT);
|
||||
final ChoiceMode choiceMode = ChoiceMode.values()[getArguments().getInt(KEY_CHOICE_MODE)];
|
||||
final ArrayList<RemoteClient> clients = getArguments().getParcelableArrayList(KEY_CLIENTS);
|
||||
|
||||
final Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setTitle(title);
|
||||
|
||||
final String[] clientNames = new String[clients.size()];
|
||||
for (int i = 0; i < clients.size(); i++) {
|
||||
clientNames[i] = clients.get(i).name;
|
||||
}
|
||||
|
||||
if (choiceMode == ChoiceMode.MULTIPLE) {
|
||||
builder.setMultiChoiceItems(clientNames, null, null);
|
||||
builder.setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int which) {
|
||||
if (which != Dialog.BUTTON_POSITIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
final AlertDialog dialog = (AlertDialog) dialogInterface;
|
||||
final SparseBooleanArray checkedItemPositions = dialog.getListView().getCheckedItemPositions();
|
||||
final ArrayList<RemoteClient> checked = new ArrayList<RemoteClient>();
|
||||
for (int i = 0; i < clients.size(); i++) {
|
||||
if (checkedItemPositions.get(i)) {
|
||||
checked.add(clients.get(i));
|
||||
}
|
||||
}
|
||||
notifyListener(checked);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
builder.setItems(clientNames, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int index) {
|
||||
final ArrayList<RemoteClient> checked = new ArrayList<RemoteClient>();
|
||||
checked.add(clients.get(index));
|
||||
notifyListener(checked);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user