CellBroadcastContentProvider.java revision eef14be1b2b77fc08a6cc5ef301ba49ea54c0c0a
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    private static final String QUERY_BY_SERIAL = Telephony.CellBroadcasts.SERIAL_NUMBER + "=?";
185
186    private static final String QUERY_BY_SERIAL_PLMN = QUERY_BY_SERIAL + " AND "
187            + Telephony.CellBroadcasts.PLMN + "=?";
188
189    private static final String QUERY_BY_SERIAL_PLMN_LAC = QUERY_BY_SERIAL_PLMN + " AND "
190            + Telephony.CellBroadcasts.LAC + "=?";
191
192    private static final String QUERY_BY_SERIAL_PLMN_LAC_CID = QUERY_BY_SERIAL_PLMN_LAC + " AND "
193            + Telephony.CellBroadcasts.CID + "=?";
194
195    private static final String[] SELECT_ID_COLUMN = {Telephony.CellBroadcasts._ID};
196
197    /**
198     * Internal method to insert a new Cell Broadcast into the database and notify observers.
199     * @param message the message to insert
200     * @return true if the broadcast is new, false if it's a duplicate broadcast.
201     */
202    boolean insertNewBroadcast(CellBroadcastMessage message) {
203        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
204        ContentValues cv = message.getContentValues();
205
206        // Check for existing alert with same serial number and geo scope
207        String serial = cv.getAsString(Telephony.CellBroadcasts.SERIAL_NUMBER);
208        String plmn = cv.getAsString(Telephony.CellBroadcasts.PLMN);
209        String lac = cv.getAsString(Telephony.CellBroadcasts.LAC);
210        String cid = cv.getAsString(Telephony.CellBroadcasts.CID);
211        String selection;
212        String[] selectionArgs;
213
214        if (plmn != null) {
215            if (lac != null) {
216                if (cid != null) {
217                    selection = QUERY_BY_SERIAL_PLMN_LAC_CID;
218                    selectionArgs = new String[] {serial, plmn, lac, cid};
219                } else {
220                    selection = QUERY_BY_SERIAL_PLMN_LAC;
221                    selectionArgs = new String[] {serial, plmn, lac};
222                }
223            } else {
224                selection = QUERY_BY_SERIAL_PLMN;
225                selectionArgs = new String[] {serial, plmn};
226            }
227        } else {
228            selection = QUERY_BY_SERIAL;
229            selectionArgs = new String[] {serial};
230        }
231
232        Cursor c = db.query(CellBroadcastDatabaseHelper.TABLE_NAME, SELECT_ID_COLUMN,
233                selection, selectionArgs, null, null, null);
234
235        if (c.getCount() != 0) {
236            Log.d(TAG, "ignoring dup broadcast serial=" + serial + " found " + c.getCount());
237            return false;
238        }
239
240        long rowId = db.insert(CellBroadcastDatabaseHelper.TABLE_NAME, null, cv);
241        if (rowId == -1) {
242            Log.e(TAG, "failed to insert new broadcast into database");
243            // Return true on DB write failure because we still want to notify the user.
244            // The CellBroadcastMessage will be passed with the intent, so the message will be
245            // displayed in the emergency alert dialog, or the dialog that is displayed when
246            // the user selects the notification for a non-emergency broadcast, even if the
247            // broadcast could not be written to the database.
248        }
249        return true;    // broadcast is not a duplicate
250    }
251
252    /**
253     * Internal method to delete a cell broadcast by row ID and notify observers.
254     * @param rowId the row ID of the broadcast to delete
255     * @param decrementUnreadCount true to decrement the count of unread alerts
256     * @return true if the database was updated, false otherwise
257     */
258    boolean deleteBroadcast(long rowId, boolean decrementUnreadCount) {
259        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
260
261        int rowCount = db.delete(CellBroadcastDatabaseHelper.TABLE_NAME,
262                Telephony.CellBroadcasts._ID + "=?",
263                new String[]{Long.toString(rowId)});
264        if (rowCount != 0) {
265            if (decrementUnreadCount) {
266                CellBroadcastReceiverApp.decrementUnreadAlertCount();
267            }
268            return true;
269        } else {
270            Log.e(TAG, "failed to delete broadcast at row " + rowId);
271            return false;
272        }
273    }
274
275    /**
276     * Internal method to delete all cell broadcasts and notify observers.
277     * @return true if the database was updated, false otherwise
278     */
279    boolean deleteAllBroadcasts() {
280        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
281
282        int rowCount = db.delete(CellBroadcastDatabaseHelper.TABLE_NAME, null, null);
283        if (rowCount != 0) {
284            CellBroadcastReceiverApp.resetUnreadAlertCount();
285            return true;
286        } else {
287            Log.e(TAG, "failed to delete all broadcasts");
288            return false;
289        }
290    }
291
292    /**
293     * Internal method to mark a broadcast as read and notify observers. The broadcast can be
294     * identified by delivery time (for new alerts) or by row ID. The caller is responsible for
295     * decrementing the unread non-emergency alert count, if necessary.
296     *
297     * @param columnName the column name to query (ID or delivery time)
298     * @param columnValue the ID or delivery time of the broadcast to mark read
299     * @return true if the database was updated, false otherwise
300     */
301    boolean markBroadcastRead(String columnName, long columnValue) {
302        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
303
304        ContentValues cv = new ContentValues(1);
305        cv.put(Telephony.CellBroadcasts.MESSAGE_READ, 1);
306
307        String whereClause = columnName + "=?";
308        String[] whereArgs = new String[]{Long.toString(columnValue)};
309
310        int rowCount = db.update(CellBroadcastDatabaseHelper.TABLE_NAME, cv, whereClause, whereArgs);
311        if (rowCount != 0) {
312            return true;
313        } else {
314            Log.e(TAG, "failed to mark broadcast read: " + columnName + " = " + columnValue);
315            return false;
316        }
317    }
318
319    /** Callback for users of AsyncCellBroadcastOperation. */
320    interface CellBroadcastOperation {
321        /**
322         * Perform an operation using the specified provider.
323         * @param provider the CellBroadcastContentProvider to use
324         * @return true if any rows were changed, false otherwise
325         */
326        boolean execute(CellBroadcastContentProvider provider);
327    }
328
329    /**
330     * Async task to call this content provider's internal methods on a background thread.
331     * The caller supplies the CellBroadcastOperation object to call for this provider.
332     */
333    static class AsyncCellBroadcastTask extends AsyncTask<CellBroadcastOperation, Void, Void> {
334        /** Reference to this app's content resolver. */
335        private ContentResolver mContentResolver;
336
337        AsyncCellBroadcastTask(ContentResolver contentResolver) {
338            mContentResolver = contentResolver;
339        }
340
341        /**
342         * Perform a generic operation on the CellBroadcastContentProvider.
343         * @param params the CellBroadcastOperation object to call for this provider
344         * @return void
345         */
346        @Override
347        protected Void doInBackground(CellBroadcastOperation... params) {
348            ContentProviderClient cpc = mContentResolver.acquireContentProviderClient(
349                    CellBroadcastContentProvider.CB_AUTHORITY);
350            CellBroadcastContentProvider provider = (CellBroadcastContentProvider)
351                    cpc.getLocalContentProvider();
352
353            if (provider != null) {
354                try {
355                    boolean changed = params[0].execute(provider);
356                    if (changed) {
357                        Log.d(TAG, "database changed: notifying observers...");
358                        mContentResolver.notifyChange(CONTENT_URI, null, false);
359                    }
360                } finally {
361                    cpc.release();
362                }
363            } else {
364                Log.e(TAG, "getLocalContentProvider() returned null");
365            }
366
367            mContentResolver = null;    // free reference to content resolver
368            return null;
369        }
370    }
371}
372