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 com.android.contacts.ContactsActivity;
20import com.android.contacts.R;
21import com.android.contacts.model.ExchangeAccountType;
22import com.android.contacts.model.GoogleAccountType;
23
24import android.content.ContentProviderOperation;
25import android.content.ContentResolver;
26import android.content.ContentUris;
27import android.content.ContentValues;
28import android.content.Intent;
29import android.content.OperationApplicationException;
30import android.database.Cursor;
31import android.graphics.Bitmap;
32import android.net.Uri;
33import android.os.Bundle;
34import android.os.RemoteException;
35import android.provider.ContactsContract;
36import android.provider.ContactsContract.CommonDataKinds.Photo;
37import android.provider.ContactsContract.Contacts;
38import android.provider.ContactsContract.DisplayPhoto;
39import android.provider.ContactsContract.RawContacts;
40import android.widget.Toast;
41
42import java.io.ByteArrayOutputStream;
43import java.util.ArrayList;
44
45/**
46 * Provides an external interface for other applications to attach images
47 * to contacts. It will first present a contact picker and then run the
48 * image that is handed to it through the cropper to make the image the proper
49 * size and give the user a chance to use the face detector.
50 */
51public class AttachPhotoActivity extends ContactsActivity {
52    private static final int REQUEST_PICK_CONTACT = 1;
53    private static final int REQUEST_CROP_PHOTO = 2;
54
55    private static final String RAW_CONTACT_URIS_KEY = "raw_contact_uris";
56
57    private Long[] mRawContactIds;
58
59    private ContentResolver mContentResolver;
60
61    // Height/width (in pixels) to request for the photo - queried from the provider.
62    private static int mPhotoDim;
63
64    @Override
65    public void onCreate(Bundle icicle) {
66        super.onCreate(icicle);
67
68        if (icicle != null) {
69            mRawContactIds = toClassArray(icicle.getLongArray(RAW_CONTACT_URIS_KEY));
70        } else {
71            Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
72            intent.setType(Contacts.CONTENT_ITEM_TYPE);
73            startActivityForResult(intent, REQUEST_PICK_CONTACT);
74        }
75
76        mContentResolver = getContentResolver();
77
78        // Load the photo dimension to request.
79        Cursor c = mContentResolver.query(DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
80                new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null);
81        try {
82            c.moveToFirst();
83            mPhotoDim = c.getInt(0);
84        } finally {
85            c.close();
86        }
87    }
88
89    @Override
90    protected void onSaveInstanceState(Bundle outState) {
91        super.onSaveInstanceState(outState);
92
93        if (mRawContactIds != null && mRawContactIds.length != 0) {
94            outState.putLongArray(RAW_CONTACT_URIS_KEY, toPrimativeArray(mRawContactIds));
95        }
96    }
97
98    private static long[] toPrimativeArray(Long[] in) {
99        if (in == null) {
100            return null;
101        }
102        long[] out = new long[in.length];
103        for (int i = 0; i < in.length; i++) {
104            out[i] = in[i];
105        }
106        return out;
107    }
108
109    private static Long[] toClassArray(long[] in) {
110        if (in == null) {
111            return null;
112        }
113        Long[] out = new Long[in.length];
114        for (int i = 0; i < in.length; i++) {
115            out[i] = in[i];
116        }
117        return out;
118    }
119
120    @Override
121    protected void onActivityResult(int requestCode, int resultCode, Intent result) {
122        if (resultCode != RESULT_OK) {
123            finish();
124            return;
125        }
126
127        if (requestCode == REQUEST_PICK_CONTACT) {
128            // A contact was picked. Launch the cropper to get face detection, the right size, etc.
129            // TODO: get these values from constants somewhere
130            Intent myIntent = getIntent();
131            Intent intent = new Intent("com.android.camera.action.CROP", myIntent.getData());
132            if (myIntent.getStringExtra("mimeType") != null) {
133                intent.setDataAndType(myIntent.getData(), myIntent.getStringExtra("mimeType"));
134            }
135            intent.putExtra("crop", "true");
136            intent.putExtra("aspectX", 1);
137            intent.putExtra("aspectY", 1);
138            intent.putExtra("outputX", mPhotoDim);
139            intent.putExtra("outputY", mPhotoDim);
140            intent.putExtra("return-data", true);
141            startActivityForResult(intent, REQUEST_CROP_PHOTO);
142
143            // while they're cropping, convert the contact into a raw_contact
144            final long contactId = ContentUris.parseId(result.getData());
145            final ArrayList<Long> rawContactIdsList = queryForAllRawContactIds(
146                    mContentResolver, contactId);
147            mRawContactIds = new Long[rawContactIdsList.size()];
148            mRawContactIds = rawContactIdsList.toArray(mRawContactIds);
149
150            if (mRawContactIds == null || rawContactIdsList.isEmpty()) {
151                Toast.makeText(this, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
152            }
153        } else if (requestCode == REQUEST_CROP_PHOTO) {
154            final Bundle extras = result.getExtras();
155            if (extras != null && mRawContactIds != null) {
156                Bitmap photo = extras.getParcelable("data");
157                if (photo != null) {
158                    ByteArrayOutputStream stream = new ByteArrayOutputStream();
159                    photo.compress(Bitmap.CompressFormat.JPEG, 75, stream);
160
161                    final ContentValues imageValues = new ContentValues();
162                    imageValues.put(Photo.PHOTO, stream.toByteArray());
163                    imageValues.put(RawContacts.Data.IS_SUPER_PRIMARY, 1);
164
165                    // attach the photo to every raw contact
166                    for (Long rawContactId : mRawContactIds) {
167
168                        // exchange and google only allow one image, so do an update rather than insert
169                        boolean shouldUpdate = false;
170
171                        final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
172                                rawContactId);
173                        final Uri rawContactDataUri = Uri.withAppendedPath(rawContactUri,
174                                RawContacts.Data.CONTENT_DIRECTORY);
175                        insertPhoto(imageValues, rawContactDataUri, true);
176                    }
177                }
178            }
179            finish();
180        }
181    }
182
183    // TODO: move to background
184    public static ArrayList<Long> queryForAllRawContactIds(ContentResolver cr, long contactId) {
185        Cursor rawContactIdCursor = null;
186        ArrayList<Long> rawContactIds = new ArrayList<Long>();
187        try {
188            rawContactIdCursor = cr.query(RawContacts.CONTENT_URI,
189                    new String[] {RawContacts._ID},
190                    RawContacts.CONTACT_ID + "=" + contactId, null, null);
191            if (rawContactIdCursor != null) {
192                while (rawContactIdCursor.moveToNext()) {
193                    rawContactIds.add(rawContactIdCursor.getLong(0));
194                }
195            }
196        } finally {
197            if (rawContactIdCursor != null) {
198                rawContactIdCursor.close();
199            }
200        }
201        return rawContactIds;
202    }
203
204    /**
205     * Inserts a photo on the raw contact.
206     * @param values the photo values
207     * @param assertAccount if true, will check to verify that no photos exist for Google,
208     *     Exchange and unsynced phone account types. These account types only take one picture,
209     *     so if one exists, the account will be updated with the new photo.
210     */
211    private void insertPhoto(ContentValues values, Uri rawContactDataUri,
212            boolean assertAccount) {
213
214        ArrayList<ContentProviderOperation> operations =
215            new ArrayList<ContentProviderOperation>();
216
217        if (assertAccount) {
218            // Make sure no pictures exist for Google, Exchange and unsynced phone accounts.
219            operations.add(ContentProviderOperation.newAssertQuery(rawContactDataUri)
220                    .withSelection(Photo.MIMETYPE + "=? AND "
221                            + RawContacts.DATA_SET + " IS NULL AND ("
222                            + RawContacts.ACCOUNT_TYPE + " IN (?,?) OR "
223                            + RawContacts.ACCOUNT_TYPE + " IS NULL)",
224                            new String[] {Photo.CONTENT_ITEM_TYPE, GoogleAccountType.ACCOUNT_TYPE,
225                            ExchangeAccountType.ACCOUNT_TYPE})
226                            .withExpectedCount(0).build());
227        }
228
229        // insert the photo
230        values.put(Photo.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
231        operations.add(ContentProviderOperation.newInsert(rawContactDataUri)
232                .withValues(values).build());
233
234        try {
235            mContentResolver.applyBatch(ContactsContract.AUTHORITY, operations);
236        } catch (RemoteException e) {
237            throw new IllegalStateException("Problem querying raw_contacts/data", e);
238        } catch (OperationApplicationException e) {
239            // the account doesn't allow multiple photos, so update
240            if (assertAccount) {
241                updatePhoto(values, rawContactDataUri, false);
242            } else {
243                throw new IllegalStateException("Problem inserting photo into raw_contacts/data", e);
244            }
245        }
246    }
247
248    /**
249     * Tries to update the photo on the raw_contact.  If no photo exists, and allowInsert == true,
250     * then will try to {@link #updatePhoto(ContentValues, boolean)}
251     */
252    private void updatePhoto(ContentValues values, Uri rawContactDataUri,
253            boolean allowInsert) {
254        ArrayList<ContentProviderOperation> operations =
255            new ArrayList<ContentProviderOperation>();
256
257        values.remove(Photo.MIMETYPE);
258
259        // check that a photo exists
260        operations.add(ContentProviderOperation.newAssertQuery(rawContactDataUri)
261                .withSelection(Photo.MIMETYPE + "=?", new String[] {
262                    Photo.CONTENT_ITEM_TYPE
263                }).withExpectedCount(1).build());
264
265        // update that photo
266        operations.add(ContentProviderOperation.newUpdate(rawContactDataUri)
267                .withSelection(Photo.MIMETYPE + "=?", new String[] {Photo.CONTENT_ITEM_TYPE})
268                .withValues(values).build());
269
270        try {
271            mContentResolver.applyBatch(ContactsContract.AUTHORITY, operations);
272        } catch (RemoteException e) {
273            throw new IllegalStateException("Problem querying raw_contacts/data", e);
274        } catch (OperationApplicationException e) {
275            if (allowInsert) {
276                // they deleted the photo between insert and update, so insert one
277                insertPhoto(values, rawContactDataUri, false);
278            } else {
279                throw new IllegalStateException("Problem inserting photo raw_contacts/data", e);
280            }
281        }
282    }
283}
284