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