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  */
16 package com.android.providers.contacts;
17 
18 import android.content.ContentValues;
19 import android.content.Context;
20 import android.database.Cursor;
21 import android.database.sqlite.SQLiteDatabase;
22 import android.provider.ContactsContract.CommonDataKinds.Photo;
23 import android.util.Log;
24 
25 import com.android.providers.contacts.aggregation.AbstractContactAggregator;
26 
27 import java.io.IOException;
28 
29 /**
30  * Handler for photo data rows.
31  */
32 public 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 
DataRowHandlerForPhoto( Context context, ContactsDatabaseHelper dbHelper, AbstractContactAggregator aggregator, PhotoStore photoStore, int maxDisplayPhotoDim, int maxThumbnailPhotoDim)48     public DataRowHandlerForPhoto(
49             Context context, ContactsDatabaseHelper dbHelper, AbstractContactAggregator 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
insert(SQLiteDatabase db, TransactionContext txContext, long rawContactId, ContentValues values)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
update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter)78     public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
79             Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
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, callerIsMetadataSyncAdapter)) {
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      */
preProcessPhoto(ContentValues values)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 
hasNonNullPhoto(ContentValues values)125     private boolean hasNonNullPhoto(ContentValues values) {
126         byte[] photoBytes = values.getAsByteArray(Photo.PHOTO);
127         return photoBytes != null && photoBytes.length > 0;
128     }
129 
130     @Override
delete(SQLiteDatabase db, TransactionContext txContext, Cursor c)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      */
processPhoto(ContentValues values)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