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