1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.contacts.detail; 18 19 import android.app.Activity; 20 import android.content.ActivityNotFoundException; 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.PackageManager; 25 import android.content.pm.ResolveInfo; 26 import android.database.Cursor; 27 import android.net.Uri; 28 import android.provider.ContactsContract.CommonDataKinds.Photo; 29 import android.provider.ContactsContract.DisplayPhoto; 30 import android.provider.ContactsContract.RawContacts; 31 import android.provider.MediaStore; 32 import android.util.Log; 33 import android.view.View; 34 import android.view.View.OnClickListener; 35 import android.widget.ListPopupWindow; 36 import android.widget.PopupWindow.OnDismissListener; 37 import android.widget.Toast; 38 39 import com.android.contacts.R; 40 import com.android.contacts.editor.PhotoActionPopup; 41 import com.android.contacts.model.AccountTypeManager; 42 import com.android.contacts.model.RawContactDelta; 43 import com.android.contacts.model.RawContactDeltaList; 44 import com.android.contacts.model.RawContactModifier; 45 import com.android.contacts.model.ValuesDelta; 46 import com.android.contacts.model.account.AccountType; 47 import com.android.contacts.util.ContactPhotoUtils; 48 import com.android.contacts.util.UiClosables; 49 50 import java.io.FileNotFoundException; 51 import java.util.List; 52 53 /** 54 * Handles displaying a photo selection popup for a given photo view and dealing with the results 55 * that come back. 56 */ 57 public abstract class PhotoSelectionHandler implements OnClickListener { 58 59 private static final String TAG = PhotoSelectionHandler.class.getSimpleName(); 60 61 private static final int REQUEST_CODE_CAMERA_WITH_DATA = 1001; 62 private static final int REQUEST_CODE_PHOTO_PICKED_WITH_DATA = 1002; 63 private static final int REQUEST_CROP_PHOTO = 1003; 64 65 // Height and width (in pixels) to request for the photo - queried from the provider. 66 private static int mPhotoDim; 67 // Default photo dimension to use if unable to query the provider. 68 private static final int mDefaultPhotoDim = 720; 69 70 protected final Context mContext; 71 private final View mChangeAnchorView; 72 private final int mPhotoMode; 73 private final int mPhotoPickSize; 74 private final Uri mCroppedPhotoUri; 75 private final Uri mTempPhotoUri; 76 private final RawContactDeltaList mState; 77 private final boolean mIsDirectoryContact; 78 private ListPopupWindow mPopup; 79 PhotoSelectionHandler(Context context, View changeAnchorView, int photoMode, boolean isDirectoryContact, RawContactDeltaList state)80 public PhotoSelectionHandler(Context context, View changeAnchorView, int photoMode, 81 boolean isDirectoryContact, RawContactDeltaList state) { 82 mContext = context; 83 mChangeAnchorView = changeAnchorView; 84 mPhotoMode = photoMode; 85 mTempPhotoUri = ContactPhotoUtils.generateTempImageUri(context); 86 mCroppedPhotoUri = ContactPhotoUtils.generateTempCroppedImageUri(mContext); 87 mIsDirectoryContact = isDirectoryContact; 88 mState = state; 89 mPhotoPickSize = getPhotoPickSize(); 90 } 91 destroy()92 public void destroy() { 93 UiClosables.closeQuietly(mPopup); 94 } 95 getListener()96 public abstract PhotoActionListener getListener(); 97 98 @Override onClick(View v)99 public void onClick(View v) { 100 final PhotoActionListener listener = getListener(); 101 if (listener != null) { 102 if (getWritableEntityIndex() != -1) { 103 mPopup = PhotoActionPopup.createPopupMenu( 104 mContext, mChangeAnchorView, listener, mPhotoMode); 105 mPopup.setOnDismissListener(new OnDismissListener() { 106 @Override 107 public void onDismiss() { 108 listener.onPhotoSelectionDismissed(); 109 } 110 }); 111 mPopup.show(); 112 } 113 } 114 } 115 116 /** 117 * Attempts to handle the given activity result. Returns whether this handler was able to 118 * process the result successfully. 119 * @param requestCode The request code. 120 * @param resultCode The result code. 121 * @param data The intent that was returned. 122 * @return Whether the handler was able to process the result. 123 */ handlePhotoActivityResult(int requestCode, int resultCode, Intent data)124 public boolean handlePhotoActivityResult(int requestCode, int resultCode, Intent data) { 125 final PhotoActionListener listener = getListener(); 126 if (resultCode == Activity.RESULT_OK) { 127 switch (requestCode) { 128 // Cropped photo was returned 129 case REQUEST_CROP_PHOTO: { 130 if (data != null && data.getData() != null) { 131 final Uri croppedUri = data.getData(); 132 ContactPhotoUtils.savePhotoFromUriToUri(mContext, croppedUri, 133 mCroppedPhotoUri, /* deleteAfterSave */ false); 134 } 135 136 try { 137 // delete the original temporary photo if it exists 138 mContext.getContentResolver().delete(mTempPhotoUri, null, null); 139 listener.onPhotoSelected(mCroppedPhotoUri); 140 return true; 141 } catch (FileNotFoundException e) { 142 return false; 143 } 144 } 145 146 // Photo was successfully taken or selected from gallery, now crop it. 147 case REQUEST_CODE_PHOTO_PICKED_WITH_DATA: 148 case REQUEST_CODE_CAMERA_WITH_DATA: 149 final Uri uri; 150 boolean isWritable = false; 151 if (data != null && data.getData() != null) { 152 uri = data.getData(); 153 } else { 154 uri = listener.getCurrentPhotoUri(); 155 isWritable = true; 156 } 157 final Uri toCrop; 158 if (isWritable) { 159 // Since this uri belongs to our file provider, we know that it is writable 160 // by us. This means that we don't have to save it into another temporary 161 // location just to be able to crop it. 162 toCrop = uri; 163 } else { 164 toCrop = mTempPhotoUri; 165 try { 166 if (!ContactPhotoUtils.savePhotoFromUriToUri(mContext, uri, 167 toCrop, false)) { 168 return false; 169 } 170 } catch (SecurityException e) { 171 if (Log.isLoggable(TAG, Log.DEBUG)) { 172 Log.d(TAG, "Did not have read-access to uri : " + uri); 173 } 174 return false; 175 } 176 } 177 178 doCropPhoto(toCrop, mCroppedPhotoUri); 179 return true; 180 } 181 } 182 return false; 183 } 184 185 /** 186 * Return the index of the first entity in the contact data that belongs to a contact-writable 187 * account, or -1 if no such entity exists. 188 */ getWritableEntityIndex()189 private int getWritableEntityIndex() { 190 // Directory entries are non-writable. 191 if (mIsDirectoryContact) return -1; 192 return mState.indexOfFirstWritableRawContact(mContext); 193 } 194 195 /** 196 * Return the raw-contact id of the first entity in the contact data that belongs to a 197 * contact-writable account, or -1 if no such entity exists. 198 */ getWritableEntityId()199 protected long getWritableEntityId() { 200 int index = getWritableEntityIndex(); 201 if (index == -1) return -1; 202 return mState.get(index).getValues().getId(); 203 } 204 205 /** 206 * Utility method to retrieve the entity delta for attaching the given bitmap to the contact. 207 * This will attach the photo to the first contact-writable account that provided data to the 208 * contact. It is the caller's responsibility to apply the delta. 209 * @return An entity delta list that can be applied to associate the bitmap with the contact, 210 * or null if the photo could not be parsed or none of the accounts associated with the 211 * contact are writable. 212 */ getDeltaForAttachingPhotoToContact()213 public RawContactDeltaList getDeltaForAttachingPhotoToContact() { 214 // Find the first writable entity. 215 int writableEntityIndex = getWritableEntityIndex(); 216 if (writableEntityIndex != -1) { 217 // We are guaranteed to have contact data if we have a writable entity index. 218 final RawContactDelta delta = mState.get(writableEntityIndex); 219 220 // Need to find the right account so that EntityModifier knows which fields to add 221 final ContentValues entityValues = delta.getValues().getCompleteValues(); 222 final String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE); 223 final String dataSet = entityValues.getAsString(RawContacts.DATA_SET); 224 final AccountType accountType = AccountTypeManager.getInstance(mContext).getAccountType( 225 type, dataSet); 226 227 final ValuesDelta child = RawContactModifier.ensureKindExists( 228 delta, accountType, Photo.CONTENT_ITEM_TYPE); 229 child.setFromTemplate(false); 230 child.setSuperPrimary(true); 231 232 return mState; 233 } 234 return null; 235 } 236 237 /** Used by subclasses to delegate to their enclosing Activity or Fragment. */ startPhotoActivity(Intent intent, int requestCode, Uri photoUri)238 protected abstract void startPhotoActivity(Intent intent, int requestCode, Uri photoUri); 239 240 /** 241 * Sends a newly acquired photo to Gallery for cropping 242 */ doCropPhoto(Uri inputUri, Uri outputUri)243 private void doCropPhoto(Uri inputUri, Uri outputUri) { 244 final Intent intent = getCropImageIntent(inputUri, outputUri); 245 if (!hasIntentHandler(intent)) { 246 try { 247 getListener().onPhotoSelected(inputUri); 248 } catch (FileNotFoundException e) { 249 Log.e(TAG, "Cannot save uncropped photo", e); 250 Toast.makeText(mContext, R.string.contactPhotoSavedErrorToast, 251 Toast.LENGTH_LONG).show(); 252 } 253 return; 254 } 255 try { 256 // Launch gallery to crop the photo 257 startPhotoActivity(intent, REQUEST_CROP_PHOTO, inputUri); 258 } catch (Exception e) { 259 Log.e(TAG, "Cannot crop image", e); 260 Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show(); 261 } 262 } 263 264 /** 265 * Should initiate an activity to take a photo using the camera. 266 * @param photoFile The file path that will be used to store the photo. This is generally 267 * what should be returned by 268 * {@link PhotoSelectionHandler.PhotoActionListener#getCurrentPhotoFile()}. 269 */ startTakePhotoActivity(Uri photoUri)270 private void startTakePhotoActivity(Uri photoUri) { 271 final Intent intent = getTakePhotoIntent(photoUri); 272 startPhotoActivity(intent, REQUEST_CODE_CAMERA_WITH_DATA, photoUri); 273 } 274 275 /** 276 * Should initiate an activity pick a photo from the gallery. 277 * @param photoFile The temporary file that the cropped image is written to before being 278 * stored by the content-provider. 279 * {@link PhotoSelectionHandler#handlePhotoActivityResult(int, int, Intent)}. 280 */ startPickFromGalleryActivity(Uri photoUri)281 private void startPickFromGalleryActivity(Uri photoUri) { 282 final Intent intent = getPhotoPickIntent(photoUri); 283 startPhotoActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, photoUri); 284 } 285 getPhotoPickSize()286 private int getPhotoPickSize() { 287 if (mPhotoDim != 0) { 288 return mPhotoDim; 289 } 290 291 // Note that this URI is safe to call on the UI thread. 292 Cursor c = mContext.getContentResolver().query(DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, 293 new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null); 294 if (c != null) { 295 try { 296 if (c.moveToFirst()) { 297 mPhotoDim = c.getInt(0); 298 } 299 } finally { 300 c.close(); 301 } 302 } 303 return mPhotoDim != 0 ? mPhotoDim : mDefaultPhotoDim; 304 } 305 306 /** 307 * Constructs an intent for capturing a photo and storing it in a temporary output uri. 308 */ getTakePhotoIntent(Uri outputUri)309 private Intent getTakePhotoIntent(Uri outputUri) { 310 final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE, null); 311 ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri); 312 return intent; 313 } 314 315 /** 316 * Constructs an intent for picking a photo from Gallery, and returning the bitmap. 317 */ getPhotoPickIntent(Uri outputUri)318 private Intent getPhotoPickIntent(Uri outputUri) { 319 final Intent intent = new Intent(Intent.ACTION_PICK, null); 320 intent.setType("image/*"); 321 ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri); 322 return intent; 323 } 324 hasIntentHandler(Intent intent)325 private boolean hasIntentHandler(Intent intent) { 326 final List<ResolveInfo> resolveInfo = mContext.getPackageManager() 327 .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); 328 return resolveInfo != null && resolveInfo.size() > 0; 329 } 330 331 /** 332 * Constructs an intent for image cropping. 333 */ getCropImageIntent(Uri inputUri, Uri outputUri)334 private Intent getCropImageIntent(Uri inputUri, Uri outputUri) { 335 Intent intent = new Intent("com.android.camera.action.CROP"); 336 intent.setDataAndType(inputUri, "image/*"); 337 ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri); 338 ContactPhotoUtils.addCropExtras(intent, mPhotoPickSize); 339 return intent; 340 } 341 342 public abstract class PhotoActionListener implements PhotoActionPopup.Listener { 343 @Override onRemovePictureChosen()344 public void onRemovePictureChosen() { 345 // No default implementation. 346 } 347 348 @Override onTakePhotoChosen()349 public void onTakePhotoChosen() { 350 try { 351 // Launch camera to take photo for selected contact 352 startTakePhotoActivity(mTempPhotoUri); 353 } catch (ActivityNotFoundException e) { 354 Toast.makeText( 355 mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show(); 356 } 357 } 358 359 @Override onPickFromGalleryChosen()360 public void onPickFromGalleryChosen() { 361 try { 362 // Launch picker to choose photo for selected contact 363 startPickFromGalleryActivity(mTempPhotoUri); 364 } catch (ActivityNotFoundException e) { 365 Toast.makeText( 366 mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show(); 367 } 368 } 369 370 /** 371 * Called when the user has completed selection of a photo. 372 * @throws FileNotFoundException 373 */ onPhotoSelected(Uri uri)374 public abstract void onPhotoSelected(Uri uri) throws FileNotFoundException; 375 376 /** 377 * Gets the current photo file that is being interacted with. It is the activity or 378 * fragment's responsibility to maintain this in saved state, since this handler instance 379 * will not survive rotation. 380 */ getCurrentPhotoUri()381 public abstract Uri getCurrentPhotoUri(); 382 383 /** 384 * Called when the photo selection dialog is dismissed. 385 */ onPhotoSelectionDismissed()386 public abstract void onPhotoSelectionDismissed(); 387 } 388 } 389