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