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