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