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.bluetooth.BluetoothProfile; 36 import android.bluetooth.BluetoothProtoEnums; 37 import android.content.ContentProvider; 38 import android.content.ContentValues; 39 import android.content.Context; 40 import android.content.UriMatcher; 41 import android.database.Cursor; 42 import android.database.SQLException; 43 import android.database.sqlite.SQLiteDatabase; 44 import android.database.sqlite.SQLiteOpenHelper; 45 import android.database.sqlite.SQLiteQueryBuilder; 46 import android.net.Uri; 47 import android.util.Log; 48 49 import com.android.bluetooth.BluetoothStatsLog; 50 import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils; 51 52 import java.util.ArrayList; 53 import java.util.Arrays; 54 import java.util.List; 55 56 /** This provider allows application to interact with Bluetooth OPP manager */ 57 // Next tag value for ContentProfileErrorReportUtils.report(): 5 58 public final class BluetoothOppProvider extends ContentProvider { 59 private static final String TAG = "BluetoothOppProvider"; 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 create database the 101 * first time the provider is initialized and upgrade it when a new version of the provider 102 * needs an updated version of the database. 103 */ 104 private static final class DatabaseHelper extends SQLiteOpenHelper { 105 DatabaseHelper(final Context context)106 DatabaseHelper(final Context context) { 107 super(context, DB_NAME, null, DB_VERSION); 108 } 109 110 /** Creates database the first time we try to open it. */ 111 @Override onCreate(final SQLiteDatabase db)112 public void onCreate(final SQLiteDatabase db) { 113 Log.v(TAG, "populating new database"); 114 createTable(db); 115 } 116 117 /** 118 * Updates the database format when a content provider is used with a database that was 119 * created with a different format. 120 */ 121 @Override onUpgrade(final SQLiteDatabase db, int oldV, final int newV)122 public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) { 123 if (oldV == DB_VERSION_NOP_UPGRADE_FROM) { 124 if (newV == DB_VERSION_NOP_UPGRADE_TO) { 125 return; 126 } 127 // NOP_FROM and NOP_TO are identical, just in different code lines. 128 // Upgrading from NOP_FROM is the same as upgrading from NOP_TO. 129 oldV = DB_VERSION_NOP_UPGRADE_TO; 130 } 131 Log.i( 132 TAG, 133 "Upgrading downloads database from version " 134 + oldV 135 + " to " 136 + newV 137 + ", which will destroy all old data"); 138 dropTable(db); 139 createTable(db); 140 } 141 } 142 createTable(SQLiteDatabase db)143 private static void createTable(SQLiteDatabase db) { 144 try { 145 db.execSQL( 146 "CREATE TABLE " 147 + DB_TABLE 148 + "(" 149 + BluetoothShare._ID 150 + " INTEGER PRIMARY KEY AUTOINCREMENT," 151 + BluetoothShare.URI 152 + " TEXT, " 153 + BluetoothShare.FILENAME_HINT 154 + " TEXT, " 155 + BluetoothShare._DATA 156 + " TEXT, " 157 + BluetoothShare.MIMETYPE 158 + " TEXT, " 159 + BluetoothShare.DIRECTION 160 + " INTEGER, " 161 + BluetoothShare.DESTINATION 162 + " TEXT, " 163 + BluetoothShare.VISIBILITY 164 + " INTEGER, " 165 + BluetoothShare.USER_CONFIRMATION 166 + " INTEGER, " 167 + BluetoothShare.STATUS 168 + " INTEGER, " 169 + BluetoothShare.TOTAL_BYTES 170 + " INTEGER, " 171 + BluetoothShare.CURRENT_BYTES 172 + " INTEGER, " 173 + BluetoothShare.TIMESTAMP 174 + " INTEGER," 175 + Constants.MEDIA_SCANNED 176 + " INTEGER); "); 177 } catch (SQLException ex) { 178 ContentProfileErrorReportUtils.report( 179 BluetoothProfile.OPP, 180 BluetoothProtoEnums.BLUETOOTH_OPP_PROVIDER, 181 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, 182 0); 183 Log.e(TAG, "createTable: Failed."); 184 throw ex; 185 } 186 } 187 dropTable(SQLiteDatabase db)188 private static void dropTable(SQLiteDatabase db) { 189 try { 190 db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE); 191 } catch (SQLException ex) { 192 ContentProfileErrorReportUtils.report( 193 BluetoothProfile.OPP, 194 BluetoothProtoEnums.BLUETOOTH_OPP_PROVIDER, 195 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, 196 1); 197 Log.e(TAG, "dropTable: Failed."); 198 throw ex; 199 } 200 } 201 202 @Override getType(Uri uri)203 public String getType(Uri uri) { 204 int match = sURIMatcher.match(uri); 205 switch (match) { 206 case SHARES: 207 return SHARE_LIST_TYPE; 208 case SHARES_ID: 209 return SHARE_TYPE; 210 default: 211 throw new IllegalArgumentException("Unknown URI in getType(): " + uri); 212 } 213 } 214 copyString(String key, ContentValues from, ContentValues to)215 private static void copyString(String key, ContentValues from, ContentValues to) { 216 String s = from.getAsString(key); 217 if (s != null) { 218 to.put(key, s); 219 } 220 } 221 copyInteger(String key, ContentValues from, ContentValues to)222 private static void copyInteger(String key, ContentValues from, ContentValues to) { 223 Integer i = from.getAsInteger(key); 224 if (i != null) { 225 to.put(key, i); 226 } 227 } 228 copyLong(String key, ContentValues from, ContentValues to)229 private static void copyLong(String key, ContentValues from, ContentValues to) { 230 Long i = from.getAsLong(key); 231 if (i != null) { 232 to.put(key, i); 233 } 234 } 235 putString(String key, Cursor from, ContentValues to)236 private static void putString(String key, Cursor from, ContentValues to) { 237 to.put(key, from.getString(from.getColumnIndexOrThrow(key))); 238 } 239 putInteger(String key, Cursor from, ContentValues to)240 private static void putInteger(String key, Cursor from, ContentValues to) { 241 to.put(key, from.getInt(from.getColumnIndexOrThrow(key))); 242 } 243 putLong(String key, Cursor from, ContentValues to)244 private static void putLong(String key, Cursor from, ContentValues to) { 245 to.put(key, from.getLong(from.getColumnIndexOrThrow(key))); 246 } 247 oppDatabaseMigration(Context ctx, Cursor cursor)248 public static boolean oppDatabaseMigration(Context ctx, Cursor cursor) { 249 boolean result = true; 250 SQLiteDatabase db = new DatabaseHelper(ctx).getWritableDatabase(); 251 while (cursor.moveToNext()) { 252 try { 253 ContentValues values = new ContentValues(); 254 255 final List<String> stringKeys = 256 new ArrayList<>( 257 Arrays.asList( 258 BluetoothShare.URI, 259 BluetoothShare.FILENAME_HINT, 260 BluetoothShare.MIMETYPE, 261 BluetoothShare.DESTINATION)); 262 for (String k : stringKeys) { 263 putString(k, cursor, values); 264 } 265 266 final List<String> integerKeys = 267 new ArrayList<>( 268 Arrays.asList( 269 BluetoothShare.VISIBILITY, 270 BluetoothShare.USER_CONFIRMATION, 271 BluetoothShare.DIRECTION, 272 BluetoothShare.STATUS, 273 Constants.MEDIA_SCANNED)); 274 for (String k : integerKeys) { 275 putInteger(k, cursor, values); 276 } 277 278 final List<String> longKeys = 279 new ArrayList<>( 280 Arrays.asList( 281 BluetoothShare.TOTAL_BYTES, BluetoothShare.TIMESTAMP)); 282 for (String k : longKeys) { 283 putLong(k, cursor, values); 284 } 285 286 db.insert(DB_TABLE, null, values); 287 Log.d(TAG, "One item migrated: " + values); 288 } catch (IllegalArgumentException e) { 289 ContentProfileErrorReportUtils.report( 290 BluetoothProfile.OPP, 291 BluetoothProtoEnums.BLUETOOTH_OPP_PROVIDER, 292 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, 293 2); 294 Log.e(TAG, "Failed to migrate one item: " + e); 295 result = false; 296 } 297 } 298 return result; 299 } 300 301 @Override insert(Uri uri, ContentValues values)302 public Uri insert(Uri uri, ContentValues values) { 303 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 304 305 if (sURIMatcher.match(uri) != SHARES) { 306 throw new IllegalArgumentException("insert: Unknown/Invalid URI " + uri); 307 } 308 309 ContentValues filteredValues = new ContentValues(); 310 311 copyString(BluetoothShare.URI, values, filteredValues); 312 copyString(BluetoothShare.FILENAME_HINT, values, filteredValues); 313 copyString(BluetoothShare.MIMETYPE, values, filteredValues); 314 copyString(BluetoothShare.DESTINATION, values, filteredValues); 315 316 copyInteger(BluetoothShare.VISIBILITY, values, filteredValues); 317 copyLong(BluetoothShare.TOTAL_BYTES, values, filteredValues); 318 if (values.getAsInteger(BluetoothShare.VISIBILITY) == null) { 319 filteredValues.put(BluetoothShare.VISIBILITY, BluetoothShare.VISIBILITY_VISIBLE); 320 } 321 Integer dir = values.getAsInteger(BluetoothShare.DIRECTION); 322 Integer con = values.getAsInteger(BluetoothShare.USER_CONFIRMATION); 323 324 if (dir == null) { 325 dir = BluetoothShare.DIRECTION_OUTBOUND; 326 } 327 if (dir == BluetoothShare.DIRECTION_OUTBOUND && con == null) { 328 con = BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED; 329 } 330 if (dir == BluetoothShare.DIRECTION_INBOUND && con == null) { 331 con = BluetoothShare.USER_CONFIRMATION_PENDING; 332 } 333 filteredValues.put(BluetoothShare.USER_CONFIRMATION, con); 334 filteredValues.put(BluetoothShare.DIRECTION, dir); 335 336 filteredValues.put(BluetoothShare.STATUS, BluetoothShare.STATUS_PENDING); 337 filteredValues.put(Constants.MEDIA_SCANNED, 0); 338 339 Long ts = values.getAsLong(BluetoothShare.TIMESTAMP); 340 if (ts == null) { 341 ts = System.currentTimeMillis(); 342 } 343 filteredValues.put(BluetoothShare.TIMESTAMP, ts); 344 345 Context context = getContext(); 346 347 long rowID = db.insert(DB_TABLE, null, filteredValues); 348 349 if (rowID == -1) { 350 Log.w(TAG, "couldn't insert " + uri + "into btopp database"); 351 ContentProfileErrorReportUtils.report( 352 BluetoothProfile.OPP, 353 BluetoothProtoEnums.BLUETOOTH_OPP_PROVIDER, 354 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN, 355 3); 356 return null; 357 } 358 359 context.getContentResolver().notifyChange(uri, null); 360 361 return Uri.parse(BluetoothShare.CONTENT_URI + "/" + rowID); 362 } 363 364 @Override onCreate()365 public boolean onCreate() { 366 mOpenHelper = new DatabaseHelper(getContext()); 367 return true; 368 } 369 370 @Override query( Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)371 public Cursor query( 372 Uri uri, 373 String[] projection, 374 String selection, 375 String[] selectionArgs, 376 String sortOrder) { 377 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 378 379 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 380 qb.setStrict(true); 381 382 int match = sURIMatcher.match(uri); 383 switch (match) { 384 case SHARES: 385 qb.setTables(DB_TABLE); 386 break; 387 case SHARES_ID: 388 qb.setTables(DB_TABLE); 389 qb.appendWhere(BluetoothShare._ID + "="); 390 qb.appendWhere(uri.getPathSegments().get(1)); 391 break; 392 default: 393 throw new IllegalArgumentException("Unknown URI: " + uri); 394 } 395 396 // The following is a large enough debug operation such that we want to guard it with an 397 // isLoggable check 398 if (Log.isLoggable(TAG, Log.VERBOSE)) { 399 java.lang.StringBuilder sb = new java.lang.StringBuilder(); 400 sb.append("starting query, database is "); 401 if (db != null) { 402 sb.append("not "); 403 } 404 sb.append("null; "); 405 if (projection == null) { 406 sb.append("projection is null; "); 407 } else if (projection.length == 0) { 408 sb.append("projection is empty; "); 409 } else { 410 for (int i = 0; i < projection.length; ++i) { 411 sb.append("projection["); 412 sb.append(i); 413 sb.append("] is "); 414 sb.append(projection[i]); 415 sb.append("; "); 416 } 417 } 418 sb.append("selection is "); 419 sb.append(selection); 420 sb.append("; "); 421 if (selectionArgs == null) { 422 sb.append("selectionArgs is null; "); 423 } else if (selectionArgs.length == 0) { 424 sb.append("selectionArgs is empty; "); 425 } else { 426 for (int i = 0; i < selectionArgs.length; ++i) { 427 sb.append("selectionArgs["); 428 sb.append(i); 429 sb.append("] is "); 430 sb.append(selectionArgs[i]); 431 sb.append("; "); 432 } 433 } 434 sb.append("sort is "); 435 sb.append(sortOrder); 436 sb.append("."); 437 Log.v(TAG, sb.toString()); 438 } 439 440 Cursor ret = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder); 441 442 if (ret == null) { 443 Log.w(TAG, "query failed in downloads database"); 444 ContentProfileErrorReportUtils.report( 445 BluetoothProfile.OPP, 446 BluetoothProtoEnums.BLUETOOTH_OPP_PROVIDER, 447 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN, 448 4); 449 return null; 450 } 451 452 ret.setNotificationUri(getContext().getContentResolver(), uri); 453 return ret; 454 } 455 456 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)457 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 458 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 459 460 int count = 0; 461 long rowId; 462 463 int match = sURIMatcher.match(uri); 464 switch (match) { 465 case SHARES: 466 case SHARES_ID: 467 { 468 String myWhere; 469 if (selection != null) { 470 if (match == SHARES) { 471 myWhere = "( " + selection + " )"; 472 } else { 473 myWhere = "( " + selection + " ) AND "; 474 } 475 } else { 476 myWhere = ""; 477 } 478 if (match == SHARES_ID) { 479 String segment = uri.getPathSegments().get(1); 480 rowId = Long.parseLong(segment); 481 myWhere += " ( " + BluetoothShare._ID + " = " + rowId + " ) "; 482 } 483 484 if (values.size() > 0) { 485 count = db.update(DB_TABLE, values, myWhere, selectionArgs); 486 } 487 break; 488 } 489 default: 490 throw new UnsupportedOperationException("Cannot update unknown URI: " + uri); 491 } 492 getContext().getContentResolver().notifyChange(uri, null); 493 494 return count; 495 } 496 497 @Override delete(Uri uri, String selection, String[] selectionArgs)498 public int delete(Uri uri, String selection, String[] selectionArgs) { 499 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 500 int count; 501 int match = sURIMatcher.match(uri); 502 switch (match) { 503 case SHARES: 504 case SHARES_ID: 505 { 506 String myWhere; 507 if (selection != null) { 508 if (match == SHARES) { 509 myWhere = "( " + selection + " )"; 510 } else { 511 myWhere = "( " + selection + " ) AND "; 512 } 513 } else { 514 myWhere = ""; 515 } 516 if (match == SHARES_ID) { 517 String segment = uri.getPathSegments().get(1); 518 long rowId = Long.parseLong(segment); 519 myWhere += " ( " + BluetoothShare._ID + " = " + rowId + " ) "; 520 } 521 522 count = db.delete(DB_TABLE, myWhere, selectionArgs); 523 break; 524 } 525 default: 526 throw new UnsupportedOperationException("Cannot delete unknown URI: " + uri); 527 } 528 getContext().getContentResolver().notifyChange(uri, null); 529 return count; 530 } 531 } 532