1/*
2 * Copyright (c) 2015, Motorola Mobility LLC
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *     - Redistributions of source code must retain the above copyright
8 *       notice, this list of conditions and the following disclaimer.
9 *     - Redistributions in binary form must reproduce the above copyright
10 *       notice, this list of conditions and the following disclaimer in the
11 *       documentation and/or other materials provided with the distribution.
12 *     - Neither the name of Motorola Mobility nor the
13 *       names of its contributors may be used to endorse or promote products
14 *       derived from this software without specific prior written permission.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
18 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL MOTOROLA MOBILITY LLC BE LIABLE
20 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
22 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
23 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
24 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
25 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
26 * DAMAGE.
27 */
28
29package com.android.service.ims.presence;
30
31import java.io.File;
32
33import android.content.ContentProvider;
34import android.content.ContentValues;
35import android.content.Context;
36import android.content.Intent;
37import android.database.Cursor;
38import android.database.sqlite.SQLiteDatabase;
39import android.database.sqlite.SQLiteException;
40import android.database.sqlite.SQLiteFullException;
41import android.database.sqlite.SQLiteOpenHelper;
42import android.net.Uri;
43
44import com.android.ims.internal.Logger;
45
46public abstract class DatabaseContentProvider extends ContentProvider {
47    static private Logger logger = Logger.getLogger("DatabaseContentProvider");
48
49    //Constants
50    public static final String ACTION_DEVICE_STORAGE_FULL = "com.android.vmm.DEVICE_STORAGE_FULL";
51
52    //Fields
53    protected SQLiteOpenHelper mDbHelper;
54    /*package*/final int mDbVersion;
55    private final String mDbName;
56
57    /**
58     * Initializes the DatabaseContentProvider
59     * @param dbName the filename of the database
60     * @param dbVersion the current version of the database schema
61     * @param contentUri The base Uri of the syncable content in this provider
62     */
63    public DatabaseContentProvider(String dbName, int dbVersion) {
64        super();
65        mDbName = dbName;
66        mDbVersion = dbVersion;
67    }
68
69    /**
70     * bootstrapDatabase() allows the implementer to set up their database
71     * after it is opened for the first time.  this is a perfect place
72     * to create tables and triggers :)
73     * @param db
74     */
75    protected void bootstrapDatabase(SQLiteDatabase db) {
76    }
77
78    /**
79     * updgradeDatabase() allows the user to do whatever they like
80     * when the database is upgraded between versions.
81     * @param db - the SQLiteDatabase that will be upgraded
82     * @param oldVersion - the old version number as an int
83     * @param newVersion - the new version number as an int
84     * @return
85     */
86    protected abstract boolean upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion);
87
88    /**
89     * downgradeDatabase() allows the user to do whatever they like when the
90     * database is downgraded between versions.
91     *
92     * @param db - the SQLiteDatabase that will be downgraded
93     * @param oldVersion - the old version number as an int
94     * @param newVersion - the new version number as an int
95     * @return
96     */
97    protected abstract boolean downgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion);
98
99    /**
100     * Safely wraps an ALTER TABLE table ADD COLUMN columnName columnType
101     * If columnType == null then it's set to INTEGER DEFAULT 0
102     * @param db - db to alter
103     * @param table - table to alter
104     * @param columnDef
105     * @return
106     */
107    protected static boolean addColumn(SQLiteDatabase db, String table, String columnName,
108            String columnType) {
109        StringBuilder sb = new StringBuilder();
110        sb.append("ALTER TABLE ").append(table).append(" ADD COLUMN ").append(columnName).append(
111                ' ').append(columnType == null ? "INTEGER DEFAULT 0" : columnType).append(';');
112        try {
113            db.execSQL(sb.toString());
114        } catch (SQLiteException e) {
115                logger.debug("Alter table failed : "+ e.getMessage());
116            return false;
117        }
118        return true;
119    }
120
121    /**
122     * onDatabaseOpened() allows the user to do whatever they might
123     * need to do whenever the database is opened
124     * @param db - SQLiteDatabase that was just opened
125     */
126    protected void onDatabaseOpened(SQLiteDatabase db) {
127    }
128
129    private class DatabaseHelper extends SQLiteOpenHelper {
130        private File mDatabaseFile = null;
131
132        DatabaseHelper(Context context, String name) {
133            // Note: context and name may be null for temp providers
134            super(context, name, null, mDbVersion);
135            mDatabaseFile = context.getDatabasePath(name);
136        }
137
138        @Override
139        public void onCreate(SQLiteDatabase db) {
140            bootstrapDatabase(db);
141        }
142
143        @Override
144        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
145            upgradeDatabase(db, oldVersion, newVersion);
146        }
147
148        @Override
149        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
150            logger.debug("Enter: onDowngrade() - oldVersion = " + oldVersion + " newVersion = "
151                    + newVersion);
152            downgradeDatabase(db, oldVersion, newVersion);
153        }
154
155        @Override
156        public void onOpen(SQLiteDatabase db) {
157            onDatabaseOpened(db);
158        }
159
160        @Override
161        public synchronized SQLiteDatabase getWritableDatabase() {
162            try {
163                return super.getWritableDatabase();
164            } catch (InvalidDBException e) {
165                logger.error("getWritableDatabase - caught InvalidDBException ");
166            }
167
168            // try to delete the database file
169            if (null != mDatabaseFile) {
170                logger.error("deleting mDatabaseFile.");
171                mDatabaseFile.delete();
172            }
173
174            // Return a freshly created database.
175            return super.getWritableDatabase();
176        }
177
178        @Override
179        public synchronized SQLiteDatabase getReadableDatabase() {
180            try {
181                return super.getReadableDatabase();
182            } catch (InvalidDBException e) {
183                logger.error("getReadableDatabase - caught InvalidDBException ");
184            }
185
186            // try to delete the database file
187            if (null != mDatabaseFile) {
188                logger.error("deleting mDatabaseFile.");
189                mDatabaseFile.delete();
190            }
191
192            // Return a freshly created database.
193            return super.getReadableDatabase();
194        }
195    }
196
197    /**
198     * deleteInternal allows getContentResolver().delete() to occur atomically
199     * via transactions and notify the uri automatically upon completion (provided
200     * rows were deleted) - otherwise, it functions exactly as getContentResolver.delete()
201     * would on a regular ContentProvider
202     * @param uri - uri to delete from
203     * @param selection - selection used for the uri
204     * @param selectionArgs - selection args replacing ?'s in the selection
205     * @return returns the number of rows deleted
206     */
207    protected abstract int deleteInternal(final SQLiteDatabase db, Uri uri, String selection,
208            String[] selectionArgs);
209
210    @Override
211    public int delete(Uri uri, String selection, String[] selectionArgs) {
212        int result = 0;
213        SQLiteDatabase db = mDbHelper.getWritableDatabase();
214        if (isClosed(db)) {
215            return result;
216        }
217        try {
218            //acquire reference to prevent from garbage collection
219            db.acquireReference();
220            //beginTransaction can throw a runtime exception
221            //so it needs to be moved into the try
222            db.beginTransaction();
223            result = deleteInternal(db, uri, selection, selectionArgs);
224            db.setTransactionSuccessful();
225        } catch (SQLiteFullException fullEx) {
226            logger.error("" + fullEx);
227            sendStorageFullIntent(getContext());
228        } catch (Exception e) {
229            logger.error("" + e);
230        } finally {
231            try {
232                db.endTransaction();
233            } catch (SQLiteFullException fullEx) {
234                logger.error("" + fullEx);
235                sendStorageFullIntent(getContext());
236            } catch (Exception e) {
237                logger.error("" + e);
238            }
239            //release reference
240            db.releaseReference();
241        }
242        // don't check return value because it may be 0 if all rows deleted
243        getContext().getContentResolver().notifyChange(uri, null);
244        return result;
245    }
246
247    /**
248     * insertInternal allows getContentResolver().insert() to occur atomically
249     * via transactions and notify the uri automatically upon completion (provided
250     * rows were added to the db) - otherwise, it functions exactly as getContentResolver().insert()
251     * would on a regular ContentProvider
252     * @param uri - uri on which to insert
253     * @param values - values to insert
254     * @return returns the uri of the row added
255     */
256    protected abstract Uri insertInternal(final SQLiteDatabase db, Uri uri, ContentValues values);
257
258    @Override
259    public Uri insert(Uri uri, ContentValues values) {
260        Uri result = null;
261        SQLiteDatabase db = mDbHelper.getWritableDatabase();
262        if (isClosed(db)) {
263            return result;
264        }
265        try {
266            db.acquireReference();
267            //beginTransaction can throw a runtime exception
268            //so it needs to be moved into the try
269            db.beginTransaction();
270            result = insertInternal(db, uri, values);
271            db.setTransactionSuccessful();
272        } catch (SQLiteFullException fullEx) {
273            logger.warn("" + fullEx);
274            sendStorageFullIntent(getContext());
275        } catch (Exception e) {
276            logger.warn("" + e);
277        } finally {
278            try {
279                db.endTransaction();
280            } catch (SQLiteFullException fullEx) {
281                logger.warn("" + fullEx);
282                sendStorageFullIntent(getContext());
283            } catch (Exception e) {
284                logger.warn("" + e);
285            }
286            db.releaseReference();
287        }
288        if (result != null) {
289            getContext().getContentResolver().notifyChange(uri, null);
290        }
291        return result;
292    }
293
294    @Override
295    public boolean onCreate() {
296        mDbHelper = new DatabaseHelper(getContext(), mDbName);
297        return onCreateInternal();
298    }
299
300    /**
301     * Called by onCreate.  Should be overridden by any subclasses
302     * to handle the onCreate lifecycle event.
303     *
304     * @return
305     */
306    protected boolean onCreateInternal() {
307        return true;
308    }
309
310    /**
311     * queryInternal allows getContentResolver().query() to occur
312     * @param uri
313     * @param projection
314     * @param selection
315     * @param selectionArgs
316     * @param sortOrder
317     * @return Cursor holding the contents of the requested query
318     */
319    protected abstract Cursor queryInternal(final SQLiteDatabase db, Uri uri, String[] projection,
320            String selection, String[] selectionArgs, String sortOrder);
321
322    @Override
323    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
324            String sortOrder) {
325        SQLiteDatabase db = mDbHelper.getReadableDatabase();
326        if (isClosed(db)) {
327            return null;
328        }
329
330        try {
331            db.acquireReference();
332            return queryInternal(db, uri, projection, selection, selectionArgs, sortOrder);
333        } finally {
334            db.releaseReference();
335        }
336    }
337
338    protected abstract int updateInternal(final SQLiteDatabase db, Uri uri, ContentValues values,
339            String selection, String[] selectionArgs);
340
341    @Override
342    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
343        int result = 0;
344        SQLiteDatabase db = mDbHelper.getWritableDatabase();
345        if (isClosed(db)) {
346            return result;
347        }
348        try {
349            db.acquireReference();
350            //beginTransaction can throw a runtime exception
351            //so it needs to be moved into the try
352            db.beginTransaction();
353            result = updateInternal(db, uri, values, selection, selectionArgs);
354            db.setTransactionSuccessful();
355        } catch (SQLiteFullException fullEx) {
356            logger.error("" + fullEx);
357            sendStorageFullIntent(getContext());
358        } catch (Exception e) {
359            logger.error("" + e);
360        } finally {
361            try {
362                db.endTransaction();
363            } catch (SQLiteFullException fullEx) {
364                logger.error("" + fullEx);
365                sendStorageFullIntent(getContext());
366            } catch (Exception e) {
367                logger.error("" + e);
368            }
369            db.releaseReference();
370        }
371        if (result > 0) {
372            getContext().getContentResolver().notifyChange(uri, null);
373        }
374        return result;
375    }
376
377    @Override
378    public int bulkInsert(Uri uri, ContentValues[] values) {
379        int added = 0;
380        if (values != null) {
381            int numRows = values.length;
382            SQLiteDatabase db = mDbHelper.getWritableDatabase();
383            if (isClosed(db)) {
384                return added;
385            }
386            try {
387                db.acquireReference();
388                //beginTransaction can throw a runtime exception
389                //so it needs to be moved into the try
390                db.beginTransaction();
391
392                for (int i = 0; i < numRows; i++) {
393                    if (insertInternal(db, uri, values[i]) != null) {
394                        added++;
395                    }
396                }
397                db.setTransactionSuccessful();
398                if (added > 0) {
399                    getContext().getContentResolver().notifyChange(uri, null);
400                }
401            } catch (SQLiteFullException fullEx) {
402                logger.error("" + fullEx);
403                sendStorageFullIntent(getContext());
404            } catch (Exception e) {
405                logger.error("" + e);
406            } finally {
407                try {
408                    db.endTransaction();
409                } catch (SQLiteFullException fullEx) {
410                    logger.error("" + fullEx);
411                    sendStorageFullIntent(getContext());
412                } catch (Exception e) {
413                    logger.error("" + e);
414                }
415                db.releaseReference();
416            }
417        }
418        return added;
419    }
420
421    private void sendStorageFullIntent(Context context) {
422        Intent fullStorageIntent = new Intent(ACTION_DEVICE_STORAGE_FULL);
423        context.sendBroadcast(fullStorageIntent);
424    }
425
426    private boolean isClosed(SQLiteDatabase db) {
427        if (db == null || !db.isOpen()) {
428            logger.warn("Null DB returned from DBHelper for a writable/readable database.");
429            return true;
430        }
431        return false;
432    }
433
434}
435