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