am e286c04b: Merge "Support for high-resolution contact photos."
* commit 'e286c04b300feb9e4bdd13281a764103c1548323':
Support for high-resolution contact photos.
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index be84cc4..5dd1942 100644
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -35,11 +35,13 @@
import android.content.Context;
import android.content.Intent;
import android.content.OperationApplicationException;
+import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Parcelable;
+import android.os.Bundle;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.AggregationExceptions;
@@ -53,10 +55,16 @@
import android.util.Log;
import android.widget.Toast;
+import java.lang.Long;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.Iterator;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
/**
* A service responsible for saving changes to the content provider.
@@ -80,6 +88,7 @@
public static final String EXTRA_SAVE_MODE = "saveMode";
public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
+ public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
public static final String ACTION_CREATE_GROUP = "createGroup";
public static final String ACTION_RENAME_GROUP = "renameGroup";
@@ -269,15 +278,38 @@
/**
* Creates an intent that can be sent to this service to create a new raw contact
* using data presented as a set of ContentValues.
+ * This variant is more convenient to use when there is only one photo that can
+ * possibly be updated, as in the Contact Details screen.
+ * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
+ * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
*/
public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
String saveModeExtraKey, int saveMode, boolean isProfile, Class<?> callbackActivity,
- String callbackAction) {
+ String callbackAction, long rawContactId, String updatedPhotoPath) {
+ Bundle bundle = new Bundle();
+ bundle.putString(String.valueOf(rawContactId), updatedPhotoPath);
+ return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
+ callbackActivity, callbackAction, bundle);
+ }
+
+ /**
+ * Creates an intent that can be sent to this service to create a new raw contact
+ * using data presented as a set of ContentValues.
+ * This variant is used when multiple contacts' photos may be updated, as in the
+ * Contact Editor.
+ * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
+ */
+ public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
+ String saveModeExtraKey, int saveMode, boolean isProfile, Class<?> callbackActivity,
+ String callbackAction, Bundle updatedPhotos) {
Intent serviceIntent = new Intent(
context, ContactSaveService.class);
serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
+ if (updatedPhotos != null) {
+ serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
+ }
// Callback intent will be invoked by the service once the contact is
// saved. The service will put the URI of the new contact as "data" on
@@ -293,6 +325,7 @@
EntityDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
+ Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
// Trim any empty fields, and RawContacts, before persisting
final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
@@ -301,6 +334,7 @@
Uri lookupUri = null;
final ContentResolver resolver = getContentResolver();
+ boolean succeeded = false;
// Attempt to persist changes
int tries = 0;
@@ -346,10 +380,9 @@
lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
}
Log.v(TAG, "Saved contact. New URI: " + lookupUri);
- // Mark the intent to indicate that the save was successful (even if the lookup URI
- // is now null). For local contacts or the local profile, it's possible that the
- // save triggered removal of the contact, so no lookup URI would exist..
- callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
+
+ // We can change this back to false later, if we fail to save the contact photo.
+ succeeded = true;
break;
} catch (RemoteException e) {
@@ -395,11 +428,69 @@
}
}
+ // Now save any updated photos. We do this at the end to ensure that
+ // the ContactProvider already knows about newly-created contacts.
+ if (updatedPhotos != null) {
+ for (String key : updatedPhotos.keySet()) {
+ String photoFilePath = updatedPhotos.getString(key);
+ long rawContactId = Long.parseLong(key);
+ File photoFile = new File(photoFilePath);
+ if (!saveUpdatedPhoto(rawContactId, photoFile)) succeeded = false;
+ }
+ }
+
+ if (succeeded) {
+ // Mark the intent to indicate that the save was successful (even if the lookup URI
+ // is now null). For local contacts or the local profile, it's possible that the
+ // save triggered removal of the contact, so no lookup URI would exist..
+ callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
+ }
callbackIntent.setData(lookupUri);
deliverCallback(callbackIntent);
}
+ /**
+ * Save updated photo for the specified raw-contact.
+ * @return true for success, false for failure
+ */
+ private boolean saveUpdatedPhoto(long rawContactId, File photoFile) {
+ Uri outputUri = Uri.withAppendedPath(
+ ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+ RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
+
+ FileOutputStream outputStream = null;
+ FileInputStream inputStream = null;
+ byte[] buffer = new byte[16 * 1024];
+ int length;
+ int totalLength = 0;
+ try {
+ AssetFileDescriptor fd = getContentResolver().openAssetFileDescriptor(outputUri, "rw");
+ outputStream = fd.createOutputStream();
+ inputStream = new FileInputStream(photoFile);
+ while ((length = inputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, length);
+ totalLength += length;
+ }
+ return true; // yay!
+ } catch(IOException e) {
+ Log.e(TAG, "Failed to write photo: " + photoFile.toString() + " because: " + e);
+ } finally {
+ Log.v(TAG, "Wrote " + totalLength + " bytes for photo " + photoFile.toString());
+ try {
+ inputStream.close();
+ } catch(IOException e) {
+ Log.e(TAG, "Failed to close photo input stream");
+ }
+ try {
+ outputStream.close();
+ } catch(IOException e) {
+ Log.e(TAG, "Failed to close photo output stream");
+ }
+ }
+ return false; // failed
+ }
+
private long getRawContactId(EntityDeltaList state,
final ArrayList<ContentProviderOperation> diff,
final ContentProviderResult[] results) {
diff --git a/src/com/android/contacts/activities/PhotoSelectionActivity.java b/src/com/android/contacts/activities/PhotoSelectionActivity.java
index 9ac0fa8..02843e9 100644
--- a/src/com/android/contacts/activities/PhotoSelectionActivity.java
+++ b/src/com/android/contacts/activities/PhotoSelectionActivity.java
@@ -459,17 +459,21 @@
}
@Override
- public void startPickFromGalleryActivity(Intent intent, int requestCode) {
+ public void startPickFromGalleryActivity(Intent intent, int requestCode,
+ File photoFile) {
mSubActivityInProgress = true;
+ mCurrentPhotoFile = photoFile;
startActivityForResult(intent, requestCode);
}
@Override
public void onPhotoSelected(Bitmap bitmap) {
- EntityDeltaList delta = getDeltaForAttachingPhotoToContact(bitmap);
+ EntityDeltaList delta = getDeltaForAttachingPhotoToContact();
+ long rawContactId = getWritableEntityId();
+ String filePath = mCurrentPhotoFile.getAbsolutePath();
Intent intent = ContactSaveService.createSaveContactIntent(mContext, delta,
"", 0, mIsProfile, PhotoSelectionActivity.class,
- ContactEditorActivity.ACTION_SAVE_COMPLETED);
+ ContactEditorActivity.ACTION_SAVE_COMPLETED, rawContactId, filePath);
startService(intent);
finish();
}
diff --git a/src/com/android/contacts/detail/ContactDetailFragment.java b/src/com/android/contacts/detail/ContactDetailFragment.java
index beee359..7c79a4c 100644
--- a/src/com/android/contacts/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailFragment.java
@@ -2000,7 +2000,7 @@
// should update the ui
final Intent intent = ContactSaveService.createSaveContactIntent(getActivity(),
contactDeltaList, "", 0, false, getActivity().getClass(),
- Intent.ACTION_VIEW);
+ Intent.ACTION_VIEW, null);
getActivity().startService(intent);
}
}
diff --git a/src/com/android/contacts/detail/PhotoSelectionHandler.java b/src/com/android/contacts/detail/PhotoSelectionHandler.java
index 72397c0..1423c65 100644
--- a/src/com/android/contacts/detail/PhotoSelectionHandler.java
+++ b/src/com/android/contacts/detail/PhotoSelectionHandler.java
@@ -32,6 +32,7 @@
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.os.Environment;
@@ -131,7 +132,8 @@
if (resultCode == Activity.RESULT_OK) {
switch (requestCode) {
case REQUEST_CODE_PHOTO_PICKED_WITH_DATA: {
- Bitmap bitmap = data.getParcelableExtra("data");
+ Bitmap bitmap = BitmapFactory.decodeFile(
+ mListener.getCurrentPhotoFile().getAbsolutePath());
mListener.onPhotoSelected(bitmap);
return true;
}
@@ -172,6 +174,16 @@
}
/**
+ * Return the raw-contact id of the first entity in the contact data that belongs to a
+ * contact-writable account, or -1 if no such entity exists.
+ */
+ protected long getWritableEntityId() {
+ int index = getWritableEntityIndex();
+ if (index == -1) return -1;
+ return mState.get(index).getValues().getId();
+ }
+
+ /**
* Utility method to retrieve the entity delta for attaching the given bitmap to the contact.
* This will attach the photo to the first contact-writable account that provided data to the
* contact. It is the caller's responsibility to apply the delta.
@@ -180,28 +192,14 @@
* or null if the photo could not be parsed or none of the accounts associated with the
* contact are writable.
*/
- public EntityDeltaList getDeltaForAttachingPhotoToContact(Bitmap bitmap) {
+ public EntityDeltaList getDeltaForAttachingPhotoToContact() {
// Find the first writable entity.
int writableEntityIndex = getWritableEntityIndex();
if (writableEntityIndex != -1) {
- // Convert the photo to a byte array.
- final int size = bitmap.getWidth() * bitmap.getHeight() * 4;
- final ByteArrayOutputStream out = new ByteArrayOutputStream(size);
-
- try {
- bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
- out.flush();
- out.close();
- } catch (IOException e) {
- Log.w(TAG, "Unable to serialize photo: " + e.toString());
- return null;
- }
-
// Note - guaranteed to have contact data if we have a writable entity index.
EntityDelta delta = mState.get(writableEntityIndex);
ValuesDelta child = EntityModifier.ensureKindExists(
delta, mWritableAccount, Photo.CONTENT_ITEM_TYPE);
- child.put(Photo.PHOTO, out.toByteArray());
child.setFromTemplate(false);
child.put(Photo.IS_SUPER_PRIMARY, 1);
@@ -223,8 +221,8 @@
null);
// Launch gallery to crop the photo
- final Intent intent = getCropImageIntent(Uri.fromFile(f));
- mListener.startPickFromGalleryActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA);
+ final Intent intent = getCropImageIntent(f);
+ mListener.startPickFromGalleryActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, f);
} catch (Exception e) {
Log.e(TAG, "Cannot crop image", e);
Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
@@ -237,6 +235,11 @@
return dateFormat.format(date) + ".jpg";
}
+ private File getPhotoFile() {
+ PHOTO_DIR.mkdirs();
+ return new File(PHOTO_DIR, getPhotoFileName());
+ }
+
private int getPhotoPickSize() {
// Note that this URI is safe to call on the UI thread.
Cursor c = mContext.getContentResolver().query(DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
@@ -252,7 +255,8 @@
/**
* Constructs an intent for picking a photo from Gallery, cropping it and returning the bitmap.
*/
- private Intent getPhotoPickIntent() {
+ private Intent getPhotoPickIntent(File photoFile) {
+ Uri photoUri = Uri.fromFile(photoFile);
Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
intent.setType("image/*");
intent.putExtra("crop", "true");
@@ -260,14 +264,15 @@
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", mPhotoPickSize);
intent.putExtra("outputY", mPhotoPickSize);
- intent.putExtra("return-data", true);
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
return intent;
}
/**
* Constructs an intent for image cropping.
*/
- private Intent getCropImageIntent(Uri photoUri) {
+ private Intent getCropImageIntent(File photoFile) {
+ Uri photoUri = Uri.fromFile(photoFile);
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(photoUri, "image/*");
intent.putExtra("crop", "true");
@@ -275,7 +280,7 @@
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", mPhotoPickSize);
intent.putExtra("outputY", mPhotoPickSize);
- intent.putExtra("return-data", true);
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
return intent;
}
@@ -303,10 +308,9 @@
public void onTakePhotoChosen() {
try {
// Launch camera to take photo for selected contact
- PHOTO_DIR.mkdirs();
- File photoFile = new File(PHOTO_DIR, getPhotoFileName());
- startTakePhotoActivity(getTakePhotoIntent(photoFile),
- REQUEST_CODE_CAMERA_WITH_DATA, photoFile);
+ File f = getPhotoFile();
+ final Intent intent = getTakePhotoIntent(f);
+ startTakePhotoActivity(intent, REQUEST_CODE_CAMERA_WITH_DATA, f);
} catch (ActivityNotFoundException e) {
Toast.makeText(mContext, R.string.photoPickerNotFoundText,
Toast.LENGTH_LONG).show();
@@ -317,8 +321,9 @@
public void onPickFromGalleryChosen() {
try {
// Launch picker to choose photo for selected contact
- final Intent intent = getPhotoPickIntent();
- startPickFromGalleryActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA);
+ File f = getPhotoFile();
+ final Intent intent = getPhotoPickIntent(f);
+ startPickFromGalleryActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, f);
} catch (ActivityNotFoundException e) {
Toast.makeText(mContext, R.string.photoPickerNotFoundText,
Toast.LENGTH_LONG).show();
@@ -340,9 +345,12 @@
* Should initiate an activity pick a photo from the gallery.
* @param intent The image capture intent.
* @param requestCode The request code to use, suitable for handling by
+ * @param photoFile The temporary file that the cropped image is written to before being
+ * stored by the content-provider.
* {@link PhotoSelectionHandler#handlePhotoActivityResult(int, int, Intent)}.
*/
- public abstract void startPickFromGalleryActivity(Intent intent, int requestCode);
+ public abstract void startPickFromGalleryActivity(Intent intent, int requestCode,
+ File photoFile);
/**
* Called when the user has completed selection of a photo.
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index 97b1c47..2ede872 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -188,9 +188,7 @@
private static final int REQUEST_CODE_JOIN = 0;
private static final int REQUEST_CODE_ACCOUNTS_CHANGED = 1;
- private Bitmap mPhoto = null;
private long mRawContactIdRequestingPhoto = -1;
- private long mRawContactIdRequestingPhotoAfterLoad = -1;
private PhotoSelectionHandler mPhotoSelectionHandler;
private final EntityDeltaComparator mComparator = new EntityDeltaComparator();
@@ -198,6 +196,7 @@
private Cursor mGroupMetaData;
private File mCurrentPhotoFile;
+ private final Bundle mUpdatedPhotos = new Bundle();
private Context mContext;
private String mAction;
@@ -975,8 +974,9 @@
// Save contact
Intent intent = ContactSaveService.createSaveContactIntent(getActivity(), mState,
- SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),
- getActivity().getClass(), ContactEditorActivity.ACTION_SAVE_COMPLETED);
+ SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(), getActivity().getClass(),
+ ContactEditorActivity.ACTION_SAVE_COMPLETED, mUpdatedPhotos);
+
getActivity().startService(intent);
return true;
}
@@ -1564,13 +1564,21 @@
/**
* Sets the photo stored in mPhoto and writes it to the RawContact with the given id
*/
- private void setPhoto(long rawContact, Bitmap photo) {
+ private void setPhoto(long rawContact, Bitmap photo, File photoFile) {
BaseRawContactEditorView requestingEditor = getRawContactEditorView(rawContact);
+
+ if (photo == null || photo.getHeight() < 0 || photo.getWidth() < 0) {
+ // This is unexpected.
+ Log.w(TAG, "Invalid bitmap passed to setPhoto()");
+ }
+
if (requestingEditor != null) {
requestingEditor.setPhotoBitmap(photo);
} else {
Log.w(TAG, "The contact that requested the photo is no longer present.");
}
+
+ mUpdatedPhotos.putString(String.valueOf(rawContact), photoFile.getAbsolutePath());
}
/**
@@ -1637,12 +1645,6 @@
setData(data);
final long setDataEndTime = SystemClock.elapsedRealtime();
- // If we are coming back from the photo trimmer, this will be set.
- if (mRawContactIdRequestingPhotoAfterLoad != -1) {
- setPhoto(mRawContactIdRequestingPhotoAfterLoad, mPhoto);
- mRawContactIdRequestingPhotoAfterLoad = -1;
- mPhoto = null;
- }
Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime-setDataStartTime));
}
@@ -1757,16 +1759,17 @@
}
@Override
- public void startPickFromGalleryActivity(Intent intent, int requestCode) {
+ public void startPickFromGalleryActivity(Intent intent, int requestCode,
+ File photoFile) {
mRawContactIdRequestingPhoto = mEditor.getRawContactId();
mStatus = Status.SUB_ACTIVITY;
+ mCurrentPhotoFile = photoFile;
startActivityForResult(intent, requestCode);
}
@Override
public void onPhotoSelected(Bitmap bitmap) {
- setPhoto(mRawContactIdRequestingPhoto, bitmap);
- mRawContactIdRequestingPhotoAfterLoad = mRawContactIdRequestingPhoto;
+ setPhoto(mRawContactIdRequestingPhoto, bitmap, mCurrentPhotoFile);
mRawContactIdRequestingPhoto = -1;
}
diff --git a/src/com/android/contacts/editor/PhotoEditorView.java b/src/com/android/contacts/editor/PhotoEditorView.java
index 086b07f..db29544 100644
--- a/src/com/android/contacts/editor/PhotoEditorView.java
+++ b/src/com/android/contacts/editor/PhotoEditorView.java
@@ -132,25 +132,13 @@
return;
}
- final int size = photo.getWidth() * photo.getHeight() * 4;
- final ByteArrayOutputStream out = new ByteArrayOutputStream(size);
+ mPhotoImageView.setImageBitmap(photo);
+ mFrameView.setEnabled(isEnabled());
+ mHasSetPhoto = true;
+ mEntry.setFromTemplate(false);
- try {
- photo.compress(Bitmap.CompressFormat.PNG, 100, out);
- out.flush();
- out.close();
-
- mEntry.put(Photo.PHOTO, out.toByteArray());
- mPhotoImageView.setImageBitmap(photo);
- mFrameView.setEnabled(isEnabled());
- mHasSetPhoto = true;
- mEntry.setFromTemplate(false);
-
- // When the user chooses a new photo mark it as super primary
- mEntry.put(Photo.IS_SUPER_PRIMARY, 1);
- } catch (IOException e) {
- Log.w(TAG, "Unable to serialize photo: " + e.toString());
- }
+ // When the user chooses a new photo mark it as super primary
+ mEntry.put(Photo.IS_SUPER_PRIMARY, 1);
}
/**