1/*
2 * Copyright (C) 2013 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.photos.data;
18
19import android.content.ContentProvider;
20import android.content.ContentProviderOperation;
21import android.content.ContentProviderResult;
22import android.content.ContentResolver;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.OperationApplicationException;
26import android.database.sqlite.SQLiteDatabase;
27import android.database.sqlite.SQLiteOpenHelper;
28import android.net.Uri;
29
30import java.util.ArrayList;
31import java.util.HashSet;
32import java.util.Set;
33
34/**
35 * General purpose {@link ContentProvider} base class that uses SQLiteDatabase
36 * for storage.
37 */
38public abstract class SQLiteContentProvider extends ContentProvider {
39
40    @SuppressWarnings("unused")
41    private static final String TAG = "SQLiteContentProvider";
42
43    private SQLiteOpenHelper mOpenHelper;
44    private Set<Uri> mChangedUris;
45
46    private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>();
47    private static final int SLEEP_AFTER_YIELD_DELAY = 4000;
48
49    /**
50     * Maximum number of operations allowed in a batch between yield points.
51     */
52    private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500;
53
54    @Override
55    public boolean onCreate() {
56        Context context = getContext();
57        mOpenHelper = getDatabaseHelper(context);
58        mChangedUris = new HashSet<Uri>();
59        return true;
60    }
61
62    @Override
63    public void shutdown() {
64        getDatabaseHelper().close();
65    }
66
67    /**
68     * Returns a {@link SQLiteOpenHelper} that can open the database.
69     */
70    public abstract SQLiteOpenHelper getDatabaseHelper(Context context);
71
72    /**
73     * The equivalent of the {@link #insert} method, but invoked within a
74     * transaction.
75     */
76    public abstract Uri insertInTransaction(Uri uri, ContentValues values,
77            boolean callerIsSyncAdapter);
78
79    /**
80     * The equivalent of the {@link #update} method, but invoked within a
81     * transaction.
82     */
83    public abstract int updateInTransaction(Uri uri, ContentValues values, String selection,
84            String[] selectionArgs, boolean callerIsSyncAdapter);
85
86    /**
87     * The equivalent of the {@link #delete} method, but invoked within a
88     * transaction.
89     */
90    public abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs,
91            boolean callerIsSyncAdapter);
92
93    /**
94     * Call this to add a URI to the list of URIs to be notified when the
95     * transaction is committed.
96     */
97    protected void postNotifyUri(Uri uri) {
98        synchronized (mChangedUris) {
99            mChangedUris.add(uri);
100        }
101    }
102
103    public boolean isCallerSyncAdapter(Uri uri) {
104        return false;
105    }
106
107    public SQLiteOpenHelper getDatabaseHelper() {
108        return mOpenHelper;
109    }
110
111    private boolean applyingBatch() {
112        return mApplyingBatch.get() != null && mApplyingBatch.get();
113    }
114
115    @Override
116    public Uri insert(Uri uri, ContentValues values) {
117        Uri result = null;
118        boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
119        boolean applyingBatch = applyingBatch();
120        if (!applyingBatch) {
121            SQLiteDatabase db = mOpenHelper.getWritableDatabase();
122            db.beginTransaction();
123            try {
124                result = insertInTransaction(uri, values, callerIsSyncAdapter);
125                db.setTransactionSuccessful();
126            } finally {
127                db.endTransaction();
128            }
129
130            onEndTransaction(callerIsSyncAdapter);
131        } else {
132            result = insertInTransaction(uri, values, callerIsSyncAdapter);
133        }
134        return result;
135    }
136
137    @Override
138    public int bulkInsert(Uri uri, ContentValues[] values) {
139        int numValues = values.length;
140        boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
141        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
142        db.beginTransaction();
143        try {
144            for (int i = 0; i < numValues; i++) {
145                @SuppressWarnings("unused")
146                Uri result = insertInTransaction(uri, values[i], callerIsSyncAdapter);
147                db.yieldIfContendedSafely();
148            }
149            db.setTransactionSuccessful();
150        } finally {
151            db.endTransaction();
152        }
153
154        onEndTransaction(callerIsSyncAdapter);
155        return numValues;
156    }
157
158    @Override
159    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
160        int count = 0;
161        boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
162        boolean applyingBatch = applyingBatch();
163        if (!applyingBatch) {
164            SQLiteDatabase db = mOpenHelper.getWritableDatabase();
165            db.beginTransaction();
166            try {
167                count = updateInTransaction(uri, values, selection, selectionArgs,
168                        callerIsSyncAdapter);
169                db.setTransactionSuccessful();
170            } finally {
171                db.endTransaction();
172            }
173
174            onEndTransaction(callerIsSyncAdapter);
175        } else {
176            count = updateInTransaction(uri, values, selection, selectionArgs, callerIsSyncAdapter);
177        }
178
179        return count;
180    }
181
182    @Override
183    public int delete(Uri uri, String selection, String[] selectionArgs) {
184        int count = 0;
185        boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
186        boolean applyingBatch = applyingBatch();
187        if (!applyingBatch) {
188            SQLiteDatabase db = mOpenHelper.getWritableDatabase();
189            db.beginTransaction();
190            try {
191                count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter);
192                db.setTransactionSuccessful();
193            } finally {
194                db.endTransaction();
195            }
196
197            onEndTransaction(callerIsSyncAdapter);
198        } else {
199            count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter);
200        }
201        return count;
202    }
203
204    @Override
205    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
206            throws OperationApplicationException {
207        int ypCount = 0;
208        int opCount = 0;
209        boolean callerIsSyncAdapter = false;
210        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
211        db.beginTransaction();
212        try {
213            mApplyingBatch.set(true);
214            final int numOperations = operations.size();
215            final ContentProviderResult[] results = new ContentProviderResult[numOperations];
216            for (int i = 0; i < numOperations; i++) {
217                if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) {
218                    throw new OperationApplicationException(
219                            "Too many content provider operations between yield points. "
220                                    + "The maximum number of operations per yield point is "
221                                    + MAX_OPERATIONS_PER_YIELD_POINT, ypCount);
222                }
223                final ContentProviderOperation operation = operations.get(i);
224                if (!callerIsSyncAdapter && isCallerSyncAdapter(operation.getUri())) {
225                    callerIsSyncAdapter = true;
226                }
227                if (i > 0 && operation.isYieldAllowed()) {
228                    opCount = 0;
229                    if (db.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) {
230                        ypCount++;
231                    }
232                }
233                results[i] = operation.apply(this, results, i);
234            }
235            db.setTransactionSuccessful();
236            return results;
237        } finally {
238            mApplyingBatch.set(false);
239            db.endTransaction();
240            onEndTransaction(callerIsSyncAdapter);
241        }
242    }
243
244    protected Set<Uri> onEndTransaction(boolean callerIsSyncAdapter) {
245        Set<Uri> changed;
246        synchronized (mChangedUris) {
247            changed = new HashSet<Uri>(mChangedUris);
248            mChangedUris.clear();
249        }
250        ContentResolver resolver = getContext().getContentResolver();
251        for (Uri uri : changed) {
252            boolean syncToNetwork = !callerIsSyncAdapter && syncToNetwork(uri);
253            notifyChange(resolver, uri, syncToNetwork);
254        }
255        return changed;
256    }
257
258    protected void notifyChange(ContentResolver resolver, Uri uri, boolean syncToNetwork) {
259        resolver.notifyChange(uri, null, syncToNetwork);
260    }
261
262    protected boolean syncToNetwork(Uri uri) {
263        return false;
264    }
265}