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