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