Commit e2f8cd51 authored by Evan Stade's avatar Evan Stade Committed by Commit Bot

Use PhotoPicker in WebLayer.

DecoderService is split into the image decoding portion, which is
renamed ImageDecoder, and the service portion, which is embedder
specific and provided by [...].photo_picker.DecoderService in Chrome.
In WL, it's provided by ImageDecoderService. ImageDecoderService
proxies from the WL client to implementation library for access to
process setup (sandbox init) and image decoding via ImageDecoder.

Bug: 1110930

Change-Id: Ifae62538ea841f3a1183aea98e10da1062a05841
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2376553
Commit-Queue: Evan Stade <estade@chromium.org>
Reviewed-by: default avatarTed Choc <tedchoc@chromium.org>
Reviewed-by: default avatarScott Violet <sky@chromium.org>
Reviewed-by: default avatarClark DuVall <cduvall@chromium.org>
Cr-Commit-Position: refs/heads/master@{#804563}
parent e56635dd
......@@ -1296,6 +1296,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/payments/ui/SectionUiUtils.java",
"java/src/org/chromium/chrome/browser/payments/ui/ShoppingCart.java",
"java/src/org/chromium/chrome/browser/permissions/PermissionSettingsBridge.java",
"java/src/org/chromium/chrome/browser/photo_picker/DecoderService.java",
"java/src/org/chromium/chrome/browser/policy/EnterpriseInfo.java",
"java/src/org/chromium/chrome/browser/policy/PolicyAuditor.java",
"java/src/org/chromium/chrome/browser/prerender/ChromePrerenderService.java",
......
......@@ -1140,6 +1140,13 @@
android:name="org.chromium.chrome.browser.notifications.NotificationService">
</service> # DIFF-ANCHOR: 3224d309
<service android:exported="false" android:name="org.chromium.chrome.browser.omaha.OmahaClient"/>
<service # DIFF-ANCHOR: 53256720
android:description="@string/decoder_description"
android:exported="false"
android:isolatedProcess="true"
android:name="org.chromium.chrome.browser.photo_picker.DecoderService"
android:process=":decoder_service">
</service> # DIFF-ANCHOR: 53256720
<service # DIFF-ANCHOR: 064aae37
android:exported="true"
android:name="org.chromium.chrome.browser.prerender.ChromePrerenderService"
......@@ -1177,13 +1184,6 @@
android:name="org.chromium.components.background_task_scheduler.internal.BackgroundTaskJobService"
android:permission="android.permission.BIND_JOB_SERVICE">
</service> # DIFF-ANCHOR: a550decc
<service # DIFF-ANCHOR: f6df5542
android:description="@string/decoder_description"
android:exported="false"
android:isolatedProcess="true"
android:name="org.chromium.components.browser_ui.photo_picker.DecoderService"
android:process=":decoder_service">
</service> # DIFF-ANCHOR: f6df5542
<service # DIFF-ANCHOR: 2ce68981
android:exported="true"
android:name="org.chromium.components.payments.PaymentDetailsUpdateService">
......
......@@ -1048,6 +1048,13 @@
android:name="org.chromium.chrome.browser.notifications.NotificationService">
</service> # DIFF-ANCHOR: 3224d309
<service android:exported="false" android:name="org.chromium.chrome.browser.omaha.OmahaClient"/>
<service # DIFF-ANCHOR: 53256720
android:description="@string/decoder_description"
android:exported="false"
android:isolatedProcess="true"
android:name="org.chromium.chrome.browser.photo_picker.DecoderService"
android:process=":decoder_service">
</service> # DIFF-ANCHOR: 53256720
<service # DIFF-ANCHOR: 064aae37
android:exported="true"
android:name="org.chromium.chrome.browser.prerender.ChromePrerenderService"
......@@ -1085,13 +1092,6 @@
android:name="org.chromium.components.background_task_scheduler.internal.BackgroundTaskJobService"
android:permission="android.permission.BIND_JOB_SERVICE">
</service> # DIFF-ANCHOR: a550decc
<service # DIFF-ANCHOR: f6df5542
android:description="@string/decoder_description"
android:exported="false"
android:isolatedProcess="true"
android:name="org.chromium.components.browser_ui.photo_picker.DecoderService"
android:process=":decoder_service">
</service> # DIFF-ANCHOR: f6df5542
<service # DIFF-ANCHOR: 2ce68981
android:exported="true"
android:name="org.chromium.components.payments.PaymentDetailsUpdateService">
......
......@@ -872,7 +872,7 @@ by a child template that "extends" this file.
<!-- Service for decoding images in a sandboxed process. -->
<service
android:description="@string/decoder_description"
android:name="org.chromium.components.browser_ui.photo_picker.DecoderService"
android:name="org.chromium.chrome.browser.photo_picker.DecoderService"
android:exported="false"
android:isolatedProcess="true"
android:process=":decoder_service" />
......
......@@ -6,6 +6,7 @@ package org.chromium.chrome.browser.init;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.SystemClock;
import android.text.format.DateUtils;
......@@ -63,6 +64,7 @@ import org.chromium.chrome.browser.net.spdyproxy.DataReductionProxySettings;
import org.chromium.chrome.browser.notifications.channels.ChannelsUpdater;
import org.chromium.chrome.browser.ntp.NewTabPage;
import org.chromium.chrome.browser.partnercustomizations.PartnerBrowserCustomizations;
import org.chromium.chrome.browser.photo_picker.DecoderService;
import org.chromium.chrome.browser.policy.EnterpriseInfo;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
......@@ -77,6 +79,7 @@ import org.chromium.chrome.browser.webapps.WebApkVersionManager;
import org.chromium.chrome.browser.webapps.WebappRegistry;
import org.chromium.components.background_task_scheduler.BackgroundTaskSchedulerFactory;
import org.chromium.components.browser_ui.contacts_picker.ContactsPickerDialog;
import org.chromium.components.browser_ui.photo_picker.DecoderServiceHost;
import org.chromium.components.browser_ui.photo_picker.PhotoPickerDialog;
import org.chromium.components.browser_ui.share.ShareImageFileUtils;
import org.chromium.components.browser_ui.util.ConversionUtils;
......@@ -215,6 +218,10 @@ public class ProcessInitializationHandler {
Clipboard.getInstance().setImageFileProvider(new ClipboardImageFileProvider());
if (ChromeFeatureList.isEnabled(ChromeFeatureList.NEW_PHOTO_PICKER)) {
DecoderServiceHost.setIntentSupplier(() -> {
return new Intent(ContextUtils.getApplicationContext(), DecoderService.class);
});
SelectFileDialog.setPhotoPickerDelegate(new PhotoPickerDelegate() {
@Override
public PhotoPicker showPhotoPicker(WindowAndroid windowAndroid,
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.photo_picker;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import org.chromium.base.CommandLine;
import org.chromium.base.Log;
import org.chromium.base.PathUtils;
import org.chromium.base.annotations.MainDex;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.task.PostTask;
import org.chromium.chrome.browser.ChromeApplication;
import org.chromium.components.browser_ui.photo_picker.ImageDecoder;
import org.chromium.content_public.browser.UiThreadTaskTraits;
/**
* A service to accept requests to take image file contents and decode them.
*/
@MainDex
public class DecoderService extends Service {
private static final String TAG = "DecoderService";
private final ImageDecoder mDecoder = new ImageDecoder();
@Override
public void onCreate() {
Log.i(TAG, "Decoder service process started");
// DecoderService does not require flags, but LibraryLoader.ensureInitialized() checks for
// --enable-low-end-device-mode. Rather than forwarding the flags from the browser process,
// just assume no flags.
if (!CommandLine.isInitialized()) {
CommandLine.init(null);
}
// The decoder service relies on PathUtils.
PostTask.runSynchronously(UiThreadTaskTraits.DEFAULT, () -> {
PathUtils.setPrivateDataDirectorySuffix(
ChromeApplication.PRIVATE_DATA_DIRECTORY_SUFFIX);
});
LibraryLoader.getInstance().ensureInitialized();
mDecoder.initializeSandbox();
Log.i(TAG, "Decoder service process initialized");
}
@Override
public IBinder onBind(Intent intent) {
Log.i(TAG, "Decoder process binding");
return mDecoder;
}
}
file://components/browser_ui/photo_picker/android/OWNERS
......@@ -56,6 +56,12 @@
<activity android:name="org.chromium.ui.test.util.DummyUiActivity"
android:theme="@style/Theme.Chromium.Activity"
android:exported="true"/>
<service
android:name="org.chromium.components.browser_ui.photo_picker.TestImageDecoderService"
android:exported="false"
android:isolatedProcess="true"
android:process=":decoder_service" />
{% endblock %}
{% block extra_application_attributes %}
......
......@@ -19,9 +19,9 @@ android_library("java") {
"java/src/org/chromium/components/browser_ui/photo_picker/BitmapScalerTask.java",
"java/src/org/chromium/components/browser_ui/photo_picker/BitmapUtils.java",
"java/src/org/chromium/components/browser_ui/photo_picker/DecodeVideoTask.java",
"java/src/org/chromium/components/browser_ui/photo_picker/DecoderService.java",
"java/src/org/chromium/components/browser_ui/photo_picker/DecoderServiceHost.java",
"java/src/org/chromium/components/browser_ui/photo_picker/FileEnumWorkerTask.java",
"java/src/org/chromium/components/browser_ui/photo_picker/ImageDecoder.java",
"java/src/org/chromium/components/browser_ui/photo_picker/PhotoPickerDialog.java",
"java/src/org/chromium/components/browser_ui/photo_picker/PhotoPickerToolbar.java",
"java/src/org/chromium/components/browser_ui/photo_picker/PickerAdapter.java",
......@@ -53,7 +53,7 @@ android_library("java") {
}
generate_jni("photo_picker_jni_headers") {
sources = [ "java/src/org/chromium/components/browser_ui/photo_picker/DecoderService.java" ]
sources = [ "java/src/org/chromium/components/browser_ui/photo_picker/ImageDecoder.java" ]
}
android_aidl("photo_picker_aidl") {
......@@ -128,8 +128,9 @@ android_library("javatests") {
sources = [
"java/src/org/chromium/components/browser_ui/photo_picker/DecoderServiceHostTest.java",
"java/src/org/chromium/components/browser_ui/photo_picker/DecoderServiceTest.java",
"java/src/org/chromium/components/browser_ui/photo_picker/ImageDecoderTest.java",
"java/src/org/chromium/components/browser_ui/photo_picker/PhotoPickerDialogTest.java",
"java/src/org/chromium/components/browser_ui/photo_picker/TestImageDecoderService.java",
]
deps = [
":java",
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.browser_ui.photo_picker;
import android.app.Service;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Pair;
import org.chromium.base.CommandLine;
import org.chromium.base.Log;
import org.chromium.base.PathUtils;
import org.chromium.base.annotations.MainDex;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.task.PostTask;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import java.io.FileDescriptor;
import java.io.IOException;
/**
* A service to accept requests to take image file contents and decode them.
*/
@MainDex
public class DecoderService extends Service {
// The keys for the bundle when passing data to and from this service.
static final String KEY_FILE_DESCRIPTOR = "file_descriptor";
static final String KEY_FILE_PATH = "file_path";
static final String KEY_IMAGE_BITMAP = "image_bitmap";
static final String KEY_WIDTH = "width";
static final String KEY_RATIO = "ratio";
static final String KEY_FULL_WIDTH = "full_width";
static final String KEY_SUCCESS = "success";
static final String KEY_DECODE_TIME = "decode_time";
// A tag for logging error messages.
private static final String TAG = "ImageDecoder";
// Whether the native library and the sandbox have been initialized.
private boolean mNativeLibraryAndSandboxInitialized;
@Override
public void onCreate() {
Log.i(TAG, "Decoder service process started");
// DecoderService does not require flags, but LibraryLoader.ensureInitialized() checks for
// --enable-low-end-device-mode. Rather than forwarding the flags from the browser process,
// just assume no flags.
if (!CommandLine.isInitialized()) {
CommandLine.init(null);
}
// The decoder service relies on PathUtils.
PostTask.runSynchronously(UiThreadTaskTraits.DEFAULT, () -> {
// TODO(estade): get this suffix from the embedder.
PathUtils.setPrivateDataDirectorySuffix("chrome");
});
LibraryLoader.getInstance().ensureInitialized();
DecoderServiceJni.get().initializePhotoPickerSandbox();
mNativeLibraryAndSandboxInitialized = true;
super.onCreate();
Log.i(TAG, "Decoder service process initialized");
}
@Override
public IBinder onBind(Intent intent) {
Log.i(TAG, "Decoder process binding");
return mBinder;
}
private final IDecoderService.Stub mBinder = new IDecoderService.Stub() {
@Override
public void decodeImage(Bundle payload, IDecoderServiceCallback callback) {
Bundle bundle = null;
String filePath = "";
int width = 0;
boolean fullWidth = false;
try {
filePath = payload.getString(KEY_FILE_PATH);
ParcelFileDescriptor pfd = payload.getParcelable(KEY_FILE_DESCRIPTOR);
width = payload.getInt(KEY_WIDTH);
fullWidth = payload.getBoolean(KEY_FULL_WIDTH);
// Setup a minimum viable response to parent process. Will be fleshed out
// further below.
bundle = new Bundle();
bundle.putString(KEY_FILE_PATH, filePath);
bundle.putBoolean(KEY_SUCCESS, false);
if (!mNativeLibraryAndSandboxInitialized) {
Log.e(TAG, "Decode failed %s (width: %d): no sandbox", filePath, width);
sendReply(callback, bundle); // Sends SUCCESS == false;
return;
}
FileDescriptor fd = pfd.getFileDescriptor();
long begin = SystemClock.elapsedRealtime();
Pair<Bitmap, Float> decodedBitmap =
BitmapUtils.decodeBitmapFromFileDescriptor(fd, width, fullWidth);
long decodeTime = SystemClock.elapsedRealtime() - begin;
try {
pfd.close();
} catch (IOException e) {
Log.e(TAG, "Closing failed " + filePath + " (width: " + width + ") " + e);
}
Bitmap bitmap = decodedBitmap != null ? decodedBitmap.first : null;
if (bitmap == null) {
Log.e(TAG, "Decode failed " + filePath + " (width: " + width + ")");
sendReply(callback, bundle); // Sends SUCCESS == false;
return;
}
// The most widely supported, easiest, and reasonably efficient method is to
// decode to an immutable bitmap and just return the bitmap over binder. It
// will internally memcpy itself to ashmem and then just send over the file
// descriptor. In the receiving process it will just leave the bitmap on
// ashmem since it's immutable and carry on.
bundle.putParcelable(KEY_IMAGE_BITMAP, bitmap);
bundle.putFloat(KEY_RATIO, decodedBitmap.second);
bundle.putBoolean(KEY_SUCCESS, true);
bundle.putLong(KEY_DECODE_TIME, decodeTime);
bundle.putBoolean(KEY_FULL_WIDTH, payload.getBoolean(KEY_FULL_WIDTH));
sendReply(callback, bundle);
bitmap.recycle();
} catch (Exception e) {
// This service has no UI and maintains no state so if it crashes on
// decoding a photo, it is better UX to eat the exception instead of showing
// a crash dialog and discarding other requests that have already been sent.
Log.e(TAG,
"Unexpected error during decoding " + filePath + " (width: " + width + ") "
+ e);
if (bundle != null) sendReply(callback, bundle);
}
}
private void sendReply(IDecoderServiceCallback callback, Bundle bundle) {
try {
callback.onDecodeImageDone(bundle);
} catch (RemoteException remoteException) {
Log.e(TAG, "Remote error while replying: " + remoteException);
}
}
};
@NativeMethods
interface Natives {
// Initializes the seccomp-bpf sandbox when it's supported by the device. Records the
// sandbox status to the Android.SeccompStatus.PhotoPickerSandbox histogram.
void initializePhotoPickerSandbox();
}
}
......@@ -19,6 +19,7 @@ import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
......@@ -26,6 +27,7 @@ import org.chromium.base.Log;
import org.chromium.base.StrictModeContext;
import org.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.task.AsyncTask;
import org.chromium.base.task.PostTask;
import org.chromium.components.browser_ui.util.ConversionUtils;
......@@ -83,6 +85,19 @@ public class DecoderServiceHost
// A callback to use for testing to see if decoder is ready.
static DecoderStatusCallback sStatusCallbackForTesting;
// Used to create intents for launching the {@link DecoderService} service.
private static Supplier<Intent> sIntentSupplier;
/**
* Sets a factory for creating intents that launch the {@link DecoderService} service.
* This must be called prior to using the PhotoPicker.
* @param intentSupplier a factory that creates a new Intent. Will be called every time the
* PhotoPicker is launched.
*/
public static void setIntentSupplier(@NonNull Supplier<Intent> intentSupplier) {
sIntentSupplier = intentSupplier;
}
IDecoderService mIRemoteService;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
......@@ -226,7 +241,7 @@ public class DecoderServiceHost
* @param context The context to use.
*/
public void bind(Context context) {
Intent intent = new Intent(mContext, DecoderService.class);
Intent intent = sIntentSupplier.get();
intent.setAction(IDecoderService.class.getName());
mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
......@@ -398,14 +413,14 @@ public class DecoderServiceHost
long decodeTime = -1;
try {
// Read the reply back from the service.
filePath = payload.getString(DecoderService.KEY_FILE_PATH);
Boolean success = payload.getBoolean(DecoderService.KEY_SUCCESS);
filePath = payload.getString(ImageDecoder.KEY_FILE_PATH);
Boolean success = payload.getBoolean(ImageDecoder.KEY_SUCCESS);
Bitmap bitmap = success
? (Bitmap) payload.getParcelable(DecoderService.KEY_IMAGE_BITMAP)
? (Bitmap) payload.getParcelable(ImageDecoder.KEY_IMAGE_BITMAP)
: null;
ratio = payload.getFloat(DecoderService.KEY_RATIO);
decodeTime = payload.getLong(DecoderService.KEY_DECODE_TIME);
fullWidth = payload.getBoolean(DecoderService.KEY_FULL_WIDTH);
ratio = payload.getFloat(ImageDecoder.KEY_RATIO);
decodeTime = payload.getLong(ImageDecoder.KEY_DECODE_TIME);
fullWidth = payload.getBoolean(ImageDecoder.KEY_FULL_WIDTH);
mSuccessfulImageDecodes++;
bitmaps = new ArrayList<>(1);
bitmaps.add(bitmap);
......@@ -541,10 +556,10 @@ public class DecoderServiceHost
}
// Prepare and send the data over.
bundle.putString(DecoderService.KEY_FILE_PATH, params.mUri.getPath());
bundle.putParcelable(DecoderService.KEY_FILE_DESCRIPTOR, pfd);
bundle.putInt(DecoderService.KEY_WIDTH, params.mWidth);
bundle.putBoolean(DecoderService.KEY_FULL_WIDTH, params.mFullWidth);
bundle.putString(ImageDecoder.KEY_FILE_PATH, params.mUri.getPath());
bundle.putParcelable(ImageDecoder.KEY_FILE_DESCRIPTOR, pfd);
bundle.putInt(ImageDecoder.KEY_WIDTH, params.mWidth);
bundle.putBoolean(ImageDecoder.KEY_FULL_WIDTH, params.mFullWidth);
try {
mIRemoteService.decodeImage(bundle, this);
pfd.close();
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.browser_ui.photo_picker;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Pair;
import org.chromium.base.Log;
import org.chromium.base.annotations.NativeMethods;
import java.io.FileDescriptor;
import java.io.IOException;
/**
* A helper to accept requests to take image file contents and decode them.
* As this is intended to be run in a separate, sandboxed process, it also requires calling code to
* initialize the sandbox.
*/
public class ImageDecoder extends IDecoderService.Stub {
// The keys for the bundle when passing data to and from this service.
public static final String KEY_FILE_DESCRIPTOR = "file_descriptor";
public static final String KEY_FILE_PATH = "file_path";
public static final String KEY_IMAGE_BITMAP = "image_bitmap";
public static final String KEY_WIDTH = "width";
public static final String KEY_RATIO = "ratio";
public static final String KEY_FULL_WIDTH = "full_width";
public static final String KEY_SUCCESS = "success";
public static final String KEY_DECODE_TIME = "decode_time";
// A tag for logging error messages.
private static final String TAG = "ImageDecoder";
// Whether the native library and the sandbox have been initialized.
private boolean mSandboxInitialized;
/**
* Initializes the seccomp-bpf sandbox when it's supported by the device. Records the
* sandbox status to the Android.SeccompStatus.PhotoPickerSandbox histogram.
*/
public void initializeSandbox() {
ImageDecoderJni.get().initializePhotoPickerSandbox();
mSandboxInitialized = true;
}
@Override
public void decodeImage(Bundle payload, IDecoderServiceCallback callback) {
Bundle bundle = null;
String filePath = "";
int width = 0;
boolean fullWidth = false;
try {
filePath = payload.getString(KEY_FILE_PATH);
ParcelFileDescriptor pfd = payload.getParcelable(KEY_FILE_DESCRIPTOR);
width = payload.getInt(KEY_WIDTH);
fullWidth = payload.getBoolean(KEY_FULL_WIDTH);
// Setup a minimum viable response to parent process. Will be fleshed out
// further below.
bundle = new Bundle();
bundle.putString(KEY_FILE_PATH, filePath);
bundle.putBoolean(KEY_SUCCESS, false);
if (!mSandboxInitialized) {
Log.e(TAG, "Decode failed " + filePath + " (width: " + width + "): no sandbox");
sendReply(callback, bundle); // Sends SUCCESS == false;
return;
}
FileDescriptor fd = pfd.getFileDescriptor();
long begin = SystemClock.elapsedRealtime();
Pair<Bitmap, Float> decodedBitmap =
BitmapUtils.decodeBitmapFromFileDescriptor(fd, width, fullWidth);
long decodeTime = SystemClock.elapsedRealtime() - begin;
try {
pfd.close();
} catch (IOException e) {
Log.e(TAG, "Closing failed " + filePath + " (width: " + width + ") " + e);
}
Bitmap bitmap = decodedBitmap != null ? decodedBitmap.first : null;
if (bitmap == null) {
Log.e(TAG, "Decode failed " + filePath + " (width: " + width + ")");
sendReply(callback, bundle); // Sends SUCCESS == false;
return;
}
// The most widely supported, easiest, and reasonably efficient method is to
// decode to an immutable bitmap and just return the bitmap over binder. It
// will internally memcpy itself to ashmem and then just send over the file
// descriptor. In the receiving process it will just leave the bitmap on
// ashmem since it's immutable and carry on.
bundle.putParcelable(KEY_IMAGE_BITMAP, bitmap);
bundle.putFloat(KEY_RATIO, decodedBitmap.second);
bundle.putBoolean(KEY_SUCCESS, true);
bundle.putLong(KEY_DECODE_TIME, decodeTime);
bundle.putBoolean(KEY_FULL_WIDTH, payload.getBoolean(KEY_FULL_WIDTH));
sendReply(callback, bundle);
bitmap.recycle();
} catch (Exception e) {
// This service has no UI and maintains no state so if it crashes on
// decoding a photo, it is better UX to eat the exception instead of showing
// a crash dialog and discarding other requests that have already been sent.
Log.e(TAG,
"Unexpected error during decoding " + filePath + " (width: " + width + ") "
+ e);
if (bundle != null) sendReply(callback, bundle);
}
}
private void sendReply(IDecoderServiceCallback callback, Bundle bundle) {
try {
callback.onDecodeImageDone(bundle);
} catch (RemoteException remoteException) {
Log.e(TAG, "Remote error while replying: " + remoteException);
}
}
@NativeMethods
interface Natives {
void initializePhotoPickerSandbox();
}
}
......@@ -30,10 +30,10 @@ import java.io.FileDescriptor;
import java.io.FileInputStream;
/**
* Tests for the out-of-process DecoderService.
* Tests for ImageDecoder and the aidl interfaces used for out-of-process decoding..
*/
@RunWith(BaseJUnit4ClassRunner.class)
public class DecoderServiceTest {
public class ImageDecoderTest {
// By default, the test will wait for 3 seconds to create the decoder process, which (at least
// in the emulators) brushes up against the actual time it takes to create the process, so these
// tests are frequently flaky when run locally.
......@@ -83,7 +83,7 @@ public class DecoderServiceTest {
}
private void startDecoderService() {
Intent intent = new Intent(mContext, DecoderService.class);
Intent intent = new Intent(mContext, TestImageDecoderService.class);
intent.setAction(IDecoderService.class.getName());
mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
......@@ -95,14 +95,14 @@ public class DecoderServiceTest {
private void decode(String filePath, FileDescriptor fd, int width,
final DecoderServiceCallback callback) throws Exception {
Bundle bundle = new Bundle();
bundle.putString(DecoderService.KEY_FILE_PATH, filePath);
bundle.putString(ImageDecoder.KEY_FILE_PATH, filePath);
ParcelFileDescriptor pfd = null;
if (fd != null) {
pfd = ParcelFileDescriptor.dup(fd);
Assert.assertTrue(pfd != null);
}
bundle.putParcelable(DecoderService.KEY_FILE_DESCRIPTOR, pfd);
bundle.putInt(DecoderService.KEY_WIDTH, width);
bundle.putParcelable(ImageDecoder.KEY_FILE_DESCRIPTOR, pfd);
bundle.putInt(ImageDecoder.KEY_WIDTH, width);
mIRemoteService.decodeImage(bundle, callback);
CriteriaHelper.pollUiThread(() -> callback.resolved());
......@@ -118,11 +118,10 @@ public class DecoderServiceTest {
decode("path", null, 50, callback);
Bundle bundle = callback.getBundle();
Assert.assertFalse(
"Expected decode to fail", bundle.getBoolean(DecoderService.KEY_SUCCESS));
Assert.assertEquals("path", bundle.getString(DecoderService.KEY_FILE_PATH));
Assert.assertEquals(null, bundle.getParcelable(DecoderService.KEY_IMAGE_BITMAP));
Assert.assertEquals(0, bundle.getLong(DecoderService.KEY_DECODE_TIME));
Assert.assertFalse("Expected decode to fail", bundle.getBoolean(ImageDecoder.KEY_SUCCESS));
Assert.assertEquals("path", bundle.getString(ImageDecoder.KEY_FILE_PATH));
Assert.assertEquals(null, bundle.getParcelable(ImageDecoder.KEY_IMAGE_BITMAP));
Assert.assertEquals(0, bundle.getLong(ImageDecoder.KEY_DECODE_TIME));
}
@Test
......@@ -140,12 +139,12 @@ public class DecoderServiceTest {
Bundle bundle = callback.getBundle();
Assert.assertTrue(
"Expecting success being returned", bundle.getBoolean(DecoderService.KEY_SUCCESS));
Assert.assertEquals(file.getPath(), bundle.getString(DecoderService.KEY_FILE_PATH));
"Expecting success being returned", bundle.getBoolean(ImageDecoder.KEY_SUCCESS));
Assert.assertEquals(file.getPath(), bundle.getString(ImageDecoder.KEY_FILE_PATH));
Assert.assertFalse("Decoding should take a non-zero amount of time",
0 == bundle.getLong(DecoderService.KEY_DECODE_TIME));
0 == bundle.getLong(ImageDecoder.KEY_DECODE_TIME));
Bitmap decodedBitmap = bundle.getParcelable(DecoderService.KEY_IMAGE_BITMAP);
Bitmap decodedBitmap = bundle.getParcelable(ImageDecoder.KEY_IMAGE_BITMAP);
Assert.assertFalse("Decoded bitmap should not be null", null == decodedBitmap);
Assert.assertEquals(50, decodedBitmap.getWidth());
Assert.assertEquals(50, decodedBitmap.getHeight());
......
......@@ -5,6 +5,7 @@
package org.chromium.components.browser_ui.photo_picker;
import android.content.ContentResolver;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.StrictMode;
......@@ -128,6 +129,10 @@ public class PhotoPickerDialogTest extends DummyUiActivityTestCase
public void setUp() throws Exception {
mWindowAndroid = TestThreadUtils.runOnUiThreadBlocking(
() -> { return new ActivityWindowAndroid(getActivity()); });
TestThreadUtils.runOnUiThreadBlocking(() -> {
DecoderServiceHost.setIntentSupplier(
() -> { return new Intent(getActivity(), TestImageDecoderService.class); });
});
PickerVideoPlayer.setProgressCallback(this);
PickerBitmapView.setAnimationListenerForTest(this);
DecoderServiceHost.setStatusCallback(this);
......
......@@ -4,7 +4,6 @@
package org.chromium.components.browser_ui.photo_picker;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
......@@ -20,6 +19,7 @@ import android.util.DisplayMetrics;
import android.util.LruCache;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RelativeLayout;
......@@ -88,7 +88,7 @@ public class PickerCategoryView extends RelativeLayout
// The view containing the RecyclerView and the toolbar, etc.
private SelectableListLayout<PickerBitmap> mSelectableListLayout;
// The {@link WindowAndroid} for the {@link Activity}.
// The {@link WindowAndroid} for the hosting WebContents.
private WindowAndroid mWindowAndroid;
// The ContentResolver to use to retrieve image metadata from disk.
......@@ -188,7 +188,8 @@ public class PickerCategoryView extends RelativeLayout
private ImageView mZoom;
/**
* @param windowAndroid The window of the hosting {@link Activity}.
* @param windowAndroid The window of the {@link WebContents} that requested the photo
* selection.
* @param contentResolver The ContentResolver to use to retrieve image metadata from disk.
* @param multiSelectionAllowed Whether to allow the user to select more than one image.
*/
......@@ -528,15 +529,17 @@ public class PickerCategoryView extends RelativeLayout
*/
private void calculateGridMetrics() {
DisplayMetrics displayMetrics = new DisplayMetrics();
Activity activity = (Activity) mWindowAndroid.getContext().get();
activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
Context context = mWindowAndroid.getContext().get();
WindowManager windowManager =
(WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
int width = displayMetrics.widthPixels;
int minSize =
activity.getResources().getDimensionPixelSize(R.dimen.photo_picker_tile_min_size);
context.getResources().getDimensionPixelSize(R.dimen.photo_picker_tile_min_size);
mPadding = mMagnifyingMode
? 0
: activity.getResources().getDimensionPixelSize(R.dimen.photo_picker_tile_gap);
: context.getResources().getDimensionPixelSize(R.dimen.photo_picker_tile_gap);
mColumns = mMagnifyingMode ? 1 : Math.max(1, (width - mPadding) / (minSize + mPadding));
mImageWidth = (width - mPadding * (mColumns + 1)) / (mColumns);
mImageHeight = mMagnifyingMode
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.browser_ui.photo_picker;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import org.chromium.base.annotations.MainDex;
import org.chromium.base.library_loader.LibraryLoader;
/**
* A service to accept requests to take image file contents and decode them, used for tests.
*/
@MainDex
public class TestImageDecoderService extends Service {
private final ImageDecoder mDecoder = new ImageDecoder();
@Override
public void onCreate() {
LibraryLoader.getInstance().ensureInitialized();
mDecoder.initializeSandbox();
}
@Override
public IBinder onBind(Intent intent) {
return mDecoder;
}
}
......@@ -6,7 +6,7 @@
#include "base/android/jni_android.h"
#include "base/android/scoped_java_ref.h"
#include "base/metrics/histogram_macros.h"
#include "components/browser_ui/photo_picker/android/photo_picker_jni_headers/DecoderService_jni.h"
#include "components/browser_ui/photo_picker/android/photo_picker_jni_headers/ImageDecoder_jni.h"
#include "sandbox/linux/seccomp-bpf-helpers/seccomp_starter_android.h"
#include "sandbox/sandbox_buildflags.h"
......@@ -14,7 +14,7 @@
#include "sandbox/linux/seccomp-bpf-helpers/baseline_policy_android.h"
#endif
void JNI_DecoderService_InitializePhotoPickerSandbox(JNIEnv* env) {
void JNI_ImageDecoder_InitializePhotoPickerSandbox(JNIEnv* env) {
auto* info = base::android::BuildInfo::GetInstance();
sandbox::SeccompStarterAndroid starter(info->sdk_int(), info->device());
......
......@@ -574,6 +574,7 @@ source_set("weblayer_lib_base") {
"//components/android_system_error_page",
"//components/autofill/android/provider",
"//components/browser_ui/client_certificate/android",
"//components/browser_ui/photo_picker/android",
"//components/browser_ui/site_settings/android",
"//components/browser_ui/sms/android",
"//components/cdm/browser",
......@@ -698,6 +699,7 @@ if (is_android) {
# find the native side functions.
if (is_component_build) {
deps += [
"//components/browser_ui/photo_picker/android",
"//device/gamepad",
"//media/midi",
"//ui/events/devices",
......
......@@ -162,6 +162,7 @@ android_library("java") {
"//components/browser_ui/media/android:java",
"//components/browser_ui/modaldialog/android:java",
"//components/browser_ui/notifications/android:java",
"//components/browser_ui/photo_picker/android:java",
"//components/browser_ui/settings/android:java",
"//components/browser_ui/share/android:java",
"//components/browser_ui/site_settings/android:java",
......
......@@ -16,6 +16,7 @@ import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.AndroidRuntimeException;
......@@ -42,6 +43,9 @@ import org.chromium.base.annotations.NativeMethods;
import org.chromium.base.compat.ApiHelperForO;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.components.browser_ui.photo_picker.DecoderServiceHost;
import org.chromium.components.browser_ui.photo_picker.ImageDecoder;
import org.chromium.components.browser_ui.photo_picker.PhotoPickerDialog;
import org.chromium.components.embedder_support.application.ClassLoaderContextWrapperFactory;
import org.chromium.components.embedder_support.application.FirebaseConfig;
import org.chromium.components.embedder_support.util.Origin;
......@@ -50,7 +54,12 @@ import org.chromium.content_public.browser.ChildProcessCreationParams;
import org.chromium.content_public.browser.DeviceUtils;
import org.chromium.content_public.browser.SelectionPopupController;
import org.chromium.net.NetworkChangeNotifier;
import org.chromium.ui.base.PhotoPicker;
import org.chromium.ui.base.PhotoPickerDelegate;
import org.chromium.ui.base.PhotoPickerListener;
import org.chromium.ui.base.ResourceBundle;
import org.chromium.ui.base.SelectFileDialog;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.weblayer_private.interfaces.APICallException;
import org.chromium.weblayer_private.interfaces.IBrowserFragment;
import org.chromium.weblayer_private.interfaces.ICrashReporterController;
......@@ -255,6 +264,24 @@ public final class WebLayerImpl extends IWebLayer.Stub {
MediaStreamManager.onWebLayerInit();
WebLayerNotificationChannels.updateChannelsIfNecessary();
DecoderServiceHost.setIntentSupplier(() -> { return createImageDecoderServiceIntent(); });
SelectFileDialog.setPhotoPickerDelegate(new PhotoPickerDelegate() {
@Override
public PhotoPicker showPhotoPicker(WindowAndroid windowAndroid,
PhotoPickerListener listener, boolean allowMultiple, List<String> mimeTypes) {
PhotoPickerDialog dialog = new PhotoPickerDialog(windowAndroid,
windowAndroid.getContext().get().getContentResolver(), listener,
allowMultiple, mimeTypes);
dialog.show();
return dialog;
}
@Override
public boolean supportsVideos() {
return false;
}
});
}
@Override
......@@ -300,7 +327,7 @@ public final class WebLayerImpl extends IWebLayer.Stub {
IObjectWrapper appContext, IObjectWrapper remoteContext) {
StrictModeWorkaround.apply();
// This is a no-op if init has already happened.
WebLayerImpl.minimalInitForContext(ObjectWrapper.unwrap(appContext, Context.class),
minimalInitForContext(ObjectWrapper.unwrap(appContext, Context.class),
processRemoteContext(ObjectWrapper.unwrap(remoteContext, Context.class)));
return CrashReporterControllerImpl.getInstance();
}
......@@ -332,6 +359,23 @@ public final class WebLayerImpl extends IWebLayer.Stub {
MediaSessionManager.serviceDestroyed();
}
@Override
public IBinder initializeImageDecoder(IObjectWrapper appContext, IObjectWrapper remoteContext) {
StrictModeWorkaround.apply();
assert ContextUtils.getApplicationContext() == null;
CommandLine.init(null);
minimalInitForContext(ObjectWrapper.unwrap(appContext, Context.class),
processRemoteContext(ObjectWrapper.unwrap(remoteContext, Context.class)));
LibraryLoader.getInstance().setLibraryProcessType(
LibraryProcessType.PROCESS_WEBLAYER_CHILD);
LibraryLoader.getInstance().ensureInitialized();
ImageDecoder imageDecoder = new ImageDecoder();
imageDecoder.initializeSandbox();
return imageDecoder;
}
@Override
public void enumerateAllProfileNames(IObjectWrapper valueCallback) {
StrictModeWorkaround.apply();
......@@ -382,6 +426,18 @@ public final class WebLayerImpl extends IWebLayer.Stub {
}
}
public static Intent createImageDecoderServiceIntent() {
if (sClient == null) {
throw new IllegalStateException("WebLayer should have been initialized already.");
}
try {
return sClient.createImageDecoderServiceIntent();
} catch (RemoteException e) {
throw new APICallException(e);
}
}
public static int getMediaSessionNotificationId() {
if (sClient == null) {
throw new IllegalStateException("WebLayer should have been initialized already.");
......
......@@ -94,4 +94,8 @@ interface IWebLayer {
// Added in Version 85.
void onMediaSessionServiceStarted(in IObjectWrapper sessionService, in Intent intent) = 17;
void onMediaSessionServiceDestroyed() = 18;
// Added in Version 86.
IBinder initializeImageDecoder(in IObjectWrapper appContext,
in IObjectWrapper remoteContext) = 19;
}
......@@ -10,4 +10,7 @@ interface IWebLayerClient {
Intent createIntent() = 0;
Intent createMediaSessionServiceIntent() = 1;
int getMediaSessionNotificationId() = 2;
// Since Version 86.
Intent createImageDecoderServiceIntent() = 3;
}
......@@ -83,5 +83,12 @@
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
<!-- Service for decoding images in a sandboxed process. -->
<service
android:name="org.chromium.weblayer.ImageDecoderService"
android:exported="false"
android:isolatedProcess="true"
android:process=":decoder_service" />
</application>
</manifest>
......@@ -63,6 +63,7 @@ android_library("java") {
"org/chromium/weblayer/GoogleAccountServiceType.java",
"org/chromium/weblayer/GoogleAccountsCallback.java",
"org/chromium/weblayer/GoogleAccountsParams.java",
"org/chromium/weblayer/ImageDecoderService.java",
"org/chromium/weblayer/LoadError.java",
"org/chromium/weblayer/MediaCaptureCallback.java",
"org/chromium/weblayer/MediaCaptureController.java",
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.weblayer;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import org.chromium.weblayer_private.interfaces.APICallException;
import org.chromium.weblayer_private.interfaces.ObjectWrapper;
/**
* A service used internally by WebLayer for decoding images on the local device.
* @since 87
*/
public class ImageDecoderService extends Service {
private IBinder mImageDecoder;
@Override
public void onCreate() {
try {
mImageDecoder =
WebLayer.getIWebLayer(this).initializeImageDecoder(ObjectWrapper.wrap(this),
ObjectWrapper.wrap(WebLayer.getOrCreateRemoteContext(this)));
} catch (Exception e) {
throw new APICallException(e);
}
}
@Override
public IBinder onBind(Intent intent) {
return mImageDecoder;
}
}
......@@ -690,6 +690,12 @@ public class WebLayer {
return new Intent(WebLayer.getAppContext(), MediaSessionService.class);
}
@Override
public Intent createImageDecoderServiceIntent() {
StrictModeWorkaround.apply();
return new Intent(WebLayer.getAppContext(), ImageDecoderService.class);
}
@Override
public int getMediaSessionNotificationId() {
StrictModeWorkaround.apply();
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment