Files
packages_apps_Settings/src/com/android/settings/users/EditUserPhotoController.java
Jeff Sharkey d29c6aedbd Only crop photos owned by Settings.
A recent security change locks down the ability for the system UID to
issue Uri permission grants.  This helps mitigate an entire class of
confused deputy security issues.

However, Settings (which runs as the system UID) was still relying on
issuing Uri permission grants to the photo cropper.  The simplest way
to keep that working is to add the "com.android.settings.files"
authority to a whitelist, and only request cropping of Uris from that
location.

This means that if the GET_CONTENT decides to return a Uri (instead
of streaming it into mTakePictureUri), then we need to copy it
ourselves locally before we can send it along to the cropper.

Test: builds, boots, both take/choose photos work
Bug: 33019296, 35158271
Change-Id: I2541c33e8d9452357cb9fc2e021ca74d5a43d5ff
2017-03-13 12:40:48 -06:00

477 lines
18 KiB
Java

/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.users;
import android.app.Activity;
import android.app.Fragment;
import android.content.ClipData;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.StrictMode;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.ContactsContract.DisplayPhoto;
import android.provider.MediaStore;
import android.support.v4.content.FileProvider;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListPopupWindow;
import android.widget.TextView;
import com.android.settings.R;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.drawable.CircleFramedDrawable;
import libcore.io.Streams;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
public class EditUserPhotoController {
private static final String TAG = "EditUserPhotoController";
// It seems that this class generates custom request codes and they may
// collide with ours, these values are very unlikely to have a conflict.
private static final int REQUEST_CODE_CHOOSE_PHOTO = 1001;
private static final int REQUEST_CODE_TAKE_PHOTO = 1002;
private static final int REQUEST_CODE_CROP_PHOTO = 1003;
private static final String CROP_PICTURE_FILE_NAME = "CropEditUserPhoto.jpg";
private static final String TAKE_PICTURE_FILE_NAME = "TakeEditUserPhoto2.jpg";
private static final String NEW_USER_PHOTO_FILE_NAME = "NewUserPhoto.png";
private final int mPhotoSize;
private final Context mContext;
private final Fragment mFragment;
private final ImageView mImageView;
private final Uri mCropPictureUri;
private final Uri mTakePictureUri;
private Bitmap mNewUserPhotoBitmap;
private Drawable mNewUserPhotoDrawable;
public EditUserPhotoController(Fragment fragment, ImageView view,
Bitmap bitmap, Drawable drawable, boolean waiting) {
mContext = view.getContext();
mFragment = fragment;
mImageView = view;
mCropPictureUri = createTempImageUri(mContext, CROP_PICTURE_FILE_NAME, !waiting);
mTakePictureUri = createTempImageUri(mContext, TAKE_PICTURE_FILE_NAME, !waiting);
mPhotoSize = getPhotoSize(mContext);
mImageView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
showUpdatePhotoPopup();
}
});
mNewUserPhotoBitmap = bitmap;
mNewUserPhotoDrawable = drawable;
}
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
return false;
}
final Uri pictureUri = data != null && data.getData() != null
? data.getData() : mTakePictureUri;
switch (requestCode) {
case REQUEST_CODE_CROP_PHOTO:
onPhotoCropped(pictureUri, true);
return true;
case REQUEST_CODE_TAKE_PHOTO:
case REQUEST_CODE_CHOOSE_PHOTO:
if (mTakePictureUri.equals(pictureUri)) {
cropPhoto();
} else {
copyAndCropPhoto(pictureUri);
}
return true;
}
return false;
}
public Bitmap getNewUserPhotoBitmap() {
return mNewUserPhotoBitmap;
}
public Drawable getNewUserPhotoDrawable() {
return mNewUserPhotoDrawable;
}
private void showUpdatePhotoPopup() {
final boolean canTakePhoto = canTakePhoto();
final boolean canChoosePhoto = canChoosePhoto();
if (!canTakePhoto && !canChoosePhoto) {
return;
}
final Context context = mImageView.getContext();
final List<EditUserPhotoController.RestrictedMenuItem> items = new ArrayList<>();
if (canTakePhoto) {
final String title = context.getString(R.string.user_image_take_photo);
final Runnable action = new Runnable() {
@Override
public void run() {
takePhoto();
}
};
items.add(new RestrictedMenuItem(context, title, UserManager.DISALLOW_SET_USER_ICON,
action));
}
if (canChoosePhoto) {
final String title = context.getString(R.string.user_image_choose_photo);
final Runnable action = new Runnable() {
@Override
public void run() {
choosePhoto();
}
};
items.add(new RestrictedMenuItem(context, title, UserManager.DISALLOW_SET_USER_ICON,
action));
}
final ListPopupWindow listPopupWindow = new ListPopupWindow(context);
listPopupWindow.setAnchorView(mImageView);
listPopupWindow.setModal(true);
listPopupWindow.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
listPopupWindow.setAdapter(new RestrictedPopupMenuAdapter(context, items));
final int width = Math.max(mImageView.getWidth(), context.getResources()
.getDimensionPixelSize(R.dimen.update_user_photo_popup_min_width));
listPopupWindow.setWidth(width);
listPopupWindow.setDropDownGravity(Gravity.START);
listPopupWindow.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
listPopupWindow.dismiss();
final RestrictedMenuItem item =
(RestrictedMenuItem) parent.getAdapter().getItem(position);
item.doAction();
}
});
listPopupWindow.show();
}
private boolean canTakePhoto() {
return mImageView.getContext().getPackageManager().queryIntentActivities(
new Intent(MediaStore.ACTION_IMAGE_CAPTURE),
PackageManager.MATCH_DEFAULT_ONLY).size() > 0;
}
private boolean canChoosePhoto() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
return mImageView.getContext().getPackageManager().queryIntentActivities(
intent, 0).size() > 0;
}
private void takePhoto() {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
appendOutputExtra(intent, mTakePictureUri);
mFragment.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO);
}
private void choosePhoto() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
intent.setType("image/*");
appendOutputExtra(intent, mTakePictureUri);
mFragment.startActivityForResult(intent, REQUEST_CODE_CHOOSE_PHOTO);
}
private void copyAndCropPhoto(final Uri pictureUri) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
final ContentResolver cr = mContext.getContentResolver();
try (InputStream in = cr.openInputStream(pictureUri);
OutputStream out = cr.openOutputStream(mTakePictureUri)) {
Streams.copy(in, out);
} catch (IOException e) {
Log.w(TAG, "Failed to copy photo", e);
}
return null;
}
@Override
protected void onPostExecute(Void result) {
cropPhoto();
}
}.execute();
}
private void cropPhoto() {
// TODO: Use a public intent, when there is one.
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(mTakePictureUri, "image/*");
appendOutputExtra(intent, mCropPictureUri);
appendCropExtras(intent);
if (intent.resolveActivity(mContext.getPackageManager()) != null) {
try {
StrictMode.disableDeathOnFileUriExposure();
mFragment.startActivityForResult(intent, REQUEST_CODE_CROP_PHOTO);
} finally {
StrictMode.enableDeathOnFileUriExposure();
}
} else {
onPhotoCropped(mTakePictureUri, false);
}
}
private void appendOutputExtra(Intent intent, Uri pictureUri) {
intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setClipData(ClipData.newRawUri(MediaStore.EXTRA_OUTPUT, pictureUri));
}
private void appendCropExtras(Intent intent) {
intent.putExtra("crop", "true");
intent.putExtra("scale", true);
intent.putExtra("scaleUpIfNeeded", true);
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", mPhotoSize);
intent.putExtra("outputY", mPhotoSize);
}
private void onPhotoCropped(final Uri data, final boolean cropped) {
new AsyncTask<Void, Void, Bitmap>() {
@Override
protected Bitmap doInBackground(Void... params) {
if (cropped) {
InputStream imageStream = null;
try {
imageStream = mContext.getContentResolver()
.openInputStream(data);
return BitmapFactory.decodeStream(imageStream);
} catch (FileNotFoundException fe) {
Log.w(TAG, "Cannot find image file", fe);
return null;
} finally {
if (imageStream != null) {
try {
imageStream.close();
} catch (IOException ioe) {
Log.w(TAG, "Cannot close image stream", ioe);
}
}
}
} else {
// Scale and crop to a square aspect ratio
Bitmap croppedImage = Bitmap.createBitmap(mPhotoSize, mPhotoSize,
Config.ARGB_8888);
Canvas canvas = new Canvas(croppedImage);
Bitmap fullImage = null;
try {
InputStream imageStream = mContext.getContentResolver()
.openInputStream(data);
fullImage = BitmapFactory.decodeStream(imageStream);
} catch (FileNotFoundException fe) {
return null;
}
if (fullImage != null) {
final int squareSize = Math.min(fullImage.getWidth(),
fullImage.getHeight());
final int left = (fullImage.getWidth() - squareSize) / 2;
final int top = (fullImage.getHeight() - squareSize) / 2;
Rect rectSource = new Rect(left, top,
left + squareSize, top + squareSize);
Rect rectDest = new Rect(0, 0, mPhotoSize, mPhotoSize);
Paint paint = new Paint();
canvas.drawBitmap(fullImage, rectSource, rectDest, paint);
return croppedImage;
} else {
// Bah! Got nothin.
return null;
}
}
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (bitmap != null) {
mNewUserPhotoBitmap = bitmap;
mNewUserPhotoDrawable = CircleFramedDrawable
.getInstance(mImageView.getContext(), mNewUserPhotoBitmap);
mImageView.setImageDrawable(mNewUserPhotoDrawable);
}
new File(mContext.getCacheDir(), TAKE_PICTURE_FILE_NAME).delete();
new File(mContext.getCacheDir(), CROP_PICTURE_FILE_NAME).delete();
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
}
private static int getPhotoSize(Context context) {
Cursor cursor = context.getContentResolver().query(
DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null);
try {
cursor.moveToFirst();
return cursor.getInt(0);
} finally {
cursor.close();
}
}
private Uri createTempImageUri(Context context, String fileName, boolean purge) {
final File folder = context.getCacheDir();
folder.mkdirs();
final File fullPath = new File(folder, fileName);
if (purge) {
fullPath.delete();
}
return FileProvider.getUriForFile(context,
RestrictedProfileSettings.FILE_PROVIDER_AUTHORITY, fullPath);
}
File saveNewUserPhotoBitmap() {
if (mNewUserPhotoBitmap == null) {
return null;
}
try {
File file = new File(mContext.getCacheDir(), NEW_USER_PHOTO_FILE_NAME);
OutputStream os = new FileOutputStream(file);
mNewUserPhotoBitmap.compress(Bitmap.CompressFormat.PNG, 100, os);
os.flush();
os.close();
return file;
} catch (IOException e) {
Log.e(TAG, "Cannot create temp file", e);
}
return null;
}
static Bitmap loadNewUserPhotoBitmap(File file) {
return BitmapFactory.decodeFile(file.getAbsolutePath());
}
void removeNewUserPhotoBitmapFile() {
new File(mContext.getCacheDir(), NEW_USER_PHOTO_FILE_NAME).delete();
}
private static final class RestrictedMenuItem {
private final Context mContext;
private final String mTitle;
private final Runnable mAction;
private final RestrictedLockUtils.EnforcedAdmin mAdmin;
// Restriction may be set by system or something else via UserManager.setUserRestriction().
private final boolean mIsRestrictedByBase;
/**
* The menu item, used for popup menu. Any element of such a menu can be disabled by admin.
* @param context A context.
* @param title The title of the menu item.
* @param restriction The restriction, that if is set, blocks the menu item.
* @param action The action on menu item click.
*/
public RestrictedMenuItem(Context context, String title, String restriction,
Runnable action) {
mContext = context;
mTitle = title;
mAction = action;
final int myUserId = UserHandle.myUserId();
mAdmin = RestrictedLockUtils.checkIfRestrictionEnforced(context,
restriction, myUserId);
mIsRestrictedByBase = RestrictedLockUtils.hasBaseUserRestriction(mContext,
restriction, myUserId);
}
@Override
public String toString() {
return mTitle;
}
final void doAction() {
if (isRestrictedByBase()) {
return;
}
if (isRestrictedByAdmin()) {
RestrictedLockUtils.sendShowAdminSupportDetailsIntent(mContext, mAdmin);
return;
}
mAction.run();
}
final boolean isRestrictedByAdmin() {
return mAdmin != null;
}
final boolean isRestrictedByBase() {
return mIsRestrictedByBase;
}
}
/**
* Provide this adapter to ListPopupWindow.setAdapter() to have a popup window menu, where
* any element can be restricted by admin (profile owner or device owner).
*/
private static final class RestrictedPopupMenuAdapter extends ArrayAdapter<RestrictedMenuItem> {
public RestrictedPopupMenuAdapter(Context context, List<RestrictedMenuItem> items) {
super(context, R.layout.restricted_popup_menu_item, R.id.text, items);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final View view = super.getView(position, convertView, parent);
final RestrictedMenuItem item = getItem(position);
final TextView text = (TextView) view.findViewById(R.id.text);
final ImageView image = (ImageView) view.findViewById(R.id.restricted_icon);
text.setEnabled(!item.isRestrictedByAdmin() && !item.isRestrictedByBase());
image.setVisibility(item.isRestrictedByAdmin() && !item.isRestrictedByBase() ?
ImageView.VISIBLE : ImageView.GONE);
return view;
}
}
}