1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License
15 */
16package com.android.providers.contacts;
17
18import android.content.ContentValues;
19import android.content.Context;
20import android.database.Cursor;
21import android.database.sqlite.SQLiteDatabase;
22import android.provider.ContactsContract.CommonDataKinds.Photo;
23import android.util.Log;
24
25import com.android.providers.contacts.aggregation.ContactAggregator;
26
27import java.io.IOException;
28
29/**
30 * Handler for photo data rows.
31 */
32public class DataRowHandlerForPhoto extends DataRowHandler {
33
34    private static final String TAG = "DataRowHandlerForPhoto";
35
36    private final PhotoStore mPhotoStore;
37    private final int mMaxDisplayPhotoDim;
38    private final int mMaxThumbnailPhotoDim;
39
40    /**
41     * If this is set in the ContentValues passed in, it indicates that the caller has
42     * already taken care of photo processing, and that the row should be ready for
43     * insert/update.  This is used when the photo has been written directly to an
44     * asset file.
45     */
46    /* package */ static final String SKIP_PROCESSING_KEY = "skip_processing";
47
48    public DataRowHandlerForPhoto(
49            Context context, ContactsDatabaseHelper dbHelper, ContactAggregator aggregator,
50            PhotoStore photoStore, int maxDisplayPhotoDim, int maxThumbnailPhotoDim) {
51        super(context, dbHelper, aggregator, Photo.CONTENT_ITEM_TYPE);
52        mPhotoStore = photoStore;
53        mMaxDisplayPhotoDim = maxDisplayPhotoDim;
54        mMaxThumbnailPhotoDim = maxThumbnailPhotoDim;
55    }
56
57    @Override
58    public long insert(SQLiteDatabase db, TransactionContext txContext, long rawContactId,
59            ContentValues values) {
60
61        if (values.containsKey(SKIP_PROCESSING_KEY)) {
62            values.remove(SKIP_PROCESSING_KEY);
63        } else {
64            // Pre-process the photo if one exists.
65            if (!preProcessPhoto(values)) {
66                return 0;
67            }
68        }
69
70        long dataId = super.insert(db, txContext, rawContactId, values);
71        if (!txContext.isNewRawContact(rawContactId)) {
72            mContactAggregator.updatePhotoId(db, rawContactId);
73        }
74        return dataId;
75    }
76
77    @Override
78    public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
79            Cursor c, boolean callerIsSyncAdapter) {
80        long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
81
82        if (values.containsKey(SKIP_PROCESSING_KEY)) {
83            values.remove(SKIP_PROCESSING_KEY);
84        } else {
85            // Pre-process the photo if one exists.
86            if (!preProcessPhoto(values)) {
87                return false;
88            }
89        }
90
91        // Do the actual update.
92        if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) {
93            return false;
94        }
95
96        mContactAggregator.updatePhotoId(db, rawContactId);
97        return true;
98    }
99
100    /**
101     * Pre-processes the given content values for update or insert.  If the photo column contains
102     * null or an empty byte array, both that column and the photo file ID will be nulled out.
103     * If a photo was specified but could not be processed, this will return false.
104     * @param values The content values passed in.
105     * @return Whether processing was successful - on failure, the operation should abort.
106     */
107    private boolean preProcessPhoto(ContentValues values) {
108        if (values.containsKey(Photo.PHOTO)) {
109            boolean photoExists = hasNonNullPhoto(values);
110            if (photoExists) {
111                if (!processPhoto(values)) {
112                    // A photo was passed in, but we couldn't process it.  Update failed.
113                    return false;
114                }
115            } else {
116                // The photo key was passed in, but it was either null or an empty byte[].
117                // We should set the photo and photo file ID fields to null for the update.
118                values.putNull(Photo.PHOTO);
119                values.putNull(Photo.PHOTO_FILE_ID);
120            }
121        }
122        return true;
123    }
124
125    private boolean hasNonNullPhoto(ContentValues values) {
126        byte[] photoBytes = values.getAsByteArray(Photo.PHOTO);
127        return photoBytes != null && photoBytes.length > 0;
128    }
129
130    @Override
131    public int delete(SQLiteDatabase db, TransactionContext txContext, Cursor c) {
132        long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
133        int count = super.delete(db, txContext, c);
134        mContactAggregator.updatePhotoId(db, rawContactId);
135        return count;
136    }
137
138    /**
139     * Reads the photo out of the given values object and processes it, placing the processed
140     * photos (a photo store file ID and a compressed thumbnail) back into the ContentValues
141     * object.
142     * @param values The values being inserted or updated - assumed to contain a photo BLOB.
143     * @return Whether an image was successfully decoded and processed.
144     */
145    private boolean processPhoto(ContentValues values) {
146        byte[] originalPhoto = values.getAsByteArray(Photo.PHOTO);
147        if (originalPhoto != null) {
148            try {
149                PhotoProcessor processor = new PhotoProcessor(
150                        originalPhoto, mMaxDisplayPhotoDim, mMaxThumbnailPhotoDim);
151                long photoFileId = mPhotoStore.insert(processor);
152                if (photoFileId != 0) {
153                    values.put(Photo.PHOTO_FILE_ID, photoFileId);
154                } else {
155                    values.putNull(Photo.PHOTO_FILE_ID);
156                }
157                values.put(Photo.PHOTO, processor.getThumbnailPhotoBytes());
158                return true;
159            } catch (IOException ioe) {
160                Log.e(TAG, "Could not process photo for insert or update", ioe);
161            }
162        }
163        return false;
164    }
165}
166