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