blob: 7688abd86e5982c8c935f21a9e93fbfa6cb7c225 [file] [log] [blame]
/*
* Copyright (C) 2015 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.messaging.datamodel;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import com.android.messaging.BugleApplication;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
import com.android.messaging.datamodel.DatabaseHelper.ConversationParticipantsColumns;
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.datamodel.data.ConversationMessageData;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.widget.BugleWidgetProvider;
import com.android.messaging.widget.WidgetConversationProvider;
import com.google.common.annotations.VisibleForTesting;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
/**
* A centralized provider for Uris exposed by Bugle.
* */
public class MessagingContentProvider extends ContentProvider {
private static final String TAG = LogUtil.BUGLE_TAG;
@VisibleForTesting
public static final String AUTHORITY =
"com.android.messaging.datamodel.MessagingContentProvider";
private static final String CONTENT_AUTHORITY = "content://" + AUTHORITY + '/';
// Conversations query
private static final String CONVERSATIONS_QUERY = "conversations";
public static final Uri CONVERSATIONS_URI = Uri.parse(CONTENT_AUTHORITY + CONVERSATIONS_QUERY);
static final Uri PARTS_URI = Uri.parse(CONTENT_AUTHORITY + DatabaseHelper.PARTS_TABLE);
// Messages query
private static final String MESSAGES_QUERY = "messages";
static final Uri MESSAGES_URI = Uri.parse(CONTENT_AUTHORITY + MESSAGES_QUERY);
public static final Uri CONVERSATION_MESSAGES_URI = Uri.parse(CONTENT_AUTHORITY +
MESSAGES_QUERY + "/conversation");
// Conversation participants query
private static final String PARTICIPANTS_QUERY = "participants";
static class ConversationParticipantsQueryColumns extends ParticipantColumns {
static final String CONVERSATION_ID = ConversationParticipantsColumns.CONVERSATION_ID;
}
static final Uri CONVERSATION_PARTICIPANTS_URI = Uri.parse(CONTENT_AUTHORITY +
PARTICIPANTS_QUERY + "/conversation");
public static final Uri PARTICIPANTS_URI = Uri.parse(CONTENT_AUTHORITY + PARTICIPANTS_QUERY);
// Conversation images query
private static final String CONVERSATION_IMAGES_QUERY = "conversation_images";
public static final Uri CONVERSATION_IMAGES_URI = Uri.parse(CONTENT_AUTHORITY +
CONVERSATION_IMAGES_QUERY);
private static final String DRAFT_IMAGES_QUERY = "draft_images";
public static final Uri DRAFT_IMAGES_URI = Uri.parse(CONTENT_AUTHORITY +
DRAFT_IMAGES_QUERY);
/**
* Notifies that <i>all</i> data exposed by the provider needs to be refreshed.
* <p>
* <b>IMPORTANT!</b> You probably shouldn't be calling this. Prefer to notify more specific
* uri's instead. Currently only sync uses this, because sync can potentially update many
* different tables at once.
*/
public static void notifyEverythingChanged() {
final Uri uri = Uri.parse(CONTENT_AUTHORITY);
final Context context = Factory.get().getApplicationContext();
final ContentResolver cr = context.getContentResolver();
cr.notifyChange(uri, null);
// Notify any conversations widgets the conversation list has changed.
BugleWidgetProvider.notifyConversationListChanged(context);
// Notify all conversation widgets to update.
WidgetConversationProvider.notifyMessagesChanged(context, null /*conversationId*/);
}
/**
* Build a participant uri from the conversation id.
*/
public static Uri buildConversationParticipantsUri(final String conversationId) {
final Uri.Builder builder = CONVERSATION_PARTICIPANTS_URI.buildUpon();
builder.appendPath(conversationId);
return builder.build();
}
public static void notifyParticipantsChanged(final String conversationId) {
final Uri uri = buildConversationParticipantsUri(conversationId);
final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
cr.notifyChange(uri, null);
}
public static void notifyAllMessagesChanged() {
final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
cr.notifyChange(CONVERSATION_MESSAGES_URI, null);
}
public static void notifyAllParticipantsChanged() {
final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
cr.notifyChange(CONVERSATION_PARTICIPANTS_URI, null);
}
// Default value for unknown dimension of image
public static final int UNSPECIFIED_SIZE = -1;
// Internal
private static final int CONVERSATIONS_QUERY_CODE = 10;
private static final int CONVERSATION_QUERY_CODE = 20;
private static final int CONVERSATION_MESSAGES_QUERY_CODE = 30;
private static final int CONVERSATION_PARTICIPANTS_QUERY_CODE = 40;
private static final int CONVERSATION_IMAGES_QUERY_CODE = 50;
private static final int DRAFT_IMAGES_QUERY_CODE = 60;
private static final int PARTICIPANTS_QUERY_CODE = 70;
// TODO: Move to a better structured URI namespace.
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sURIMatcher.addURI(AUTHORITY, CONVERSATIONS_QUERY, CONVERSATIONS_QUERY_CODE);
sURIMatcher.addURI(AUTHORITY, CONVERSATIONS_QUERY + "/*", CONVERSATION_QUERY_CODE);
sURIMatcher.addURI(AUTHORITY, MESSAGES_QUERY + "/conversation/*",
CONVERSATION_MESSAGES_QUERY_CODE);
sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY + "/conversation/*",
CONVERSATION_PARTICIPANTS_QUERY_CODE);
sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY, PARTICIPANTS_QUERY_CODE);
sURIMatcher.addURI(AUTHORITY, CONVERSATION_IMAGES_QUERY + "/*",
CONVERSATION_IMAGES_QUERY_CODE);
sURIMatcher.addURI(AUTHORITY, DRAFT_IMAGES_QUERY + "/*",
DRAFT_IMAGES_QUERY_CODE);
}
/**
* Build a messages uri from the conversation id.
*/
public static Uri buildConversationMessagesUri(final String conversationId) {
final Uri.Builder builder = CONVERSATION_MESSAGES_URI.buildUpon();
builder.appendPath(conversationId);
return builder.build();
}
public static void notifyMessagesChanged(final String conversationId) {
final Uri uri = buildConversationMessagesUri(conversationId);
final Context context = Factory.get().getApplicationContext();
final ContentResolver cr = context.getContentResolver();
cr.notifyChange(uri, null);
notifyConversationListChanged();
// Notify the widget the messages changed
WidgetConversationProvider.notifyMessagesChanged(context, conversationId);
}
/**
* Build a conversation metadata uri from a conversation id.
*/
public static Uri buildConversationMetadataUri(final String conversationId) {
final Uri.Builder builder = CONVERSATIONS_URI.buildUpon();
builder.appendPath(conversationId);
return builder.build();
}
public static void notifyConversationMetadataChanged(final String conversationId) {
final Uri uri = buildConversationMetadataUri(conversationId);
final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
cr.notifyChange(uri, null);
notifyConversationListChanged();
}
public static void notifyPartsChanged() {
final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
cr.notifyChange(PARTS_URI, null);
}
public static void notifyConversationListChanged() {
final Context context = Factory.get().getApplicationContext();
final ContentResolver cr = context.getContentResolver();
cr.notifyChange(CONVERSATIONS_URI, null);
// Notify the widget the conversation list changed
BugleWidgetProvider.notifyConversationListChanged(context);
}
/**
* Build a conversation images uri from a conversation id.
*/
public static Uri buildConversationImagesUri(final String conversationId) {
final Uri.Builder builder = CONVERSATION_IMAGES_URI.buildUpon();
builder.appendPath(conversationId);
return builder.build();
}
/**
* Build a draft images uri from a conversation id.
*/
public static Uri buildDraftImagesUri(final String conversationId) {
final Uri.Builder builder = DRAFT_IMAGES_URI.buildUpon();
builder.appendPath(conversationId);
return builder.build();
}
private DatabaseHelper mDatabaseHelper;
private DatabaseWrapper mDatabaseWrapper;
public MessagingContentProvider() {
super();
}
@VisibleForTesting
public void setDatabaseForTest(final DatabaseWrapper db) {
Assert.isTrue(BugleApplication.isRunningTests());
mDatabaseWrapper = db;
}
private DatabaseWrapper getDatabaseWrapper() {
if (mDatabaseWrapper == null) {
mDatabaseWrapper = mDatabaseHelper.getDatabase();
}
return mDatabaseWrapper;
}
@Override
public Cursor query(final Uri uri, final String[] projection, String selection,
final String[] selectionArgs, String sortOrder) {
// Processes other than self are allowed to temporarily access the media
// scratch space; we grant uri read access on a case-by-case basis. Dialer app and
// contacts app would doQuery() on the vCard uri before trying to open the inputStream.
// There's nothing that we need to return for this uri so just No-Op.
//if (isMediaScratchSpaceUri(uri)) {
// return null;
//}
final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
String[] queryArgs = selectionArgs;
final int match = sURIMatcher.match(uri);
String groupBy = null;
String limit = null;
switch (match) {
case CONVERSATIONS_QUERY_CODE:
queryBuilder.setTables(ConversationListItemData.getConversationListView());
// Hide empty conversations (ones with 0 sort_timestamp)
queryBuilder.appendWhere(ConversationColumns.SORT_TIMESTAMP + " > 0 ");
break;
case CONVERSATION_QUERY_CODE:
queryBuilder.setTables(ConversationListItemData.getConversationListView());
if (uri.getPathSegments().size() == 2) {
queryBuilder.appendWhere(ConversationColumns._ID + "=?");
// Get the conversation id from the uri
queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(1));
} else {
throw new IllegalArgumentException("Malformed URI " + uri);
}
break;
case CONVERSATION_PARTICIPANTS_QUERY_CODE:
queryBuilder.setTables(DatabaseHelper.PARTICIPANTS_TABLE);
if (uri.getPathSegments().size() == 3 &&
TextUtils.equals(uri.getPathSegments().get(1), "conversation")) {
queryBuilder.appendWhere(ParticipantColumns._ID + " IN ( " + "SELECT "
+ ConversationParticipantsColumns.PARTICIPANT_ID + " AS "
+ ParticipantColumns._ID
+ " FROM " + DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE
+ " WHERE " + ConversationParticipantsColumns.CONVERSATION_ID
+ " =? UNION SELECT " + ParticipantColumns._ID + " FROM "
+ DatabaseHelper.PARTICIPANTS_TABLE + " WHERE "
+ ParticipantColumns.SUB_ID + " != "
+ ParticipantData.OTHER_THAN_SELF_SUB_ID + " )");
// Get the conversation id from the uri
queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(2));
} else {
throw new IllegalArgumentException("Malformed URI " + uri);
}
break;
case PARTICIPANTS_QUERY_CODE:
queryBuilder.setTables(DatabaseHelper.PARTICIPANTS_TABLE);
if (uri.getPathSegments().size() != 1) {
throw new IllegalArgumentException("Malformed URI " + uri);
}
break;
case CONVERSATION_MESSAGES_QUERY_CODE:
if (uri.getPathSegments().size() == 3 &&
TextUtils.equals(uri.getPathSegments().get(1), "conversation")) {
// Get the conversation id from the uri
final String conversationId = uri.getPathSegments().get(2);
// We need to handle this query differently, instead of falling through to the
// generic query call at the bottom. For performance reasons, the conversation
// messages query is executed as a raw query. It is invalid to specify
// selection/sorting for this query.
if (selection == null && selectionArgs == null && sortOrder == null) {
return queryConversationMessages(conversationId, uri);
} else {
throw new IllegalArgumentException(
"Cannot set selection or sort order with this query");
}
} else {
throw new IllegalArgumentException("Malformed URI " + uri);
}
case CONVERSATION_IMAGES_QUERY_CODE:
queryBuilder.setTables(ConversationImagePartsView.getViewName());
if (uri.getPathSegments().size() == 2) {
// Exclude draft.
queryBuilder.appendWhere(
ConversationImagePartsView.Columns.CONVERSATION_ID + " =? AND " +
ConversationImagePartsView.Columns.STATUS + "<>" +
MessageData.BUGLE_STATUS_OUTGOING_DRAFT);
// Get the conversation id from the uri
queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(1));
} else {
throw new IllegalArgumentException("Malformed URI " + uri);
}
break;
case DRAFT_IMAGES_QUERY_CODE:
queryBuilder.setTables(ConversationImagePartsView.getViewName());
if (uri.getPathSegments().size() == 2) {
// Draft only.
queryBuilder.appendWhere(
ConversationImagePartsView.Columns.CONVERSATION_ID + " =? AND " +
ConversationImagePartsView.Columns.STATUS + "=" +
MessageData.BUGLE_STATUS_OUTGOING_DRAFT);
// Get the conversation id from the uri
queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(1));
} else {
throw new IllegalArgumentException("Malformed URI " + uri);
}
break;
default: {
throw new IllegalArgumentException("Unknown URI " + uri);
}
}
final Cursor cursor = getDatabaseWrapper().query(queryBuilder, projection, selection,
queryArgs, groupBy, null, sortOrder, limit);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
private Cursor queryConversationMessages(final String conversationId, final Uri notifyUri) {
final String[] queryArgs = { conversationId };
final Cursor cursor = getDatabaseWrapper().rawQuery(
ConversationMessageData.getConversationMessagesQuerySql(), queryArgs);
cursor.setNotificationUri(getContext().getContentResolver(), notifyUri);
return cursor;
}
@Override
public String getType(final Uri uri) {
final StringBuilder sb = new
StringBuilder("vnd.android.cursor.dir/vnd.android.messaging.");
switch (sURIMatcher.match(uri)) {
case CONVERSATIONS_QUERY_CODE: {
sb.append(CONVERSATIONS_QUERY);
break;
}
default: {
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
return sb.toString();
}
protected DatabaseHelper getDatabase() {
return DatabaseHelper.getInstance(getContext());
}
@Override
public ParcelFileDescriptor openFile(final Uri uri, final String fileMode)
throws FileNotFoundException {
throw new IllegalArgumentException("openFile not supported: " + uri);
}
@Override
public Uri insert(final Uri uri, final ContentValues values) {
throw new IllegalStateException("Insert not supported " + uri);
}
@Override
public int delete(final Uri uri, final String selection, final String[] selectionArgs) {
throw new IllegalArgumentException("Delete not supported: " + uri);
}
@Override
public int update(final Uri uri, final ContentValues values, final String selection,
final String[] selectionArgs) {
throw new IllegalArgumentException("Update not supported: " + uri);
}
/**
* Prepends new arguments to the existing argument list.
*
* @param oldArgList The current list of arguments. May be {@code null}
* @param args The new arguments to prepend
* @return A new argument list with the given arguments prepended
*/
private String[] prependArgs(final String[] oldArgList, final String... args) {
if (args == null || args.length == 0) {
return oldArgList;
}
final int oldArgCount = (oldArgList == null ? 0 : oldArgList.length);
final int newArgCount = args.length;
final String[] newArgs = new String[oldArgCount + newArgCount];
System.arraycopy(args, 0, newArgs, 0, newArgCount);
if (oldArgCount > 0) {
System.arraycopy(oldArgList, 0, newArgs, newArgCount, oldArgCount);
}
return newArgs;
}
/**
* {@inheritDoc}
*/
@Override
public void dump(final FileDescriptor fd, final PrintWriter writer, final String[] args) {
// First dump out the default SMS app package name
String defaultSmsApp = PhoneUtils.getDefault().getDefaultSmsApp();
if (TextUtils.isEmpty(defaultSmsApp)) {
if (OsUtil.isAtLeastKLP()) {
defaultSmsApp = "None";
} else {
defaultSmsApp = "None (pre-Kitkat)";
}
}
writer.println("Default SMS app: " + defaultSmsApp);
// Now dump logs
LogUtil.dump(writer);
}
@Override
public boolean onCreate() {
// This is going to wind up calling into createDatabase() below.
mDatabaseHelper = (DatabaseHelper) getDatabase();
// We cannot initialize mDatabaseWrapper yet as the Factory may not be initialized
return true;
}
}