Commit 83c9f3f4 authored by finnur's avatar finnur Committed by Commit bot

Photo Picker Dialog: Use sandboxed utility process for decoding images.

BUG=656015

Review-Url: https://codereview.chromium.org/2816733002
Cr-Commit-Position: refs/heads/master@{#468209}
parent cf2d176c
...@@ -632,6 +632,14 @@ by a child template that "extends" this file. ...@@ -632,6 +632,14 @@ by a child template that "extends" this file.
android:exported="false"> android:exported="false">
</service> </service>
<!-- Service for decoding images in a sandboxed process. -->
<service
android:description="@string/decoder_description"
android:name="org.chromium.chrome.browser.photo_picker.DecoderService"
android:exported="false"
android:isolatedProcess="true"
android:process=":decoder_service" />
<!-- Providers for chrome data. --> <!-- Providers for chrome data. -->
<provider android:name="org.chromium.chrome.browser.provider.ChromeBrowserProvider" <provider android:name="org.chromium.chrome.browser.provider.ChromeBrowserProvider"
android:authorities="{{ manifest_package }}.ChromeBrowserProvider;{{ manifest_package }}.browser;{{ manifest_package }}" android:authorities="{{ manifest_package }}.ChromeBrowserProvider;{{ manifest_package }}.browser;{{ manifest_package }}"
......
// 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.chrome.browser.photo_picker;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import java.io.FileDescriptor;
/**
* A collection of utility functions for dealing with bitmaps.
*/
class BitmapUtils {
/**
* Takes a |bitmap| and returns a square thumbnail of |size|x|size| from the center of the
* bitmap specified.
* @param bitmap The bitmap to adjust.
* @param size The desired size (width and height).
* @return The new bitmap thumbnail.
*/
private static Bitmap sizeBitmap(Bitmap bitmap, int size) {
// TODO(finnur): Investigate options that require fewer bitmaps to be created.
bitmap = ensureMinSize(bitmap, size);
bitmap = cropToSquare(bitmap, size);
return bitmap;
}
/**
* Given a FileDescriptor, decodes the contents and returns a bitmap of
* dimensions |size|x|size|.
* @param descriptor The FileDescriptor for the file to read.
* @param size The width and height of the bitmap to return.
* @return The resulting bitmap.
*/
public static Bitmap decodeBitmapFromFileDescriptor(FileDescriptor descriptor, int size) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(descriptor, null, options);
options.inSampleSize = calculateInSampleSize(options.outWidth, options.outHeight, size);
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(descriptor, null, options);
if (bitmap == null) return null;
return sizeBitmap(bitmap, size);
}
/**
* Calculates the sub-sampling factor {@link BitmapFactory#inSampleSize} option for a given
* image dimensions, which will be used to create a bitmap of a pre-determined size (as small as
* possible without either dimension shrinking below |minSize|.
* @param width The calculated width of the image to decode.
* @param height The calculated height of the image to decode.
* @param minSize The maximum size the image should be (in either dimension).
* @return The sub-sampling factor (power of two: 1 = no change, 2 = half-size, etc).
*/
private static int calculateInSampleSize(int width, int height, int minSize) {
int inSampleSize = 1;
if (width > minSize && height > minSize) {
inSampleSize = Math.min(width, height) / minSize;
}
return inSampleSize;
}
/**
* Ensures a |bitmap| is at least |size| in both width and height.
* @param bitmap The bitmap to modify.
* @param size The minimum size (width and height).
* @return The resulting (scaled) bitmap.
*/
private static Bitmap ensureMinSize(Bitmap bitmap, int size) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
if (width >= size && height >= size) return bitmap;
if (width < size) {
float scale = (float) size / width;
width = size;
height *= scale;
}
if (height < size) {
float scale = (float) size / height;
height = size;
width *= scale;
}
return Bitmap.createScaledBitmap(bitmap, width, height, true);
}
/**
* Crops a |bitmap| to a certain square |size|
* @param bitmap The bitmap to crop.
* @param size The size desired (width and height).
* @return The resulting (square) bitmap.
*/
private static Bitmap cropToSquare(Bitmap bitmap, int size) {
int x = 0;
int y = 0;
int width = bitmap.getWidth();
int height = bitmap.getHeight();
if (width == size && height == size) return bitmap;
if (width > size) x = (width - size) / 2;
if (height > size) y = (height - size) / 2;
return Bitmap.createBitmap(bitmap, x, y, size, size);
}
}
// 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.chrome.browser.photo_picker;
import android.app.Service;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import org.chromium.base.Log;
import java.io.FileDescriptor;
import java.io.IOException;
/**
* A service to accept requests to take image file contents and decode them.
*/
public class DecoderService extends Service {
// Message ids for communicating with the client.
// A message sent by the client to decode an image.
static final int MSG_DECODE_IMAGE = 1;
// A message sent by the server to notify the client of the results of the decoding.
static final int MSG_IMAGE_DECODED_REPLY = 2;
// 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_IMAGE_BYTE_COUNT = "image_byte_count";
static final String KEY_IMAGE_DESCRIPTOR = "image_descriptor";
static final String KEY_SIZE = "size";
static final String KEY_SUCCESS = "success";
// A tag for logging error messages.
private static final String TAG = "ImageDecoder";
/**
* Handler for incoming messages from clients.
*/
static class IncomingHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DECODE_IMAGE:
Bundle bundle = null;
Messenger client = null;
String filePath = "";
int size = 0;
try {
Bundle payload = msg.getData();
client = msg.replyTo;
filePath = payload.getString(KEY_FILE_PATH);
ParcelFileDescriptor pfd = payload.getParcelable(KEY_FILE_DESCRIPTOR);
size = payload.getInt(KEY_SIZE);
// 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);
FileDescriptor fd = pfd.getFileDescriptor();
Bitmap bitmap = BitmapUtils.decodeBitmapFromFileDescriptor(fd, size);
try {
pfd.close();
} catch (IOException e) {
Log.e(TAG, "Closing failed " + filePath + " (size: " + size + ") " + e);
}
if (bitmap == null) {
Log.e(TAG, "Decode failed " + filePath + " (size: " + size + ")");
sendReply(client, 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.putBoolean(KEY_SUCCESS, true);
sendReply(client, 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 + " (size: " + size
+ ") " + e);
if (bundle != null && client != null) sendReply(client, bundle);
}
break;
default:
super.handleMessage(msg);
}
}
private void sendReply(Messenger client, Bundle bundle) {
Message reply = Message.obtain(null, MSG_IMAGE_DECODED_REPLY);
reply.setData(bundle);
try {
client.send(reply);
} catch (RemoteException remoteException) {
Log.e(TAG, "Remote error while replying: " + remoteException);
}
}
}
/**
* The target we publish for clients to send messages to IncomingHandler.
*/
final Messenger mMessenger = new Messenger(new IncomingHandler());
/**
* When binding to the service, we return an interface to our messenger
* for sending messages to the service.
*/
@Override
public IBinder onBind(Intent intent) {
return mMessenger.getBinder();
}
}
...@@ -10,6 +10,7 @@ import android.webkit.MimeTypeMap; ...@@ -10,6 +10,7 @@ import android.webkit.MimeTypeMap;
import java.io.File; import java.io.File;
import java.io.FileFilter; import java.io.FileFilter;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Locale; import java.util.Locale;
/** /**
...@@ -23,13 +24,12 @@ class MimeTypeFileFilter implements FileFilter { ...@@ -23,13 +24,12 @@ class MimeTypeFileFilter implements FileFilter {
/** /**
* Contructs a MimeTypeFileFilter object. * Contructs a MimeTypeFileFilter object.
* @param acceptAttr A comma seperated list of MIME types this filter accepts. * @param mimeTypes A list of MIME types this filter accepts.
* For example: images/gif, video/*. * For example: images/gif, video/*.
*/ */
// TODO(finnur): Convert param to List. public MimeTypeFileFilter(@NonNull List<String> mimeTypes) {
public MimeTypeFileFilter(@NonNull String acceptAttr) { for (String field : mimeTypes) {
for (String field : acceptAttr.toLowerCase(Locale.US).split(",")) { field = field.trim().toLowerCase(Locale.US);
field = field.trim();
if (field.startsWith(".")) { if (field.startsWith(".")) {
mExtensions.add(field.substring(1)); mExtensions.add(field.substring(1));
} else if (field.endsWith("/*")) { } else if (field.endsWith("/*")) {
......
...@@ -189,8 +189,13 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> { ...@@ -189,8 +189,13 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> {
mBitmapDetails = bitmapDetails; mBitmapDetails = bitmapDetails;
setItem(bitmapDetails); setItem(bitmapDetails);
if (isCameraTile() || isGalleryTile()) {
initializeSpecialTile(mBitmapDetails);
mImageLoaded = true;
} else {
setThumbnailBitmap(thumbnail); setThumbnailBitmap(thumbnail);
mImageLoaded = !placeholder; mImageLoaded = !placeholder;
}
updateSelectionState(); updateSelectionState();
} }
...@@ -217,8 +222,6 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> { ...@@ -217,8 +222,6 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> {
mSpecialTile, null, image, null, null); mSpecialTile, null, image, null, null);
mSpecialTile.setText(labelStringId); mSpecialTile.setText(labelStringId);
initialize(bitmapDetails, null, false);
// Reset visibility, since #initialize() sets mSpecialTile visibility to GONE. // Reset visibility, since #initialize() sets mSpecialTile visibility to GONE.
mSpecialTile.setVisibility(View.VISIBLE); mSpecialTile.setVisibility(View.VISIBLE);
} }
...@@ -266,6 +269,7 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> { ...@@ -266,6 +269,7 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> {
* re-used. * re-used.
*/ */
private void resetTile() { private void resetTile() {
mIconView.setImageBitmap(null);
mUnselectedView.setVisibility(View.GONE); mUnselectedView.setVisibility(View.GONE);
mSelectedView.setVisibility(View.GONE); mSelectedView.setVisibility(View.GONE);
mScrim.setVisibility(View.GONE); mScrim.setVisibility(View.GONE);
...@@ -323,15 +327,14 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> { ...@@ -323,15 +327,14 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> {
} }
private boolean isGalleryTile() { private boolean isGalleryTile() {
// TODO(finnur): Remove the null checks here and below. return mBitmapDetails.type() == PickerBitmap.GALLERY;
return mBitmapDetails != null && mBitmapDetails.type() == PickerBitmap.GALLERY;
} }
private boolean isCameraTile() { private boolean isCameraTile() {
return mBitmapDetails != null && mBitmapDetails.type() == PickerBitmap.CAMERA; return mBitmapDetails.type() == PickerBitmap.CAMERA;
} }
private boolean isPictureTile() { private boolean isPictureTile() {
return mBitmapDetails == null || mBitmapDetails.type() == PickerBitmap.PICTURE; return mBitmapDetails.type() == PickerBitmap.PICTURE;
} }
} }
...@@ -5,9 +5,6 @@ ...@@ -5,9 +5,6 @@
package org.chromium.chrome.browser.photo_picker; package org.chromium.chrome.browser.photo_picker;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.v7.widget.RecyclerView.ViewHolder; import android.support.v7.widget.RecyclerView.ViewHolder;
import android.text.TextUtils; import android.text.TextUtils;
...@@ -16,7 +13,8 @@ import java.util.List; ...@@ -16,7 +13,8 @@ import java.util.List;
/** /**
* Holds on to a {@link PickerBitmapView} that displays information about a picker bitmap. * Holds on to a {@link PickerBitmapView} that displays information about a picker bitmap.
*/ */
public class PickerBitmapViewHolder extends ViewHolder { public class PickerBitmapViewHolder
extends ViewHolder implements DecoderServiceHost.ImageDecodedCallback {
// Our parent category. // Our parent category.
private PickerCategoryView mCategoryView; private PickerCategoryView mCategoryView;
...@@ -35,11 +33,9 @@ public class PickerBitmapViewHolder extends ViewHolder { ...@@ -35,11 +33,9 @@ public class PickerBitmapViewHolder extends ViewHolder {
mItemView = itemView; mItemView = itemView;
} }
/** // DecoderServiceHost.ImageDecodedCallback
* The notification handler for when an image has been decoded.
* @param filePath The file path for the newly decoded image. @Override
* @param bitmap The results of the decoding (or placeholder image, if failed).
*/
public void imageDecodedCallback(String filePath, Bitmap bitmap) { public void imageDecodedCallback(String filePath, Bitmap bitmap) {
if (bitmap == null || bitmap.getWidth() == 0 || bitmap.getHeight() == 0) { if (bitmap == null || bitmap.getWidth() == 0 || bitmap.getHeight() == 0) {
return; return;
...@@ -65,31 +61,16 @@ public class PickerBitmapViewHolder extends ViewHolder { ...@@ -65,31 +61,16 @@ public class PickerBitmapViewHolder extends ViewHolder {
if (mBitmapDetails.type() == PickerBitmap.CAMERA if (mBitmapDetails.type() == PickerBitmap.CAMERA
|| mBitmapDetails.type() == PickerBitmap.GALLERY) { || mBitmapDetails.type() == PickerBitmap.GALLERY) {
mItemView.initializeSpecialTile(mBitmapDetails); mItemView.initialize(mBitmapDetails, null, false);
return; return;
} }
// TODO(finnur): Use cached image, if available. // TODO(finnur): Use cached image, if available.
// TODO(finnur): Use decoder instead. mItemView.initialize(mBitmapDetails, null, true);
int size = mCategoryView.getImageSize();
imageDecodedCallback(mBitmapDetails.getFilePath(), createPlaceholderBitmap(size, size));
}
/** int size = mCategoryView.getImageSize();
* Creates a placeholder bitmap. mCategoryView.getDecoderServiceHost().decodeImage(mBitmapDetails.getFilePath(), size, this);
* @param width The requested width of the resulting bitmap.
* @param height The requested height of the resulting bitmap.
* @return Placeholder bitmap.
*/
// TODO(finnur): Remove once the decoder is in place.
private Bitmap createPlaceholderBitmap(int width, int height) {
Bitmap placeholder = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(placeholder);
Paint paint = new Paint();
paint.setColor(Color.GRAY);
canvas.drawRect(0, 0, (float) width, (float) height, paint);
return placeholder;
} }
/** /**
......
...@@ -20,6 +20,7 @@ import org.chromium.chrome.browser.widget.selection.SelectableListLayout; ...@@ -20,6 +20,7 @@ import org.chromium.chrome.browser.widget.selection.SelectableListLayout;
import org.chromium.chrome.browser.widget.selection.SelectionDelegate; import org.chromium.chrome.browser.widget.selection.SelectionDelegate;
import org.chromium.ui.PhotoPickerListener; import org.chromium.ui.PhotoPickerListener;
import java.util.Arrays;
import java.util.List; import java.util.List;
/** /**
...@@ -27,7 +28,8 @@ import java.util.List; ...@@ -27,7 +28,8 @@ import java.util.List;
* the photo picker, for example the RecyclerView and the bitmap caches. * the photo picker, for example the RecyclerView and the bitmap caches.
*/ */
public class PickerCategoryView extends RelativeLayout public class PickerCategoryView extends RelativeLayout
implements FileEnumWorkerTask.FilesEnumeratedCallback, OnMenuItemClickListener { implements FileEnumWorkerTask.FilesEnumeratedCallback, RecyclerView.RecyclerListener,
DecoderServiceHost.ServiceReadyCallback, OnMenuItemClickListener {
// The dialog that owns us. // The dialog that owns us.
private PhotoPickerDialog mDialog; private PhotoPickerDialog mDialog;
...@@ -46,6 +48,9 @@ public class PickerCategoryView extends RelativeLayout ...@@ -46,6 +48,9 @@ public class PickerCategoryView extends RelativeLayout
// The callback to notify the listener of decisions reached in the picker. // The callback to notify the listener of decisions reached in the picker.
private PhotoPickerListener mListener; private PhotoPickerListener mListener;
// The host class for the decoding service.
private DecoderServiceHost mDecoderServiceHost;
// The RecyclerView showing the images. // The RecyclerView showing the images.
private RecyclerView mRecyclerView; private RecyclerView mRecyclerView;
...@@ -71,6 +76,9 @@ public class PickerCategoryView extends RelativeLayout ...@@ -71,6 +76,9 @@ public class PickerCategoryView extends RelativeLayout
// A worker task for asynchronously enumerating files off the main thread. // A worker task for asynchronously enumerating files off the main thread.
private FileEnumWorkerTask mWorkerTask; private FileEnumWorkerTask mWorkerTask;
// Whether the connection to the service has been established.
private boolean mServiceReady;
public PickerCategoryView(Context context) { public PickerCategoryView(Context context) {
super(context); super(context);
postConstruction(context); postConstruction(context);
...@@ -84,6 +92,11 @@ public class PickerCategoryView extends RelativeLayout ...@@ -84,6 +92,11 @@ public class PickerCategoryView extends RelativeLayout
private void postConstruction(Context context) { private void postConstruction(Context context) {
mContext = context; mContext = context;
mDecoderServiceHost = new DecoderServiceHost(this);
mDecoderServiceHost.bind(mContext);
enumerateBitmaps();
mSelectionDelegate = new SelectionDelegate<PickerBitmap>(); mSelectionDelegate = new SelectionDelegate<PickerBitmap>();
View root = LayoutInflater.from(context).inflate(R.layout.photo_picker_dialog, this); View root = LayoutInflater.from(context).inflate(R.layout.photo_picker_dialog, this);
...@@ -107,18 +120,21 @@ public class PickerCategoryView extends RelativeLayout ...@@ -107,18 +120,21 @@ public class PickerCategoryView extends RelativeLayout
mRecyclerView.addItemDecoration(new GridSpacingItemDecoration(mColumns, mPadding)); mRecyclerView.addItemDecoration(new GridSpacingItemDecoration(mColumns, mPadding));
// TODO(finnur): Implement caching. // TODO(finnur): Implement caching.
// TODO(finnur): Remove this once the decoder service is in place.
prepareBitmaps();
} }
/** /**
* Cancels any outstanding requests. * Severs the connection to the decoding utility process and cancels any outstanding requests.
*/ */
public void onDialogDismissed() { public void onDialogDismissed() {
if (mWorkerTask != null) { if (mWorkerTask != null) {
mWorkerTask.cancel(true); mWorkerTask.cancel(true);
mWorkerTask = null; mWorkerTask = null;
} }
if (mDecoderServiceHost != null) {
mDecoderServiceHost.unbind(mContext);
mDecoderServiceHost = null;
}
} }
/** /**
...@@ -141,8 +157,25 @@ public class PickerCategoryView extends RelativeLayout ...@@ -141,8 +157,25 @@ public class PickerCategoryView extends RelativeLayout
@Override @Override
public void filesEnumeratedCallback(List<PickerBitmap> files) { public void filesEnumeratedCallback(List<PickerBitmap> files) {
mPickerBitmaps = files; mPickerBitmaps = files;
if (files != null && files.size() > 0) { processBitmaps();
mPickerAdapter.notifyDataSetChanged(); }
// DecoderServiceHost.ServiceReadyCallback:
@Override
public void serviceReady() {
mServiceReady = true;
processBitmaps();
}
// RecyclerView.RecyclerListener:
@Override
public void onViewRecycled(RecyclerView.ViewHolder holder) {
PickerBitmapViewHolder bitmapHolder = (PickerBitmapViewHolder) holder;
String filePath = bitmapHolder.getFilePath();
if (filePath != null) {
getDecoderServiceHost().cancelDecodeImage(filePath);
} }
} }
...@@ -162,6 +195,16 @@ public class PickerCategoryView extends RelativeLayout ...@@ -162,6 +195,16 @@ public class PickerCategoryView extends RelativeLayout
return false; return false;
} }
/**
* Start loading of bitmaps, once files have been enumerated and service is
* ready to decode.
*/
private void processBitmaps() {
if (mServiceReady && mPickerBitmaps != null) {
mPickerAdapter.notifyDataSetChanged();
}
}
// Simple accessors: // Simple accessors:
public int getImageSize() { public int getImageSize() {
...@@ -176,6 +219,10 @@ public class PickerCategoryView extends RelativeLayout ...@@ -176,6 +219,10 @@ public class PickerCategoryView extends RelativeLayout
return mPickerBitmaps; return mPickerBitmaps;
} }
public DecoderServiceHost getDecoderServiceHost() {
return mDecoderServiceHost;
}
public boolean isMultiSelectAllowed() { public boolean isMultiSelectAllowed() {
return mMultiSelectionAllowed; return mMultiSelectionAllowed;
} }
...@@ -212,14 +259,15 @@ public class PickerCategoryView extends RelativeLayout ...@@ -212,14 +259,15 @@ public class PickerCategoryView extends RelativeLayout
} }
/** /**
* Prepares bitmaps for loading. * Asynchronously enumerates bitmaps on disk.
*/ */
private void prepareBitmaps() { private void enumerateBitmaps() {
if (mWorkerTask != null) { if (mWorkerTask != null) {
mWorkerTask.cancel(true); mWorkerTask.cancel(true);
} }
mWorkerTask = new FileEnumWorkerTask(this, new MimeTypeFileFilter("image/*")); mWorkerTask =
new FileEnumWorkerTask(this, new MimeTypeFileFilter(Arrays.asList("image/*")));
mWorkerTask.execute(); mWorkerTask.execute();
} }
......
...@@ -2816,6 +2816,11 @@ You must have Bluetooth and Location turned on in order to use the Physical Web. ...@@ -2816,6 +2816,11 @@ You must have Bluetooth and Location turned on in order to use the Physical Web.
<ph name="BEGIN_LINK">&lt;link&gt;</ph>Get help<ph name="END_LINK">&lt;/link&gt;</ph> <ph name="BEGIN_LINK">&lt;link&gt;</ph>Get help<ph name="END_LINK">&lt;/link&gt;</ph>
</message> </message>
<!-- Photo Picker strings -->
<message name="IDS_DECODER_DESCRIPTION" desc="The title for the image decoder utility service.">
Image decoder
</message>
<!-- Migration strings --> <!-- Migration strings -->
<message name="IDS_TAB_SWITCHER_CALLOUT_BODY" desc="Indicates that clicking the tab switcher button gives you quick access to your tabs."> <message name="IDS_TAB_SWITCHER_CALLOUT_BODY" desc="Indicates that clicking the tab switcher button gives you quick access to your tabs.">
Tap this button for quick access to your tabs. Tap this button for quick access to your tabs.
......
...@@ -784,6 +784,9 @@ chrome_java_sources = [ ...@@ -784,6 +784,9 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/permissions/PermissionDialogController.java", "java/src/org/chromium/chrome/browser/permissions/PermissionDialogController.java",
"java/src/org/chromium/chrome/browser/permissions/PermissionDialogDelegate.java", "java/src/org/chromium/chrome/browser/permissions/PermissionDialogDelegate.java",
"java/src/org/chromium/chrome/browser/physicalweb/BitmapHttpRequest.java", "java/src/org/chromium/chrome/browser/physicalweb/BitmapHttpRequest.java",
"java/src/org/chromium/chrome/browser/photo_picker/BitmapUtils.java",
"java/src/org/chromium/chrome/browser/photo_picker/DecoderService.java",
"java/src/org/chromium/chrome/browser/photo_picker/DecoderServiceHost.java",
"java/src/org/chromium/chrome/browser/photo_picker/FileEnumWorkerTask.java", "java/src/org/chromium/chrome/browser/photo_picker/FileEnumWorkerTask.java",
"java/src/org/chromium/chrome/browser/photo_picker/MimeTypeFileFilter.java", "java/src/org/chromium/chrome/browser/photo_picker/MimeTypeFileFilter.java",
"java/src/org/chromium/chrome/browser/photo_picker/PhotoPickerDialog.java", "java/src/org/chromium/chrome/browser/photo_picker/PhotoPickerDialog.java",
......
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