blob: 3053714b96fe9851cd6587236df2ce889f075343 [file] [log] [blame]
Dave Santoro6fa73842011-09-28 14:37:06 -07001/*
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
Dave Santoro6fa73842011-09-28 14:37:06 -070019import android.app.Activity;
20import android.content.ActivityNotFoundException;
Daniel Lehmann9a409d42012-04-24 11:29:51 -070021import android.content.ContentValues;
Dave Santoro6fa73842011-09-28 14:37:06 -070022import android.content.Context;
23import android.content.Intent;
Brian Attwell5234be92015-05-13 19:26:43 -070024import android.content.pm.PackageManager;
25import android.content.pm.ResolveInfo;
Dave Santoro6fa73842011-09-28 14:37:06 -070026import android.database.Cursor;
Dave Santoro6fa73842011-09-28 14:37:06 -070027import android.net.Uri;
Dave Santoro6fa73842011-09-28 14:37:06 -070028import android.provider.ContactsContract.CommonDataKinds.Photo;
29import android.provider.ContactsContract.DisplayPhoto;
Daniel Lehmann9a409d42012-04-24 11:29:51 -070030import android.provider.ContactsContract.RawContacts;
Dave Santoro6fa73842011-09-28 14:37:06 -070031import android.provider.MediaStore;
32import android.util.Log;
33import android.view.View;
34import android.view.View.OnClickListener;
35import android.widget.ListPopupWindow;
36import android.widget.PopupWindow.OnDismissListener;
37import android.widget.Toast;
Chiao Cheng61414c22012-06-18 16:59:06 -070038
39import com.android.contacts.R;
40import com.android.contacts.editor.PhotoActionPopup;
Gary Mai69c182a2016-12-05 13:07:03 -080041import com.android.contacts.model.AccountTypeManager;
Gary Mai69c182a2016-12-05 13:07:03 -080042import com.android.contacts.model.RawContactDelta;
Gary Mai0a49afa2016-12-05 15:53:58 -080043import com.android.contacts.model.RawContactDeltaList;
44import com.android.contacts.model.RawContactModifier;
Gary Mai69c182a2016-12-05 13:07:03 -080045import com.android.contacts.model.ValuesDelta;
46import com.android.contacts.model.account.AccountType;
Chiao Cheng61414c22012-06-18 16:59:06 -070047import com.android.contacts.util.ContactPhotoUtils;
Chiao Cheng619ac162012-12-20 14:29:03 -080048import com.android.contacts.util.UiClosables;
Chiao Cheng61414c22012-06-18 16:59:06 -070049
Yorke Lee637a38e2013-09-14 08:36:33 -070050import java.io.FileNotFoundException;
Brian Attwell5234be92015-05-13 19:26:43 -070051import java.util.List;
Dave Santoro6fa73842011-09-28 14:37:06 -070052
53/**
54 * Handles displaying a photo selection popup for a given photo view and dealing with the results
55 * that come back.
56 */
Josh Garguse5d3f892012-04-11 11:56:15 -070057public abstract class PhotoSelectionHandler implements OnClickListener {
Dave Santoro6fa73842011-09-28 14:37:06 -070058
59 private static final String TAG = PhotoSelectionHandler.class.getSimpleName();
60
Dave Santoro6fa73842011-09-28 14:37:06 -070061 private static final int REQUEST_CODE_CAMERA_WITH_DATA = 1001;
62 private static final int REQUEST_CODE_PHOTO_PICKED_WITH_DATA = 1002;
Yorke Lee637a38e2013-09-14 08:36:33 -070063 private static final int REQUEST_CROP_PHOTO = 1003;
Dave Santoro6fa73842011-09-28 14:37:06 -070064
Jay Shraunerb126f4a2014-01-18 10:45:01 -080065 // 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
Josh Garguse5d3f892012-04-11 11:56:15 -070070 protected final Context mContext;
Brian Attwell7e670822014-11-07 16:36:30 -080071 private final View mChangeAnchorView;
Dave Santoro6fa73842011-09-28 14:37:06 -070072 private final int mPhotoMode;
73 private final int mPhotoPickSize;
Yorke Lee637a38e2013-09-14 08:36:33 -070074 private final Uri mCroppedPhotoUri;
75 private final Uri mTempPhotoUri;
Maurice Chu851222a2012-06-21 11:43:08 -070076 private final RawContactDeltaList mState;
Dave Santoro6fa73842011-09-28 14:37:06 -070077 private final boolean mIsDirectoryContact;
78 private ListPopupWindow mPopup;
Dave Santoro6fa73842011-09-28 14:37:06 -070079
Brian Attwell7e670822014-11-07 16:36:30 -080080 public PhotoSelectionHandler(Context context, View changeAnchorView, int photoMode,
Maurice Chu851222a2012-06-21 11:43:08 -070081 boolean isDirectoryContact, RawContactDeltaList state) {
Dave Santoro6fa73842011-09-28 14:37:06 -070082 mContext = context;
Brian Attwell7e670822014-11-07 16:36:30 -080083 mChangeAnchorView = changeAnchorView;
Dave Santoro6fa73842011-09-28 14:37:06 -070084 mPhotoMode = photoMode;
Yorke Lee637a38e2013-09-14 08:36:33 -070085 mTempPhotoUri = ContactPhotoUtils.generateTempImageUri(context);
86 mCroppedPhotoUri = ContactPhotoUtils.generateTempCroppedImageUri(mContext);
Dave Santoro6fa73842011-09-28 14:37:06 -070087 mIsDirectoryContact = isDirectoryContact;
88 mState = state;
89 mPhotoPickSize = getPhotoPickSize();
90 }
91
92 public void destroy() {
Chiao Cheng619ac162012-12-20 14:29:03 -080093 UiClosables.closeQuietly(mPopup);
Dave Santoro6fa73842011-09-28 14:37:06 -070094 }
95
Josh Garguse5d3f892012-04-11 11:56:15 -070096 public abstract PhotoActionListener getListener();
Dave Santoro6fa73842011-09-28 14:37:06 -070097
98 @Override
99 public void onClick(View v) {
Daniel Lehmann9a409d42012-04-24 11:29:51 -0700100 final PhotoActionListener listener = getListener();
101 if (listener != null) {
Dave Santoro6fa73842011-09-28 14:37:06 -0700102 if (getWritableEntityIndex() != -1) {
103 mPopup = PhotoActionPopup.createPopupMenu(
Brian Attwell7e670822014-11-07 16:36:30 -0800104 mContext, mChangeAnchorView, listener, mPhotoMode);
Dave Santoro6fa73842011-09-28 14:37:06 -0700105 mPopup.setOnDismissListener(new OnDismissListener() {
106 @Override
107 public void onDismiss() {
Josh Garguse5d3f892012-04-11 11:56:15 -0700108 listener.onPhotoSelectionDismissed();
Dave Santoro6fa73842011-09-28 14:37:06 -0700109 }
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) {
Daniel Lehmann9a409d42012-04-24 11:29:51 -0700125 final PhotoActionListener listener = getListener();
Dave Santoro6fa73842011-09-28 14:37:06 -0700126 if (resultCode == Activity.RESULT_OK) {
127 switch (requestCode) {
Yorke Lee637a38e2013-09-14 08:36:33 -0700128 // Cropped photo was returned
129 case REQUEST_CROP_PHOTO: {
Yorke Lee637a38e2013-09-14 08:36:33 -0700130 if (data != null && data.getData() != null) {
Gary Mai84b16542016-12-15 17:25:56 -0800131 final Uri croppedUri = data.getData();
132 ContactPhotoUtils.savePhotoFromUriToUri(mContext, croppedUri,
133 mCroppedPhotoUri, /* deleteAfterSave */ false);
Yorke Lee637a38e2013-09-14 08:36:33 -0700134 }
135
136 try {
137 // delete the original temporary photo if it exists
138 mContext.getContentResolver().delete(mTempPhotoUri, null, null);
Gary Mai84b16542016-12-15 17:25:56 -0800139 listener.onPhotoSelected(mCroppedPhotoUri);
Yorke Lee637a38e2013-09-14 08:36:33 -0700140 return true;
141 } catch (FileNotFoundException e) {
142 return false;
143 }
Dave Santoro6fa73842011-09-28 14:37:06 -0700144 }
Yorke Lee637a38e2013-09-14 08:36:33 -0700145
146 // Photo was successfully taken or selected from gallery, now crop it.
147 case REQUEST_CODE_PHOTO_PICKED_WITH_DATA:
148 case REQUEST_CODE_CAMERA_WITH_DATA:
149 final Uri uri;
150 boolean isWritable = false;
151 if (data != null && data.getData() != null) {
152 uri = data.getData();
153 } else {
154 uri = listener.getCurrentPhotoUri();
155 isWritable = true;
156 }
157 final Uri toCrop;
158 if (isWritable) {
159 // Since this uri belongs to our file provider, we know that it is writable
160 // by us. This means that we don't have to save it into another temporary
161 // location just to be able to crop it.
162 toCrop = uri;
163 } else {
164 toCrop = mTempPhotoUri;
165 try {
Jay Shraunera9433712015-02-27 15:47:09 -0800166 if (!ContactPhotoUtils.savePhotoFromUriToUri(mContext, uri,
167 toCrop, false)) {
168 return false;
169 }
Yorke Lee637a38e2013-09-14 08:36:33 -0700170 } catch (SecurityException e) {
171 Log.d(TAG, "Did not have read-access to uri : " + uri);
172 return false;
173 }
174 }
175
176 doCropPhoto(toCrop, mCroppedPhotoUri);
Dave Santoro6fa73842011-09-28 14:37:06 -0700177 return true;
Dave Santoro6fa73842011-09-28 14:37:06 -0700178 }
179 }
180 return false;
181 }
182
183 /**
184 * Return the index of the first entity in the contact data that belongs to a contact-writable
185 * account, or -1 if no such entity exists.
186 */
187 private int getWritableEntityIndex() {
188 // Directory entries are non-writable.
Josh Garguse5d3f892012-04-11 11:56:15 -0700189 if (mIsDirectoryContact) return -1;
190 return mState.indexOfFirstWritableRawContact(mContext);
Dave Santoro6fa73842011-09-28 14:37:06 -0700191 }
192
193 /**
Josh Garguse692e012012-01-18 14:53:11 -0800194 * Return the raw-contact id of the first entity in the contact data that belongs to a
195 * contact-writable account, or -1 if no such entity exists.
196 */
197 protected long getWritableEntityId() {
198 int index = getWritableEntityIndex();
199 if (index == -1) return -1;
200 return mState.get(index).getValues().getId();
201 }
202
203 /**
Dave Santoro6fa73842011-09-28 14:37:06 -0700204 * Utility method to retrieve the entity delta for attaching the given bitmap to the contact.
205 * This will attach the photo to the first contact-writable account that provided data to the
206 * contact. It is the caller's responsibility to apply the delta.
Dave Santoro6fa73842011-09-28 14:37:06 -0700207 * @return An entity delta list that can be applied to associate the bitmap with the contact,
208 * or null if the photo could not be parsed or none of the accounts associated with the
209 * contact are writable.
210 */
Maurice Chu851222a2012-06-21 11:43:08 -0700211 public RawContactDeltaList getDeltaForAttachingPhotoToContact() {
Dave Santoro6fa73842011-09-28 14:37:06 -0700212 // Find the first writable entity.
213 int writableEntityIndex = getWritableEntityIndex();
214 if (writableEntityIndex != -1) {
Daniel Lehmann9a409d42012-04-24 11:29:51 -0700215 // We are guaranteed to have contact data if we have a writable entity index.
Maurice Chu851222a2012-06-21 11:43:08 -0700216 final RawContactDelta delta = mState.get(writableEntityIndex);
Daniel Lehmann9a409d42012-04-24 11:29:51 -0700217
218 // Need to find the right account so that EntityModifier knows which fields to add
219 final ContentValues entityValues = delta.getValues().getCompleteValues();
220 final String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
221 final String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
222 final AccountType accountType = AccountTypeManager.getInstance(mContext).getAccountType(
223 type, dataSet);
224
Maurice Chu851222a2012-06-21 11:43:08 -0700225 final ValuesDelta child = RawContactModifier.ensureKindExists(
Daniel Lehmann9a409d42012-04-24 11:29:51 -0700226 delta, accountType, Photo.CONTENT_ITEM_TYPE);
Dave Santoro6fa73842011-09-28 14:37:06 -0700227 child.setFromTemplate(false);
Maurice Chu851222a2012-06-21 11:43:08 -0700228 child.setSuperPrimary(true);
Dave Santoro6fa73842011-09-28 14:37:06 -0700229
230 return mState;
231 }
232 return null;
233 }
234
Josh Garguse5d3f892012-04-11 11:56:15 -0700235 /** Used by subclasses to delegate to their enclosing Activity or Fragment. */
Yorke Lee637a38e2013-09-14 08:36:33 -0700236 protected abstract void startPhotoActivity(Intent intent, int requestCode, Uri photoUri);
Josh Garguse5d3f892012-04-11 11:56:15 -0700237
Dave Santoro6fa73842011-09-28 14:37:06 -0700238 /**
239 * Sends a newly acquired photo to Gallery for cropping
240 */
Yorke Lee637a38e2013-09-14 08:36:33 -0700241 private void doCropPhoto(Uri inputUri, Uri outputUri) {
Brian Attwell5234be92015-05-13 19:26:43 -0700242 final Intent intent = getCropImageIntent(inputUri, outputUri);
243 if (!hasIntentHandler(intent)) {
244 try {
245 getListener().onPhotoSelected(inputUri);
246 } catch (FileNotFoundException e) {
247 Log.e(TAG, "Cannot save uncropped photo", e);
248 Toast.makeText(mContext, R.string.contactPhotoSavedErrorToast,
249 Toast.LENGTH_LONG).show();
250 }
251 return;
252 }
Dave Santoro6fa73842011-09-28 14:37:06 -0700253 try {
Dave Santoro6fa73842011-09-28 14:37:06 -0700254 // Launch gallery to crop the photo
Yorke Lee637a38e2013-09-14 08:36:33 -0700255 startPhotoActivity(intent, REQUEST_CROP_PHOTO, inputUri);
Dave Santoro6fa73842011-09-28 14:37:06 -0700256 } catch (Exception e) {
257 Log.e(TAG, "Cannot crop image", e);
258 Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
259 }
260 }
261
Josh Garguse5d3f892012-04-11 11:56:15 -0700262 /**
263 * Should initiate an activity to take a photo using the camera.
264 * @param photoFile The file path that will be used to store the photo. This is generally
265 * what should be returned by
266 * {@link PhotoSelectionHandler.PhotoActionListener#getCurrentPhotoFile()}.
267 */
Yorke Lee637a38e2013-09-14 08:36:33 -0700268 private void startTakePhotoActivity(Uri photoUri) {
269 final Intent intent = getTakePhotoIntent(photoUri);
270 startPhotoActivity(intent, REQUEST_CODE_CAMERA_WITH_DATA, photoUri);
Dave Santoro6fa73842011-09-28 14:37:06 -0700271 }
272
Josh Garguse5d3f892012-04-11 11:56:15 -0700273 /**
274 * Should initiate an activity pick a photo from the gallery.
275 * @param photoFile The temporary file that the cropped image is written to before being
276 * stored by the content-provider.
277 * {@link PhotoSelectionHandler#handlePhotoActivityResult(int, int, Intent)}.
278 */
Yorke Lee637a38e2013-09-14 08:36:33 -0700279 private void startPickFromGalleryActivity(Uri photoUri) {
280 final Intent intent = getPhotoPickIntent(photoUri);
281 startPhotoActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, photoUri);
Josh Garguse692e012012-01-18 14:53:11 -0800282 }
283
Dave Santoro6fa73842011-09-28 14:37:06 -0700284 private int getPhotoPickSize() {
Jay Shraunerb126f4a2014-01-18 10:45:01 -0800285 if (mPhotoDim != 0) {
286 return mPhotoDim;
287 }
288
Dave Santoro6fa73842011-09-28 14:37:06 -0700289 // Note that this URI is safe to call on the UI thread.
290 Cursor c = mContext.getContentResolver().query(DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
291 new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null);
Jay Shraunerb126f4a2014-01-18 10:45:01 -0800292 if (c != null) {
293 try {
294 if (c.moveToFirst()) {
295 mPhotoDim = c.getInt(0);
296 }
297 } finally {
298 c.close();
299 }
Dave Santoro6fa73842011-09-28 14:37:06 -0700300 }
Jay Shraunerb126f4a2014-01-18 10:45:01 -0800301 return mPhotoDim != 0 ? mPhotoDim : mDefaultPhotoDim;
Dave Santoro6fa73842011-09-28 14:37:06 -0700302 }
303
304 /**
Yorke Lee637a38e2013-09-14 08:36:33 -0700305 * Constructs an intent for capturing a photo and storing it in a temporary output uri.
Dave Santoro6fa73842011-09-28 14:37:06 -0700306 */
Yorke Lee637a38e2013-09-14 08:36:33 -0700307 private Intent getTakePhotoIntent(Uri outputUri) {
308 final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE, null);
309 ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri);
310 return intent;
311 }
312
313 /**
314 * Constructs an intent for picking a photo from Gallery, and returning the bitmap.
315 */
316 private Intent getPhotoPickIntent(Uri outputUri) {
Wenyi Wangbfea74f2015-10-19 13:30:18 -0700317 final Intent intent = new Intent(Intent.ACTION_PICK, null);
Dave Santoro6fa73842011-09-28 14:37:06 -0700318 intent.setType("image/*");
Yorke Lee637a38e2013-09-14 08:36:33 -0700319 ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri);
Dave Santoro6fa73842011-09-28 14:37:06 -0700320 return intent;
321 }
322
Brian Attwell5234be92015-05-13 19:26:43 -0700323 private boolean hasIntentHandler(Intent intent) {
324 final List<ResolveInfo> resolveInfo = mContext.getPackageManager()
325 .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
326 return resolveInfo != null && resolveInfo.size() > 0;
327 }
328
Dave Santoro6fa73842011-09-28 14:37:06 -0700329 /**
330 * Constructs an intent for image cropping.
331 */
Yorke Lee637a38e2013-09-14 08:36:33 -0700332 private Intent getCropImageIntent(Uri inputUri, Uri outputUri) {
Dave Santoro6fa73842011-09-28 14:37:06 -0700333 Intent intent = new Intent("com.android.camera.action.CROP");
Yorke Lee637a38e2013-09-14 08:36:33 -0700334 intent.setDataAndType(inputUri, "image/*");
335 ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri);
336 ContactPhotoUtils.addCropExtras(intent, mPhotoPickSize);
Dave Santoro6fa73842011-09-28 14:37:06 -0700337 return intent;
338 }
339
340 public abstract class PhotoActionListener implements PhotoActionPopup.Listener {
341 @Override
Dave Santoro6fa73842011-09-28 14:37:06 -0700342 public void onRemovePictureChosen() {
343 // No default implementation.
344 }
345
346 @Override
347 public void onTakePhotoChosen() {
348 try {
349 // Launch camera to take photo for selected contact
Yorke Lee637a38e2013-09-14 08:36:33 -0700350 startTakePhotoActivity(mTempPhotoUri);
Dave Santoro6fa73842011-09-28 14:37:06 -0700351 } catch (ActivityNotFoundException e) {
Josh Gargusebc17922012-05-04 18:47:09 -0700352 Toast.makeText(
353 mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
Dave Santoro6fa73842011-09-28 14:37:06 -0700354 }
355 }
356
357 @Override
358 public void onPickFromGalleryChosen() {
359 try {
360 // Launch picker to choose photo for selected contact
Yorke Lee637a38e2013-09-14 08:36:33 -0700361 startPickFromGalleryActivity(mTempPhotoUri);
Dave Santoro6fa73842011-09-28 14:37:06 -0700362 } catch (ActivityNotFoundException e) {
Josh Gargusebc17922012-05-04 18:47:09 -0700363 Toast.makeText(
364 mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
Dave Santoro6fa73842011-09-28 14:37:06 -0700365 }
366 }
367
368 /**
Dave Santoro6fa73842011-09-28 14:37:06 -0700369 * Called when the user has completed selection of a photo.
Yorke Lee637a38e2013-09-14 08:36:33 -0700370 * @throws FileNotFoundException
Dave Santoro6fa73842011-09-28 14:37:06 -0700371 */
Yorke Lee637a38e2013-09-14 08:36:33 -0700372 public abstract void onPhotoSelected(Uri uri) throws FileNotFoundException;
Dave Santoro6fa73842011-09-28 14:37:06 -0700373
374 /**
375 * Gets the current photo file that is being interacted with. It is the activity or
376 * fragment's responsibility to maintain this in saved state, since this handler instance
377 * will not survive rotation.
378 */
Yorke Lee637a38e2013-09-14 08:36:33 -0700379 public abstract Uri getCurrentPhotoUri();
Dave Santoro6fa73842011-09-28 14:37:06 -0700380
381 /**
382 * Called when the photo selection dialog is dismissed.
383 */
384 public abstract void onPhotoSelectionDismissed();
385 }
386}