forked from mirrors/gecko-dev
MozReview-Commit-ID: BEQmkKajTxF --HG-- extra : rebase_source : 294aa4baaa6234517bfa8cdb6e5fdf9414e504f3
875 lines
36 KiB
Java
875 lines
36 KiB
Java
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
package org.mozilla.gecko;
|
|
|
|
import android.Manifest;
|
|
import android.app.Activity;
|
|
import android.app.Application;
|
|
import android.content.ContentResolver;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.res.Configuration;
|
|
import android.graphics.Bitmap;
|
|
import android.net.Uri;
|
|
import android.os.Bundle;
|
|
import android.os.Environment;
|
|
import android.os.Process;
|
|
import android.os.SystemClock;
|
|
import android.provider.MediaStore;
|
|
import android.support.annotation.NonNull;
|
|
import android.support.annotation.Nullable;
|
|
import android.support.design.widget.Snackbar;
|
|
import android.support.multidex.MultiDex;
|
|
import android.text.TextUtils;
|
|
import android.util.Base64;
|
|
import android.util.Log;
|
|
|
|
import com.squareup.leakcanary.LeakCanary;
|
|
import com.squareup.leakcanary.RefWatcher;
|
|
|
|
import org.mozilla.gecko.annotation.WrapForJNI;
|
|
import org.mozilla.gecko.db.BrowserContract;
|
|
import org.mozilla.gecko.db.BrowserDB;
|
|
import org.mozilla.gecko.db.LocalBrowserDB;
|
|
import org.mozilla.gecko.distribution.Distribution;
|
|
import org.mozilla.gecko.home.HomePanelsManager;
|
|
import org.mozilla.gecko.icons.IconCallback;
|
|
import org.mozilla.gecko.icons.IconResponse;
|
|
import org.mozilla.gecko.icons.Icons;
|
|
import org.mozilla.gecko.lwt.LightweightTheme;
|
|
import org.mozilla.gecko.mdns.MulticastDNSManager;
|
|
import org.mozilla.gecko.media.AudioFocusAgent;
|
|
import org.mozilla.gecko.mozglue.SafeIntent;
|
|
import org.mozilla.gecko.notifications.NotificationClient;
|
|
import org.mozilla.gecko.notifications.NotificationHelper;
|
|
import org.mozilla.gecko.permissions.Permissions;
|
|
import org.mozilla.gecko.preferences.DistroSharedPrefsImport;
|
|
import org.mozilla.gecko.pwa.PwaUtils;
|
|
import org.mozilla.gecko.telemetry.TelemetryBackgroundReceiver;
|
|
import org.mozilla.gecko.util.ActivityResultHandler;
|
|
import org.mozilla.gecko.util.BitmapUtils;
|
|
import org.mozilla.gecko.util.BundleEventListener;
|
|
import org.mozilla.gecko.util.EventCallback;
|
|
import org.mozilla.gecko.util.GeckoBundle;
|
|
import org.mozilla.gecko.util.HardwareUtils;
|
|
import org.mozilla.gecko.util.PRNGFixes;
|
|
import org.mozilla.gecko.util.ShortcutUtils;
|
|
import org.mozilla.gecko.util.ThreadUtils;
|
|
import org.mozilla.geckoview.GeckoRuntime;
|
|
import org.mozilla.geckoview.GeckoRuntimeSettings;
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.File;
|
|
import java.io.InputStream;
|
|
import java.io.IOException;
|
|
import java.lang.reflect.Method;
|
|
import java.net.URL;
|
|
import java.util.UUID;
|
|
|
|
public class GeckoApplication extends Application
|
|
implements HapticFeedbackDelegate {
|
|
private static final String LOG_TAG = "GeckoApplication";
|
|
public static final String ACTION_DEBUG = "org.mozilla.gecko.DEBUG";
|
|
private static final String MEDIA_DECODING_PROCESS_CRASH = "MEDIA_DECODING_PROCESS_CRASH";
|
|
|
|
private boolean mInBackground;
|
|
private boolean mPausedGecko;
|
|
private boolean mIsInitialResume;
|
|
|
|
private LightweightTheme mLightweightTheme;
|
|
|
|
private RefWatcher mRefWatcher;
|
|
|
|
private final EventListener mListener = new EventListener();
|
|
|
|
private static String sSessionUUID = null;
|
|
|
|
public GeckoApplication() {
|
|
super();
|
|
}
|
|
|
|
public static RefWatcher getRefWatcher(Context context) {
|
|
GeckoApplication app = (GeckoApplication) context.getApplicationContext();
|
|
return app.mRefWatcher;
|
|
}
|
|
|
|
public static void watchReference(Context context, Object object) {
|
|
if (context == null) {
|
|
return;
|
|
}
|
|
|
|
getRefWatcher(context).watch(object);
|
|
}
|
|
|
|
/**
|
|
* @return The string representation of an UUID that changes on each application startup.
|
|
*/
|
|
public static String getSessionUUID() {
|
|
return sSessionUUID;
|
|
}
|
|
|
|
public static @Nullable String[] getDefaultGeckoArgs() {
|
|
if (!AppConstants.MOZILLA_OFFICIAL) {
|
|
// In un-official builds, we want to load Javascript resources fresh
|
|
// with each build. In official builds, the startup cache is purged by
|
|
// the buildid mechanism, but most un-official builds don't bump the
|
|
// buildid, so we purge here instead.
|
|
Log.w(LOG_TAG, "STARTUP PERFORMANCE WARNING: un-official build: purging the " +
|
|
"startup (JavaScript) caches.");
|
|
return new String[] { "-purgecaches" };
|
|
}
|
|
return new String[0];
|
|
}
|
|
|
|
public static String getDefaultUAString() {
|
|
return HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET :
|
|
AppConstants.USER_AGENT_FENNEC_MOBILE;
|
|
}
|
|
|
|
public static void shutdown(final Intent restartIntent) {
|
|
ThreadUtils.assertOnUiThread();
|
|
|
|
// Wait for Gecko to handle any pause events.
|
|
if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
|
|
GeckoThread.waitOnGecko();
|
|
}
|
|
|
|
if (restartIntent == null) {
|
|
// Exiting, so kill our own process.
|
|
Process.killProcess(Process.myPid());
|
|
return;
|
|
}
|
|
|
|
// Restarting, so let Restarter kill us.
|
|
final Context context = GeckoAppShell.getApplicationContext();
|
|
final Intent intent = new Intent();
|
|
intent.setClass(context, Restarter.class)
|
|
.putExtra("pid", Process.myPid())
|
|
.putExtra(Intent.EXTRA_INTENT, restartIntent);
|
|
context.startService(intent);
|
|
}
|
|
|
|
/**
|
|
* 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 GeckoApp
|
|
// doesn't get a chance to.
|
|
try {
|
|
BrowserLocaleManager.getInstance().correctLocale(this, getResources(), config);
|
|
} catch (IllegalStateException ex) {
|
|
// GeckoApp hasn't started, so we have no ContextGetter in BrowserLocaleManager.
|
|
Log.w(LOG_TAG, "Couldn't correct locale.", ex);
|
|
}
|
|
|
|
super.onConfigurationChanged(config);
|
|
}
|
|
|
|
public void onApplicationBackground() {
|
|
mInBackground = true;
|
|
|
|
// Notify Gecko 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.
|
|
EventDispatcher.getInstance().dispatch("Session:FlushTabs", null);
|
|
GeckoThread.onPause();
|
|
mPausedGecko = true;
|
|
|
|
final BrowserDB db = BrowserDB.from(this);
|
|
ThreadUtils.postToBackgroundThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
db.expireHistory(getContentResolver(), BrowserContract.ExpirePriority.NORMAL);
|
|
}
|
|
});
|
|
|
|
GeckoNetworkManager.getInstance().stop();
|
|
}
|
|
|
|
public void onApplicationForeground() {
|
|
if (mIsInitialResume) {
|
|
GeckoBatteryManager.getInstance().start(this);
|
|
GeckoFontScaleListener.getInstance().initialize(this);
|
|
GeckoNetworkManager.getInstance().start(this);
|
|
mIsInitialResume = false;
|
|
} else if (mPausedGecko) {
|
|
GeckoThread.onResume();
|
|
mPausedGecko = false;
|
|
GeckoNetworkManager.getInstance().start(this);
|
|
}
|
|
|
|
mInBackground = false;
|
|
}
|
|
|
|
private static GeckoRuntime sGeckoRuntime;
|
|
public static GeckoRuntime getRuntime() {
|
|
return sGeckoRuntime;
|
|
}
|
|
|
|
public static GeckoRuntime ensureRuntime(@NonNull Context context) {
|
|
if (sGeckoRuntime != null) {
|
|
return sGeckoRuntime;
|
|
}
|
|
|
|
return createRuntime(context, null);
|
|
}
|
|
|
|
private static GeckoRuntimeSettings.Builder createSettingsBuilder() {
|
|
return new GeckoRuntimeSettings.Builder()
|
|
.javaCrashReportingEnabled(true)
|
|
.nativeCrashReportingEnabled(true)
|
|
.arguments(getDefaultGeckoArgs());
|
|
}
|
|
|
|
public static GeckoRuntime createRuntime(@NonNull Context context,
|
|
@Nullable SafeIntent intent) {
|
|
if (sGeckoRuntime != null) {
|
|
throw new IllegalStateException("Already have a GeckoRuntime!");
|
|
}
|
|
|
|
if (context == null) {
|
|
throw new IllegalArgumentException("Context must not be null");
|
|
}
|
|
|
|
GeckoRuntimeSettings.Builder builder = createSettingsBuilder();
|
|
if (intent != null) {
|
|
builder.pauseForDebugger(ACTION_DEBUG.equals(intent.getAction()));
|
|
|
|
Bundle extras = intent.getExtras();
|
|
if (extras != null) {
|
|
builder.extras(extras);
|
|
}
|
|
}
|
|
|
|
sGeckoRuntime = GeckoRuntime.create(context, builder.build());
|
|
return sGeckoRuntime;
|
|
}
|
|
|
|
@Override
|
|
public void onCreate() {
|
|
Log.i(LOG_TAG, "zerdatime " + SystemClock.elapsedRealtime() +
|
|
" - application start");
|
|
|
|
final Context oldContext = GeckoAppShell.getApplicationContext();
|
|
if (oldContext instanceof GeckoApplication) {
|
|
((GeckoApplication) oldContext).onDestroy();
|
|
}
|
|
|
|
final Context context = getApplicationContext();
|
|
GeckoAppShell.ensureCrashHandling();
|
|
GeckoAppShell.setApplicationContext(context);
|
|
|
|
// PRNG is a pseudorandom number generator.
|
|
// We need to apply PRNG Fixes before any use of Java Cryptography Architecture.
|
|
// We make use of various JCA methods in data providers for generating GUIDs, as part of FxA
|
|
// flow and during syncing. Note that this is a no-op for devices running API>18, and so we
|
|
// accept the performance penalty on older devices.
|
|
try {
|
|
PRNGFixes.apply();
|
|
} catch (Exception e) {
|
|
// Not much to be done here: it was weak before, so it's weak now. Not worth aborting.
|
|
Log.e(LOG_TAG, "Got exception applying PRNGFixes! Cryptographic data produced on this device may be weak. Ignoring.", e);
|
|
}
|
|
|
|
mIsInitialResume = true;
|
|
|
|
mRefWatcher = LeakCanary.install(this);
|
|
|
|
sSessionUUID = UUID.randomUUID().toString();
|
|
|
|
GeckoActivityMonitor.getInstance().initialize(this);
|
|
MemoryMonitor.getInstance().init(this);
|
|
|
|
GeckoAppShell.setHapticFeedbackDelegate(this);
|
|
GeckoAppShell.setGeckoInterface(new GeckoAppShell.GeckoInterface() {
|
|
@Override
|
|
public boolean openUriExternal(final String targetURI, final String mimeType,
|
|
final String packageName, final String className,
|
|
final String action, final String title) {
|
|
// Default to showing prompt in private browsing to be safe.
|
|
return IntentHelper.openUriExternal(targetURI, mimeType, packageName,
|
|
className, action, title, true);
|
|
}
|
|
|
|
@Override
|
|
public String[] getHandlersForMimeType(final String mimeType,
|
|
final String action) {
|
|
final Intent intent = IntentHelper.getIntentForActionString(action);
|
|
if (mimeType != null && mimeType.length() > 0) {
|
|
intent.setType(mimeType);
|
|
}
|
|
return IntentHelper.getHandlersForIntent(intent);
|
|
}
|
|
|
|
@Override
|
|
public String[] getHandlersForURL(final String url, final String action) {
|
|
// May contain the whole URL or just the protocol.
|
|
final Uri uri = url.indexOf(':') >= 0 ? Uri.parse(url)
|
|
: new Uri.Builder().scheme(url).build();
|
|
final Intent intent = IntentHelper.getOpenURIIntent(
|
|
getApplicationContext(), uri.toString(), "",
|
|
TextUtils.isEmpty(action) ? Intent.ACTION_VIEW : action, "");
|
|
return IntentHelper.getHandlersForIntent(intent);
|
|
}
|
|
});
|
|
|
|
HardwareUtils.init(context);
|
|
FilePicker.init(context);
|
|
DownloadsIntegration.init();
|
|
HomePanelsManager.getInstance().init(context);
|
|
|
|
GlobalPageMetadata.getInstance().init();
|
|
|
|
TelemetryBackgroundReceiver.getInstance().init(context);
|
|
|
|
// We need to set the notification client before launching Gecko, since Gecko could start
|
|
// sending notifications immediately after startup, which we don't want to lose/crash on.
|
|
GeckoAppShell.setNotificationListener(new NotificationClient(context));
|
|
// This getInstance call will force initialization of the NotificationHelper, but does nothing with the result
|
|
NotificationHelper.getInstance(context).init();
|
|
|
|
MulticastDNSManager.getInstance(context).init();
|
|
|
|
GeckoService.register();
|
|
|
|
IntentHelper.init();
|
|
|
|
EventDispatcher.getInstance().registerGeckoThreadListener(mListener,
|
|
"Distribution:GetDirectories",
|
|
null);
|
|
EventDispatcher.getInstance().registerUiThreadListener(mListener,
|
|
"Gecko:Exited",
|
|
"RuntimePermissions:Check",
|
|
"Snackbar:Show",
|
|
"Share:Text",
|
|
null);
|
|
EventDispatcher.getInstance().registerBackgroundThreadListener(mListener,
|
|
"PushServiceAndroidGCM:Configure",
|
|
"Bookmark:Insert",
|
|
"Image:SetAs",
|
|
"Profile:Create",
|
|
null);
|
|
|
|
super.onCreate();
|
|
}
|
|
|
|
@Override
|
|
protected void attachBaseContext(Context base) {
|
|
super.attachBaseContext(base);
|
|
|
|
// Enable multiDex only for local builds.
|
|
final boolean isLocalBuild = BuildConfig.FLAVOR_audience.equals("local");
|
|
|
|
// API >= 21 natively supports loading multiple DEX files from APK files.
|
|
// Needs just 'multiDexEnabled true' inside the gradle build configuration.
|
|
final boolean isMultidexLibNeeded = BuildConfig.FLAVOR_minApi.equals("noMinApi");
|
|
|
|
if (isLocalBuild && isMultidexLibNeeded) {
|
|
MultiDex.install(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* May be called when a new GeckoApplication object
|
|
* replaces an old one due to assets change.
|
|
*/
|
|
private void onDestroy() {
|
|
EventDispatcher.getInstance().unregisterGeckoThreadListener(mListener,
|
|
"Distribution:GetDirectories",
|
|
null);
|
|
EventDispatcher.getInstance().unregisterUiThreadListener(mListener,
|
|
"Gecko:Exited",
|
|
"RuntimePermissions:Check",
|
|
"Snackbar:Show",
|
|
"Share:Text",
|
|
null);
|
|
EventDispatcher.getInstance().unregisterBackgroundThreadListener(mListener,
|
|
"PushServiceAndroidGCM:Configure",
|
|
"Bookmark:Insert",
|
|
"Image:SetAs",
|
|
"Profile:Create",
|
|
null);
|
|
|
|
GeckoService.unregister();
|
|
}
|
|
|
|
/* package */ boolean initPushService() {
|
|
// It's fine to throw GCM initialization onto a background thread; the registration process requires
|
|
// network access, so is naturally asynchronous. This, of course, races against Gecko page load of
|
|
// content requiring GCM-backed services, like Web Push. There's nothing to be done here.
|
|
try {
|
|
final Class<?> clazz = Class.forName("org.mozilla.gecko.push.PushService");
|
|
final Method onCreate = clazz.getMethod("onCreate", Context.class);
|
|
return (Boolean) onCreate.invoke(null, getApplicationContext()); // Method is static.
|
|
} catch (Exception e) {
|
|
Log.e(LOG_TAG, "Got exception during startup; ignoring.", e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public void onDelayedStartup() {
|
|
if (AppConstants.MOZ_ANDROID_GCM) {
|
|
// TODO: only run in main process.
|
|
ThreadUtils.postToBackgroundThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
initPushService();
|
|
}
|
|
});
|
|
}
|
|
|
|
AudioFocusAgent.getInstance().attachToContext(this);
|
|
}
|
|
|
|
private class EventListener implements BundleEventListener
|
|
{
|
|
private void onProfileCreate(final String name, final String path) {
|
|
// Add everything when we're done loading the distribution.
|
|
final Context context = GeckoApplication.this;
|
|
final GeckoProfile profile = GeckoProfile.get(context, name);
|
|
final Distribution distribution = Distribution.getInstance(context);
|
|
|
|
distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
|
|
@Override
|
|
public void distributionNotFound() {
|
|
this.distributionFound(null);
|
|
}
|
|
|
|
@Override
|
|
public void distributionFound(final Distribution distribution) {
|
|
Log.d(LOG_TAG, "Running post-distribution task: bookmarks.");
|
|
// Because we are running in the background, we want to synchronize on the
|
|
// GeckoProfile instance so that we don't race with main thread operations
|
|
// such as locking/unlocking/removing the profile.
|
|
synchronized (profile.getLock()) {
|
|
distributionFoundLocked(distribution);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void distributionArrivedLate(final Distribution distribution) {
|
|
Log.d(LOG_TAG, "Running late distribution task: bookmarks.");
|
|
// Recover as best we can.
|
|
synchronized (profile.getLock()) {
|
|
distributionArrivedLateLocked(distribution);
|
|
}
|
|
}
|
|
|
|
private void distributionFoundLocked(final Distribution distribution) {
|
|
// Skip initialization if the profile directory has been removed.
|
|
if (!(new File(path)).exists()) {
|
|
return;
|
|
}
|
|
|
|
final ContentResolver cr = context.getContentResolver();
|
|
final LocalBrowserDB db = new LocalBrowserDB(profile.getName());
|
|
|
|
// 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 int offset = distribution == null ? 0 :
|
|
db.addDistributionBookmarks(cr, distribution, 0);
|
|
db.addDefaultBookmarks(context, cr, offset);
|
|
|
|
Log.d(LOG_TAG, "Running post-distribution task: android preferences.");
|
|
DistroSharedPrefsImport.importPreferences(context, distribution);
|
|
}
|
|
|
|
private void distributionArrivedLateLocked(final Distribution distribution) {
|
|
// Skip initialization if the profile directory has been removed.
|
|
if (!(new File(path)).exists()) {
|
|
return;
|
|
}
|
|
|
|
final ContentResolver cr = context.getContentResolver();
|
|
final LocalBrowserDB db = new LocalBrowserDB(profile.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 int offset = db.getCount(cr, "bookmarks");
|
|
db.addDistributionBookmarks(cr, distribution, offset);
|
|
|
|
Log.d(LOG_TAG, "Running late distribution task: android preferences.");
|
|
DistroSharedPrefsImport.importPreferences(context, distribution);
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override // BundleEventListener
|
|
public void handleMessage(final String event, final GeckoBundle message,
|
|
final EventCallback callback) {
|
|
if ("Profile:Create".equals(event)) {
|
|
onProfileCreate(message.getString("name"),
|
|
message.getString("path"));
|
|
|
|
} else if ("Gecko:Exited".equals(event)) {
|
|
// Gecko thread exited first; shutdown the application.
|
|
final Intent restartIntent;
|
|
if (message.getBoolean("restart")) {
|
|
restartIntent = new Intent(Intent.ACTION_MAIN);
|
|
restartIntent.setClassName(GeckoAppShell.getApplicationContext(),
|
|
AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
|
|
} else {
|
|
restartIntent = null;
|
|
}
|
|
shutdown(restartIntent);
|
|
|
|
} else if ("RuntimePermissions:Check".equals(event)) {
|
|
final String[] permissions = message.getStringArray("permissions");
|
|
final boolean shouldPrompt = message.getBoolean("shouldPrompt", false);
|
|
final Activity currentActivity =
|
|
GeckoActivityMonitor.getInstance().getCurrentActivity();
|
|
final Context context = (currentActivity != null) ?
|
|
currentActivity : GeckoAppShell.getApplicationContext();
|
|
|
|
Permissions.from(context)
|
|
.withPermissions(permissions)
|
|
.doNotPromptIf(!shouldPrompt || currentActivity == null)
|
|
.andFallback(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
callback.sendSuccess(false);
|
|
}
|
|
})
|
|
.run(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
callback.sendSuccess(true);
|
|
}
|
|
});
|
|
|
|
} else if ("Share:Text".equals(event)) {
|
|
final String text = message.getString("text");
|
|
final String title = message.getString("title", "");
|
|
IntentHelper.openUriExternal(text, "text/plain", "", "",
|
|
Intent.ACTION_SEND, title, false);
|
|
|
|
// Context: Sharing via chrome list (no explicit session is active)
|
|
Telemetry.sendUIEvent(TelemetryContract.Event.SHARE,
|
|
TelemetryContract.Method.LIST, "text");
|
|
|
|
} else if ("Snackbar:Show".equals(event)) {
|
|
final Activity currentActivity =
|
|
GeckoActivityMonitor.getInstance().getCurrentActivity();
|
|
if (currentActivity == null) {
|
|
if (callback != null) {
|
|
callback.sendError("No activity");
|
|
}
|
|
return;
|
|
}
|
|
SnackbarBuilder.builder(currentActivity)
|
|
.fromEvent(message)
|
|
.callback(callback)
|
|
.buildAndShow();
|
|
|
|
} else if ("Distribution:GetDirectories".equals(event)) {
|
|
callback.sendSuccess(Distribution.getDistributionDirectories());
|
|
|
|
} else if ("Bookmark:Insert".equals(event)) {
|
|
final Context context = GeckoAppShell.getApplicationContext();
|
|
final BrowserDB db = BrowserDB.from(GeckoThread.getActiveProfile());
|
|
final boolean bookmarkAdded = db.addBookmark(context.getContentResolver(),
|
|
message.getString("title"),
|
|
message.getString("url"));
|
|
final int resId = bookmarkAdded ? R.string.bookmark_added
|
|
: R.string.bookmark_already_added;
|
|
ThreadUtils.postToUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
final Activity currentActivity =
|
|
GeckoActivityMonitor.getInstance().getCurrentActivity();
|
|
if (currentActivity == null) {
|
|
return;
|
|
}
|
|
SnackbarBuilder.builder(currentActivity)
|
|
.message(resId)
|
|
.duration(Snackbar.LENGTH_LONG)
|
|
.buildAndShow();
|
|
}
|
|
});
|
|
|
|
} else if ("Image:SetAs".equals(event)) {
|
|
setImageAs(message.getString("url"));
|
|
|
|
} else if ("PushServiceAndroidGCM:Configure".equals(event)) {
|
|
// Init push service and redirect the event to it.
|
|
if (initPushService()) {
|
|
EventDispatcher.getInstance().dispatch(event, message, callback);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public boolean isApplicationInBackground() {
|
|
return mInBackground;
|
|
}
|
|
|
|
public LightweightTheme getLightweightTheme() {
|
|
return mLightweightTheme;
|
|
}
|
|
|
|
public void prepareLightweightTheme() {
|
|
mLightweightTheme = new LightweightTheme(this);
|
|
}
|
|
|
|
public static void createShortcut() {
|
|
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
|
|
if (selectedTab != null) {
|
|
createShortcut(selectedTab.getTitle(), selectedTab.getURL());
|
|
}
|
|
}
|
|
|
|
// Creates a homescreen shortcut for a web page.
|
|
// This is the entry point from nsIShellService.
|
|
@WrapForJNI(calledFrom = "gecko")
|
|
public static void createShortcut(final String title, final String url) {
|
|
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
|
|
final String manifestUrl = selectedTab.getManifestUrl();
|
|
|
|
if (manifestUrl != null) {
|
|
// If a page has associated manifest, lets install it (PWA A2HS)
|
|
// At this time, this page must be a secure page.
|
|
// Please hide PWA badge UI in front end side.
|
|
final boolean safeForPwa = PwaUtils.shouldAddPwaShortcut(selectedTab);
|
|
if (!safeForPwa) {
|
|
final String message = "This page is not safe for PWA";
|
|
// For release and beta, we record an error message
|
|
if (AppConstants.RELEASE_OR_BETA) {
|
|
Log.e(LOG_TAG, message);
|
|
} else {
|
|
// For nightly and local build, we'll throw an exception here.
|
|
throw new IllegalStateException(message);
|
|
}
|
|
|
|
}
|
|
|
|
final GeckoBundle message = new GeckoBundle();
|
|
message.putInt("iconSize", GeckoAppShell.getPreferredIconSize());
|
|
message.putString("manifestUrl", manifestUrl);
|
|
message.putString("originalUrl", url);
|
|
message.putString("originalTitle", title);
|
|
EventDispatcher.getInstance().dispatch("Browser:LoadManifest", message);
|
|
return;
|
|
}
|
|
|
|
createBrowserShortcut(title, url);
|
|
}
|
|
|
|
public static void createBrowserShortcut(final String title, final String url) {
|
|
Icons.with(GeckoAppShell.getApplicationContext())
|
|
.pageUrl(url)
|
|
.skipNetwork()
|
|
.skipMemory()
|
|
.forLauncherIcon()
|
|
.build()
|
|
.execute(new IconCallback() {
|
|
@Override
|
|
public void onIconResponse(final IconResponse response) {
|
|
createShortcutWithIcon(title, url, response.getBitmap());
|
|
}
|
|
});
|
|
}
|
|
|
|
/* package */ static void createShortcutWithIcon(final String aTitle, final String aURI,
|
|
final Bitmap aIcon) {
|
|
final Intent shortcutIntent = new Intent();
|
|
shortcutIntent.setAction(GeckoApp.ACTION_HOMESCREEN_SHORTCUT);
|
|
shortcutIntent.setData(Uri.parse(aURI));
|
|
shortcutIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
|
|
AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
|
|
|
|
ShortcutUtils.createHomescreenIcon(shortcutIntent, aTitle, aURI, aIcon);
|
|
}
|
|
|
|
public static void createAppShortcut(final String aTitle, final String aURI,
|
|
final String manifestPath, final String manifestUrl,
|
|
final Bitmap aIcon) {
|
|
final Intent shortcutIntent = new Intent();
|
|
shortcutIntent.setAction(GeckoApp.ACTION_WEBAPP);
|
|
shortcutIntent.setData(Uri.parse(aURI));
|
|
shortcutIntent.putExtra("MANIFEST_PATH", manifestPath);
|
|
shortcutIntent.putExtra("MANIFEST_URL", manifestUrl);
|
|
shortcutIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
|
|
LauncherActivity.class.getName());
|
|
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION,
|
|
TelemetryContract.Method.CONTEXT_MENU,
|
|
"pwa_add_to_launcher");
|
|
ShortcutUtils.createHomescreenIcon(shortcutIntent, aTitle, aURI, aIcon);
|
|
}
|
|
|
|
/* package */ static void showSetImageResult(final boolean success, final int message,
|
|
final String path) {
|
|
ThreadUtils.postToUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
final Activity currentActivity =
|
|
GeckoActivityMonitor.getInstance().getCurrentActivity();
|
|
if (currentActivity == null) {
|
|
return;
|
|
}
|
|
|
|
if (!success) {
|
|
SnackbarBuilder.builder(currentActivity)
|
|
.message(message)
|
|
.duration(Snackbar.LENGTH_LONG)
|
|
.buildAndShow();
|
|
return;
|
|
}
|
|
|
|
final Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
|
|
intent.addCategory(Intent.CATEGORY_DEFAULT);
|
|
intent.setData(Uri.parse(path));
|
|
|
|
// Removes the image from storage once the chooser activity ends.
|
|
final Context context = GeckoAppShell.getApplicationContext();
|
|
final Intent chooser = Intent.createChooser(intent,
|
|
context.getString(message));
|
|
ActivityResultHandler handler = new ActivityResultHandler() {
|
|
@Override
|
|
public void onActivityResult(int resultCode, Intent data) {
|
|
context.getContentResolver().delete(intent.getData(), null, null);
|
|
}
|
|
};
|
|
ActivityHandlerHelper.startIntentForActivity(currentActivity, chooser,
|
|
handler);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Checks the necessary permissions before attempting to download and
|
|
// set the image as wallpaper.
|
|
private static void setImageAs(final String aSrc) {
|
|
final Activity currentActivity =
|
|
GeckoActivityMonitor.getInstance().getCurrentActivity();
|
|
final Context context = (currentActivity != null) ?
|
|
currentActivity : GeckoAppShell.getApplicationContext();
|
|
Permissions
|
|
.from(context)
|
|
.doNotPromptIf(currentActivity == null)
|
|
.onBackgroundThread()
|
|
.withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
|
.andFallback(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
showSetImageResult(/* success */ false,
|
|
R.string.set_image_path_fail, null);
|
|
}
|
|
})
|
|
.run(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
downloadImageForSetImage(aSrc);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Downloads the image given by <code>aSrc</code> synchronously and
|
|
* then displays the Chooser activity to set the image as wallpaper.
|
|
*
|
|
* @param aSrc The URI to download the image from.
|
|
*/
|
|
private static void downloadImageForSetImage(final String aSrc) {
|
|
// Network access from the main thread can cause a StrictMode crash on release builds.
|
|
ThreadUtils.assertOnBackgroundThread();
|
|
|
|
final boolean isDataURI = aSrc.startsWith("data:");
|
|
Bitmap image = null;
|
|
InputStream is = null;
|
|
ByteArrayOutputStream os = null;
|
|
try {
|
|
if (isDataURI) {
|
|
int dataStart = aSrc.indexOf(",");
|
|
byte[] buf = Base64.decode(aSrc.substring(dataStart + 1), Base64.DEFAULT);
|
|
image = BitmapUtils.decodeByteArray(buf);
|
|
} else {
|
|
int byteRead;
|
|
byte[] buf = new byte[4192];
|
|
os = new ByteArrayOutputStream();
|
|
URL url = new URL(aSrc);
|
|
is = url.openStream();
|
|
|
|
// Cannot read from same stream twice. Also, InputStream from
|
|
// URL does not support reset. So converting to byte array.
|
|
|
|
while ((byteRead = is.read(buf)) != -1) {
|
|
os.write(buf, 0, byteRead);
|
|
}
|
|
byte[] imgBuffer = os.toByteArray();
|
|
image = BitmapUtils.decodeByteArray(imgBuffer);
|
|
}
|
|
|
|
if (image != null) {
|
|
// Some devices don't have a DCIM folder and the
|
|
// Media.insertImage call will fail.
|
|
final File dcimDir = Environment.getExternalStoragePublicDirectory(
|
|
Environment.DIRECTORY_PICTURES);
|
|
if (!dcimDir.mkdirs() && !dcimDir.isDirectory()) {
|
|
showSetImageResult(/* success */ false,
|
|
R.string.set_image_path_fail, null);
|
|
return;
|
|
}
|
|
|
|
final Context context = GeckoAppShell.getApplicationContext();
|
|
final String path = MediaStore.Images.Media.insertImage(
|
|
context.getContentResolver(), image, null, null);
|
|
if (path == null) {
|
|
showSetImageResult(/* success */ false,
|
|
R.string.set_image_path_fail, null);
|
|
return;
|
|
}
|
|
showSetImageResult(/* success */ true,
|
|
R.string.set_image_chooser_title, path);
|
|
} else {
|
|
showSetImageResult(/* success */ false, R.string.set_image_fail, null);
|
|
}
|
|
} catch (final OutOfMemoryError ome) {
|
|
Log.e(LOG_TAG, "Out of Memory when converting to byte array", ome);
|
|
} catch (final IOException ioe) {
|
|
Log.e(LOG_TAG, "I/O Exception while setting wallpaper", ioe);
|
|
} finally {
|
|
if (is != null) {
|
|
try {
|
|
is.close();
|
|
} catch (final IOException ioe) {
|
|
Log.w(LOG_TAG, "I/O Exception while closing stream", ioe);
|
|
}
|
|
}
|
|
if (os != null) {
|
|
try {
|
|
os.close();
|
|
} catch (final IOException ioe) {
|
|
Log.w(LOG_TAG, "I/O Exception while closing stream", ioe);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override // HapticFeedbackDelegate
|
|
public void performHapticFeedback(final int effect) {
|
|
final Activity currentActivity =
|
|
GeckoActivityMonitor.getInstance().getCurrentActivity();
|
|
if (currentActivity != null) {
|
|
currentActivity.getWindow().getDecorView().performHapticFeedback(effect);
|
|
}
|
|
}
|
|
}
|