AttachPhotoActivity.java revision adeadcf055fee765df22c479fc0afc9a3e4d5e66
1/*
2 * Copyright (C) 2006 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.activities;
18
19import android.content.ContentResolver;
20import android.content.Intent;
21import android.content.Loader;
22import android.content.Loader.OnLoadCompleteListener;
23import android.content.pm.PackageManager;
24import android.database.Cursor;
25import android.graphics.Bitmap;
26import android.graphics.BitmapFactory;
27import android.net.Uri;
28import android.os.Bundle;
29import android.provider.ContactsContract.CommonDataKinds.Photo;
30import android.provider.ContactsContract.Contacts;
31import android.provider.ContactsContract.DisplayPhoto;
32import android.util.Log;
33
34import com.android.contacts.ContactSaveService;
35import com.android.contacts.ContactsActivity;
36import com.android.contacts.ContactsUtils;
37import com.android.contacts.model.Contact;
38import com.android.contacts.model.ContactLoader;
39import com.android.contacts.model.RawContactDelta;
40import com.android.contacts.model.RawContactDeltaList;
41import com.android.contacts.model.RawContactModifier;
42import com.android.contacts.common.model.account.AccountType;
43import com.android.contacts.common.model.ValuesDelta;
44import com.android.contacts.util.ContactPhotoUtils;
45
46import java.io.File;
47import java.io.FileNotFoundException;
48
49/**
50 * Provides an external interface for other applications to attach images
51 * to contacts. It will first present a contact picker and then run the
52 * image that is handed to it through the cropper to make the image the proper
53 * size and give the user a chance to use the face detector.
54 */
55public class AttachPhotoActivity extends ContactsActivity {
56    private static final String TAG = AttachPhotoActivity.class.getSimpleName();
57
58    private static final int REQUEST_PICK_CONTACT = 1;
59    private static final int REQUEST_CROP_PHOTO = 2;
60
61    private static final String KEY_CONTACT_URI = "contact_uri";
62    private static final String KEY_TEMP_PHOTO_URI = "temp_photo_uri";
63    private static final String KEY_CROPPED_PHOTO_URI = "cropped_photo_uri";
64
65    private Uri mTempPhotoUri;
66    private Uri mCroppedPhotoUri;
67
68    private ContentResolver mContentResolver;
69
70    // Height and width (in pixels) to request for the photo - queried from the provider.
71    private static int mPhotoDim;
72
73    private Uri mContactUri;
74
75    @Override
76    public void onCreate(Bundle icicle) {
77        super.onCreate(icicle);
78
79        if (icicle != null) {
80            final String uri = icicle.getString(KEY_CONTACT_URI);
81            mContactUri = (uri == null) ? null : Uri.parse(uri);
82            mTempPhotoUri = Uri.parse(icicle.getString(KEY_TEMP_PHOTO_URI));
83            mCroppedPhotoUri = Uri.parse(icicle.getString(KEY_CROPPED_PHOTO_URI));
84        } else {
85            mTempPhotoUri = ContactPhotoUtils.generateTempImageUri(this);
86            mCroppedPhotoUri = ContactPhotoUtils.generateTempCroppedImageUri(this);
87            Intent intent = new Intent(Intent.ACTION_PICK);
88            intent.setType(Contacts.CONTENT_TYPE);
89            startActivityForResult(intent, REQUEST_PICK_CONTACT);
90        }
91
92        mContentResolver = getContentResolver();
93
94        // Load the photo dimension to request.
95        Cursor c = mContentResolver.query(DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
96                new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null);
97        try {
98            c.moveToFirst();
99            mPhotoDim = c.getInt(0);
100        } finally {
101            c.close();
102        }
103    }
104
105    @Override
106    protected void onSaveInstanceState(Bundle outState) {
107        super.onSaveInstanceState(outState);
108        if (mContactUri != null) {
109            outState.putString(KEY_CONTACT_URI, mContactUri.toString());
110        }
111        if (mTempPhotoUri != null) {
112            outState.putString(KEY_TEMP_PHOTO_URI, mTempPhotoUri.toString());
113        }
114    }
115
116    @Override
117    protected void onActivityResult(int requestCode, int resultCode, Intent result) {
118        if (requestCode == REQUEST_PICK_CONTACT) {
119            if (resultCode != RESULT_OK) {
120                finish();
121                return;
122            }
123            // A contact was picked. Launch the cropper to get face detection, the right size, etc.
124            // TODO: get these values from constants somewhere
125            final Intent myIntent = getIntent();
126            final Uri inputUri = myIntent.getData();
127
128            final int perm = checkUriPermission(inputUri, android.os.Process.myPid(),
129                    android.os.Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION |
130                            Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
131
132            final Uri toCrop;
133
134            if (perm == PackageManager.PERMISSION_DENIED) {
135                // Work around to save a read-only URI into a temporary file provider URI so that
136                // we can add the FLAG_GRANT_WRITE_URI_PERMISSION flag to the eventual
137                // crop intent b/10837468
138                ContactPhotoUtils.savePhotoFromUriToUri(this, inputUri, mTempPhotoUri, false);
139                toCrop = mTempPhotoUri;
140            } else {
141                toCrop = inputUri;
142            }
143
144            final Intent intent = new Intent("com.android.camera.action.CROP", toCrop);
145            if (myIntent.getStringExtra("mimeType") != null) {
146                intent.setDataAndType(toCrop, myIntent.getStringExtra("mimeType"));
147            }
148            ContactPhotoUtils.addPhotoPickerExtras(intent, mCroppedPhotoUri);
149            ContactPhotoUtils.addCropExtras(intent, mPhotoDim);
150
151            startActivityForResult(intent, REQUEST_CROP_PHOTO);
152
153            mContactUri = result.getData();
154
155        } else if (requestCode == REQUEST_CROP_PHOTO) {
156            // Delete the temporary photo from cache now that we have a cropped version.
157            // We should do this even if the crop failed and we eventually bail
158            getContentResolver().delete(mTempPhotoUri, null, null);
159            if (resultCode != RESULT_OK) {
160                finish();
161                return;
162            }
163            loadContact(mContactUri, new Listener() {
164                @Override
165                public void onContactLoaded(Contact contact) {
166                    saveContact(contact);
167                }
168            });
169        }
170    }
171
172    // TODO: consider moving this to ContactLoader, especially if we keep adding similar
173    // code elsewhere (ViewNotificationService is another case).  The only concern is that,
174    // although this is convenient, it isn't quite as robust as using LoaderManager... for
175    // instance, the loader doesn't persist across Activity restarts.
176    private void loadContact(Uri contactUri, final Listener listener) {
177        final ContactLoader loader = new ContactLoader(this, contactUri, true);
178        loader.registerListener(0, new OnLoadCompleteListener<Contact>() {
179            @Override
180            public void onLoadComplete(
181                    Loader<Contact> loader, Contact contact) {
182                try {
183                    loader.reset();
184                }
185                catch (RuntimeException e) {
186                    Log.e(TAG, "Error resetting loader", e);
187                }
188                listener.onContactLoaded(contact);
189            }
190        });
191        loader.startLoading();
192    }
193
194    private interface Listener {
195        public void onContactLoaded(Contact contact);
196    }
197
198    /**
199     * If prerequisites have been met, attach the photo to a raw-contact and save.
200     * The prerequisites are:
201     * - photo has been cropped
202     * - contact has been loaded
203     */
204    private void saveContact(Contact contact) {
205
206        // Obtain the raw-contact that we will save to.
207        RawContactDeltaList deltaList = contact.createRawContactDeltaList();
208        RawContactDelta raw = deltaList.getFirstWritableRawContact(this);
209        if (raw == null) {
210            Log.w(TAG, "no writable raw-contact found");
211            return;
212        }
213
214        // Create a scaled, compressed bitmap to add to the entity-delta list.
215        final int size = ContactsUtils.getThumbnailSize(this);
216        Bitmap bitmap;
217        try {
218            bitmap = ContactPhotoUtils.getBitmapFromUri(this, mCroppedPhotoUri);
219        } catch (FileNotFoundException e) {
220            Log.w(TAG, "Could not find bitmap");
221            return;
222        }
223
224        final Bitmap scaled = Bitmap.createScaledBitmap(bitmap, size, size, false);
225        final byte[] compressed = ContactPhotoUtils.compressBitmap(scaled);
226        if (compressed == null) {
227            Log.w(TAG, "could not create scaled and compressed Bitmap");
228            return;
229        }
230        // Add compressed bitmap to entity-delta... this allows us to save to
231        // a new contact; otherwise the entity-delta-list would be empty, and
232        // the ContactSaveService would not create the new contact, and the
233        // full-res photo would fail to be saved to the non-existent contact.
234        AccountType account = raw.getRawContactAccountType(this);
235        ValuesDelta values =
236                RawContactModifier.ensureKindExists(raw, account, Photo.CONTENT_ITEM_TYPE);
237        if (values == null) {
238            Log.w(TAG, "cannot attach photo to this account type");
239            return;
240        }
241        values.setPhoto(compressed);
242
243        // Finally, invoke the ContactSaveService.
244        Log.v(TAG, "all prerequisites met, about to save photo to contact");
245        Intent intent = ContactSaveService.createSaveContactIntent(
246                this,
247                deltaList,
248                "", 0,
249                contact.isUserProfile(),
250                null, null,
251                raw.getRawContactId(),
252                mCroppedPhotoUri
253                );
254        startService(intent);
255        finish();
256    }
257}
258