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