fune/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
Petru Lingurar edc9580e01 Bug 1468487 - Enable multidex for local builds; r=nalexander
MozReview-Commit-ID: BEQmkKajTxF

--HG--
extra : rebase_source : 294aa4baaa6234517bfa8cdb6e5fdf9414e504f3
2018-06-15 13:25:00 +03:00

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