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.common.model.AccountTypeManager; 42 import com.android.contacts.common.model.RawContactModifier; 43 import com.android.contacts.common.model.RawContactDelta; 44 import com.android.contacts.common.model.ValuesDelta; 45 import com.android.contacts.common.model.account.AccountType; 46 import com.android.contacts.common.model.RawContactDeltaList; 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 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 92 public void destroy() { 93 UiClosables.closeQuietly(mPopup); 94 } 95 96 public abstract PhotoActionListener getListener(); 97 98 @Override 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 */ 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 final Uri uri; 131 if (data != null && data.getData() != null) { 132 uri = data.getData(); 133 } else { 134 uri = mCroppedPhotoUri; 135 } 136 137 try { 138 // delete the original temporary photo if it exists 139 mContext.getContentResolver().delete(mTempPhotoUri, null, null); 140 listener.onPhotoSelected(uri); 141 return true; 142 } catch (FileNotFoundException e) { 143 return false; 144 } 145 } 146 147 // Photo was successfully taken or selected from gallery, now crop it. 148 case REQUEST_CODE_PHOTO_PICKED_WITH_DATA: 149 case REQUEST_CODE_CAMERA_WITH_DATA: 150 final Uri uri; 151 boolean isWritable = false; 152 if (data != null && data.getData() != null) { 153 uri = data.getData(); 154 } else { 155 uri = listener.getCurrentPhotoUri(); 156 isWritable = true; 157 } 158 final Uri toCrop; 159 if (isWritable) { 160 // Since this uri belongs to our file provider, we know that it is writable 161 // by us. This means that we don't have to save it into another temporary 162 // location just to be able to crop it. 163 toCrop = uri; 164 } else { 165 toCrop = mTempPhotoUri; 166 try { 167 if (!ContactPhotoUtils.savePhotoFromUriToUri(mContext, uri, 168 toCrop, false)) { 169 return false; 170 } 171 } catch (SecurityException e) { 172 Log.d(TAG, "Did not have read-access to uri : " + uri); 173 return false; 174 } 175 } 176 177 doCropPhoto(toCrop, mCroppedPhotoUri); 178 return true; 179 } 180 } 181 return false; 182 } 183 184 /** 185 * Return the index of the first entity in the contact data that belongs to a contact-writable 186 * account, or -1 if no such entity exists. 187 */ 188 private int getWritableEntityIndex() { 189 // Directory entries are non-writable. 190 if (mIsDirectoryContact) return -1; 191 return mState.indexOfFirstWritableRawContact(mContext); 192 } 193 194 /** 195 * Return the raw-contact id of the first entity in the contact data that belongs to a 196 * contact-writable account, or -1 if no such entity exists. 197 */ 198 protected long getWritableEntityId() { 199 int index = getWritableEntityIndex(); 200 if (index == -1) return -1; 201 return mState.get(index).getValues().getId(); 202 } 203 204 /** 205 * Utility method to retrieve the entity delta for attaching the given bitmap to the contact. 206 * This will attach the photo to the first contact-writable account that provided data to the 207 * contact. It is the caller's responsibility to apply the delta. 208 * @return An entity delta list that can be applied to associate the bitmap with the contact, 209 * or null if the photo could not be parsed or none of the accounts associated with the 210 * contact are writable. 211 */ 212 public RawContactDeltaList getDeltaForAttachingPhotoToContact() { 213 // Find the first writable entity. 214 int writableEntityIndex = getWritableEntityIndex(); 215 if (writableEntityIndex != -1) { 216 // We are guaranteed to have contact data if we have a writable entity index. 217 final RawContactDelta delta = mState.get(writableEntityIndex); 218 219 // Need to find the right account so that EntityModifier knows which fields to add 220 final ContentValues entityValues = delta.getValues().getCompleteValues(); 221 final String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE); 222 final String dataSet = entityValues.getAsString(RawContacts.DATA_SET); 223 final AccountType accountType = AccountTypeManager.getInstance(mContext).getAccountType( 224 type, dataSet); 225 226 final ValuesDelta child = RawContactModifier.ensureKindExists( 227 delta, accountType, Photo.CONTENT_ITEM_TYPE); 228 child.setFromTemplate(false); 229 child.setSuperPrimary(true); 230 231 return mState; 232 } 233 return null; 234 } 235 236 /** Used by subclasses to delegate to their enclosing Activity or Fragment. */ 237 protected abstract void startPhotoActivity(Intent intent, int requestCode, Uri photoUri); 238 239 /** 240 * Sends a newly acquired photo to Gallery for cropping 241 */ 242 private void doCropPhoto(Uri inputUri, Uri outputUri) { 243 final Intent intent = getCropImageIntent(inputUri, outputUri); 244 if (!hasIntentHandler(intent)) { 245 try { 246 getListener().onPhotoSelected(inputUri); 247 } catch (FileNotFoundException e) { 248 Log.e(TAG, "Cannot save uncropped photo", e); 249 Toast.makeText(mContext, R.string.contactPhotoSavedErrorToast, 250 Toast.LENGTH_LONG).show(); 251 } 252 return; 253 } 254 try { 255 // Launch gallery to crop the photo 256 startPhotoActivity(intent, REQUEST_CROP_PHOTO, inputUri); 257 } catch (Exception e) { 258 Log.e(TAG, "Cannot crop image", e); 259 Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show(); 260 } 261 } 262 263 /** 264 * Should initiate an activity to take a photo using the camera. 265 * @param photoFile The file path that will be used to store the photo. This is generally 266 * what should be returned by 267 * {@link PhotoSelectionHandler.PhotoActionListener#getCurrentPhotoFile()}. 268 */ 269 private void startTakePhotoActivity(Uri photoUri) { 270 final Intent intent = getTakePhotoIntent(photoUri); 271 startPhotoActivity(intent, REQUEST_CODE_CAMERA_WITH_DATA, photoUri); 272 } 273 274 /** 275 * Should initiate an activity pick a photo from the gallery. 276 * @param photoFile The temporary file that the cropped image is written to before being 277 * stored by the content-provider. 278 * {@link PhotoSelectionHandler#handlePhotoActivityResult(int, int, Intent)}. 279 */ 280 private void startPickFromGalleryActivity(Uri photoUri) { 281 final Intent intent = getPhotoPickIntent(photoUri); 282 startPhotoActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, photoUri); 283 } 284 285 private int getPhotoPickSize() { 286 if (mPhotoDim != 0) { 287 return mPhotoDim; 288 } 289 290 // Note that this URI is safe to call on the UI thread. 291 Cursor c = mContext.getContentResolver().query(DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, 292 new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null); 293 if (c != null) { 294 try { 295 if (c.moveToFirst()) { 296 mPhotoDim = c.getInt(0); 297 } 298 } finally { 299 c.close(); 300 } 301 } 302 return mPhotoDim != 0 ? mPhotoDim : mDefaultPhotoDim; 303 } 304 305 /** 306 * Constructs an intent for capturing a photo and storing it in a temporary output uri. 307 */ 308 private Intent getTakePhotoIntent(Uri outputUri) { 309 final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE, null); 310 ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri); 311 return intent; 312 } 313 314 /** 315 * Constructs an intent for picking a photo from Gallery, and returning the bitmap. 316 */ 317 private Intent getPhotoPickIntent(Uri outputUri) { 318 final Intent intent = new Intent(Intent.ACTION_PICK, null); 319 intent.setType("image/*"); 320 ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri); 321 return intent; 322 } 323 324 private boolean hasIntentHandler(Intent intent) { 325 final List<ResolveInfo> resolveInfo = mContext.getPackageManager() 326 .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); 327 return resolveInfo != null && resolveInfo.size() > 0; 328 } 329 330 /** 331 * Constructs an intent for image cropping. 332 */ 333 private Intent getCropImageIntent(Uri inputUri, Uri outputUri) { 334 Intent intent = new Intent("com.android.camera.action.CROP"); 335 intent.setDataAndType(inputUri, "image/*"); 336 ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri); 337 ContactPhotoUtils.addCropExtras(intent, mPhotoPickSize); 338 return intent; 339 } 340 341 public abstract class PhotoActionListener implements PhotoActionPopup.Listener { 342 @Override 343 public void onRemovePictureChosen() { 344 // No default implementation. 345 } 346 347 @Override 348 public void onTakePhotoChosen() { 349 try { 350 // Launch camera to take photo for selected contact 351 startTakePhotoActivity(mTempPhotoUri); 352 } catch (ActivityNotFoundException e) { 353 Toast.makeText( 354 mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show(); 355 } 356 } 357 358 @Override 359 public void onPickFromGalleryChosen() { 360 try { 361 // Launch picker to choose photo for selected contact 362 startPickFromGalleryActivity(mTempPhotoUri); 363 } catch (ActivityNotFoundException e) { 364 Toast.makeText( 365 mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show(); 366 } 367 } 368 369 /** 370 * Called when the user has completed selection of a photo. 371 * @throws FileNotFoundException 372 */ 373 public abstract void onPhotoSelected(Uri uri) throws FileNotFoundException; 374 375 /** 376 * Gets the current photo file that is being interacted with. It is the activity or 377 * fragment's responsibility to maintain this in saved state, since this handler instance 378 * will not survive rotation. 379 */ 380 public abstract Uri getCurrentPhotoUri(); 381 382 /** 383 * Called when the photo selection dialog is dismissed. 384 */ 385 public abstract void onPhotoSelectionDismissed(); 386 } 387 } 388