import changes from rmottola/Arctic-Fox:

- remove mobile-android (cf8ef1e27)
- remove also android examples (94f68c0e5)
- remove android mozglue (d0114f339)
This commit is contained in:
2020-01-11 10:02:37 +08:00
parent 3e6694dfa6
commit ec05a9b22f
3765 changed files with 0 additions and 407037 deletions
-1
View File
@@ -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',
]
-51
View File
@@ -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)
-47
View File
@@ -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,
-988
View File
@@ -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>
-3
View File
@@ -6,9 +6,6 @@
DIRS += ['components', 'browser']
if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android':
DIRS += ['android/goannaview_example']
TEST_DIRS += ['test']
if CONFIG['ENABLE_TESTS']:
-373
View File
@@ -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.
-11
View File
@@ -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)
-21
View File
@@ -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

-819
View File
@@ -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%&currentAppVersion=%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%&currentAppVersion=%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);
-19
View File
@@ -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',
]
-592
View File
@@ -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);
}
}
-107
View File
@@ -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);
}
}
-127
View File
@@ -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;
}
}
-125
View File
@@ -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;
}
}
-463
View File
@@ -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>
-296
View File
@@ -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));
}
}
-84
View File
@@ -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;
}
}
-499
View File
@@ -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
-15
View File
@@ -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();
}
-468
View File
@@ -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;
}
};
}
}
-88
View File
@@ -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);
}
}
}
-349
View File
@@ -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;
}
}
}
}
-167
View File
@@ -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;
}
}
}
-248
View File
@@ -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();
}
}
-292
View File
@@ -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);
}
}
}
}
-228
View File
@@ -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);
}
}
}
}
-259
View File
@@ -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);
}
});
}
});
}
}
-447
View File
@@ -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;
}
}
}
-159
View File
@@ -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;
}
}
}
-100
View File
@@ -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
-168
View File
@@ -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);
}
-850
View File
@@ -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;
}
}
-25
View File
@@ -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
-218
View File
@@ -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());
}
}
-888
View File
@@ -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;
}
}
}
-263
View File
@@ -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
-209
View File
@@ -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);
}
}
}
}
-695
View File
@@ -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);
}
}
-81
View File
@@ -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) {}
}
-70
View File
@@ -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);
}
}
-76
View File
@@ -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);
}
}
-138
View File
@@ -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);
}
}
}
-196
View File
@@ -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&lt;String, Handler.Callback&gt;. 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();
}
-42
View File
@@ -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();
}
-116
View File
@@ -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);
}
}
-484
View File
@@ -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)
-120
View File
@@ -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();
}
}
});
}
}
-224
View File
@@ -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);
}
}
-254
View File
@@ -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);
}
-55
View File
@@ -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);
}
}
-17
View File
@@ -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();
}
}
-212
View File
@@ -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);
}
}
}
-377
View File
@@ -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;
}
}
}
-254
View File
@@ -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);
}
}
-196
View File
@@ -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;
}
}
}
-29
View File
@@ -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;
}
}
-48
View File
@@ -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;
}
}
-303
View File
@@ -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