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