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