1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.cellbroadcastreceiver;
18
19import android.content.ContentProvider;
20import android.content.ContentProviderClient;
21import android.content.ContentResolver;
22import android.content.ContentValues;
23import android.content.UriMatcher;
24import android.database.Cursor;
25import android.database.sqlite.SQLiteDatabase;
26import android.database.sqlite.SQLiteOpenHelper;
27import android.database.sqlite.SQLiteQueryBuilder;
28import android.net.Uri;
29import android.os.AsyncTask;
30import android.provider.Telephony;
31import android.telephony.CellBroadcastMessage;
32import android.text.TextUtils;
33import android.util.Log;
34
35/**
36 * ContentProvider for the database of received cell broadcasts.
37 */
38public class CellBroadcastContentProvider extends ContentProvider {
39    private static final String TAG = "CellBroadcastContentProvider";
40
41    /** URI matcher for ContentProvider queries. */
42    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
43
44    /** Authority string for content URIs. */
45    static final String CB_AUTHORITY = "cellbroadcasts";
46
47    /** Content URI for notifying observers. */
48    static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts/");
49
50    /** URI matcher type to get all cell broadcasts. */
51    private static final int CB_ALL = 0;
52
53    /** URI matcher type to get a cell broadcast by ID. */
54    private static final int CB_ALL_ID = 1;
55
56    /** MIME type for the list of all cell broadcasts. */
57    private static final String CB_LIST_TYPE = "vnd.android.cursor.dir/cellbroadcast";
58
59    /** MIME type for an individual cell broadcast. */
60    private static final String CB_TYPE = "vnd.android.cursor.item/cellbroadcast";
61
62    static {
63        sUriMatcher.addURI(CB_AUTHORITY, null, CB_ALL);
64        sUriMatcher.addURI(CB_AUTHORITY, "#", CB_ALL_ID);
65    }
66
67    /** The database for this content provider. */
68    private SQLiteOpenHelper mOpenHelper;
69
70    /**
71     * Initialize content provider.
72     * @return true if the provider was successfully loaded, false otherwise
73     */
74    @Override
75    public boolean onCreate() {
76        mOpenHelper = new CellBroadcastDatabaseHelper(getContext());
77        return true;
78    }
79
80    /**
81     * Return a cursor for the cell broadcast table.
82     * @param uri the URI to query.
83     * @param projection the list of columns to put into the cursor, or null.
84     * @param selection the selection criteria to apply when filtering rows, or null.
85     * @param selectionArgs values to replace ?s in selection string.
86     * @param sortOrder how the rows in the cursor should be sorted, or null to sort from most
87     *  recently received to least recently received.
88     * @return a Cursor or null.
89     */
90    @Override
91    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
92            String sortOrder) {
93        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
94        qb.setTables(CellBroadcastDatabaseHelper.TABLE_NAME);
95
96        int match = sUriMatcher.match(uri);
97        switch (match) {
98            case CB_ALL:
99                // get all broadcasts
100                break;
101
102            case CB_ALL_ID:
103                // get broadcast by ID
104                qb.appendWhere("(_id=" + uri.getPathSegments().get(0) + ')');
105                break;
106
107            default:
108                Log.e(TAG, "Invalid query: " + uri);
109                throw new IllegalArgumentException("Unknown URI: " + uri);
110        }
111
112        String orderBy;
113        if (!TextUtils.isEmpty(sortOrder)) {
114            orderBy = sortOrder;
115        } else {
116            orderBy = Telephony.CellBroadcasts.DEFAULT_SORT_ORDER;
117        }
118
119        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
120        Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);
121        if (c != null) {
122            c.setNotificationUri(getContext().getContentResolver(), CONTENT_URI);
123        }
124        return c;
125    }
126
127    /**
128     * Return the MIME type of the data at the specified URI.
129     * @param uri the URI to query.
130     * @return a MIME type string, or null if there is no type.
131     */
132    @Override
133    public String getType(Uri uri) {
134        int match = sUriMatcher.match(uri);
135        switch (match) {
136            case CB_ALL:
137                return CB_LIST_TYPE;
138
139            case CB_ALL_ID:
140                return CB_TYPE;
141
142            default:
143                return null;
144        }
145    }
146
147    /**
148     * Insert a new row. This throws an exception, as the database can only be modified by
149     * calling custom methods in this class, and not via the ContentProvider interface.
150     * @param uri the content:// URI of the insertion request.
151     * @param values a set of column_name/value pairs to add to the database.
152     * @return the URI for the newly inserted item.
153     */
154    @Override
155    public Uri insert(Uri uri, ContentValues values) {
156        throw new UnsupportedOperationException("insert not supported");
157    }
158
159    /**
160     * Delete one or more rows. This throws an exception, as the database can only be modified by
161     * calling custom methods in this class, and not via the ContentProvider interface.
162     * @param uri the full URI to query, including a row ID (if a specific record is requested).
163     * @param selection an optional restriction to apply to rows when deleting.
164     * @return the number of rows affected.
165     */
166    @Override
167    public int delete(Uri uri, String selection, String[] selectionArgs) {
168        throw new UnsupportedOperationException("delete not supported");
169    }
170
171    /**
172     * Update one or more rows. This throws an exception, as the database can only be modified by
173     * calling custom methods in this class, and not via the ContentProvider interface.
174     * @param uri the URI to query, potentially including the row ID.
175     * @param values a Bundle mapping from column names to new column values.
176     * @param selection an optional filter to match rows to update.
177     * @return the number of rows affected.
178     */
179    @Override
180    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
181        throw new UnsupportedOperationException("update not supported");
182    }
183
184    /**
185     * Internal method to insert a new Cell Broadcast into the database and notify observers.
186     * @param message the message to insert
187     * @return true if the broadcast is new, false if it's a duplicate broadcast.
188     */
189    boolean insertNewBroadcast(CellBroadcastMessage message) {
190        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
191        ContentValues cv = message.getContentValues();
192
193        // Note: this method previously queried the database for duplicate message IDs, but this
194        // is not compatible with CMAS carrier requirements and could also cause other emergency
195        // alerts, e.g. ETWS, to not display if the database is filled with old messages.
196        // Use duplicate message ID detection in CellBroadcastAlertService instead of DB query.
197
198        long rowId = db.insert(CellBroadcastDatabaseHelper.TABLE_NAME, null, cv);
199        if (rowId == -1) {
200            Log.e(TAG, "failed to insert new broadcast into database");
201            // Return true on DB write failure because we still want to notify the user.
202            // The CellBroadcastMessage will be passed with the intent, so the message will be
203            // displayed in the emergency alert dialog, or the dialog that is displayed when
204            // the user selects the notification for a non-emergency broadcast, even if the
205            // broadcast could not be written to the database.
206        }
207        return true;    // broadcast is not a duplicate
208    }
209
210    /**
211     * Internal method to delete a cell broadcast by row ID and notify observers.
212     * @param rowId the row ID of the broadcast to delete
213     * @return true if the database was updated, false otherwise
214     */
215    boolean deleteBroadcast(long rowId) {
216        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
217
218        int rowCount = db.delete(CellBroadcastDatabaseHelper.TABLE_NAME,
219                Telephony.CellBroadcasts._ID + "=?",
220                new String[]{Long.toString(rowId)});
221        if (rowCount != 0) {
222            return true;
223        } else {
224            Log.e(TAG, "failed to delete broadcast at row " + rowId);
225            return false;
226        }
227    }
228
229    /**
230     * Internal method to delete all cell broadcasts and notify observers.
231     * @return true if the database was updated, false otherwise
232     */
233    boolean deleteAllBroadcasts() {
234        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
235
236        int rowCount = db.delete(CellBroadcastDatabaseHelper.TABLE_NAME, null, null);
237        if (rowCount != 0) {
238            return true;
239        } else {
240            Log.e(TAG, "failed to delete all broadcasts");
241            return false;
242        }
243    }
244
245    /**
246     * Internal method to mark a broadcast as read and notify observers. The broadcast can be
247     * identified by delivery time (for new alerts) or by row ID. The caller is responsible for
248     * decrementing the unread non-emergency alert count, if necessary.
249     *
250     * @param columnName the column name to query (ID or delivery time)
251     * @param columnValue the ID or delivery time of the broadcast to mark read
252     * @return true if the database was updated, false otherwise
253     */
254    boolean markBroadcastRead(String columnName, long columnValue) {
255        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
256
257        ContentValues cv = new ContentValues(1);
258        cv.put(Telephony.CellBroadcasts.MESSAGE_READ, 1);
259
260        String whereClause = columnName + "=?";
261        String[] whereArgs = new String[]{Long.toString(columnValue)};
262
263        int rowCount = db.update(CellBroadcastDatabaseHelper.TABLE_NAME, cv, whereClause, whereArgs);
264        if (rowCount != 0) {
265            return true;
266        } else {
267            Log.e(TAG, "failed to mark broadcast read: " + columnName + " = " + columnValue);
268            return false;
269        }
270    }
271
272    /** Callback for users of AsyncCellBroadcastOperation. */
273    interface CellBroadcastOperation {
274        /**
275         * Perform an operation using the specified provider.
276         * @param provider the CellBroadcastContentProvider to use
277         * @return true if any rows were changed, false otherwise
278         */
279        boolean execute(CellBroadcastContentProvider provider);
280    }
281
282    /**
283     * Async task to call this content provider's internal methods on a background thread.
284     * The caller supplies the CellBroadcastOperation object to call for this provider.
285     */
286    static class AsyncCellBroadcastTask extends AsyncTask<CellBroadcastOperation, Void, Void> {
287        /** Reference to this app's content resolver. */
288        private ContentResolver mContentResolver;
289
290        AsyncCellBroadcastTask(ContentResolver contentResolver) {
291            mContentResolver = contentResolver;
292        }
293
294        /**
295         * Perform a generic operation on the CellBroadcastContentProvider.
296         * @param params the CellBroadcastOperation object to call for this provider
297         * @return void
298         */
299        @Override
300        protected Void doInBackground(CellBroadcastOperation... params) {
301            ContentProviderClient cpc = mContentResolver.acquireContentProviderClient(
302                    CellBroadcastContentProvider.CB_AUTHORITY);
303            CellBroadcastContentProvider provider = (CellBroadcastContentProvider)
304                    cpc.getLocalContentProvider();
305
306            if (provider != null) {
307                try {
308                    boolean changed = params[0].execute(provider);
309                    if (changed) {
310                        Log.d(TAG, "database changed: notifying observers...");
311                        mContentResolver.notifyChange(CONTENT_URI, null, false);
312                    }
313                } finally {
314                    cpc.release();
315                }
316            } else {
317                Log.e(TAG, "getLocalContentProvider() returned null");
318            }
319
320            mContentResolver = null;    // free reference to content resolver
321            return null;
322        }
323    }
324}
325