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.Intent; 39 import android.database.Cursor; 40 import android.database.SQLException; 41 import android.content.UriMatcher; 42 import android.database.sqlite.SQLiteDatabase; 43 import android.database.sqlite.SQLiteOpenHelper; 44 import android.database.sqlite.SQLiteQueryBuilder; 45 import android.net.Uri; 46 import android.provider.LiveFolders; 47 import android.util.Log; 48 49 import java.util.ArrayList; 50 import java.util.HashMap; 51 import java.util.List; 52 53 /** 54 * This provider allows application to interact with Bluetooth OPP manager 55 */ 56 57 public final class BluetoothOppProvider extends ContentProvider { 58 59 private static final String TAG = "BluetoothOppProvider"; 60 private static final boolean D = Constants.DEBUG; 61 private static final boolean V = Constants.VERBOSE; 62 63 /** Database filename */ 64 private static final String DB_NAME = "btopp.db"; 65 66 /** Current database version */ 67 private static final int DB_VERSION = 1; 68 69 /** Database version from which upgrading is a nop */ 70 private static final int DB_VERSION_NOP_UPGRADE_FROM = 0; 71 72 /** Database version to which upgrading is a nop */ 73 private static final int DB_VERSION_NOP_UPGRADE_TO = 1; 74 75 /** Name of table in the database */ 76 private static final String DB_TABLE = "btopp"; 77 78 /** MIME type for the entire share list */ 79 private static final String SHARE_LIST_TYPE = "vnd.android.cursor.dir/vnd.android.btopp"; 80 81 /** MIME type for an individual share */ 82 private static final String SHARE_TYPE = "vnd.android.cursor.item/vnd.android.btopp"; 83 84 /** URI matcher used to recognize URIs sent by applications */ 85 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 86 87 /** URI matcher constant for the URI of the entire share list */ 88 private static final int SHARES = 1; 89 90 /** URI matcher constant for the URI of an individual share */ 91 private static final int SHARES_ID = 2; 92 93 /** URI matcher constant for the URI of live folder */ 94 private static final int LIVE_FOLDER_RECEIVED_FILES = 3; 95 static { 96 sURIMatcher.addURI("com.android.bluetooth.opp", "btopp", SHARES); 97 sURIMatcher.addURI("com.android.bluetooth.opp", "btopp/#", SHARES_ID); 98 sURIMatcher.addURI("com.android.bluetooth.opp", "live_folders/received", 99 LIVE_FOLDER_RECEIVED_FILES); 100 } 101 102 private static final HashMap<String, String> LIVE_FOLDER_PROJECTION_MAP; 103 static { 104 LIVE_FOLDER_PROJECTION_MAP = new HashMap<String, String>(); 105 LIVE_FOLDER_PROJECTION_MAP.put(LiveFolders._ID, BluetoothShare._ID + " AS " 106 + LiveFolders._ID); 107 LIVE_FOLDER_PROJECTION_MAP.put(LiveFolders.NAME, BluetoothShare.FILENAME_HINT + " AS " 108 + LiveFolders.NAME); 109 } 110 111 /** The database that lies underneath this content provider */ 112 private SQLiteOpenHelper mOpenHelper = null; 113 114 /** 115 * Creates and updated database on demand when opening it. Helper class to 116 * create database the first time the provider is initialized and upgrade it 117 * when a new version of the provider needs an updated version of the 118 * database. 119 */ 120 private final class DatabaseHelper extends SQLiteOpenHelper { 121 122 public DatabaseHelper(final Context context) { 123 super(context, DB_NAME, null, DB_VERSION); 124 } 125 126 /** 127 * Creates database the first time we try to open it. 128 */ 129 @Override 130 public void onCreate(final SQLiteDatabase db) { 131 if (V) Log.v(TAG, "populating new database"); 132 createTable(db); 133 } 134 135 //TODO: use this function to check garbage transfer left in db, for example, 136 // a crash incoming file 137 /* 138 * (not a javadoc comment) Checks data integrity when opening the 139 * database. 140 */ 141 /* 142 * @Override public void onOpen(final SQLiteDatabase db) { 143 * super.onOpen(db); } 144 */ 145 146 /** 147 * Updates the database format when a content provider is used with a 148 * database that was created with a different format. 149 */ 150 // Note: technically, this could also be a downgrade, so if we want 151 // to gracefully handle upgrades we should be careful about 152 // what to do on downgrades. 153 @Override 154 public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) { 155 if (oldV == DB_VERSION_NOP_UPGRADE_FROM) { 156 if (newV == DB_VERSION_NOP_UPGRADE_TO) { // that's a no-op 157 // upgrade. 158 return; 159 } 160 // NOP_FROM and NOP_TO are identical, just in different 161 // codelines. Upgrading 162 // from NOP_FROM is the same as upgrading from NOP_TO. 163 oldV = DB_VERSION_NOP_UPGRADE_TO; 164 } 165 Log.i(TAG, "Upgrading downloads database from version " + oldV + " to " 166 + newV + ", which will destroy all old data"); 167 dropTable(db); 168 createTable(db); 169 } 170 171 } 172 173 private void createTable(SQLiteDatabase db) { 174 try { 175 db.execSQL("CREATE TABLE " + DB_TABLE + "(" + BluetoothShare._ID 176 + " INTEGER PRIMARY KEY AUTOINCREMENT," + BluetoothShare.URI + " TEXT, " 177 + BluetoothShare.FILENAME_HINT + " TEXT, " + BluetoothShare._DATA + " TEXT, " 178 + BluetoothShare.MIMETYPE + " TEXT, " + BluetoothShare.DIRECTION + " INTEGER, " 179 + BluetoothShare.DESTINATION + " TEXT, " + BluetoothShare.VISIBILITY 180 + " INTEGER, " + BluetoothShare.USER_CONFIRMATION + " INTEGER, " 181 + BluetoothShare.STATUS + " INTEGER, " + BluetoothShare.TOTAL_BYTES 182 + " INTEGER, " + BluetoothShare.CURRENT_BYTES + " INTEGER, " 183 + BluetoothShare.TIMESTAMP + " INTEGER," + Constants.MEDIA_SCANNED 184 + " INTEGER); "); 185 } catch (SQLException ex) { 186 Log.e(TAG, "couldn't create table in downloads database"); 187 throw ex; 188 } 189 } 190 191 private void dropTable(SQLiteDatabase db) { 192 try { 193 db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE); 194 } catch (SQLException ex) { 195 Log.e(TAG, "couldn't drop table in downloads database"); 196 throw ex; 197 } 198 } 199 200 @Override 201 public String getType(Uri uri) { 202 int match = sURIMatcher.match(uri); 203 switch (match) { 204 case SHARES: { 205 return SHARE_LIST_TYPE; 206 } 207 case SHARES_ID: { 208 return SHARE_TYPE; 209 } 210 default: { 211 if (D) Log.d(TAG, "calling getType on an unknown URI: " + uri); 212 throw new IllegalArgumentException("Unknown URI: " + uri); 213 } 214 } 215 } 216 217 private static final void copyString(String key, ContentValues from, ContentValues to) { 218 String s = from.getAsString(key); 219 if (s != null) { 220 to.put(key, s); 221 } 222 } 223 224 private static final void copyInteger(String key, ContentValues from, ContentValues to) { 225 Integer i = from.getAsInteger(key); 226 if (i != null) { 227 to.put(key, i); 228 } 229 } 230 231 @Override 232 public Uri insert(Uri uri, ContentValues values) { 233 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 234 235 if (sURIMatcher.match(uri) != SHARES) { 236 if (D) Log.d(TAG, "calling insert on an unknown/invalid URI: " + uri); 237 throw new IllegalArgumentException("Unknown/Invalid URI " + uri); 238 } 239 240 ContentValues filteredValues = new ContentValues(); 241 242 copyString(BluetoothShare.URI, values, filteredValues); 243 copyString(BluetoothShare.FILENAME_HINT, values, filteredValues); 244 copyString(BluetoothShare.MIMETYPE, values, filteredValues); 245 copyString(BluetoothShare.DESTINATION, values, filteredValues); 246 247 copyInteger(BluetoothShare.VISIBILITY, values, filteredValues); 248 copyInteger(BluetoothShare.TOTAL_BYTES, values, filteredValues); 249 250 if (values.getAsInteger(BluetoothShare.VISIBILITY) == null) { 251 filteredValues.put(BluetoothShare.VISIBILITY, BluetoothShare.VISIBILITY_VISIBLE); 252 } 253 Integer dir = values.getAsInteger(BluetoothShare.DIRECTION); 254 Integer con = values.getAsInteger(BluetoothShare.USER_CONFIRMATION); 255 String address = values.getAsString(BluetoothShare.DESTINATION); 256 257 if (values.getAsInteger(BluetoothShare.DIRECTION) == null) { 258 dir = BluetoothShare.DIRECTION_OUTBOUND; 259 } 260 if (dir == BluetoothShare.DIRECTION_OUTBOUND && con == null) { 261 con = BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED; 262 } 263 if (dir == BluetoothShare.DIRECTION_INBOUND && con == null) { 264 con = BluetoothShare.USER_CONFIRMATION_PENDING; 265 } 266 filteredValues.put(BluetoothShare.USER_CONFIRMATION, con); 267 filteredValues.put(BluetoothShare.DIRECTION, dir); 268 269 filteredValues.put(BluetoothShare.STATUS, BluetoothShare.STATUS_PENDING); 270 filteredValues.put(Constants.MEDIA_SCANNED, 0); 271 272 Long ts = values.getAsLong(BluetoothShare.TIMESTAMP); 273 if (ts == null) { 274 ts = System.currentTimeMillis(); 275 } 276 filteredValues.put(BluetoothShare.TIMESTAMP, ts); 277 278 Context context = getContext(); 279 context.startService(new Intent(context, BluetoothOppService.class)); 280 281 long rowID = db.insert(DB_TABLE, null, filteredValues); 282 283 Uri ret = null; 284 285 if (rowID != -1) { 286 context.startService(new Intent(context, BluetoothOppService.class)); 287 ret = Uri.parse(BluetoothShare.CONTENT_URI + "/" + rowID); 288 context.getContentResolver().notifyChange(uri, null); 289 } else { 290 if (D) Log.d(TAG, "couldn't insert into btopp database"); 291 } 292 293 return ret; 294 } 295 296 @Override 297 public boolean onCreate() { 298 mOpenHelper = new DatabaseHelper(getContext()); 299 return true; 300 } 301 302 @Override 303 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 304 String sortOrder) { 305 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 306 307 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 308 309 int match = sURIMatcher.match(uri); 310 switch (match) { 311 case SHARES: { 312 qb.setTables(DB_TABLE); 313 break; 314 } 315 case SHARES_ID: { 316 qb.setTables(DB_TABLE); 317 qb.appendWhere(BluetoothShare._ID + "="); 318 qb.appendWhere(uri.getPathSegments().get(1)); 319 break; 320 } 321 case LIVE_FOLDER_RECEIVED_FILES: { 322 qb.setTables(DB_TABLE); 323 qb.setProjectionMap(LIVE_FOLDER_PROJECTION_MAP); 324 qb.appendWhere(BluetoothShare.DIRECTION + "=" + BluetoothShare.DIRECTION_INBOUND 325 + " AND " + BluetoothShare.STATUS + "=" + BluetoothShare.STATUS_SUCCESS); 326 sortOrder = "_id DESC, " + sortOrder; 327 break; 328 } 329 default: { 330 if (D) Log.d(TAG, "querying unknown URI: " + uri); 331 throw new IllegalArgumentException("Unknown URI: " + uri); 332 } 333 } 334 335 if (V) { 336 java.lang.StringBuilder sb = new java.lang.StringBuilder(); 337 sb.append("starting query, database is "); 338 if (db != null) { 339 sb.append("not "); 340 } 341 sb.append("null; "); 342 if (projection == null) { 343 sb.append("projection is null; "); 344 } else if (projection.length == 0) { 345 sb.append("projection is empty; "); 346 } else { 347 for (int i = 0; i < projection.length; ++i) { 348 sb.append("projection["); 349 sb.append(i); 350 sb.append("] is "); 351 sb.append(projection[i]); 352 sb.append("; "); 353 } 354 } 355 sb.append("selection is "); 356 sb.append(selection); 357 sb.append("; "); 358 if (selectionArgs == null) { 359 sb.append("selectionArgs is null; "); 360 } else if (selectionArgs.length == 0) { 361 sb.append("selectionArgs is empty; "); 362 } else { 363 for (int i = 0; i < selectionArgs.length; ++i) { 364 sb.append("selectionArgs["); 365 sb.append(i); 366 sb.append("] is "); 367 sb.append(selectionArgs[i]); 368 sb.append("; "); 369 } 370 } 371 sb.append("sort is "); 372 sb.append(sortOrder); 373 sb.append("."); 374 Log.v(TAG, sb.toString()); 375 } 376 377 Cursor ret = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder); 378 379 if (ret != null) { 380 ret.setNotificationUri(getContext().getContentResolver(), uri); 381 if (V) Log.v(TAG, "created cursor " + ret + " on behalf of ");// + 382 } else { 383 if (D) Log.d(TAG, "query failed in downloads database"); 384 } 385 386 return ret; 387 } 388 389 @Override 390 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 391 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 392 393 int count; 394 long rowId = 0; 395 396 int match = sURIMatcher.match(uri); 397 switch (match) { 398 case SHARES: 399 case SHARES_ID: { 400 String myWhere; 401 if (selection != null) { 402 if (match == SHARES) { 403 myWhere = "( " + selection + " )"; 404 } else { 405 myWhere = "( " + selection + " ) AND "; 406 } 407 } else { 408 myWhere = ""; 409 } 410 if (match == SHARES_ID) { 411 String segment = uri.getPathSegments().get(1); 412 rowId = Long.parseLong(segment); 413 myWhere += " ( " + BluetoothShare._ID + " = " + rowId + " ) "; 414 } 415 416 if (values.size() > 0) { 417 count = db.update(DB_TABLE, values, myWhere, selectionArgs); 418 } else { 419 count = 0; 420 } 421 break; 422 } 423 default: { 424 if (D) Log.d(TAG, "updating unknown/invalid URI: " + uri); 425 throw new UnsupportedOperationException("Cannot update URI: " + uri); 426 } 427 } 428 getContext().getContentResolver().notifyChange(uri, null); 429 430 return count; 431 } 432 433 @Override 434 public int delete(Uri uri, String selection, String[] selectionArgs) { 435 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 436 int count; 437 int match = sURIMatcher.match(uri); 438 switch (match) { 439 case SHARES: 440 case SHARES_ID: { 441 String myWhere; 442 if (selection != null) { 443 if (match == SHARES) { 444 myWhere = "( " + selection + " )"; 445 } else { 446 myWhere = "( " + selection + " ) AND "; 447 } 448 } else { 449 myWhere = ""; 450 } 451 if (match == SHARES_ID) { 452 String segment = uri.getPathSegments().get(1); 453 long rowId = Long.parseLong(segment); 454 myWhere += " ( " + BluetoothShare._ID + " = " + rowId + " ) "; 455 } 456 457 count = db.delete(DB_TABLE, myWhere, selectionArgs); 458 break; 459 } 460 default: { 461 if (D) Log.d(TAG, "deleting unknown/invalid URI: " + uri); 462 throw new UnsupportedOperationException("Cannot delete URI: " + uri); 463 } 464 } 465 getContext().getContentResolver().notifyChange(uri, null); 466 return count; 467 } 468 } 469