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