1 /* 2 * Copyright (C) 2018 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 package com.android.providers.telephony; 17 18 import static android.provider.Telephony.CanonicalAddressesColumns.ADDRESS; 19 import static android.provider.Telephony.RcsColumns.RcsParticipantColumns.CANONICAL_ADDRESS_ID_COLUMN; 20 import static android.provider.Telephony.RcsColumns.RcsParticipantColumns.RCS_ALIAS_COLUMN; 21 import static android.provider.Telephony.RcsColumns.RcsParticipantColumns.RCS_PARTICIPANT_ID_COLUMN; 22 import static android.provider.Telephony.RcsColumns.RcsParticipantColumns.RCS_PARTICIPANT_URI_PART; 23 import static android.provider.Telephony.RcsColumns.RcsParticipantHelpers.RCS_PARTICIPANT_WITH_ADDRESS_VIEW; 24 import static android.provider.Telephony.RcsColumns.RcsParticipantHelpers.RCS_PARTICIPANT_WITH_THREAD_VIEW; 25 import static android.provider.Telephony.RcsColumns.RcsThreadColumns.RCS_THREAD_ID_COLUMN; 26 import static android.provider.Telephony.RcsColumns.TRANSACTION_FAILED; 27 import static android.telephony.ims.RcsParticipantQueryParams.PARTICIPANT_QUERY_PARAMETERS_KEY; 28 29 import static android.telephony.ims.RcsQueryContinuationToken.PARTICIPANT_QUERY_CONTINUATION_TOKEN_TYPE; 30 import static android.telephony.ims.RcsQueryContinuationToken.QUERY_CONTINUATION_TOKEN; 31 import static com.android.providers.telephony.RcsProvider.GROUP_THREAD_URI_PREFIX; 32 import static com.android.providers.telephony.RcsProvider.RCS_PARTICIPANT_TABLE; 33 import static com.android.providers.telephony.RcsProvider.RCS_PARTICIPANT_THREAD_JUNCTION_TABLE; 34 import static com.android.providers.telephony.RcsProvider.RCS_THREAD_TABLE; 35 import static com.android.providers.telephony.RcsProvider.TAG; 36 import static com.android.providers.telephony.RcsProviderThreadHelper.getThreadIdFromUri; 37 import static com.android.providers.telephony.RcsProviderUtil.INSERTION_FAILED; 38 39 import android.content.ContentValues; 40 import android.database.Cursor; 41 import android.database.sqlite.SQLiteDatabase; 42 import android.database.sqlite.SQLiteOpenHelper; 43 import android.net.Uri; 44 import android.os.Bundle; 45 import android.telephony.ims.RcsParticipantQueryParams; 46 import android.telephony.ims.RcsQueryContinuationToken; 47 import android.text.TextUtils; 48 import android.util.Log; 49 50 import com.android.internal.annotations.VisibleForTesting; 51 52 /** 53 * Constants and helpers related to participants for {@link RcsProvider} to keep the code clean. 54 * 55 * @hide 56 */ 57 class RcsProviderParticipantHelper { 58 private static final int PARTICIPANT_ID_INDEX_IN_URI = 1; 59 private static final int PARTICIPANT_ID_INDEX_IN_THREAD_URI = 3; 60 61 @VisibleForTesting createParticipantTables(SQLiteDatabase db)62 public static void createParticipantTables(SQLiteDatabase db) { 63 Log.d(TAG, "Creating participant tables"); 64 65 // create participant tables 66 db.execSQL("CREATE TABLE " + RCS_PARTICIPANT_TABLE + " (" + 67 RCS_PARTICIPANT_ID_COLUMN + " INTEGER PRIMARY KEY AUTOINCREMENT, " + 68 CANONICAL_ADDRESS_ID_COLUMN + " INTEGER ," + 69 RCS_ALIAS_COLUMN + " TEXT, " + 70 "FOREIGN KEY(" + CANONICAL_ADDRESS_ID_COLUMN + ") " 71 + "REFERENCES canonical_addresses(_id)" + 72 ");"); 73 74 db.execSQL("CREATE TABLE " + RCS_PARTICIPANT_THREAD_JUNCTION_TABLE + " (" + 75 RCS_THREAD_ID_COLUMN + " INTEGER, " + 76 RCS_PARTICIPANT_ID_COLUMN + " INTEGER, " + 77 "CONSTRAINT thread_participant PRIMARY KEY(" 78 + RCS_THREAD_ID_COLUMN + ", " + RCS_PARTICIPANT_ID_COLUMN + "), " + 79 "FOREIGN KEY(" + RCS_THREAD_ID_COLUMN 80 + ") REFERENCES " + RCS_THREAD_TABLE + "(" + RCS_THREAD_ID_COLUMN + "), " + 81 "FOREIGN KEY(" + RCS_PARTICIPANT_ID_COLUMN 82 + ") REFERENCES " + RCS_PARTICIPANT_TABLE + "(" + RCS_PARTICIPANT_ID_COLUMN + "))"); 83 84 // create views 85 86 // The following view joins rcs_participant table with canonical_addresses table to add the 87 // actual address of a participant in the result. 88 db.execSQL("CREATE VIEW " + RCS_PARTICIPANT_WITH_ADDRESS_VIEW + " AS SELECT " 89 + "rcs_participant.rcs_participant_id, rcs_participant.canonical_address_id, " 90 + "rcs_participant.rcs_alias, canonical_addresses.address FROM rcs_participant " 91 + "LEFT JOIN canonical_addresses ON " 92 + "rcs_participant.canonical_address_id=canonical_addresses._id"); 93 94 // The following view is the rcs_participant_with_address_view above, plus the information 95 // on which threads this participant contributes to, to enable getting participants of a 96 // thread 97 db.execSQL("CREATE VIEW " + RCS_PARTICIPANT_WITH_THREAD_VIEW + " AS SELECT " 98 + "rcs_participant.rcs_participant_id, rcs_participant.canonical_address_id, " 99 + "rcs_participant.rcs_alias, canonical_addresses.address, rcs_thread_participant" 100 + ".rcs_thread_id FROM rcs_participant LEFT JOIN canonical_addresses ON " 101 + "rcs_participant.canonical_address_id=canonical_addresses._id LEFT JOIN " 102 + "rcs_thread_participant ON rcs_participant.rcs_participant_id=" 103 + "rcs_thread_participant.rcs_participant_id"); 104 105 // TODO - create indexes for faster querying 106 } 107 108 private final SQLiteOpenHelper mSqLiteOpenHelper; 109 RcsProviderParticipantHelper(SQLiteOpenHelper sqLiteOpenHelper)110 RcsProviderParticipantHelper(SQLiteOpenHelper sqLiteOpenHelper) { 111 mSqLiteOpenHelper = sqLiteOpenHelper; 112 } 113 queryParticipant(Bundle bundle)114 Cursor queryParticipant(Bundle bundle) { 115 RcsParticipantQueryParams queryParameters = null; 116 RcsQueryContinuationToken continuationToken = null; 117 118 if (bundle != null) { 119 queryParameters = bundle.getParcelable(PARTICIPANT_QUERY_PARAMETERS_KEY); 120 continuationToken = bundle.getParcelable(QUERY_CONTINUATION_TOKEN); 121 } 122 123 if (continuationToken != null) { 124 return RcsProviderUtil.performContinuationQuery(mSqLiteOpenHelper.getReadableDatabase(), 125 continuationToken); 126 } 127 128 if (queryParameters == null) { 129 queryParameters = new RcsParticipantQueryParams.Builder().build(); 130 } 131 132 return performInitialQuery(queryParameters); 133 } 134 performInitialQuery(RcsParticipantQueryParams queryParameters)135 private Cursor performInitialQuery(RcsParticipantQueryParams queryParameters) { 136 SQLiteDatabase db = mSqLiteOpenHelper.getWritableDatabase(); 137 138 StringBuilder rawQuery = buildInitialRawQuery(queryParameters); 139 RcsProviderUtil.appendLimit(rawQuery, queryParameters.getLimit()); 140 String rawQueryAsString = rawQuery.toString(); 141 Cursor cursor = db.rawQuery(rawQueryAsString, null); 142 143 // If the query was paginated, build the next query 144 int limit = queryParameters.getLimit(); 145 if (limit > 0) { 146 RcsProviderUtil.createContinuationTokenBundle(cursor, 147 new RcsQueryContinuationToken(PARTICIPANT_QUERY_CONTINUATION_TOKEN_TYPE, 148 rawQueryAsString, limit, limit), QUERY_CONTINUATION_TOKEN); 149 } 150 151 return cursor; 152 } 153 buildInitialRawQuery(RcsParticipantQueryParams queryParameters)154 private StringBuilder buildInitialRawQuery(RcsParticipantQueryParams queryParameters) { 155 StringBuilder rawQuery = new StringBuilder("SELECT * FROM "); 156 157 boolean isThreadFiltered = queryParameters.getThreadId() > 0; 158 159 if (isThreadFiltered) { 160 rawQuery.append(RCS_PARTICIPANT_WITH_THREAD_VIEW); 161 } else { 162 rawQuery.append(RCS_PARTICIPANT_WITH_ADDRESS_VIEW); 163 } 164 165 boolean isAliasFiltered = !TextUtils.isEmpty(queryParameters.getAliasLike()); 166 boolean isCanonicalAddressFiltered = !TextUtils.isEmpty( 167 queryParameters.getCanonicalAddressLike()); 168 169 if (isAliasFiltered || isCanonicalAddressFiltered || isThreadFiltered) { 170 rawQuery.append(" WHERE "); 171 } 172 173 if (isAliasFiltered) { 174 rawQuery.append(RCS_ALIAS_COLUMN).append(" LIKE \"").append( 175 queryParameters.getAliasLike()).append("\""); 176 } 177 178 if (isCanonicalAddressFiltered) { 179 if (isAliasFiltered) { 180 rawQuery.append(" AND "); 181 } 182 rawQuery.append(ADDRESS).append(" LIKE \"").append( 183 queryParameters.getCanonicalAddressLike()).append("\""); 184 } 185 186 if (isThreadFiltered) { 187 if (isAliasFiltered || isCanonicalAddressFiltered) { 188 rawQuery.append(" AND "); 189 } 190 rawQuery.append(RCS_THREAD_ID_COLUMN).append("=").append(queryParameters.getThreadId()); 191 } 192 193 rawQuery.append(" ORDER BY "); 194 195 int sortingProperty = queryParameters.getSortingProperty(); 196 if (sortingProperty == RcsParticipantQueryParams.SORT_BY_ALIAS) { 197 rawQuery.append(RCS_ALIAS_COLUMN); 198 } else if (sortingProperty == RcsParticipantQueryParams.SORT_BY_CANONICAL_ADDRESS) { 199 rawQuery.append(ADDRESS); 200 } else { 201 rawQuery.append(RCS_PARTICIPANT_ID_COLUMN); 202 } 203 rawQuery.append(queryParameters.getSortDirection() ? " ASC " : " DESC "); 204 205 return rawQuery; 206 } 207 queryParticipantWithId(Uri uri, String[] projection)208 Cursor queryParticipantWithId(Uri uri, String[] projection) { 209 SQLiteDatabase db = mSqLiteOpenHelper.getReadableDatabase(); 210 return db.query(RCS_PARTICIPANT_WITH_ADDRESS_VIEW, projection, 211 getParticipantIdSelection(uri), null, null, null, null); 212 } 213 queryParticipantIn1To1Thread(Uri uri)214 Cursor queryParticipantIn1To1Thread(Uri uri) { 215 String threadId = getThreadIdFromUri(uri); 216 SQLiteDatabase db = mSqLiteOpenHelper.getReadableDatabase(); 217 218 return db.rawQuery( 219 " SELECT * " 220 + "FROM rcs_participant " 221 + "WHERE rcs_participant.rcs_participant_id = (" 222 + " SELECT rcs_thread_participant.rcs_participant_id " 223 + " FROM rcs_thread_participant " 224 + " WHERE rcs_thread_participant.rcs_thread_id=" + threadId + ")", null); 225 } 226 queryParticipantsInGroupThread(Uri uri)227 Cursor queryParticipantsInGroupThread(Uri uri) { 228 String threadId = getThreadIdFromUri(uri); 229 SQLiteDatabase db = mSqLiteOpenHelper.getReadableDatabase(); 230 231 return db.rawQuery(" SELECT * " 232 + "FROM rcs_participant " 233 + "WHERE rcs_participant.rcs_participant_id = (" 234 + " SELECT rcs_participant_id " 235 + " FROM rcs_thread_participant " 236 + " WHERE rcs_thread_id= " + threadId + ")", null); 237 } 238 queryParticipantInGroupThreadWithId(Uri uri)239 Cursor queryParticipantInGroupThreadWithId(Uri uri) { 240 String threadId = getThreadIdFromUri(uri); 241 String participantId = getParticipantIdFromUri(uri); 242 SQLiteDatabase db = mSqLiteOpenHelper.getReadableDatabase(); 243 244 return db.rawQuery(" SELECT * " 245 + "FROM rcs_participant " 246 + "WHERE rcs_participant.rcs_participant_id = (" 247 + " SELECT rcs_participant_id " 248 + " FROM rcs_thread_participant " 249 + " WHERE rcs_thread_id=? AND rcs_participant_id=?)", 250 new String[]{threadId, participantId}); 251 } 252 insertParticipant(ContentValues contentValues)253 long insertParticipant(ContentValues contentValues) { 254 if (!contentValues.containsKey(CANONICAL_ADDRESS_ID_COLUMN) || TextUtils.isEmpty( 255 contentValues.getAsString(CANONICAL_ADDRESS_ID_COLUMN))) { 256 Log.e(TAG, 257 "RcsProviderParticipantHelper: Inserting participants without canonical " 258 + "address is not supported"); 259 return TRANSACTION_FAILED; 260 } 261 SQLiteDatabase db = mSqLiteOpenHelper.getWritableDatabase(); 262 long rowId = db.insert(RCS_PARTICIPANT_TABLE, RCS_PARTICIPANT_ID_COLUMN, contentValues); 263 Log.e(TAG, "Inserted participant with rowId=" + rowId); 264 if (rowId < 0) { 265 return TRANSACTION_FAILED; 266 } 267 return rowId; 268 } 269 270 /** 271 * Inserts a participant into group thread. This function returns the participant ID instead of 272 * the row id in the junction table 273 */ insertParticipantIntoGroupThread(ContentValues values)274 long insertParticipantIntoGroupThread(ContentValues values) { 275 if (!values.containsKey(RCS_THREAD_ID_COLUMN) || !values.containsKey( 276 RCS_PARTICIPANT_ID_COLUMN)) { 277 Log.e(TAG, "RcsProviderParticipantHelper: Cannot insert participant into group."); 278 return TRANSACTION_FAILED; 279 } 280 SQLiteDatabase db = mSqLiteOpenHelper.getWritableDatabase(); 281 long insertedRowId = db.insert(RCS_PARTICIPANT_THREAD_JUNCTION_TABLE, 282 RCS_PARTICIPANT_ID_COLUMN, 283 values); 284 285 if (insertedRowId == INSERTION_FAILED) { 286 return TRANSACTION_FAILED; 287 } 288 289 return values.getAsLong(RCS_PARTICIPANT_ID_COLUMN); 290 } 291 292 /** 293 * Inserts a participant into group thread. This function returns the participant ID instead of 294 * the row id in the junction table 295 */ insertParticipantIntoGroupThreadWithId(Uri uri)296 long insertParticipantIntoGroupThreadWithId(Uri uri) { 297 String threadId = getThreadIdFromUri(uri); 298 String participantId = getParticipantIdFromUri(uri); 299 SQLiteDatabase db = mSqLiteOpenHelper.getWritableDatabase(); 300 301 ContentValues contentValues = new ContentValues(2); 302 contentValues.put(RCS_THREAD_ID_COLUMN, threadId); 303 contentValues.put(RCS_PARTICIPANT_ID_COLUMN, participantId); 304 305 long insertedRowId = db.insert( 306 RCS_PARTICIPANT_THREAD_JUNCTION_TABLE, RCS_PARTICIPANT_ID_COLUMN, contentValues); 307 308 if (insertedRowId == INSERTION_FAILED) { 309 return TRANSACTION_FAILED; 310 } 311 312 return Long.parseLong(participantId); 313 } 314 deleteParticipantWithId(Uri uri)315 int deleteParticipantWithId(Uri uri) { 316 String participantId = uri.getPathSegments().get(PARTICIPANT_ID_INDEX_IN_URI); 317 SQLiteDatabase db = mSqLiteOpenHelper.getWritableDatabase(); 318 319 // See if this participant is involved in any threads 320 Cursor cursor = db.query(RCS_PARTICIPANT_THREAD_JUNCTION_TABLE, null, 321 RCS_PARTICIPANT_ID_COLUMN + "=?", new String[]{participantId}, null, null, null); 322 323 int participatingThreadCount = 0; 324 if (cursor != null) { 325 participatingThreadCount = cursor.getCount(); 326 cursor.close(); 327 } 328 329 if (participatingThreadCount > 0) { 330 Log.e(TAG, 331 "RcsProviderParticipantHelper: Can't delete participant while it is still in " 332 + "RCS threads, uri:" 333 + uri); 334 return 0; 335 } 336 337 return db.delete(RCS_PARTICIPANT_TABLE, RCS_PARTICIPANT_ID_COLUMN + "=?", 338 new String[]{participantId}); 339 } 340 deleteParticipantFromGroupThread(Uri uri)341 int deleteParticipantFromGroupThread(Uri uri) { 342 String threadId = getThreadIdFromUri(uri); 343 String participantId = getParticipantIdFromUri(uri); 344 SQLiteDatabase db = mSqLiteOpenHelper.getWritableDatabase(); 345 // TODO check to remove owner 346 return db.delete(RCS_PARTICIPANT_THREAD_JUNCTION_TABLE, 347 RCS_THREAD_ID_COLUMN + "=? AND " + RCS_PARTICIPANT_ID_COLUMN + "=?", 348 new String[]{threadId, participantId}); 349 } 350 updateParticipant(ContentValues contentValues, String selection, String[] selectionArgs)351 int updateParticipant(ContentValues contentValues, String selection, String[] selectionArgs) { 352 if (contentValues.containsKey(RCS_PARTICIPANT_ID_COLUMN)) { 353 Log.e(TAG, "RcsProviderParticipantHelper: Updating participant id is not supported"); 354 return 0; 355 } 356 357 SQLiteDatabase db = mSqLiteOpenHelper.getWritableDatabase(); 358 return db.update(RCS_PARTICIPANT_TABLE, contentValues, selection, selectionArgs); 359 } 360 updateParticipantWithId(ContentValues contentValues, Uri uri)361 int updateParticipantWithId(ContentValues contentValues, Uri uri) { 362 return updateParticipant(contentValues, getParticipantIdSelection(uri), null); 363 } 364 getParticipantIdSelection(Uri uri)365 private String getParticipantIdSelection(Uri uri) { 366 return RCS_PARTICIPANT_ID_COLUMN + "=" + uri.getPathSegments().get( 367 PARTICIPANT_ID_INDEX_IN_URI); 368 } 369 getParticipantInThreadUri(ContentValues values, long rowId)370 Uri getParticipantInThreadUri(ContentValues values, long rowId) { 371 if (values == null) { 372 return null; 373 } 374 Integer threadId = values.getAsInteger(RCS_THREAD_ID_COLUMN); 375 if (threadId == null) { 376 return null; 377 } 378 379 return GROUP_THREAD_URI_PREFIX.buildUpon().appendPath( 380 Integer.toString(threadId)).appendPath(RCS_PARTICIPANT_URI_PART).appendPath( 381 Long.toString(rowId)).build(); 382 } 383 getParticipantIdFromUri(Uri uri)384 private String getParticipantIdFromUri(Uri uri) { 385 return uri.getPathSegments().get(PARTICIPANT_ID_INDEX_IN_THREAD_URI); 386 } 387 } 388