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 final ResolveInfo intentHandler = getIntentHandler(intent); 246 if (intentHandler == null) { 247 try { 248 getListener().onPhotoSelected(inputUri); 249 } catch (FileNotFoundException e) { 250 Log.e(TAG, "Cannot save uncropped photo", e); 251 Toast.makeText(mContext, R.string.contactPhotoSavedErrorToast, 252 Toast.LENGTH_LONG).show(); 253 } 254 return; 255 } 256 intent.setPackage(intentHandler.activityInfo.packageName); 257 try { 258 // Launch gallery to crop the photo 259 startPhotoActivity(intent, REQUEST_CROP_PHOTO, inputUri); 260 } catch (Exception e) { 261 Log.e(TAG, "Cannot crop image", e); 262 Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show(); 263 } 264 } 265 266 /** 267 * Should initiate an activity to take a photo using the camera. 268 * @param photoFile The file path that will be used to store the photo. This is generally 269 * what should be returned by 270 * {@link PhotoSelectionHandler.PhotoActionListener#getCurrentPhotoFile()}. 271 */ startTakePhotoActivity(Uri photoUri)272 private void startTakePhotoActivity(Uri photoUri) { 273 final Intent intent = getTakePhotoIntent(photoUri); 274 startPhotoActivity(intent, REQUEST_CODE_CAMERA_WITH_DATA, photoUri); 275 } 276 277 /** 278 * Should initiate an activity pick a photo from the gallery. 279 * @param photoFile The temporary file that the cropped image is written to before being 280 * stored by the content-provider. 281 * {@link PhotoSelectionHandler#handlePhotoActivityResult(int, int, Intent)}. 282 */ startPickFromGalleryActivity(Uri photoUri)283 private void startPickFromGalleryActivity(Uri photoUri) { 284 final Intent intent = getPhotoPickIntent(photoUri); 285 startPhotoActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, photoUri); 286 } 287 getPhotoPickSize()288 private int getPhotoPickSize() { 289 if (mPhotoDim != 0) { 290 return mPhotoDim; 291 } 292 293 // Note that this URI is safe to call on the UI thread. 294 Cursor c = mContext.getContentResolver().query(DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, 295 new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null); 296 if (c != null) { 297 try { 298 if (c.moveToFirst()) { 299 mPhotoDim = c.getInt(0); 300 } 301 } finally { 302 c.close(); 303 } 304 } 305 return mPhotoDim != 0 ? mPhotoDim : mDefaultPhotoDim; 306 } 307 308 /** 309 * Constructs an intent for capturing a photo and storing it in a temporary output uri. 310 */ getTakePhotoIntent(Uri outputUri)311 private Intent getTakePhotoIntent(Uri outputUri) { 312 final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE, null); 313 ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri); 314 return intent; 315 } 316 317 /** 318 * Constructs an intent for picking a photo from Gallery, and returning the bitmap. 319 */ getPhotoPickIntent(Uri outputUri)320 private Intent getPhotoPickIntent(Uri outputUri) { 321 final Intent intent = new Intent(Intent.ACTION_PICK, null); 322 intent.setType("image/*"); 323 ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri); 324 return intent; 325 } 326 getIntentHandler(Intent intent)327 private ResolveInfo getIntentHandler(Intent intent) { 328 final List<ResolveInfo> resolveInfos = mContext.getPackageManager() 329 .queryIntentActivities(intent, 330 PackageManager.MATCH_DEFAULT_ONLY | PackageManager.MATCH_SYSTEM_ONLY); 331 return (resolveInfos != null && resolveInfos.size() > 0) ? resolveInfos.get(0) : null; 332 } 333 334 /** 335 * Constructs an intent for image cropping. 336 */ getCropImageIntent(Uri inputUri, Uri outputUri)337 private Intent getCropImageIntent(Uri inputUri, Uri outputUri) { 338 Intent intent = new Intent("com.android.camera.action.CROP"); 339 intent.setDataAndType(inputUri, "image/*"); 340 ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri); 341 ContactPhotoUtils.addCropExtras(intent, mPhotoPickSize); 342 return intent; 343 } 344 345 public abstract class PhotoActionListener implements PhotoActionPopup.Listener { 346 @Override onRemovePictureChosen()347 public void onRemovePictureChosen() { 348 // No default implementation. 349 } 350 351 @Override onTakePhotoChosen()352 public void onTakePhotoChosen() { 353 try { 354 // Launch camera to take photo for selected contact 355 startTakePhotoActivity(mTempPhotoUri); 356 } catch (ActivityNotFoundException e) { 357 Toast.makeText( 358 mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show(); 359 } 360 } 361 362 @Override onPickFromGalleryChosen()363 public void onPickFromGalleryChosen() { 364 try { 365 // Launch picker to choose photo for selected contact 366 startPickFromGalleryActivity(mTempPhotoUri); 367 } catch (ActivityNotFoundException e) { 368 Toast.makeText( 369 mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show(); 370 } 371 } 372 373 /** 374 * Called when the user has completed selection of a photo. 375 * @throws FileNotFoundException 376 */ onPhotoSelected(Uri uri)377 public abstract void onPhotoSelected(Uri uri) throws FileNotFoundException; 378 379 /** 380 * Gets the current photo file that is being interacted with. It is the activity or 381 * fragment's responsibility to maintain this in saved state, since this handler instance 382 * will not survive rotation. 383 */ getCurrentPhotoUri()384 public abstract Uri getCurrentPhotoUri(); 385 386 /** 387 * Called when the photo selection dialog is dismissed. 388 */ onPhotoSelectionDismissed()389 public abstract void onPhotoSelectionDismissed(); 390 } 391 } 392