summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorYorke Lee <yorkelee@google.com>2013-09-14 08:36:33 -0700
committerSteve Kondik <shade@chemlab.org>2013-12-08 19:45:29 -0800
commitca137e05e8f830d9c0d6be86bc5c801c4aefcc88 (patch)
tree4de94d21936409105571703d8ad7e6e192b908ed
parent78bba93db3df7209cc29708b39470e003f745bf6 (diff)
downloadpackages_apps_Contacts-ca137e05e8f830d9c0d6be86bc5c801c4aefcc88.tar.gz
packages_apps_Contacts-ca137e05e8f830d9c0d6be86bc5c801c4aefcc88.tar.bz2
packages_apps_Contacts-ca137e05e8f830d9c0d6be86bc5c801c4aefcc88.zip
Make contacts photo pickers compatible with new documents UI
The old contacts photo picker code was using unguaranteed behavior (that Intent.GET_CONTENT would support MediaStore.EXTRA_OUTPUT) and this caused it to not work anymore with the new document picker. This CL changes all usages of files to instead use URIs. Also, a FileProvider has been added to Contacts, to allow us to pass in URI pointing to our private cache in intent.setClipData with Intent.FLAG_GRANT_WRITE_URI_PERMISSION and Intent.FLAG_GRANT_READ_URI_PERMISSION so we no longer have to reply on the MediaStore.EXTRA_OUTPUT being parsed and supported. The use of the FileProvider also prevents unauthorized access to temporary files during the caching process. Bug: 10745342 Change-Id: Iaee3d7d112dd124a2f5596c4b9704ea75d3b3419
-rw-r--r--AndroidManifest.xml10
-rw-r--r--res/xml/file_paths.xml20
-rw-r--r--src/com/android/contacts/ContactSaveService.java41
-rw-r--r--src/com/android/contacts/activities/AttachPhotoActivity.java33
-rw-r--r--src/com/android/contacts/activities/PhotoSelectionActivity.java31
-rw-r--r--src/com/android/contacts/detail/PhotoSelectionHandler.java144
-rw-r--r--src/com/android/contacts/editor/ContactEditorFragment.java37
-rw-r--r--src/com/android/contacts/util/ContactPhotoUtils.java112
8 files changed, 267 insertions, 161 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 76bd152fb..0f64f0fa5 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -539,6 +539,16 @@
</intent-filter>
</service>
+ <provider
+ android:name="android.support.v4.content.FileProvider"
+ android:authorities="com.android.contacts.files"
+ android:grantUriPermissions="true"
+ android:exported="false">
+ <meta-data
+ android:name="android.support.FILE_PROVIDER_PATHS"
+ android:resource="@xml/file_paths" />
+ </provider>
+
<meta-data android:name="android.nfc.disable_beam_default" android:value="true" />
</application>
</manifest>
diff --git a/res/xml/file_paths.xml b/res/xml/file_paths.xml
new file mode 100644
index 000000000..294c0cbfc
--- /dev/null
+++ b/res/xml/file_paths.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<paths xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- Offer access to files under Context.getCacheDir() -->
+ <cache-path name="my_cache" />
+</paths>
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index 7c8782f76..55d78e71a 100644
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -53,6 +53,8 @@ import com.android.contacts.model.RawContactDeltaList;
import com.android.contacts.model.RawContactModifier;
import com.android.contacts.common.model.account.AccountWithDataSet;
import com.android.contacts.util.CallerInfoCacheUtils;
+import com.android.contacts.util.ContactPhotoUtils;
+
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
@@ -60,6 +62,7 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -293,9 +296,9 @@ public class ContactSaveService extends IntentService {
public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
String saveModeExtraKey, int saveMode, boolean isProfile,
Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
- String updatedPhotoPath) {
+ Uri updatedPhotoPath) {
Bundle bundle = new Bundle();
- bundle.putString(String.valueOf(rawContactId), updatedPhotoPath);
+ bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
callbackActivity, callbackAction, bundle);
}
@@ -448,7 +451,7 @@ public class ContactSaveService extends IntentService {
// the ContactProvider already knows about newly-created contacts.
if (updatedPhotos != null) {
for (String key : updatedPhotos.keySet()) {
- String photoFilePath = updatedPhotos.getString(key);
+ Uri photoUri = updatedPhotos.getParcelable(key);
long rawContactId = Long.parseLong(key);
// If the raw-contact ID is negative, we are saving a new raw-contact;
@@ -461,8 +464,7 @@ public class ContactSaveService extends IntentService {
}
}
- File photoFile = new File(photoFilePath);
- if (!saveUpdatedPhoto(rawContactId, photoFile)) succeeded = false;
+ if (!saveUpdatedPhoto(rawContactId, photoUri)) succeeded = false;
}
}
@@ -483,37 +485,12 @@ public class ContactSaveService extends IntentService {
* Save updated photo for the specified raw-contact.
* @return true for success, false for failure
*/
- private boolean saveUpdatedPhoto(long rawContactId, File photoFile) {
+ private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri) {
final Uri outputUri = Uri.withAppendedPath(
ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
- try {
- final FileOutputStream outputStream = getContentResolver()
- .openAssetFileDescriptor(outputUri, "rw").createOutputStream();
- try {
- final FileInputStream inputStream = new FileInputStream(photoFile);
- try {
- final byte[] buffer = new byte[16 * 1024];
- int length;
- int totalLength = 0;
- while ((length = inputStream.read(buffer)) > 0) {
- outputStream.write(buffer, 0, length);
- totalLength += length;
- }
- Log.v(TAG, "Wrote " + totalLength + " bytes for photo " + photoFile.toString());
- } finally {
- inputStream.close();
- }
- } finally {
- outputStream.close();
- photoFile.delete();
- }
- } catch (IOException e) {
- Log.e(TAG, "Failed to write photo: " + photoFile.toString() + " because: " + e);
- return false;
- }
- return true;
+ return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, true);
}
/**
diff --git a/src/com/android/contacts/activities/AttachPhotoActivity.java b/src/com/android/contacts/activities/AttachPhotoActivity.java
index c0a751c36..3239f55f1 100644
--- a/src/com/android/contacts/activities/AttachPhotoActivity.java
+++ b/src/com/android/contacts/activities/AttachPhotoActivity.java
@@ -43,6 +43,7 @@ import com.android.contacts.common.model.ValuesDelta;
import com.android.contacts.util.ContactPhotoUtils;
import java.io.File;
+import java.io.FileNotFoundException;
/**
* Provides an external interface for other applications to attach images
@@ -59,7 +60,6 @@ public class AttachPhotoActivity extends ContactsActivity {
private static final String KEY_CONTACT_URI = "contact_uri";
private static final String KEY_TEMP_PHOTO_URI = "temp_photo_uri";
- private File mTempPhotoFile;
private Uri mTempPhotoUri;
private ContentResolver mContentResolver;
@@ -76,13 +76,9 @@ public class AttachPhotoActivity extends ContactsActivity {
if (icicle != null) {
final String uri = icicle.getString(KEY_CONTACT_URI);
mContactUri = (uri == null) ? null : Uri.parse(uri);
-
mTempPhotoUri = Uri.parse(icicle.getString(KEY_TEMP_PHOTO_URI));
- mTempPhotoFile = new File(mTempPhotoUri.getPath());
} else {
- mTempPhotoFile = ContactPhotoUtils.generateTempPhotoFile(this);
- mTempPhotoUri = Uri.fromFile(mTempPhotoFile);
-
+ mTempPhotoUri = ContactPhotoUtils.generateTempImageUri(this);
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType(Contacts.CONTENT_TYPE);
startActivityForResult(intent, REQUEST_PICK_CONTACT);
@@ -104,8 +100,12 @@ public class AttachPhotoActivity extends ContactsActivity {
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
- if (mContactUri != null) outState.putString(KEY_CONTACT_URI, mContactUri.toString());
- outState.putString(KEY_TEMP_PHOTO_URI, mTempPhotoUri.toString());
+ if (mContactUri != null) {
+ outState.putString(KEY_CONTACT_URI, mContactUri.toString());
+ }
+ if (mTempPhotoUri != null) {
+ outState.putString(KEY_TEMP_PHOTO_URI, mTempPhotoUri.toString());
+ }
}
@Override
@@ -123,7 +123,9 @@ public class AttachPhotoActivity extends ContactsActivity {
if (myIntent.getStringExtra("mimeType") != null) {
intent.setDataAndType(myIntent.getData(), myIntent.getStringExtra("mimeType"));
}
- ContactPhotoUtils.addGalleryIntentExtras(intent, mTempPhotoUri, mPhotoDim);
+
+ ContactPhotoUtils.addPhotoPickerExtras(intent, mTempPhotoUri);
+ ContactPhotoUtils.addCropExtras(intent, mPhotoDim);
startActivityForResult(intent, REQUEST_CROP_PHOTO);
@@ -183,14 +185,20 @@ public class AttachPhotoActivity extends ContactsActivity {
// Create a scaled, compressed bitmap to add to the entity-delta list.
final int size = ContactsUtils.getThumbnailSize(this);
- final Bitmap bitmap = BitmapFactory.decodeFile(mTempPhotoFile.getAbsolutePath());
+ Bitmap bitmap;
+ try {
+ bitmap = ContactPhotoUtils.getBitmapFromUri(this, mTempPhotoUri);
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "Could not find bitmap");
+ return;
+ }
+
final Bitmap scaled = Bitmap.createScaledBitmap(bitmap, size, size, false);
final byte[] compressed = ContactPhotoUtils.compressBitmap(scaled);
if (compressed == null) {
Log.w(TAG, "could not create scaled and compressed Bitmap");
return;
}
-
// Add compressed bitmap to entity-delta... this allows us to save to
// a new contact; otherwise the entity-delta-list would be empty, and
// the ContactSaveService would not create the new contact, and the
@@ -213,7 +221,8 @@ public class AttachPhotoActivity extends ContactsActivity {
contact.isUserProfile(),
null, null,
raw.getRawContactId(),
- mTempPhotoFile.getAbsolutePath());
+ mTempPhotoUri
+ );
startService(intent);
finish();
}
diff --git a/src/com/android/contacts/activities/PhotoSelectionActivity.java b/src/com/android/contacts/activities/PhotoSelectionActivity.java
index 3b1032f35..6d7486314 100644
--- a/src/com/android/contacts/activities/PhotoSelectionActivity.java
+++ b/src/com/android/contacts/activities/PhotoSelectionActivity.java
@@ -28,6 +28,7 @@ import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
+import android.support.v4.content.FileProvider;
import android.view.View;
import android.view.ViewGroup.MarginLayoutParams;
import android.widget.FrameLayout.LayoutParams;
@@ -42,6 +43,9 @@ import com.android.contacts.model.RawContactDeltaList;
import com.android.contacts.util.ContactPhotoUtils;
import com.android.contacts.util.SchedulingUtils;
+import java.io.File;
+import java.io.FileNotFoundException;
+
/**
* Popup activity for choosing a contact photo within the Contacts app.
@@ -59,8 +63,8 @@ public class PhotoSelectionActivity extends Activity {
/** Number of ms for the animation to hide the backdrop on finish. */
private static final int BACKDROP_FADEOUT_DURATION = 100;
- /** Key used to persist photo-filename (NOT full file-path). */
- private static final String KEY_CURRENT_PHOTO_FILE = "currentphotofile";
+ /** Key used to persist photo uri. */
+ private static final String KEY_CURRENT_PHOTO_URI = "currentphotouri";
/** Key used to persist whether a sub-activity is currently in progress. */
private static final String KEY_SUB_ACTIVITY_IN_PROGRESS = "subinprogress";
@@ -151,16 +155,16 @@ public class PhotoSelectionActivity extends Activity {
private PendingPhotoResult mPendingPhotoResult;
/**
- * The photo file being interacted with, if any. Saved/restored between activity instances.
+ * The photo uri being interacted with, if any. Saved/restored between activity instances.
*/
- private String mCurrentPhotoFile;
+ private Uri mCurrentPhotoUri;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.photoselection_activity);
if (savedInstanceState != null) {
- mCurrentPhotoFile = savedInstanceState.getString(KEY_CURRENT_PHOTO_FILE);
+ mCurrentPhotoUri = savedInstanceState.getParcelable(KEY_CURRENT_PHOTO_URI);
mSubActivityInProgress = savedInstanceState.getBoolean(KEY_SUB_ACTIVITY_IN_PROGRESS);
}
@@ -456,7 +460,7 @@ public class PhotoSelectionActivity extends Activity {
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
- outState.putString(KEY_CURRENT_PHOTO_FILE, mCurrentPhotoFile);
+ outState.putParcelable(KEY_CURRENT_PHOTO_URI, mCurrentPhotoUri);
outState.putBoolean(KEY_SUB_ACTIVITY_IN_PROGRESS, mSubActivityInProgress);
}
@@ -527,28 +531,27 @@ public class PhotoSelectionActivity extends Activity {
}
@Override
- public void startPhotoActivity(Intent intent, int requestCode, String photoFile) {
+ public void startPhotoActivity(Intent intent, int requestCode, Uri photoUri) {
mSubActivityInProgress = true;
- mCurrentPhotoFile = photoFile;
+ mCurrentPhotoUri = photoUri;
PhotoSelectionActivity.this.startActivityForResult(intent, requestCode);
}
private final class PhotoListener extends PhotoActionListener {
@Override
- public void onPhotoSelected(Bitmap bitmap) {
+ public void onPhotoSelected(Uri uri) {
RawContactDeltaList delta = getDeltaForAttachingPhotoToContact();
long rawContactId = getWritableEntityId();
- final String croppedPath = ContactPhotoUtils.pathForCroppedPhoto(
- PhotoSelectionActivity.this, mCurrentPhotoFile);
+
Intent intent = ContactSaveService.createSaveContactIntent(
- mContext, delta, "", 0, mIsProfile, null, null, rawContactId, croppedPath);
+ mContext, delta, "", 0, mIsProfile, null, null, rawContactId, uri);
startService(intent);
finish();
}
@Override
- public String getCurrentPhotoFile() {
- return mCurrentPhotoFile;
+ public Uri getCurrentPhotoUri() {
+ return mCurrentPhotoUri;
}
@Override
diff --git a/src/com/android/contacts/detail/PhotoSelectionHandler.java b/src/com/android/contacts/detail/PhotoSelectionHandler.java
index 9689acc0e..6e2d4fa87 100644
--- a/src/com/android/contacts/detail/PhotoSelectionHandler.java
+++ b/src/com/android/contacts/detail/PhotoSelectionHandler.java
@@ -22,9 +22,6 @@ import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.media.MediaScannerConnection;
import android.net.Uri;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.DisplayPhoto;
@@ -48,7 +45,7 @@ import com.android.contacts.model.RawContactDeltaList;
import com.android.contacts.util.ContactPhotoUtils;
import com.android.contacts.util.UiClosables;
-import java.io.File;
+import java.io.FileNotFoundException;
/**
* Handles displaying a photo selection popup for a given photo view and dealing with the results
@@ -60,11 +57,14 @@ public abstract class PhotoSelectionHandler implements OnClickListener {
private static final int REQUEST_CODE_CAMERA_WITH_DATA = 1001;
private static final int REQUEST_CODE_PHOTO_PICKED_WITH_DATA = 1002;
+ private static final int REQUEST_CROP_PHOTO = 1003;
protected final Context mContext;
private final View mPhotoView;
private final int mPhotoMode;
private final int mPhotoPickSize;
+ private final Uri mCroppedPhotoUri;
+ private final Uri mTempPhotoUri;
private final RawContactDeltaList mState;
private final boolean mIsDirectoryContact;
private ListPopupWindow mPopup;
@@ -74,6 +74,8 @@ public abstract class PhotoSelectionHandler implements OnClickListener {
mContext = context;
mPhotoView = photoView;
mPhotoMode = photoMode;
+ mTempPhotoUri = ContactPhotoUtils.generateTempImageUri(context);
+ mCroppedPhotoUri = ContactPhotoUtils.generateTempCroppedImageUri(mContext);
mIsDirectoryContact = isDirectoryContact;
mState = state;
mPhotoPickSize = getPhotoPickSize();
@@ -115,19 +117,55 @@ public abstract class PhotoSelectionHandler implements OnClickListener {
final PhotoActionListener listener = getListener();
if (resultCode == Activity.RESULT_OK) {
switch (requestCode) {
- // Photo was chosen (either new or existing from gallery), and cropped.
- case REQUEST_CODE_PHOTO_PICKED_WITH_DATA: {
- final String path = ContactPhotoUtils.pathForCroppedPhoto(
- mContext, listener.getCurrentPhotoFile());
- Bitmap bitmap = BitmapFactory.decodeFile(path);
- listener.onPhotoSelected(bitmap);
- return true;
+ // Cropped photo was returned
+ case REQUEST_CROP_PHOTO: {
+ final Uri uri;
+ if (data != null && data.getData() != null) {
+ uri = data.getData();
+ } else {
+ uri = mCroppedPhotoUri;
+ }
+
+ try {
+ // delete the original temporary photo if it exists
+ mContext.getContentResolver().delete(mTempPhotoUri, null, null);
+ listener.onPhotoSelected(uri);
+ return true;
+ } catch (FileNotFoundException e) {
+ return false;
+ }
}
- // Photo was successfully taken, now crop it.
- case REQUEST_CODE_CAMERA_WITH_DATA: {
- doCropPhoto(listener.getCurrentPhotoFile());
+
+ // Photo was successfully taken or selected from gallery, now crop it.
+ case REQUEST_CODE_PHOTO_PICKED_WITH_DATA:
+ case REQUEST_CODE_CAMERA_WITH_DATA:
+ final Uri uri;
+ boolean isWritable = false;
+ if (data != null && data.getData() != null) {
+ uri = data.getData();
+ } else {
+ uri = listener.getCurrentPhotoUri();
+ isWritable = true;
+ }
+ final Uri toCrop;
+ if (isWritable) {
+ // Since this uri belongs to our file provider, we know that it is writable
+ // by us. This means that we don't have to save it into another temporary
+ // location just to be able to crop it.
+ toCrop = uri;
+ } else {
+ toCrop = mTempPhotoUri;
+ try {
+ ContactPhotoUtils.savePhotoFromUriToUri(mContext, uri,
+ toCrop, false);
+ } catch (SecurityException e) {
+ Log.d(TAG, "Did not have read-access to uri : " + uri);
+ return false;
+ }
+ }
+
+ doCropPhoto(toCrop, mCroppedPhotoUri);
return true;
- }
}
}
return false;
@@ -186,28 +224,16 @@ public abstract class PhotoSelectionHandler implements OnClickListener {
}
/** Used by subclasses to delegate to their enclosing Activity or Fragment. */
- protected abstract void startPhotoActivity(Intent intent, int requestCode, String photoFile);
+ protected abstract void startPhotoActivity(Intent intent, int requestCode, Uri photoUri);
/**
* Sends a newly acquired photo to Gallery for cropping
*/
- private void doCropPhoto(String fileName) {
+ private void doCropPhoto(Uri inputUri, Uri outputUri) {
try {
- // Obtain the absolute paths for the newly-taken photo, and the destination
- // for the soon-to-be-cropped photo.
- final String newPath = ContactPhotoUtils.pathForNewCameraPhoto(fileName);
- final String croppedPath = ContactPhotoUtils.pathForCroppedPhoto(mContext, fileName);
-
- // Add the image to the media store
- MediaScannerConnection.scanFile(
- mContext,
- new String[] { newPath },
- new String[] { null },
- null);
-
// Launch gallery to crop the photo
- final Intent intent = getCropImageIntent(newPath, croppedPath);
- startPhotoActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, fileName);
+ final Intent intent = getCropImageIntent(inputUri, outputUri);
+ startPhotoActivity(intent, REQUEST_CROP_PHOTO, inputUri);
} catch (Exception e) {
Log.e(TAG, "Cannot crop image", e);
Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
@@ -220,9 +246,9 @@ public abstract class PhotoSelectionHandler implements OnClickListener {
* what should be returned by
* {@link PhotoSelectionHandler.PhotoActionListener#getCurrentPhotoFile()}.
*/
- private void startTakePhotoActivity(String photoFile) {
- final Intent intent = getTakePhotoIntent(photoFile);
- startPhotoActivity(intent, REQUEST_CODE_CAMERA_WITH_DATA, photoFile);
+ private void startTakePhotoActivity(Uri photoUri) {
+ final Intent intent = getTakePhotoIntent(photoUri);
+ startPhotoActivity(intent, REQUEST_CODE_CAMERA_WITH_DATA, photoUri);
}
/**
@@ -231,9 +257,9 @@ public abstract class PhotoSelectionHandler implements OnClickListener {
* stored by the content-provider.
* {@link PhotoSelectionHandler#handlePhotoActivityResult(int, int, Intent)}.
*/
- private void startPickFromGalleryActivity(String photoFile) {
- final Intent intent = getPhotoPickIntent(photoFile);
- startPhotoActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, photoFile);
+ private void startPickFromGalleryActivity(Uri photoUri) {
+ final Intent intent = getPhotoPickIntent(photoUri);
+ startPhotoActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, photoUri);
}
private int getPhotoPickSize() {
@@ -249,36 +275,32 @@ public abstract class PhotoSelectionHandler implements OnClickListener {
}
/**
- * Constructs an intent for picking a photo from Gallery, cropping it and returning the bitmap.
+ * Constructs an intent for capturing a photo and storing it in a temporary output uri.
*/
- private Intent getPhotoPickIntent(String photoFile) {
- final String croppedPhotoPath = ContactPhotoUtils.pathForCroppedPhoto(mContext, photoFile);
- final Uri croppedPhotoUri = Uri.fromFile(new File(croppedPhotoPath));
- final Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
- intent.setType("image/*");
- ContactPhotoUtils.addGalleryIntentExtras(intent, croppedPhotoUri, mPhotoPickSize);
+ private Intent getTakePhotoIntent(Uri outputUri) {
+ final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE, null);
+ ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri);
return intent;
}
/**
- * Constructs an intent for image cropping.
+ * Constructs an intent for picking a photo from Gallery, and returning the bitmap.
*/
- private Intent getCropImageIntent(String inputPhotoPath, String croppedPhotoPath) {
- final Uri inputPhotoUri = Uri.fromFile(new File(inputPhotoPath));
- final Uri croppedPhotoUri = Uri.fromFile(new File(croppedPhotoPath));
- Intent intent = new Intent("com.android.camera.action.CROP");
- intent.setDataAndType(inputPhotoUri, "image/*");
- ContactPhotoUtils.addGalleryIntentExtras(intent, croppedPhotoUri, mPhotoPickSize);
+ private Intent getPhotoPickIntent(Uri outputUri) {
+ final Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
+ intent.setType("image/*");
+ ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri);
return intent;
}
/**
- * Constructs an intent for capturing a photo and storing it in a temporary file.
+ * Constructs an intent for image cropping.
*/
- private static Intent getTakePhotoIntent(String fileName) {
- Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE, null);
- final String newPhotoPath = ContactPhotoUtils.pathForNewCameraPhoto(fileName);
- intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(newPhotoPath)));
+ private Intent getCropImageIntent(Uri inputUri, Uri outputUri) {
+ Intent intent = new Intent("com.android.camera.action.CROP");
+ intent.setDataAndType(inputUri, "image/*");
+ ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri);
+ ContactPhotoUtils.addCropExtras(intent, mPhotoPickSize);
return intent;
}
@@ -297,7 +319,7 @@ public abstract class PhotoSelectionHandler implements OnClickListener {
public void onTakePhotoChosen() {
try {
// Launch camera to take photo for selected contact
- startTakePhotoActivity(ContactPhotoUtils.generateTempPhotoFileName());
+ startTakePhotoActivity(mTempPhotoUri);
} catch (ActivityNotFoundException e) {
Toast.makeText(
mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
@@ -308,7 +330,7 @@ public abstract class PhotoSelectionHandler implements OnClickListener {
public void onPickFromGalleryChosen() {
try {
// Launch picker to choose photo for selected contact
- startPickFromGalleryActivity(ContactPhotoUtils.generateTempPhotoFileName());
+ startPickFromGalleryActivity(mTempPhotoUri);
} catch (ActivityNotFoundException e) {
Toast.makeText(
mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
@@ -317,16 +339,16 @@ public abstract class PhotoSelectionHandler implements OnClickListener {
/**
* Called when the user has completed selection of a photo.
- * @param bitmap The selected and cropped photo.
+ * @throws FileNotFoundException
*/
- public abstract void onPhotoSelected(Bitmap bitmap);
+ public abstract void onPhotoSelected(Uri uri) throws FileNotFoundException;
/**
* Gets the current photo file that is being interacted with. It is the activity or
* fragment's responsibility to maintain this in saved state, since this handler instance
* will not survive rotation.
*/
- public abstract String getCurrentPhotoFile();
+ public abstract Uri getCurrentPhotoUri();
/**
* Called when the photo selection dialog is dismissed.
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index 66b6f1c0b..f2031d351 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -91,6 +91,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import java.io.File;
+import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -111,7 +112,7 @@ public class ContactEditorFragment extends Fragment implements
private static final String KEY_EDIT_STATE = "state";
private static final String KEY_RAW_CONTACT_ID_REQUESTING_PHOTO = "photorequester";
private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator";
- private static final String KEY_CURRENT_PHOTO_FILE = "currentphotofile";
+ private static final String KEY_CURRENT_PHOTO_URI = "currentphotouri";
private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin";
private static final String KEY_CONTACT_WRITABLE_FOR_JOIN = "contactwritableforjoin";
private static final String KEY_SHOW_JOIN_SUGGESTIONS = "showJoinSuggestions";
@@ -222,7 +223,7 @@ public class ContactEditorFragment extends Fragment implements
private Cursor mGroupMetaData;
- private String mCurrentPhotoFile;
+ private Uri mCurrentPhotoUri;
private Bundle mUpdatedPhotos = new Bundle();
private Context mContext;
@@ -484,7 +485,7 @@ public class ContactEditorFragment extends Fragment implements
mRawContactIdRequestingPhoto = savedState.getLong(
KEY_RAW_CONTACT_ID_REQUESTING_PHOTO);
mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR);
- mCurrentPhotoFile = savedState.getString(KEY_CURRENT_PHOTO_FILE);
+ mCurrentPhotoUri = savedState.getParcelable(KEY_CURRENT_PHOTO_URI);
mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN);
mContactWritableForJoin = savedState.getBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN);
mAggregationSuggestionsRawContactId = savedState.getLong(KEY_SHOW_JOIN_SUGGESTIONS);
@@ -1680,7 +1681,7 @@ public class ContactEditorFragment extends Fragment implements
}
outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto);
outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
- outState.putString(KEY_CURRENT_PHOTO_FILE, mCurrentPhotoFile);
+ outState.putParcelable(KEY_CURRENT_PHOTO_URI, mCurrentPhotoUri);
outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
outState.putBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN, mContactWritableForJoin);
outState.putLong(KEY_SHOW_JOIN_SUGGESTIONS, mAggregationSuggestionsRawContactId);
@@ -1748,7 +1749,7 @@ public class ContactEditorFragment extends Fragment implements
/**
* Sets the photo stored in mPhoto and writes it to the RawContact with the given id
*/
- private void setPhoto(long rawContact, Bitmap photo, String photoFile) {
+ private void setPhoto(long rawContact, Bitmap photo, Uri photoUri) {
BaseRawContactEditorView requestingEditor = getRawContactEditorView(rawContact);
if (photo == null || photo.getHeight() < 0 || photo.getWidth() < 0) {
@@ -1762,9 +1763,7 @@ public class ContactEditorFragment extends Fragment implements
Log.w(TAG, "The contact that requested the photo is no longer present.");
}
- final String croppedPhotoPath =
- ContactPhotoUtils.pathForCroppedPhoto(mContext, mCurrentPhotoFile);
- mUpdatedPhotos.putString(String.valueOf(rawContact), croppedPhotoPath);
+ mUpdatedPhotos.putParcelable(String.valueOf(rawContact), photoUri);
}
/**
@@ -1797,11 +1796,12 @@ public class ContactEditorFragment extends Fragment implements
countWithPicture++;
} else {
final long rawContactId = entity.getRawContactId();
- final String path = mUpdatedPhotos.getString(String.valueOf(rawContactId));
- if (path != null) {
- final File file = new File(path);
- if (file.exists()) {
+ final Uri uri = mUpdatedPhotos.getParcelable(String.valueOf(rawContactId));
+ if (uri != null) {
+ try {
+ mContext.getContentResolver().openInputStream(uri);
countWithPicture++;
+ } catch (FileNotFoundException e) {
}
}
}
@@ -1912,11 +1912,11 @@ public class ContactEditorFragment extends Fragment implements
}
@Override
- public void startPhotoActivity(Intent intent, int requestCode, String photoFile) {
+ public void startPhotoActivity(Intent intent, int requestCode, Uri photoUri) {
mRawContactIdRequestingPhoto = mEditor.getRawContactId();
mCurrentPhotoHandler = this;
mStatus = Status.SUB_ACTIVITY;
- mCurrentPhotoFile = photoFile;
+ mCurrentPhotoUri = photoUri;
ContactEditorFragment.this.startActivityForResult(intent, requestCode);
}
@@ -1977,15 +1977,16 @@ public class ContactEditorFragment extends Fragment implements
}
@Override
- public void onPhotoSelected(Bitmap bitmap) {
- setPhoto(mRawContactId, bitmap, mCurrentPhotoFile);
+ public void onPhotoSelected(Uri uri) throws FileNotFoundException {
+ final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(mContext, uri);
+ setPhoto(mRawContactId, bitmap, uri);
mCurrentPhotoHandler = null;
bindEditors();
}
@Override
- public String getCurrentPhotoFile() {
- return mCurrentPhotoFile;
+ public Uri getCurrentPhotoUri() {
+ return mCurrentPhotoUri;
}
@Override
diff --git a/src/com/android/contacts/util/ContactPhotoUtils.java b/src/com/android/contacts/util/ContactPhotoUtils.java
index b14b36cce..2b1c19a28 100644
--- a/src/com/android/contacts/util/ContactPhotoUtils.java
+++ b/src/com/android/contacts/util/ContactPhotoUtils.java
@@ -17,17 +17,25 @@
package com.android.contacts.util;
+import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
+import android.support.v4.content.FileProvider;
import android.util.Log;
+import com.google.common.io.Closeables;
+
import java.io.ByteArrayOutputStream;
import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
@@ -40,40 +48,57 @@ public class ContactPhotoUtils {
private static final String TAG = "ContactPhotoUtils";
private static final String PHOTO_DATE_FORMAT = "'IMG'_yyyyMMdd_HHmmss";
- private static final String NEW_PHOTO_DIR_PATH =
- Environment.getExternalStorageDirectory() + "/DCIM/Camera";
+ public static final String FILE_PROVIDER_AUTHORITY = "com.android.contacts.files";
/**
* Generate a new, unique file to be used as an out-of-band communication
* channel, since hi-res Bitmaps are too big to serialize into a Bundle.
- * This file will be passed to other activities (such as the gallery/camera/cropper/etc.),
- * and read by us once they are finished writing it.
+ * This file will be passed (as a uri) to other activities (such as the gallery/camera/
+ * cropper/etc.), and read by us once they are finished writing it.
*/
- public static File generateTempPhotoFile(Context context) {
- return new File(pathForCroppedPhoto(context, generateTempPhotoFileName()));
+ public static Uri generateTempImageUri(Context context) {
+ return FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY,
+ new File(pathForTempPhoto(context, generateTempPhotoFileName())));
}
- public static String pathForCroppedPhoto(Context context, String fileName) {
- final File dir = new File(context.getExternalCacheDir() + "/tmp");
- dir.mkdirs();
- final File f = new File(dir, fileName);
- return f.getAbsolutePath();
+ public static Uri generateTempCroppedImageUri(Context context) {
+ return FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY,
+ new File(pathForTempPhoto(context, generateTempCroppedPhotoFileName())));
}
- public static String pathForNewCameraPhoto(String fileName) {
- final File dir = new File(NEW_PHOTO_DIR_PATH);
+ private static String pathForTempPhoto(Context context, String fileName) {
+ final File dir = context.getCacheDir();
dir.mkdirs();
final File f = new File(dir, fileName);
return f.getAbsolutePath();
}
- public static String generateTempPhotoFileName() {
- Date date = new Date(System.currentTimeMillis());
+ private static String generateTempPhotoFileName() {
+ final Date date = new Date(System.currentTimeMillis());
SimpleDateFormat dateFormat = new SimpleDateFormat(PHOTO_DATE_FORMAT, Locale.US);
return "ContactPhoto-" + dateFormat.format(date) + ".jpg";
}
+ private static String generateTempCroppedPhotoFileName() {
+ final Date date = new Date(System.currentTimeMillis());
+ SimpleDateFormat dateFormat = new SimpleDateFormat(PHOTO_DATE_FORMAT, Locale.US);
+ return "ContactPhoto-" + dateFormat.format(date) + "-cropped.jpg";
+ }
+
+ /**
+ * Given a uri pointing to a bitmap, reads it into a bitmap and returns it.
+ * @throws FileNotFoundException
+ */
+ public static Bitmap getBitmapFromUri(Context context, Uri uri) throws FileNotFoundException {
+ final InputStream imageStream = context.getContentResolver().openInputStream(uri);
+ try {
+ return BitmapFactory.decodeStream(imageStream);
+ } finally {
+ Closeables.closeQuietly(imageStream);
+ }
+ }
+
/**
* Creates a byte[] containing the PNG-compressed bitmap, or null if
* something goes wrong.
@@ -92,14 +117,7 @@ public class ContactPhotoUtils {
}
}
- /**
- * Adds common extras to gallery intents.
- *
- * @param intent The intent to add extras to.
- * @param croppedPhotoUri The uri of the file to save the image to.
- * @param photoSize The size of the photo to scale to.
- */
- public static void addGalleryIntentExtras(Intent intent, Uri croppedPhotoUri, int photoSize) {
+ public static void addCropExtras(Intent intent, int photoSize) {
intent.putExtra("crop", "true");
intent.putExtra("scale", true);
intent.putExtra("scaleUpIfNeeded", true);
@@ -107,7 +125,53 @@ public class ContactPhotoUtils {
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", photoSize);
intent.putExtra("outputY", photoSize);
- intent.putExtra(MediaStore.EXTRA_OUTPUT, croppedPhotoUri);
+ }
+
+ /**
+ * Adds common extras to gallery intents.
+ *
+ * @param intent The intent to add extras to.
+ * @param photoUri The uri of the file to save the image to.
+ */
+ public static void addPhotoPickerExtras(Intent intent, Uri photoUri) {
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
+ intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
+ Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.setClipData(ClipData.newRawUri(MediaStore.EXTRA_OUTPUT, photoUri));
+ }
+
+ /**
+ * Given an input photo stored in a uri, save it to a destination uri
+ */
+ public static boolean savePhotoFromUriToUri(Context context, Uri inputUri, Uri outputUri,
+ boolean deleteAfterSave) {
+ FileOutputStream outputStream = null;
+ InputStream inputStream = null;
+ try {
+ outputStream = context.getContentResolver()
+ .openAssetFileDescriptor(outputUri, "rw").createOutputStream();
+ inputStream = context.getContentResolver().openInputStream(
+ inputUri);
+
+ final byte[] buffer = new byte[16 * 1024];
+ int length;
+ int totalLength = 0;
+ while ((length = inputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, length);
+ totalLength += length;
+ }
+ Log.v(TAG, "Wrote " + totalLength + " bytes for photo " + inputUri.toString());
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to write photo: " + inputUri.toString() + " because: " + e);
+ return false;
+ } finally {
+ Closeables.closeQuietly(inputStream);
+ Closeables.closeQuietly(outputStream);
+ if (deleteAfterSave) {
+ context.getContentResolver().delete(inputUri, null, null);
+ }
+ }
+ return true;
}
}