/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.talkback.labeling;
import android.annotation.TargetApi;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Build;
import android.support.v4.os.UserManagerCompat;
import android.text.TextUtils;
import android.util.Log;
import com.android.talkback.BuildConfig;
import com.android.utils.LogUtils;
import com.android.utils.labeling.LabelsTable;
import java.util.Locale;
/**
* A content provider for accessing TalkBack custom label data.
* <p>
* The following operations are supported at each URI:
* <ul>
* <li>{@code AUTHORITY/labels}: query and insert.
* <li>{@code AUTHORITY/labels/#}: query, update, and delete.
* </ul>
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public class LabelProvider extends ContentProvider {
public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".providers.LabelProvider";
static final String LABELS_PATH = "labels";
static final Uri LABELS_CONTENT_URI = new Uri.Builder()
.scheme("content")
.authority(AUTHORITY)
.path(LABELS_PATH)
.build();
private static final Uri LABELS_ID_CONTENT_URI = Uri.withAppendedPath(LABELS_CONTENT_URI, "#");
private static final String PACKAGE_SUMMARY_PATH = "packageSummary";
private static final Uri PACKAGE_SUMMARY_URI = new Uri.Builder()
.scheme("content")
.authority(AUTHORITY)
.path(PACKAGE_SUMMARY_PATH)
.build();
/* Codes for URI matching */
static final int LABELS = 1;
static final int LABELS_ID = 2;
private static final int PACKAGE_SUMMARY = 3;
private static final String UNKNOWN_URI_FORMAT_STRING = "Unknown URI: %s";
private static final String NULL_URI_FORMAT_STRING = "URI is null";
static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sUriMatcher.addURI(AUTHORITY, LABELS_CONTENT_URI.getPath(), LABELS);
sUriMatcher.addURI(AUTHORITY, LABELS_ID_CONTENT_URI.getPath(), LABELS_ID);
sUriMatcher.addURI(AUTHORITY, PACKAGE_SUMMARY_URI.getPath(), PACKAGE_SUMMARY);
}
private SQLiteDatabase mDatabase;
@Override
public boolean onCreate() {
return true;
}
@Override
public String getType(Uri uri) {
return null;
}
/**
* Inserts a label in the labels database.
*
* @param uri The content URI for labels.
* @param values The values to insert for the new label.
* @return The URI of the newly inserted label,
* or {@code null} if the insert failed.
*/
@Override
public Uri insert(Uri uri, ContentValues values) {
if (uri == null) {
LogUtils.log(this, Log.WARN, NULL_URI_FORMAT_STRING);
return null;
}
if (!UserManagerCompat.isUserUnlocked(getContext())) {
return null;
}
switch (sUriMatcher.match(uri)) {
case LABELS:
initializeDatabaseIfNull();
if (values == null) {
return null;
}
if (values.containsKey(LabelsTable.KEY_ID)) {
LogUtils.log(this, Log.WARN, "Label ID must be assigned by the database.");
return null;
}
long rowId = mDatabase.insert(LabelsTable.TABLE_NAME, null, values);
if (rowId < 0) {
LogUtils.log(this, Log.WARN, "Failed to insert label.");
return null;
} else {
return ContentUris.withAppendedId(LABELS_CONTENT_URI, rowId);
}
default:
LogUtils.log(this, Log.WARN, UNKNOWN_URI_FORMAT_STRING, uri);
return null;
}
}
/**
* Queries for a label or multiple labels in the labels database.
*
* @param uri The URI representing the type of query to perform:
* {@code LABELS_CONTENT_URI} for a subset of all labels,
* {@code LABELS_ID_CONTENT_URI} for a specific label, or
* {@code PACKAGE_SUMMARY} for a label count per package.
* @param projection The columns to return.
* @param selection The WHERE clause for the query.
* @param selectionArgs The arguments for the WHERE clause of the query.
* @param sortOrder the ORDER BY clause for the query.
* @return A cursor representing the data resulting from the query, or]
* {@code null} if the query failed to execute.
*/
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
if (uri == null) {
LogUtils.log(this, Log.WARN, NULL_URI_FORMAT_STRING);
return null;
}
if (!UserManagerCompat.isUserUnlocked(getContext())) {
return null;
}
final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(LabelsTable.TABLE_NAME);
String groupBy = null;
switch (sUriMatcher.match(uri)) {
case LABELS:
if (TextUtils.isEmpty(sortOrder)) {
sortOrder = LabelsTable.KEY_ID;
}
break;
case LABELS_ID:
final String labelIdString = uri.getLastPathSegment();
final int labelId;
try {
labelId = Integer.parseInt(labelIdString);
} catch (NumberFormatException e) {
LogUtils.log(this, Log.WARN, UNKNOWN_URI_FORMAT_STRING, uri);
return null;
}
final String where = String.format(Locale.ROOT,
"%s = %d", LabelsTable.KEY_ID, labelId);
queryBuilder.appendWhere(where);
break;
case PACKAGE_SUMMARY:
projection = new String[] { LabelsTable.KEY_PACKAGE_NAME, "COUNT(*)" };
groupBy = LabelsTable.KEY_PACKAGE_NAME;
sortOrder = LabelsTable.KEY_PACKAGE_NAME;
break;
default:
LogUtils.log(this, Log.WARN, UNKNOWN_URI_FORMAT_STRING, uri);
return null;
}
initializeDatabaseIfNull();
return queryBuilder.query(mDatabase, projection, selection, selectionArgs, groupBy,
null /* having */, sortOrder);
}
/**
* Updates a label in the labels database.
*
* @param uri The URI matching {code LABELS_ID_CONTENT_URI} that represents
* the specific label to update.
* @param values The values to use to update the label.
* @param selection The WHERE clause for the query.
* @param selectionArgs The arguments for the WHERE clause of the query.
* @return The number of rows affected.
*/
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
if (uri == null) {
LogUtils.log(this, Log.WARN, NULL_URI_FORMAT_STRING);
return 0;
}
if (!UserManagerCompat.isUserUnlocked(getContext())) {
return 0;
}
switch (sUriMatcher.match(uri)) {
case LABELS: {
initializeDatabaseIfNull();
int result = mDatabase.update(LabelsTable.TABLE_NAME, values, selection,
selectionArgs);
getContext().getContentResolver().notifyChange(uri, null /* observer */);
return result;
}
case LABELS_ID: {
initializeDatabaseIfNull();
final String labelIdString = uri.getLastPathSegment();
final int labelId;
try {
labelId = Integer.parseInt(labelIdString);
} catch (NumberFormatException e) {
LogUtils.log(this, Log.WARN, UNKNOWN_URI_FORMAT_STRING, uri);
return 0;
}
final String where = String.format(Locale.ROOT,
"%s = %d", LabelsTable.KEY_ID, labelId);
final int result = mDatabase.update(LabelsTable.TABLE_NAME, values,
combineSelectionAndWhere(selection, where), selectionArgs);
getContext().getContentResolver().notifyChange(uri, null /* observer */);
return result;
}
default:
LogUtils.log(this, Log.WARN, UNKNOWN_URI_FORMAT_STRING, uri);
return 0;
}
}
/**
* Deletes a label in the labels database.
*
* @param uri The URI matching {code LABELS_ID_CONTENT_URI} that represents
* the specific label to delete.
* @param selection The WHERE clause for the query.
* @param selectionArgs The arguments for the WHERE clause of the query.
* @return The number of rows affected.
*/
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
if (uri == null) {
LogUtils.log(this, Log.WARN, NULL_URI_FORMAT_STRING);
return 0;
}
if (!UserManagerCompat.isUserUnlocked(getContext())) {
return 0;
}
switch (sUriMatcher.match(uri)) {
case LABELS: {
initializeDatabaseIfNull();
int result = mDatabase.delete(LabelsTable.TABLE_NAME, selection, selectionArgs);
getContext().getContentResolver().notifyChange(uri, null /* observer */);
return result;
}
case LABELS_ID: {
initializeDatabaseIfNull();
final String labelIdString = uri.getLastPathSegment();
final int labelId;
try {
labelId = Integer.parseInt(labelIdString);
} catch (NumberFormatException e) {
LogUtils.log(this, Log.WARN, UNKNOWN_URI_FORMAT_STRING, uri);
return 0;
}
final String where = String.format(Locale.ROOT,
"%s = %d", LabelsTable.KEY_ID, labelId);
final int result = mDatabase.delete(LabelsTable.TABLE_NAME,
combineSelectionAndWhere(selection, where), selectionArgs);
getContext().getContentResolver().notifyChange(uri, null /* observer */);
return result;
}
default:
LogUtils.log(this, Log.WARN, UNKNOWN_URI_FORMAT_STRING, uri);
return 0;
}
}
@Override
public void shutdown() {
if (mDatabase != null) {
mDatabase.close();
}
}
/**
* Joins a selection clause with a where clause to form a larger selection
* clause that represents the AND of the two clauses.
*
* @param selection The selection clause.
* @param where The where clause.
* @return The joined clause.
*/
private String combineSelectionAndWhere(String selection, final String where) {
if (TextUtils.isEmpty(where)) {
return selection;
} else if (TextUtils.isEmpty(selection)) {
return where;
}
return String.format(Locale.ROOT, "(%s) AND (%s)", where, selection);
}
/**
* Initializes the database (if not already initialized) when used.
* <p>
* Note: the database is automatically cleaned up by the kernel when the
* process terminates.
*/
private void initializeDatabaseIfNull() {
if (mDatabase == null) {
mDatabase = new LabelsDatabaseOpenHelper(getContext()).getWritableDatabase();
}
}
/**
* A helper for managing a SQLite database that stores label data.
*/
private final class LabelsDatabaseOpenHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "labelsDatabase.db";
/*
* If the database structure is modified and this value is changed, be
* sure to implement the onUpgrade method for the database and each
* relevant table that it includes.
*/
private static final int DATABASE_VERSION = 3;
public LabelsDatabaseOpenHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
LabelsTable.onCreate(db);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
LabelsTable.onUpgrade(db, oldVersion, newVersion);
}
}
}