Fix regression; once again we can apply Gallery photo to contact.
The changes to support hi-res photos in Ic0cabaa5 were not applied to
AttachPhotoActivity. This CL makes the analogous changes, and many other
cleanups besides.
In addition, applying the Gallery photo works even if the AttachPhotoActivity
is killed (eg: when selecting a contact, or when cropping the photo); this
didn't work even before the regression.
The save-contact Intent which invokes the ContactSaveService no longer
needs to specify a callback Activity (it is OK to pass null if you don't
care about getting a callback).
The subclasses of PhotoSelectionHandler have been significantly simplified,
partly by pushing common behavior to the superclass, and also by directly
accessing state in their outer class instead making their own copies.
ContactLoader.Result.getEntityDeltaList() is a new instance method that
replaces more verbose incantations.
New utility class, ContactPhotoUtils. Helps with compressing Bitmaps, and
generating temp-file names used when saving hi-res photos to a contact.
Bug: 6298601
Change-Id: I2fe90c33c9fa81716f263d82ed80c0d6f63c6a7e
diff --git a/src/com/android/contacts/ContactLoader.java b/src/com/android/contacts/ContactLoader.java
index 17cd1e7..c0399e4 100644
--- a/src/com/android/contacts/ContactLoader.java
+++ b/src/com/android/contacts/ContactLoader.java
@@ -19,6 +19,7 @@
import com.android.contacts.model.AccountType;
import com.android.contacts.model.AccountTypeManager;
import com.android.contacts.model.AccountTypeWithDataSet;
+import com.android.contacts.model.EntityDeltaList;
import com.android.contacts.util.ContactLoaderUtils;
import com.android.contacts.util.DataStatus;
import com.android.contacts.util.StreamItemEntry;
@@ -71,7 +72,7 @@
* Loads a single Contact and all it constituent RawContacts.
*/
public class ContactLoader extends AsyncTaskLoader<ContactLoader.Result> {
- private static final String TAG = "ContactLoader";
+ private static final String TAG = ContactLoader.class.getSimpleName();
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -313,6 +314,13 @@
}
/**
+ * Instantiate a new EntityDeltaList for this contact.
+ */
+ public EntityDeltaList createEntityDeltaList() {
+ return EntityDeltaList.fromIterator(getEntities().iterator());
+ }
+
+ /**
* Returns the contact ID.
*/
@VisibleForTesting
@@ -419,16 +427,31 @@
* writable raw-contact, and false otherwise.
*/
public boolean isWritableContact(final Context context) {
- if (isDirectoryEntry()) return false;
+ return getFirstWritableRawContactId(context) != -1;
+ }
+
+ /**
+ * Return the ID of the first raw-contact in the contact data that belongs to a
+ * contact-writable account, or -1 if no such entity exists.
+ */
+ public long getFirstWritableRawContactId(final Context context) {
+ // Directory entries are non-writable
+ if (isDirectoryEntry()) return -1;
+
+ // Iterate through raw-contacts; if we find a writable on, return its ID.
final AccountTypeManager accountTypes = AccountTypeManager.getInstance(context);
- for (Entity rawContact : getEntities()) {
- final ContentValues rawValues = rawContact.getEntityValues();
- final String accountType = rawValues.getAsString(RawContacts.ACCOUNT_TYPE);
- final String dataSet = rawValues.getAsString(RawContacts.DATA_SET);
- final AccountType type = accountTypes.getAccountType(accountType, dataSet);
- if (type != null && type.areContactsWritable()) return true;
+ for (Entity entity : getEntities()) {
+ ContentValues values = entity.getEntityValues();
+ String type = values.getAsString(RawContacts.ACCOUNT_TYPE);
+ String dataSet = values.getAsString(RawContacts.DATA_SET);
+
+ AccountType accountType = accountTypes.getAccountType(type, dataSet);
+ if (accountType != null && accountType.areContactsWritable()) {
+ return values.getAsLong(RawContacts._ID);
+ }
}
- return false;
+ // No writable raw-contact was found.
+ return -1;
}
public int getDirectoryExportSupport() {
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index 3fbca54..fdfd0f7 100644
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -222,7 +222,7 @@
*/
public static Intent createNewRawContactIntent(Context context,
ArrayList<ContentValues> values, AccountWithDataSet account,
- Class<?> callbackActivity, String callbackAction) {
+ Class<? extends Activity> callbackActivity, String callbackAction) {
Intent serviceIntent = new Intent(
context, ContactSaveService.class);
serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
@@ -290,8 +290,9 @@
* @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, long rawContactId, String updatedPhotoPath) {
+ String saveModeExtraKey, int saveMode, boolean isProfile,
+ Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
+ String updatedPhotoPath) {
Bundle bundle = new Bundle();
bundle.putString(String.valueOf(rawContactId), updatedPhotoPath);
return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
@@ -306,8 +307,9 @@
* @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) {
+ String saveModeExtraKey, int saveMode, boolean isProfile,
+ Class<? extends Activity> callbackActivity, String callbackAction,
+ Bundle updatedPhotos) {
Intent serviceIntent = new Intent(
context, ContactSaveService.class);
serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
@@ -317,19 +319,20 @@
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
- // the callback intent.
- Intent callbackIntent = new Intent(context, callbackActivity);
- callbackIntent.putExtra(saveModeExtraKey, saveMode);
- callbackIntent.setAction(callbackAction);
- serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
+ if (callbackActivity != null) {
+ // 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
+ // the callback intent.
+ Intent callbackIntent = new Intent(context, callbackActivity);
+ callbackIntent.putExtra(saveModeExtraKey, saveMode);
+ callbackIntent.setAction(callbackAction);
+ serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
+ }
return serviceIntent;
}
private void saveContact(Intent intent) {
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);
@@ -462,15 +465,17 @@
}
}
- 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);
+ Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
+ if (callbackIntent != null) {
+ 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);
}
- callbackIntent.setData(lookupUri);
-
- deliverCallback(callbackIntent);
}
/**
@@ -554,7 +559,7 @@
* @param callbackAction is the intent action for the callback intent
*/
public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
- String label, long[] rawContactsToAdd, Class<?> callbackActivity,
+ String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
String callbackAction) {
Intent serviceIntent = new Intent(context, ContactSaveService.class);
serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
@@ -618,7 +623,7 @@
* Creates an intent that can be sent to this service to rename a group.
*/
public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
- Class<?> callbackActivity, String callbackAction) {
+ Class<? extends Activity> callbackActivity, String callbackAction) {
Intent serviceIntent = new Intent(context, ContactSaveService.class);
serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
@@ -689,7 +694,7 @@
*/
public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
long[] rawContactsToAdd, long[] rawContactsToRemove,
- Class<?> callbackActivity, String callbackAction) {
+ Class<? extends Activity> callbackActivity, String callbackAction) {
Intent serviceIntent = new Intent(context, ContactSaveService.class);
serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
@@ -959,7 +964,7 @@
*/
public static Intent createJoinContactsIntent(Context context, long contactId1,
long contactId2, boolean contactWritable,
- Class<?> callbackActivity, String callbackAction) {
+ Class<? extends Activity> callbackActivity, String callbackAction) {
Intent serviceIntent = new Intent(context, ContactSaveService.class);
serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
diff --git a/src/com/android/contacts/activities/AttachPhotoActivity.java b/src/com/android/contacts/activities/AttachPhotoActivity.java
index 8d4cb5d..5981e9b 100644
--- a/src/com/android/contacts/activities/AttachPhotoActivity.java
+++ b/src/com/android/contacts/activities/AttachPhotoActivity.java
@@ -18,29 +18,31 @@
import com.android.contacts.ContactsActivity;
import com.android.contacts.R;
-import com.android.contacts.model.ExchangeAccountType;
-import com.android.contacts.model.GoogleAccountType;
+import com.android.contacts.model.AccountType;
+import com.android.contacts.model.EntityDelta;
+import com.android.contacts.model.EntityDeltaList;
+import com.android.contacts.model.EntityModifier;
+import com.android.contacts.util.ContactPhotoUtils;
+import com.android.contacts.ContactLoader;
+import com.android.contacts.ContactSaveService;
+import com.android.contacts.ContactsUtils;
-import android.content.ContentProviderOperation;
import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
import android.content.Intent;
-import android.content.OperationApplicationException;
+import android.content.Loader;
+import android.content.Loader.OnLoadCompleteListener;
import android.database.Cursor;
import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
-import android.os.RemoteException;
-import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.DisplayPhoto;
-import android.provider.ContactsContract.RawContacts;
-import android.widget.Toast;
+import android.provider.MediaStore;
+import android.util.Log;
-import java.io.ByteArrayOutputStream;
-import java.util.ArrayList;
+import java.io.File;
/**
* Provides an external interface for other applications to attach images
@@ -49,25 +51,38 @@
* size and give the user a chance to use the face detector.
*/
public class AttachPhotoActivity extends ContactsActivity {
+ private static final String TAG = AttachPhotoActivity.class.getSimpleName();
+
private static final int REQUEST_PICK_CONTACT = 1;
private static final int REQUEST_CROP_PHOTO = 2;
- private static final String RAW_CONTACT_URIS_KEY = "raw_contact_uris";
+ private static final String KEY_CONTACT_URI = "contact_uri";
+ private static final String KEY_TEMP_PHOTO_URI = "temp_photo_uri";
- private Long[] mRawContactIds;
+ private File mTempPhotoFile;
+ private Uri mTempPhotoUri;
private ContentResolver mContentResolver;
- // Height/width (in pixels) to request for the photo - queried from the provider.
+ // Height and width (in pixels) to request for the photo - queried from the provider.
private static int mPhotoDim;
+ private Uri mContactUri;
+
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
if (icicle != null) {
- mRawContactIds = toClassArray(icicle.getLongArray(RAW_CONTACT_URIS_KEY));
+ 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();
+ mTempPhotoUri = Uri.fromFile(mTempPhotoFile);
+
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType(Contacts.CONTENT_ITEM_TYPE);
startActivityForResult(intent, REQUEST_PICK_CONTACT);
@@ -89,32 +104,8 @@
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
-
- if (mRawContactIds != null && mRawContactIds.length != 0) {
- outState.putLongArray(RAW_CONTACT_URIS_KEY, toPrimativeArray(mRawContactIds));
- }
- }
-
- private static long[] toPrimativeArray(Long[] in) {
- if (in == null) {
- return null;
- }
- long[] out = new long[in.length];
- for (int i = 0; i < in.length; i++) {
- out[i] = in[i];
- }
- return out;
- }
-
- private static Long[] toClassArray(long[] in) {
- if (in == null) {
- return null;
- }
- Long[] out = new Long[in.length];
- for (int i = 0; i < in.length; i++) {
- out[i] = in[i];
- }
- return out;
+ if (mContactUri != null) outState.putString(KEY_CONTACT_URI, mContactUri.toString());
+ outState.putString(KEY_TEMP_PHOTO_URI, mTempPhotoUri.toString());
}
@Override
@@ -137,147 +128,94 @@
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", mPhotoDim);
intent.putExtra("outputY", mPhotoDim);
- intent.putExtra("return-data", true);
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, mTempPhotoUri);
+
startActivityForResult(intent, REQUEST_CROP_PHOTO);
- // while they're cropping, convert the contact into a raw_contact
- final long contactId = ContentUris.parseId(result.getData());
- final ArrayList<Long> rawContactIdsList = queryForAllRawContactIds(
- mContentResolver, contactId);
- mRawContactIds = new Long[rawContactIdsList.size()];
- mRawContactIds = rawContactIdsList.toArray(mRawContactIds);
+ mContactUri = result.getData();
- if (mRawContactIds == null || rawContactIdsList.isEmpty()) {
- Toast.makeText(this, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
- }
} else if (requestCode == REQUEST_CROP_PHOTO) {
- final Bundle extras = result.getExtras();
- if (extras != null && mRawContactIds != null) {
- Bitmap photo = extras.getParcelable("data");
- if (photo != null) {
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
- photo.compress(Bitmap.CompressFormat.PNG, 100, stream);
-
- final ContentValues imageValues = new ContentValues();
- imageValues.put(Photo.PHOTO, stream.toByteArray());
- imageValues.put(RawContacts.Data.IS_SUPER_PRIMARY, 1);
-
- // attach the photo to every raw contact
- for (Long rawContactId : mRawContactIds) {
-
- // exchange and google only allow one image, so do an update rather than insert
- boolean shouldUpdate = false;
-
- final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
- rawContactId);
- final Uri rawContactDataUri = Uri.withAppendedPath(rawContactUri,
- RawContacts.Data.CONTENT_DIRECTORY);
- insertPhoto(imageValues, rawContactDataUri, true);
- }
+ loadContact(mContactUri, new ContactLoader.Listener() {
+ @Override
+ public void onContactLoaded(ContactLoader.Result contact) {
+ saveContact(contact);
}
- }
- finish();
+ });
}
}
- // TODO: move to background
- public static ArrayList<Long> queryForAllRawContactIds(ContentResolver cr, long contactId) {
- Cursor rawContactIdCursor = null;
- ArrayList<Long> rawContactIds = new ArrayList<Long>();
- try {
- rawContactIdCursor = cr.query(RawContacts.CONTENT_URI,
- new String[] {RawContacts._ID},
- RawContacts.CONTACT_ID + "=" + contactId, null, null);
- if (rawContactIdCursor != null) {
- while (rawContactIdCursor.moveToNext()) {
- rawContactIds.add(rawContactIdCursor.getLong(0));
+ // TODO: consider moving this to ContactLoader, especially if we keep adding similar
+ // code elsewhere (ViewNotificationService is another case). The only concern is that,
+ // although this is convenient, it isn't quite as robust as using LoaderManager... for
+ // instance, the loader doesn't persist across Activity restarts.
+ private void loadContact(Uri contactUri, final ContactLoader.Listener listener) {
+ final ContactLoader loader = new ContactLoader(this, contactUri);
+ loader.registerListener(0, new OnLoadCompleteListener<ContactLoader.Result>() {
+ @Override
+ public void onLoadComplete(
+ Loader<ContactLoader.Result> loader, ContactLoader.Result contact) {
+ try {
+ loader.reset();
}
+ catch (RuntimeException e) {
+ Log.e(TAG, "Error resetting loader", e);
+ }
+ listener.onContactLoaded(contact);
}
- } finally {
- if (rawContactIdCursor != null) {
- rawContactIdCursor.close();
- }
- }
- return rawContactIds;
+ });
+ loader.startLoading();
}
/**
- * Inserts a photo on the raw contact.
- * @param values the photo values
- * @param assertAccount if true, will check to verify that no photos exist for Google,
- * Exchange and unsynced phone account types. These account types only take one picture,
- * so if one exists, the account will be updated with the new photo.
+ * If prerequisites have been met, attach the photo to a raw-contact and save.
+ * The prerequisites are:
+ * - photo has been cropped
+ * - contact has been loaded
*/
- private void insertPhoto(ContentValues values, Uri rawContactDataUri,
- boolean assertAccount) {
+ private void saveContact(ContactLoader.Result contact) {
- ArrayList<ContentProviderOperation> operations =
- new ArrayList<ContentProviderOperation>();
-
- if (assertAccount) {
- // Make sure no pictures exist for Google, Exchange and unsynced phone accounts.
- operations.add(ContentProviderOperation.newAssertQuery(rawContactDataUri)
- .withSelection(Photo.MIMETYPE + "=? AND "
- + RawContacts.DATA_SET + " IS NULL AND ("
- + RawContacts.ACCOUNT_TYPE + " IN (?,?) OR "
- + RawContacts.ACCOUNT_TYPE + " IS NULL)",
- new String[] {Photo.CONTENT_ITEM_TYPE, GoogleAccountType.ACCOUNT_TYPE,
- ExchangeAccountType.ACCOUNT_TYPE})
- .withExpectedCount(0).build());
+ // Obtain the raw-contact that we will save to.
+ EntityDeltaList deltaList = contact.createEntityDeltaList();
+ EntityDelta raw = deltaList.getFirstWritableRawContact(this);
+ if (raw == null) {
+ Log.w(TAG, "no writable raw-contact found");
+ return;
}
- // insert the photo
- values.put(Photo.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
- operations.add(ContentProviderOperation.newInsert(rawContactDataUri)
- .withValues(values).build());
-
- try {
- mContentResolver.applyBatch(ContactsContract.AUTHORITY, operations);
- } catch (RemoteException e) {
- throw new IllegalStateException("Problem querying raw_contacts/data", e);
- } catch (OperationApplicationException e) {
- // the account doesn't allow multiple photos, so update
- if (assertAccount) {
- updatePhoto(values, rawContactDataUri, false);
- } else {
- throw new IllegalStateException("Problem inserting photo into raw_contacts/data", e);
- }
+ // 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());
+ 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;
}
- }
- /**
- * Tries to update the photo on the raw_contact. If no photo exists, and allowInsert == true,
- * then will try to {@link #updatePhoto(ContentValues, boolean)}
- */
- private void updatePhoto(ContentValues values, Uri rawContactDataUri,
- boolean allowInsert) {
- ArrayList<ContentProviderOperation> operations =
- new ArrayList<ContentProviderOperation>();
-
- values.remove(Photo.MIMETYPE);
-
- // check that a photo exists
- operations.add(ContentProviderOperation.newAssertQuery(rawContactDataUri)
- .withSelection(Photo.MIMETYPE + "=?", new String[] {
- Photo.CONTENT_ITEM_TYPE
- }).withExpectedCount(1).build());
-
- // update that photo
- operations.add(ContentProviderOperation.newUpdate(rawContactDataUri)
- .withSelection(Photo.MIMETYPE + "=?", new String[] {Photo.CONTENT_ITEM_TYPE})
- .withValues(values).build());
-
- try {
- mContentResolver.applyBatch(ContactsContract.AUTHORITY, operations);
- } catch (RemoteException e) {
- throw new IllegalStateException("Problem querying raw_contacts/data", e);
- } catch (OperationApplicationException e) {
- if (allowInsert) {
- // they deleted the photo between insert and update, so insert one
- insertPhoto(values, rawContactDataUri, false);
- } else {
- throw new IllegalStateException("Problem inserting photo raw_contacts/data", e);
- }
+ // 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
+ // full-res photo would fail to be saved to the non-existent contact.
+ AccountType account = raw.getRawContactAccountType(this);
+ EntityDelta.ValuesDelta values =
+ EntityModifier.ensureKindExists(raw, account, Photo.CONTENT_ITEM_TYPE);
+ if (values == null) {
+ Log.w(TAG, "cannot attach photo to this account type");
+ return;
}
+ values.put(Photo.PHOTO, compressed);
+
+ // Finally, invoke the ContactSaveService.
+ Log.v(TAG, "all prerequisites met, about to save photo to contact");
+ Intent intent = ContactSaveService.createSaveContactIntent(
+ this,
+ deltaList,
+ "", 0,
+ contact.isUserProfile(),
+ null, null,
+ raw.getRawContactId(),
+ mTempPhotoFile.getAbsolutePath());
+ startService(intent);
+ finish();
}
}
diff --git a/src/com/android/contacts/activities/ConfirmAddDetailActivity.java b/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
index 98abfbc..aa3be87 100644
--- a/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
+++ b/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
@@ -560,19 +560,10 @@
}
public void findEditableRawContact() {
- if (mEntityDeltaList == null) {
- return;
- }
- for (EntityDelta state : mEntityDeltaList) {
- final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
- final String dataSet = state.getValues().getAsString(RawContacts.DATA_SET);
- final AccountType type = mAccountTypeManager.getAccountType(accountType, dataSet);
-
- if (type.areContactsWritable()) {
- mEditableAccountType = type;
- mState = state;
- return;
- }
+ if (mEntityDeltaList == null) return;
+ mState = mEntityDeltaList.getFirstWritableRawContact(this);
+ if (mState != null) {
+ mEditableAccountType = mState.getRawContactAccountType(this);
}
}
diff --git a/src/com/android/contacts/activities/PhotoSelectionActivity.java b/src/com/android/contacts/activities/PhotoSelectionActivity.java
index d76af25..d443782 100644
--- a/src/com/android/contacts/activities/PhotoSelectionActivity.java
+++ b/src/com/android/contacts/activities/PhotoSelectionActivity.java
@@ -449,45 +449,36 @@
}
private final class PhotoHandler extends PhotoSelectionHandler {
- private PhotoHandler(Context context, View photoView, int photoMode,
- EntityDeltaList state) {
- super(context, photoView, photoMode, mIsDirectoryContact, state);
- setListener(new PhotoListener(context, mIsProfile));
+ private PhotoActionListener mListener;
+
+ private PhotoHandler(
+ Context context, View photoView, int photoMode, EntityDeltaList state) {
+ super(context, photoView, photoMode, PhotoSelectionActivity.this.mIsDirectoryContact,
+ state);
+ mListener = new PhotoListener();
+ }
+
+ @Override
+ public PhotoActionListener getListener() {
+ return mListener;
+ }
+
+ @Override
+ public void startPhotoActivity(Intent intent, int requestCode, File photoFile) {
+ mSubActivityInProgress = true;
+ mCurrentPhotoFile = photoFile;
+ PhotoSelectionActivity.this.startActivityForResult(intent, requestCode);
}
private final class PhotoListener extends PhotoActionListener {
- @SuppressWarnings("hiding")
- private final boolean mIsProfile;
- private final Context mContext;
-
- private PhotoListener(Context context, boolean isProfile) {
- mContext = context;
- mIsProfile = isProfile;
- }
-
- @Override
- public void startTakePhotoActivity(Intent intent, int requestCode, File photoFile) {
- mSubActivityInProgress = true;
- mCurrentPhotoFile = photoFile;
- startActivityForResult(intent, requestCode);
- }
-
- @Override
- 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();
long rawContactId = getWritableEntityId();
String filePath = mCurrentPhotoFile.getAbsolutePath();
- Intent intent = ContactSaveService.createSaveContactIntent(mContext, delta,
- "", 0, mIsProfile, PhotoSelectionActivity.class,
- ContactEditorActivity.ACTION_SAVE_COMPLETED, rawContactId, filePath);
+ Intent intent = ContactSaveService.createSaveContactIntent(
+ mContext, delta, "", 0, mIsProfile, null, null, rawContactId, filePath);
startService(intent);
finish();
}
diff --git a/src/com/android/contacts/detail/ContactDetailFragment.java b/src/com/android/contacts/detail/ContactDetailFragment.java
index 2dc7bc4..34250d5 100644
--- a/src/com/android/contacts/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailFragment.java
@@ -2016,8 +2016,7 @@
if (defaultGroupId == -1) return;
// add the group membership to the current state
- final EntityDeltaList contactDeltaList = EntityDeltaList.fromIterator(
- mContactData.getEntities().iterator());
+ final EntityDeltaList contactDeltaList = mContactData.createEntityDeltaList();
final EntityDelta rawContactEntityDelta = contactDeltaList.get(0);
final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
diff --git a/src/com/android/contacts/detail/ContactDetailPhotoSetter.java b/src/com/android/contacts/detail/ContactDetailPhotoSetter.java
index 13f5970..b674dc0 100644
--- a/src/com/android/contacts/detail/ContactDetailPhotoSetter.java
+++ b/src/com/android/contacts/detail/ContactDetailPhotoSetter.java
@@ -63,8 +63,7 @@
@Override
public void onClick(View v) {
// Assemble the intent.
- EntityDeltaList delta = EntityDeltaList.fromIterator(
- mContactData.getEntities().iterator());
+ EntityDeltaList delta = mContactData.createEntityDeltaList();
// Find location and bounds of target view, adjusting based on the
// assumed local density.
diff --git a/src/com/android/contacts/detail/PhotoSelectionHandler.java b/src/com/android/contacts/detail/PhotoSelectionHandler.java
index 1423c65..b336d90 100644
--- a/src/com/android/contacts/detail/PhotoSelectionHandler.java
+++ b/src/com/android/contacts/detail/PhotoSelectionHandler.java
@@ -19,15 +19,14 @@
import com.android.contacts.R;
import com.android.contacts.editor.PhotoActionPopup;
import com.android.contacts.model.AccountType;
-import com.android.contacts.model.AccountTypeManager;
import com.android.contacts.model.EntityDelta;
import com.android.contacts.model.EntityDelta.ValuesDelta;
import com.android.contacts.model.EntityDeltaList;
import com.android.contacts.model.EntityModifier;
+import com.android.contacts.util.ContactPhotoUtils;
import android.app.Activity;
import android.content.ActivityNotFoundException;
-import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
@@ -35,10 +34,8 @@
import android.graphics.BitmapFactory;
import android.media.MediaScannerConnection;
import android.net.Uri;
-import android.os.Environment;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.DisplayPhoto;
-import android.provider.ContactsContract.RawContacts;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
@@ -46,30 +43,20 @@
import android.widget.ListPopupWindow;
import android.widget.PopupWindow.OnDismissListener;
import android.widget.Toast;
-
-import java.io.ByteArrayOutputStream;
import java.io.File;
-import java.io.IOException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
/**
* Handles displaying a photo selection popup for a given photo view and dealing with the results
* that come back.
*/
-public class PhotoSelectionHandler implements OnClickListener {
+public abstract class PhotoSelectionHandler implements OnClickListener {
private static final String TAG = PhotoSelectionHandler.class.getSimpleName();
- private static final File PHOTO_DIR = new File(
- Environment.getExternalStorageDirectory() + "/DCIM/Camera");
-
- private static final String PHOTO_DATE_FORMAT = "'IMG'_yyyyMMdd_HHmmss";
-
private static final int REQUEST_CODE_CAMERA_WITH_DATA = 1001;
private static final int REQUEST_CODE_PHOTO_PICKED_WITH_DATA = 1002;
- private final Context mContext;
+ protected final Context mContext;
private final View mPhotoView;
private final int mPhotoMode;
private final int mPhotoPickSize;
@@ -87,6 +74,8 @@
mIsDirectoryContact = isDirectoryContact;
mState = state;
mPhotoPickSize = getPhotoPickSize();
+
+ // NOTE: subclasses should call setListener()
}
public void destroy() {
@@ -95,13 +84,7 @@
}
}
- public PhotoActionListener getListener() {
- return mListener;
- }
-
- public void setListener(PhotoActionListener listener) {
- mListener = listener;
- }
+ public abstract PhotoActionListener getListener();
@Override
public void onClick(View v) {
@@ -109,10 +92,11 @@
if (getWritableEntityIndex() != -1) {
mPopup = PhotoActionPopup.createPopupMenu(
mContext, mPhotoView, mListener, mPhotoMode);
+ final PhotoActionListener listener = mListener; // a bit more bulletproof
mPopup.setOnDismissListener(new OnDismissListener() {
@Override
public void onDismiss() {
- mListener.onPhotoSelectionDismissed();
+ listener.onPhotoSelectionDismissed();
}
});
mPopup.show();
@@ -152,25 +136,8 @@
*/
private int getWritableEntityIndex() {
// Directory entries are non-writable.
- if (mIsDirectoryContact) {
- return -1;
- }
-
- // Find the first writable entity.
- int entityIndex = 0;
- for (EntityDelta delta : mState) {
- ContentValues entityValues = delta.getValues().getCompleteValues();
- String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
- String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
- AccountType accountType = AccountTypeManager.getInstance(mContext).getAccountType(
- type, dataSet);
- if (accountType.areContactsWritable()) {
- mWritableAccount = accountType;
- return entityIndex;
- }
- entityIndex++;
- }
- return -1;
+ if (mIsDirectoryContact) return -1;
+ return mState.indexOfFirstWritableRawContact(mContext);
}
/**
@@ -187,7 +154,6 @@
* 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.
- * @param bitmap The photo to use.
* @return An entity delta list that can be applied to associate the bitmap with the contact,
* or null if the photo could not be parsed or none of the accounts associated with the
* contact are writable.
@@ -208,6 +174,9 @@
return null;
}
+ /** Used by subclasses to delegate to their enclosing Activity or Fragment. */
+ protected abstract void startPhotoActivity(Intent intent, int requestCode, File photoFile);
+
/**
* Sends a newly acquired photo to Gallery for cropping
*/
@@ -222,22 +191,33 @@
// Launch gallery to crop the photo
final Intent intent = getCropImageIntent(f);
- mListener.startPickFromGalleryActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, f);
+ startPhotoActivity(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();
}
}
- private String getPhotoFileName() {
- Date date = new Date(System.currentTimeMillis());
- SimpleDateFormat dateFormat = new SimpleDateFormat(PHOTO_DATE_FORMAT);
- return dateFormat.format(date) + ".jpg";
+ /**
+ * Should initiate an activity to take a photo using the camera.
+ * @param photoFile The file path that will be used to store the photo. This is generally
+ * what should be returned by
+ * {@link PhotoSelectionHandler.PhotoActionListener#getCurrentPhotoFile()}.
+ */
+ private void startTakePhotoActivity(File photoFile) {
+ final Intent intent = getTakePhotoIntent(photoFile);
+ startPhotoActivity(intent, REQUEST_CODE_CAMERA_WITH_DATA, photoFile);
}
- private File getPhotoFile() {
- PHOTO_DIR.mkdirs();
- return new File(PHOTO_DIR, getPhotoFileName());
+ /**
+ * Should initiate an activity pick a photo from the gallery.
+ * @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)}.
+ */
+ private void startPickFromGalleryActivity(File photoFile) {
+ final Intent intent = getPhotoPickIntent(photoFile);
+ startPhotoActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, photoFile);
}
private int getPhotoPickSize() {
@@ -308,9 +288,7 @@
public void onTakePhotoChosen() {
try {
// Launch camera to take photo for selected contact
- File f = getPhotoFile();
- final Intent intent = getTakePhotoIntent(f);
- startTakePhotoActivity(intent, REQUEST_CODE_CAMERA_WITH_DATA, f);
+ startTakePhotoActivity(ContactPhotoUtils.generateTempPhotoFile());
} catch (ActivityNotFoundException e) {
Toast.makeText(mContext, R.string.photoPickerNotFoundText,
Toast.LENGTH_LONG).show();
@@ -321,9 +299,7 @@
public void onPickFromGalleryChosen() {
try {
// Launch picker to choose photo for selected contact
- File f = getPhotoFile();
- final Intent intent = getPhotoPickIntent(f);
- startPickFromGalleryActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, f);
+ startPickFromGalleryActivity(ContactPhotoUtils.generateTempPhotoFile());
} catch (ActivityNotFoundException e) {
Toast.makeText(mContext, R.string.photoPickerNotFoundText,
Toast.LENGTH_LONG).show();
@@ -331,28 +307,6 @@
}
/**
- * Should initiate an activity to take a photo using the camera.
- * @param intent The image capture intent.
- * @param requestCode The request code to use, suitable for handling by
- * {@link PhotoSelectionHandler#handlePhotoActivityResult(int, int, Intent)}.
- * @param photoFile The file path that will be used to store the photo. This is generally
- * what should be returned by
- * {@link PhotoSelectionHandler.PhotoActionListener#getCurrentPhotoFile()}.
- */
- public abstract void startTakePhotoActivity(Intent intent, int requestCode, File photoFile);
-
- /**
- * 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,
- File photoFile);
-
- /**
* Called when the user has completed selection of a photo.
* @param bitmap The selected and cropped photo.
*/
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index b9f1afc..d93fb5c 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -462,15 +462,15 @@
mListener.onCustomEditContactActivityRequested(account, uri, null, false);
}
- private void bindEditorsForExistingContact(ContactLoader.Result data) {
+ private void bindEditorsForExistingContact(ContactLoader.Result contact) {
setEnabled(true);
- mState = EntityDeltaList.fromIterator(data.getEntities().iterator());
+ mState = contact.createEntityDeltaList();
setIntentExtras(mIntentExtras);
mIntentExtras = null;
// For user profile, change the contacts query URI
- mIsUserProfile = data.isUserProfile();
+ mIsUserProfile = contact.isUserProfile();
boolean localProfileExists = false;
if (mIsUserProfile) {
@@ -1714,21 +1714,32 @@
private final class PhotoHandler extends PhotoSelectionHandler {
final long mRawContactId;
+ private final BaseRawContactEditorView mEditor;
+ private PhotoActionListener mListener;
public PhotoHandler(Context context, BaseRawContactEditorView editor, int photoMode,
EntityDeltaList state) {
super(context, editor.getPhotoEditor(), photoMode, false, state);
- setListener(new PhotoEditorListener(editor));
+ mEditor = editor;
mRawContactId = editor.getRawContactId();
+ mListener = new PhotoEditorListener();
+ }
+
+ @Override
+ public PhotoActionListener getListener() {
+ return mListener;
+ }
+
+ @Override
+ public void startPhotoActivity(Intent intent, int requestCode, File photoFile) {
+ mRawContactIdRequestingPhoto = mEditor.getRawContactId();
+ mStatus = Status.SUB_ACTIVITY;
+ mCurrentPhotoFile = photoFile;
+ ContactEditorFragment.this.startActivityForResult(intent, requestCode);
}
private final class PhotoEditorListener extends PhotoSelectionHandler.PhotoActionListener
implements EditorListener {
- private final BaseRawContactEditorView mEditor;
-
- private PhotoEditorListener(BaseRawContactEditorView editor) {
- mEditor = editor;
- }
@Override
public void onRequest(int request) {
@@ -1776,23 +1787,6 @@
}
@Override
- public void startTakePhotoActivity(Intent intent, int requestCode, File photoFile) {
- mRawContactIdRequestingPhoto = mEditor.getRawContactId();
- mStatus = Status.SUB_ACTIVITY;
- mCurrentPhotoFile = photoFile;
- startActivityForResult(intent, requestCode);
- }
-
- @Override
- 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, mCurrentPhotoFile);
mRawContactIdRequestingPhoto = -1;
diff --git a/src/com/android/contacts/editor/PhotoEditorView.java b/src/com/android/contacts/editor/PhotoEditorView.java
index 8a0dd0e..0cbe97e 100644
--- a/src/com/android/contacts/editor/PhotoEditorView.java
+++ b/src/com/android/contacts/editor/PhotoEditorView.java
@@ -20,6 +20,7 @@
import com.android.contacts.model.DataKind;
import com.android.contacts.model.EntityDelta;
import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.util.ContactPhotoUtils;
import com.android.contacts.ContactsUtils;
import android.content.Context;
@@ -27,19 +28,14 @@
import android.graphics.BitmapFactory;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.util.AttributeSet;
-import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-
/**
* Simple editor for {@link Photo}.
*/
public class PhotoEditorView extends FrameLayout implements Editor {
- private static final String TAG = "PhotoEditorView";
private ImageView mPhotoImageView;
private View mFrameView;
@@ -127,23 +123,7 @@
return mHasSetPhoto;
}
- /**
- * Creates a byte[] containing the PNG-compressed bitmap, or null if
- * something goes wrong.
- */
- private static byte[] compressBitmap(Bitmap bitmap) {
- 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();
- return out.toByteArray();
- } catch (IOException e) {
- Log.w(TAG, "Unable to serialize photo: " + e.toString());
- return null;
- }
- }
+
/**
* Assign the given {@link Bitmap} as the new value, updating UI and
@@ -172,7 +152,8 @@
// there is a change in EITHER the delta-list OR a changed photo...
// this way, there is always a change in the delta-list.
final int size = ContactsUtils.getThumbnailSize(getContext());
- byte[] compressed = compressBitmap(Bitmap.createScaledBitmap(photo, size, size, false));
+ final Bitmap scaled = Bitmap.createScaledBitmap(photo, size, size, false);
+ final byte[] compressed = ContactPhotoUtils.compressBitmap(scaled);
if (compressed != null) mEntry.put(Photo.PHOTO, compressed);
}
diff --git a/src/com/android/contacts/model/EntityDelta.java b/src/com/android/contacts/model/EntityDelta.java
index 2cbfa26..2620fb0 100644
--- a/src/com/android/contacts/model/EntityDelta.java
+++ b/src/com/android/contacts/model/EntityDelta.java
@@ -24,6 +24,7 @@
import android.content.ContentProviderOperation;
import android.content.ContentProviderOperation.Builder;
import android.content.ContentValues;
+import android.content.Context;
import android.content.Entity;
import android.content.Entity.NamedContentValues;
import android.net.Uri;
@@ -210,6 +211,20 @@
}
/**
+ * Return the AccountType that this raw-contact belongs to.
+ */
+ public AccountType getRawContactAccountType(Context context) {
+ ContentValues entityValues = getValues().getCompleteValues();
+ String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
+ String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
+ return AccountTypeManager.getInstance(context).getAccountType(type, dataSet);
+ }
+
+ public Long getRawContactId() {
+ return getValues().getAsLong(RawContacts._ID);
+ }
+
+ /**
* Return the list of child {@link ValuesDelta} from our optimized map,
* creating the list if requested.
*/
diff --git a/src/com/android/contacts/model/EntityDeltaList.java b/src/com/android/contacts/model/EntityDeltaList.java
index 5a9355b..478c879 100644
--- a/src/com/android/contacts/model/EntityDeltaList.java
+++ b/src/com/android/contacts/model/EntityDeltaList.java
@@ -18,6 +18,7 @@
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
+import android.content.Context;
import android.content.Entity;
import android.content.EntityIterator;
import android.content.ContentProviderOperation.Builder;
@@ -27,7 +28,6 @@
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.RawContactsEntity;
import com.google.android.collect.Lists;
@@ -35,7 +35,6 @@
import java.util.ArrayList;
import java.util.Iterator;
-import java.util.List;
/**
* Container for multiple {@link EntityDelta} objects, usually when editing
@@ -290,6 +289,9 @@
return null;
}
+ /**
+ * Find the raw-contact (an {@link EntityDelta}) with the specified ID.
+ */
public EntityDelta getByRawContactId(Long rawContactId) {
final int index = this.indexOfRawContactId(rawContactId);
return (index == -1) ? null : this.get(index);
@@ -310,6 +312,23 @@
return -1;
}
+ /** Return the index of the first EntityDelta corresponding to a writable raw-contact, or -1. */
+ public int indexOfFirstWritableRawContact(Context context) {
+ // Find the first writable entity.
+ int entityIndex = 0;
+ for (EntityDelta delta : this) {
+ if (delta.getRawContactAccountType(context).areContactsWritable()) return entityIndex;
+ entityIndex++;
+ }
+ return -1;
+ }
+
+ /** Return the first EntityDelta corresponding to a writable raw-contact, or null. */
+ public EntityDelta getFirstWritableRawContact(Context context) {
+ final int index = indexOfFirstWritableRawContact(context);
+ return (index == -1) ? null : get(index);
+ }
+
public ValuesDelta getSuperPrimaryEntry(final String mimeType) {
ValuesDelta primary = null;
ValuesDelta randomEntry = null;
@@ -354,12 +373,14 @@
}
/** {@inheritDoc} */
+ @Override
public int describeContents() {
// Nothing special about this parcel
return 0;
}
/** {@inheritDoc} */
+ @Override
public void writeToParcel(Parcel dest, int flags) {
final int size = this.size();
dest.writeInt(size);
@@ -383,12 +404,14 @@
public static final Parcelable.Creator<EntityDeltaList> CREATOR =
new Parcelable.Creator<EntityDeltaList>() {
+ @Override
public EntityDeltaList createFromParcel(Parcel in) {
final EntityDeltaList state = new EntityDeltaList();
state.readFromParcel(in);
return state;
}
+ @Override
public EntityDeltaList[] newArray(int size) {
return new EntityDeltaList[size];
}
diff --git a/src/com/android/contacts/util/ContactPhotoUtils.java b/src/com/android/contacts/util/ContactPhotoUtils.java
new file mode 100644
index 0000000..f214e9f
--- /dev/null
+++ b/src/com/android/contacts/util/ContactPhotoUtils.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2012 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.contacts.util;
+
+import android.graphics.Bitmap;
+import android.os.Environment;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * Utilities related to loading/saving contact photos.
+ *
+ */
+public class ContactPhotoUtils {
+ private static final String TAG = "ContactPhotoUtils";
+
+ private static final String PHOTO_DATE_FORMAT = "'IMG'_yyyyMMdd_HHmmss";
+
+ // TODO: /DCIM/Camera isn't the ideal place to stash cropped contact photos.
+ // Where is the right place?
+ private static final File PHOTO_DIR = new File(
+ Environment.getExternalStorageDirectory() + "/DCIM/Camera");
+
+ /**
+ * 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.
+ */
+ public static File generateTempPhotoFile() {
+ PHOTO_DIR.mkdirs();
+ return new File(PHOTO_DIR, generateTempPhotoFileName());
+ }
+
+ private static String generateTempPhotoFileName() {
+ Date date = new Date(System.currentTimeMillis());
+ SimpleDateFormat dateFormat = new SimpleDateFormat(PHOTO_DATE_FORMAT);
+ return dateFormat.format(date) + ".jpg";
+ }
+
+ /**
+ * Creates a byte[] containing the PNG-compressed bitmap, or null if
+ * something goes wrong.
+ */
+ public static byte[] compressBitmap(Bitmap bitmap) {
+ 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();
+ return out.toByteArray();
+ } catch (IOException e) {
+ Log.w(TAG, "Unable to serialize photo: " + e.toString());
+ return null;
+ }
+ }
+}
+
+