AttachPhotoActivity.java revision 342de108624e4c3c796050624572d2f1f815dcd3
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.common.model.Contact;
37import com.android.contacts.common.model.ContactLoader;
38import com.android.contacts.common.model.RawContactDelta;
39import com.android.contacts.common.model.RawContactDeltaList;
40import com.android.contacts.common.model.RawContactModifier;
41import com.android.contacts.common.ContactsUtils;
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        if (mCroppedPhotoUri != null) {
115            outState.putString(KEY_CROPPED_PHOTO_URI, mCroppedPhotoUri.toString());
116        }
117    }
118
119    @Override
120    protected void onActivityResult(int requestCode, int resultCode, Intent result) {
121        if (requestCode == REQUEST_PICK_CONTACT) {
122            if (resultCode != RESULT_OK) {
123                finish();
124                return;
125            }
126            // A contact was picked. Launch the cropper to get face detection, the right size, etc.
127            // TODO: get these values from constants somewhere
128            final Intent myIntent = getIntent();
129            final Uri inputUri = myIntent.getData();
130
131            final int perm = checkUriPermission(inputUri, android.os.Process.myPid(),
132                    android.os.Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION |
133                            Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
134
135            final Uri toCrop;
136
137            if (perm == PackageManager.PERMISSION_DENIED) {
138                // Work around to save a read-only URI into a temporary file provider URI so that
139                // we can add the FLAG_GRANT_WRITE_URI_PERMISSION flag to the eventual
140                // crop intent b/10837468
141                ContactPhotoUtils.savePhotoFromUriToUri(this, inputUri, mTempPhotoUri, false);
142                toCrop = mTempPhotoUri;
143            } else {
144                toCrop = inputUri;
145            }
146
147            final Intent intent = new Intent("com.android.camera.action.CROP", toCrop);
148            if (myIntent.getStringExtra("mimeType") != null) {
149                intent.setDataAndType(toCrop, myIntent.getStringExtra("mimeType"));
150            }
151            ContactPhotoUtils.addPhotoPickerExtras(intent, mCroppedPhotoUri);
152            ContactPhotoUtils.addCropExtras(intent, mPhotoDim);
153
154            startActivityForResult(intent, REQUEST_CROP_PHOTO);
155
156            mContactUri = result.getData();
157
158        } else if (requestCode == REQUEST_CROP_PHOTO) {
159            // Delete the temporary photo from cache now that we have a cropped version.
160            // We should do this even if the crop failed and we eventually bail
161            getContentResolver().delete(mTempPhotoUri, null, null);
162            if (resultCode != RESULT_OK) {
163                finish();
164                return;
165            }
166            loadContact(mContactUri, new Listener() {
167                @Override
168                public void onContactLoaded(Contact contact) {
169                    saveContact(contact);
170                }
171            });
172        }
173    }
174
175    // TODO: consider moving this to ContactLoader, especially if we keep adding similar
176    // code elsewhere (ViewNotificationService is another case).  The only concern is that,
177    // although this is convenient, it isn't quite as robust as using LoaderManager... for
178    // instance, the loader doesn't persist across Activity restarts.
179    private void loadContact(Uri contactUri, final Listener listener) {
180        final ContactLoader loader = new ContactLoader(this, contactUri, true);
181        loader.registerListener(0, new OnLoadCompleteListener<Contact>() {
182            @Override
183            public void onLoadComplete(
184                    Loader<Contact> loader, Contact contact) {
185                try {
186                    loader.reset();
187                }
188                catch (RuntimeException e) {
189                    Log.e(TAG, "Error resetting loader", e);
190                }
191                listener.onContactLoaded(contact);
192            }
193        });
194        loader.startLoading();
195    }
196
197    private interface Listener {
198        public void onContactLoaded(Contact contact);
199    }
200
201    /**
202     * If prerequisites have been met, attach the photo to a raw-contact and save.
203     * The prerequisites are:
204     * - photo has been cropped
205     * - contact has been loaded
206     */
207    private void saveContact(Contact contact) {
208
209        if (contact.getRawContacts() == null) {
210            Log.w(TAG, "No raw contacts found for contact");
211            finish();
212            return;
213        }
214
215        // Obtain the raw-contact that we will save to.
216        RawContactDeltaList deltaList = contact.createRawContactDeltaList();
217        RawContactDelta raw = deltaList.getFirstWritableRawContact(this);
218        if (raw == null) {
219            Log.w(TAG, "no writable raw-contact found");
220            return;
221        }
222
223        // Create a scaled, compressed bitmap to add to the entity-delta list.
224        final int size = ContactsUtils.getThumbnailSize(this);
225        Bitmap bitmap;
226        try {
227            bitmap = ContactPhotoUtils.getBitmapFromUri(this, mCroppedPhotoUri);
228        } catch (FileNotFoundException e) {
229            Log.w(TAG, "Could not find bitmap");
230            return;
231        }
232
233        final Bitmap scaled = Bitmap.createScaledBitmap(bitmap, size, size, false);
234        final byte[] compressed = ContactPhotoUtils.compressBitmap(scaled);
235        if (compressed == null) {
236            Log.w(TAG, "could not create scaled and compressed Bitmap");
237            return;
238        }
239        // Add compressed bitmap to entity-delta... this allows us to save to
240        // a new contact; otherwise the entity-delta-list would be empty, and
241        // the ContactSaveService would not create the new contact, and the
242        // full-res photo would fail to be saved to the non-existent contact.
243        AccountType account = raw.getRawContactAccountType(this);
244        ValuesDelta values =
245                RawContactModifier.ensureKindExists(raw, account, Photo.CONTENT_ITEM_TYPE);
246        if (values == null) {
247            Log.w(TAG, "cannot attach photo to this account type");
248            return;
249        }
250        values.setPhoto(compressed);
251
252        // Finally, invoke the ContactSaveService.
253        Log.v(TAG, "all prerequisites met, about to save photo to contact");
254        Intent intent = ContactSaveService.createSaveContactIntent(
255                this,
256                deltaList,
257                "", 0,
258                contact.isUserProfile(),
259                null, null,
260                raw.getRawContactId(),
261                mCroppedPhotoUri
262                );
263        startService(intent);
264        finish();
265    }
266}
267