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.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 
     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 
     82     public void destroy() {
     83         UiClosables.closeQuietly(mPopup);
     84     }
     85 
     86     public abstract PhotoActionListener getListener();
     87 
     88     @Override
     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      */
    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      */
    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      */
    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      */
    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. */
    189     protected abstract void startPhotoActivity(Intent intent, int requestCode, String photoFile);
    190 
    191     /**
    192      * Sends a newly acquired photo to Gallery for cropping
    193      */
    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      */
    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      */
    234     private void startPickFromGalleryActivity(String photoFile) {
    235         final Intent intent = getPhotoPickIntent(photoFile);
    236         startPhotoActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, photoFile);
    237     }
    238 
    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      */
    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      */
    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      */
    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
    287         public void onUseAsPrimaryChosen() {
    288             // No default implementation.
    289         }
    290 
    291         @Override
    292         public void onRemovePictureChosen() {
    293             // No default implementation.
    294         }
    295 
    296         @Override
    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
    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          */
    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          */
    329         public abstract String getCurrentPhotoFile();
    330 
    331         /**
    332          * Called when the photo selection dialog is dismissed.
    333          */
    334         public abstract void onPhotoSelectionDismissed();
    335     }
    336 }
    337