• 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.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