1 /* 2 * Copyright (c) 2008-2009, Motorola, Inc. 3 * 4 * All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are met: 8 * 9 * - Redistributions of source code must retain the above copyright notice, 10 * this list of conditions and the following disclaimer. 11 * 12 * - Redistributions in binary form must reproduce the above copyright notice, 13 * this list of conditions and the following disclaimer in the documentation 14 * and/or other materials provided with the distribution. 15 * 16 * - Neither the name of the Motorola, Inc. nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 * POSSIBILITY OF SUCH DAMAGE. 31 */ 32 33 package com.android.bluetooth.opp; 34 35 import android.content.ContentProvider; 36 import android.content.ContentValues; 37 import android.content.Context; 38 import android.content.UriMatcher; 39 import android.database.Cursor; 40 import android.database.SQLException; 41 import android.database.sqlite.SQLiteDatabase; 42 import android.database.sqlite.SQLiteOpenHelper; 43 import android.database.sqlite.SQLiteQueryBuilder; 44 import android.net.Uri; 45 import android.util.Log; 46 47 import java.util.ArrayList; 48 import java.util.Arrays; 49 import java.util.List; 50 51 /** 52 * This provider allows application to interact with Bluetooth OPP manager 53 */ 54 55 public final class BluetoothOppProvider extends ContentProvider { 56 57 private static final String TAG = "BluetoothOppProvider"; 58 private static final boolean D = Constants.DEBUG; 59 private static final boolean V = Constants.VERBOSE; 60 61 /** Database filename */ 62 private static final String DB_NAME = "btopp.db"; 63 64 /** Current database version */ 65 private static final int DB_VERSION = 1; 66 67 /** Database version from which upgrading is a nop */ 68 private static final int DB_VERSION_NOP_UPGRADE_FROM = 0; 69 70 /** Database version to which upgrading is a nop */ 71 private static final int DB_VERSION_NOP_UPGRADE_TO = 1; 72 73 /** Name of table in the database */ 74 private static final String DB_TABLE = "btopp"; 75 76 /** MIME type for the entire share list */ 77 private static final String SHARE_LIST_TYPE = "vnd.android.cursor.dir/vnd.android.btopp"; 78 79 /** MIME type for an individual share */ 80 private static final String SHARE_TYPE = "vnd.android.cursor.item/vnd.android.btopp"; 81 82 /** URI matcher used to recognize URIs sent by applications */ 83 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 84 85 /** URI matcher constant for the URI of the entire share list */ 86 private static final int SHARES = 1; 87 88 /** URI matcher constant for the URI of an individual share */ 89 private static final int SHARES_ID = 2; 90 91 static { 92 sURIMatcher.addURI("com.android.bluetooth.opp", "btopp", SHARES); 93 sURIMatcher.addURI("com.android.bluetooth.opp", "btopp/#", SHARES_ID); 94 } 95 96 /** The database that lies underneath this content provider */ 97 private SQLiteOpenHelper mOpenHelper = null; 98 99 /** 100 * Creates and updated database on demand when opening it. Helper class to 101 * create database the first time the provider is initialized and upgrade it 102 * when a new version of the provider needs an updated version of the 103 * database. 104 */ 105 private static final class DatabaseHelper extends SQLiteOpenHelper { 106 DatabaseHelper(final Context context)107 DatabaseHelper(final Context context) { 108 super(context, DB_NAME, null, DB_VERSION); 109 } 110 111 /** 112 * Creates database the first time we try to open it. 113 */ 114 @Override onCreate(final SQLiteDatabase db)115 public void onCreate(final SQLiteDatabase db) { 116 if (V) { 117 Log.v(TAG, "populating new database"); 118 } 119 createTable(db); 120 } 121 122 /** 123 * Updates the database format when a content provider is used with a 124 * database that was created with a different format. 125 */ 126 @Override onUpgrade(final SQLiteDatabase db, int oldV, final int newV)127 public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) { 128 if (oldV == DB_VERSION_NOP_UPGRADE_FROM) { 129 if (newV == DB_VERSION_NOP_UPGRADE_TO) { 130 return; 131 } 132 // NOP_FROM and NOP_TO are identical, just in different code lines. 133 // Upgrading from NOP_FROM is the same as upgrading from NOP_TO. 134 oldV = DB_VERSION_NOP_UPGRADE_TO; 135 } 136 Log.i(TAG, "Upgrading downloads database from version " + oldV + " to " + newV 137 + ", which will destroy all old data"); 138 dropTable(db); 139 createTable(db); 140 } 141 142 } 143 createTable(SQLiteDatabase db)144 private static void createTable(SQLiteDatabase db) { 145 try { 146 db.execSQL("CREATE TABLE " + DB_TABLE + "(" + BluetoothShare._ID 147 + " INTEGER PRIMARY KEY AUTOINCREMENT," + BluetoothShare.URI + " TEXT, " 148 + BluetoothShare.FILENAME_HINT + " TEXT, " + BluetoothShare._DATA + " TEXT, " 149 + BluetoothShare.MIMETYPE + " TEXT, " + BluetoothShare.DIRECTION + " INTEGER, " 150 + BluetoothShare.DESTINATION + " TEXT, " + BluetoothShare.VISIBILITY 151 + " INTEGER, " + BluetoothShare.USER_CONFIRMATION + " INTEGER, " 152 + BluetoothShare.STATUS + " INTEGER, " + BluetoothShare.TOTAL_BYTES 153 + " INTEGER, " + BluetoothShare.CURRENT_BYTES + " INTEGER, " 154 + BluetoothShare.TIMESTAMP + " INTEGER," + Constants.MEDIA_SCANNED 155 + " INTEGER); "); 156 } catch (SQLException ex) { 157 Log.e(TAG, "createTable: Failed."); 158 throw ex; 159 } 160 } 161 dropTable(SQLiteDatabase db)162 private static void dropTable(SQLiteDatabase db) { 163 try { 164 db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE); 165 } catch (SQLException ex) { 166 Log.e(TAG, "dropTable: Failed."); 167 throw ex; 168 } 169 } 170 171 @Override getType(Uri uri)172 public String getType(Uri uri) { 173 int match = sURIMatcher.match(uri); 174 switch (match) { 175 case SHARES: 176 return SHARE_LIST_TYPE; 177 case SHARES_ID: 178 return SHARE_TYPE; 179 default: 180 throw new IllegalArgumentException("Unknown URI in getType(): " + uri); 181 } 182 } 183 copyString(String key, ContentValues from, ContentValues to)184 private static void copyString(String key, ContentValues from, ContentValues to) { 185 String s = from.getAsString(key); 186 if (s != null) { 187 to.put(key, s); 188 } 189 } 190 copyInteger(String key, ContentValues from, ContentValues to)191 private static void copyInteger(String key, ContentValues from, ContentValues to) { 192 Integer i = from.getAsInteger(key); 193 if (i != null) { 194 to.put(key, i); 195 } 196 } 197 copyLong(String key, ContentValues from, ContentValues to)198 private static void copyLong(String key, ContentValues from, ContentValues to) { 199 Long i = from.getAsLong(key); 200 if (i != null) { 201 to.put(key, i); 202 } 203 } 204 putString(String key, Cursor from, ContentValues to)205 private static void putString(String key, Cursor from, ContentValues to) { 206 to.put(key, from.getString(from.getColumnIndexOrThrow(key))); 207 } putInteger(String key, Cursor from, ContentValues to)208 private static void putInteger(String key, Cursor from, ContentValues to) { 209 to.put(key, from.getInt(from.getColumnIndexOrThrow(key))); 210 } putLong(String key, Cursor from, ContentValues to)211 private static void putLong(String key, Cursor from, ContentValues to) { 212 to.put(key, from.getLong(from.getColumnIndexOrThrow(key))); 213 } 214 215 /** 216 * @hide 217 */ oppDatabaseMigration(Context ctx, Cursor cursor)218 public static boolean oppDatabaseMigration(Context ctx, Cursor cursor) { 219 boolean result = true; 220 SQLiteDatabase db = new DatabaseHelper(ctx).getWritableDatabase(); 221 while (cursor.moveToNext()) { 222 try { 223 ContentValues values = new ContentValues(); 224 225 final List<String> stringKeys = new ArrayList<>(Arrays.asList( 226 BluetoothShare.URI, 227 BluetoothShare.FILENAME_HINT, 228 BluetoothShare.MIMETYPE, 229 BluetoothShare.DESTINATION)); 230 for (String k : stringKeys) { 231 putString(k, cursor, values); 232 } 233 234 final List<String> integerKeys = new ArrayList<>(Arrays.asList( 235 BluetoothShare.VISIBILITY, 236 BluetoothShare.USER_CONFIRMATION, 237 BluetoothShare.DIRECTION, 238 BluetoothShare.STATUS, 239 Constants.MEDIA_SCANNED)); 240 for (String k : integerKeys) { 241 putInteger(k, cursor, values); 242 } 243 244 final List<String> longKeys = new ArrayList<>(Arrays.asList( 245 BluetoothShare.TOTAL_BYTES, 246 BluetoothShare.TIMESTAMP)); 247 for (String k : longKeys) { 248 putLong(k, cursor, values); 249 } 250 251 db.insert(DB_TABLE, null, values); 252 Log.d(TAG, "One item migrated: " + values); 253 } catch (IllegalArgumentException e) { 254 Log.e(TAG, "Failed to migrate one item: " + e); 255 result = false; 256 } 257 } 258 return result; 259 } 260 261 @Override insert(Uri uri, ContentValues values)262 public Uri insert(Uri uri, ContentValues values) { 263 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 264 265 if (sURIMatcher.match(uri) != SHARES) { 266 throw new IllegalArgumentException("insert: Unknown/Invalid URI " + uri); 267 } 268 269 ContentValues filteredValues = new ContentValues(); 270 271 copyString(BluetoothShare.URI, values, filteredValues); 272 copyString(BluetoothShare.FILENAME_HINT, values, filteredValues); 273 copyString(BluetoothShare.MIMETYPE, values, filteredValues); 274 copyString(BluetoothShare.DESTINATION, values, filteredValues); 275 276 copyInteger(BluetoothShare.VISIBILITY, values, filteredValues); 277 copyLong(BluetoothShare.TOTAL_BYTES, values, filteredValues); 278 if (values.getAsInteger(BluetoothShare.VISIBILITY) == null) { 279 filteredValues.put(BluetoothShare.VISIBILITY, BluetoothShare.VISIBILITY_VISIBLE); 280 } 281 Integer dir = values.getAsInteger(BluetoothShare.DIRECTION); 282 Integer con = values.getAsInteger(BluetoothShare.USER_CONFIRMATION); 283 284 if (dir == null) { 285 dir = BluetoothShare.DIRECTION_OUTBOUND; 286 } 287 if (dir == BluetoothShare.DIRECTION_OUTBOUND && con == null) { 288 con = BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED; 289 } 290 if (dir == BluetoothShare.DIRECTION_INBOUND && con == null) { 291 con = BluetoothShare.USER_CONFIRMATION_PENDING; 292 } 293 filteredValues.put(BluetoothShare.USER_CONFIRMATION, con); 294 filteredValues.put(BluetoothShare.DIRECTION, dir); 295 296 filteredValues.put(BluetoothShare.STATUS, BluetoothShare.STATUS_PENDING); 297 filteredValues.put(Constants.MEDIA_SCANNED, 0); 298 299 Long ts = values.getAsLong(BluetoothShare.TIMESTAMP); 300 if (ts == null) { 301 ts = System.currentTimeMillis(); 302 } 303 filteredValues.put(BluetoothShare.TIMESTAMP, ts); 304 305 Context context = getContext(); 306 307 long rowID = db.insert(DB_TABLE, null, filteredValues); 308 309 if (rowID == -1) { 310 Log.w(TAG, "couldn't insert " + uri + "into btopp database"); 311 return null; 312 } 313 314 context.getContentResolver().notifyChange(uri, null); 315 316 return Uri.parse(BluetoothShare.CONTENT_URI + "/" + rowID); 317 } 318 319 @Override onCreate()320 public boolean onCreate() { 321 mOpenHelper = new DatabaseHelper(getContext()); 322 return true; 323 } 324 325 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)326 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 327 String sortOrder) { 328 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 329 330 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 331 qb.setStrict(true); 332 333 int match = sURIMatcher.match(uri); 334 switch (match) { 335 case SHARES: 336 qb.setTables(DB_TABLE); 337 break; 338 case SHARES_ID: 339 qb.setTables(DB_TABLE); 340 qb.appendWhere(BluetoothShare._ID + "="); 341 qb.appendWhere(uri.getPathSegments().get(1)); 342 break; 343 default: 344 throw new IllegalArgumentException("Unknown URI: " + uri); 345 } 346 347 if (V) { 348 java.lang.StringBuilder sb = new java.lang.StringBuilder(); 349 sb.append("starting query, database is "); 350 if (db != null) { 351 sb.append("not "); 352 } 353 sb.append("null; "); 354 if (projection == null) { 355 sb.append("projection is null; "); 356 } else if (projection.length == 0) { 357 sb.append("projection is empty; "); 358 } else { 359 for (int i = 0; i < projection.length; ++i) { 360 sb.append("projection["); 361 sb.append(i); 362 sb.append("] is "); 363 sb.append(projection[i]); 364 sb.append("; "); 365 } 366 } 367 sb.append("selection is "); 368 sb.append(selection); 369 sb.append("; "); 370 if (selectionArgs == null) { 371 sb.append("selectionArgs is null; "); 372 } else if (selectionArgs.length == 0) { 373 sb.append("selectionArgs is empty; "); 374 } else { 375 for (int i = 0; i < selectionArgs.length; ++i) { 376 sb.append("selectionArgs["); 377 sb.append(i); 378 sb.append("] is "); 379 sb.append(selectionArgs[i]); 380 sb.append("; "); 381 } 382 } 383 sb.append("sort is "); 384 sb.append(sortOrder); 385 sb.append("."); 386 Log.v(TAG, sb.toString()); 387 } 388 389 Cursor ret = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder); 390 391 if (ret == null) { 392 Log.w(TAG, "query failed in downloads database"); 393 return null; 394 } 395 396 ret.setNotificationUri(getContext().getContentResolver(), uri); 397 return ret; 398 } 399 400 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)401 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 402 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 403 404 int count = 0; 405 long rowId; 406 407 int match = sURIMatcher.match(uri); 408 switch (match) { 409 case SHARES: 410 case SHARES_ID: { 411 String myWhere; 412 if (selection != null) { 413 if (match == SHARES) { 414 myWhere = "( " + selection + " )"; 415 } else { 416 myWhere = "( " + selection + " ) AND "; 417 } 418 } else { 419 myWhere = ""; 420 } 421 if (match == SHARES_ID) { 422 String segment = uri.getPathSegments().get(1); 423 rowId = Long.parseLong(segment); 424 myWhere += " ( " + BluetoothShare._ID + " = " + rowId + " ) "; 425 } 426 427 if (values.size() > 0) { 428 count = db.update(DB_TABLE, values, myWhere, selectionArgs); 429 } 430 break; 431 } 432 default: 433 throw new UnsupportedOperationException("Cannot update unknown URI: " + uri); 434 } 435 getContext().getContentResolver().notifyChange(uri, null); 436 437 return count; 438 } 439 440 @Override delete(Uri uri, String selection, String[] selectionArgs)441 public int delete(Uri uri, String selection, String[] selectionArgs) { 442 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 443 int count; 444 int match = sURIMatcher.match(uri); 445 switch (match) { 446 case SHARES: 447 case SHARES_ID: { 448 String myWhere; 449 if (selection != null) { 450 if (match == SHARES) { 451 myWhere = "( " + selection + " )"; 452 } else { 453 myWhere = "( " + selection + " ) AND "; 454 } 455 } else { 456 myWhere = ""; 457 } 458 if (match == SHARES_ID) { 459 String segment = uri.getPathSegments().get(1); 460 long rowId = Long.parseLong(segment); 461 myWhere += " ( " + BluetoothShare._ID + " = " + rowId + " ) "; 462 } 463 464 count = db.delete(DB_TABLE, myWhere, selectionArgs); 465 break; 466 } 467 default: 468 throw new UnsupportedOperationException("Cannot delete unknown URI: " + uri); 469 } 470 getContext().getContentResolver().notifyChange(uri, null); 471 return count; 472 } 473 } 474