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