1 /*
2  * Copyright (C) 2020 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 
17 package com.android.ims.rcs.uce.eab;
18 
19 import static android.content.ContentResolver.NOTIFY_DELETE;
20 import static android.content.ContentResolver.NOTIFY_INSERT;
21 import static android.content.ContentResolver.NOTIFY_UPDATE;
22 
23 import android.content.ContentProvider;
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.content.UriMatcher;
27 import android.database.Cursor;
28 import android.database.sqlite.SQLiteDatabase;
29 import android.database.sqlite.SQLiteOpenHelper;
30 import android.database.sqlite.SQLiteQueryBuilder;
31 import android.net.Uri;
32 import android.provider.BaseColumns;
33 import android.text.TextUtils;
34 import android.util.Log;
35 
36 import com.android.internal.annotations.VisibleForTesting;
37 
38 import java.util.ArrayList;
39 import java.util.List;
40 
41 /**
42  * This class provides the ability to query the enhanced address book databases(A.K.A. EAB) based on
43  * both SIP options and UCE presence server data.
44  *
45  * <p>
46  * There are 4 tables in EAB DB:
47  * <ul>
48  *     <li><em>Contact:</em> It stores the name and phone number of the contact.
49  *
50  *     <li><em>Common:</em> It's a general table for storing the query results and the mechanisms of
51  *     querying UCE capabilities. It should be 1:1 mapped to the contact table and has a foreign
52  *     key(eab_contact_id) that refers to the id of contact table. If the value of mechanism is
53  *     1 ({@link android.telephony.ims.RcsContactUceCapability#CAPABILITY_MECHANISM_PRESENCE}), the
54  *     capability information should be stored in presence table, if the value of mechanism is
55  *     2({@link android.telephony.ims.RcsContactUceCapability#CAPABILITY_MECHANISM_OPTIONS}), it
56  *     should be stored in options table.
57  *
58  *     <li><em>Presence:</em> It stores the information
59  *     ({@link android.telephony.ims.RcsContactUceCapability}) that queried through presence server.
60  *     It should be *:1 mapped to the common table and has a foreign key(eab_common_id) that refers
61  *     to the id of common table.
62  *
63  *     <li><em>Options:</em> It stores the information
64  *     ({@link android.telephony.ims.RcsContactUceCapability}) that queried through SIP OPTIONS. It
65  *     should be *:1 mapped to the common table and it has a foreign key(eab_common_id) that refers
66  *     to the id of common table.
67  * </ul>
68  * </p>
69  */
70 public class EabProvider extends ContentProvider {
71     // The public URI for operating Eab DB. They support query, insert, delete and update.
72     public static final Uri CONTACT_URI = Uri.parse("content://eab/contact");
73     public static final Uri COMMON_URI = Uri.parse("content://eab/common");
74     public static final Uri PRESENCE_URI = Uri.parse("content://eab/presence");
75     public static final Uri OPTIONS_URI = Uri.parse("content://eab/options");
76 
77     // The public URI for querying EAB DB. Only support query.
78     public static final Uri ALL_DATA_URI = Uri.parse("content://eab/all");
79 
80     @VisibleForTesting
81     public static final String AUTHORITY = "eab";
82 
83     private static final String TAG = "EabProvider";
84     private static final int DATABASE_VERSION = 4;
85 
86     public static final String EAB_CONTACT_TABLE_NAME = "eab_contact";
87     public static final String EAB_COMMON_TABLE_NAME = "eab_common";
88     public static final String EAB_PRESENCE_TUPLE_TABLE_NAME = "eab_presence";
89     public static final String EAB_OPTIONS_TABLE_NAME = "eab_options";
90 
91     private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
92     private static final int URL_CONTACT = 1;
93     private static final int URL_COMMON = 2;
94     private static final int URL_PRESENCE = 3;
95     private static final int URL_OPTIONS = 4;
96     private static final int URL_ALL = 5;
97     private static final int URL_ALL_WITH_SUB_ID_AND_PHONE_NUMBER = 6;
98 
99     static {
URI_MATCHER.addURI(AUTHORITY, "contact", URL_CONTACT)100         URI_MATCHER.addURI(AUTHORITY, "contact", URL_CONTACT);
URI_MATCHER.addURI(AUTHORITY, "common", URL_COMMON)101         URI_MATCHER.addURI(AUTHORITY, "common", URL_COMMON);
URI_MATCHER.addURI(AUTHORITY, "presence", URL_PRESENCE)102         URI_MATCHER.addURI(AUTHORITY, "presence", URL_PRESENCE);
URI_MATCHER.addURI(AUTHORITY, "options", URL_OPTIONS)103         URI_MATCHER.addURI(AUTHORITY, "options", URL_OPTIONS);
URI_MATCHER.addURI(AUTHORITY, "all", URL_ALL)104         URI_MATCHER.addURI(AUTHORITY, "all", URL_ALL);
URI_MATCHER.addURI(AUTHORITY, "all/#/*", URL_ALL_WITH_SUB_ID_AND_PHONE_NUMBER)105         URI_MATCHER.addURI(AUTHORITY, "all/#/*", URL_ALL_WITH_SUB_ID_AND_PHONE_NUMBER);
106     }
107 
108     private static final String QUERY_CONTACT_TABLE =
109             " SELECT * FROM " + EAB_CONTACT_TABLE_NAME;
110 
111     private static final String JOIN_ALL_TABLES =
112             // join common table
113             " INNER JOIN " + EAB_COMMON_TABLE_NAME
114             + " ON " + EAB_CONTACT_TABLE_NAME + "." + ContactColumns._ID
115             + "=" + EAB_COMMON_TABLE_NAME + "." + EabCommonColumns.EAB_CONTACT_ID
116 
117             // join options table
118             + " LEFT JOIN " + EAB_OPTIONS_TABLE_NAME
119             + " ON " + EAB_COMMON_TABLE_NAME + "." + EabCommonColumns._ID
120             + "=" + EAB_OPTIONS_TABLE_NAME + "." + OptionsColumns.EAB_COMMON_ID
121 
122             // join presence table
123             + " LEFT JOIN " + EAB_PRESENCE_TUPLE_TABLE_NAME
124             + " ON " + EAB_COMMON_TABLE_NAME + "." + EabCommonColumns._ID
125             + "=" + EAB_PRESENCE_TUPLE_TABLE_NAME + "."
126             + PresenceTupleColumns.EAB_COMMON_ID;
127 
128     /**
129      * The contact table's columns.
130      */
131     public static class ContactColumns implements BaseColumns {
132 
133         /**
134          * The contact's phone number. It may come from contact provider or someone via
135          * {@link EabControllerImpl#saveCapabilities(List)} to save the capability but the phone
136          * number not in contact provider.
137          *
138          * <P>Type: TEXT</P>
139          */
140         public static final String PHONE_NUMBER = "phone_number";
141 
142         /**
143          * The ID of contact that store in contact provider. It refer to the
144          * {@link android.provider.ContactsContract.Data#CONTACT_ID}. If the phone number not in
145          * contact provider, the value should be null.
146          *
147          * <P>Type: INTEGER</P>
148          */
149         public static final String CONTACT_ID = "contact_id";
150 
151         /**
152          * The ID of contact that store in contact provider. It refer to the
153          * {@link android.provider.ContactsContract.Data#RAW_CONTACT_ID}. If the phone number not in
154          * contact provider, the value should be null.
155          *
156          * <P>Type: INTEGER</P>
157          */
158         public static final String RAW_CONTACT_ID = "raw_contact_id";
159 
160         /**
161          * The ID of phone number that store in contact provider. It refer to the
162          * {@link android.provider.ContactsContract.Data#_ID}. If the phone number not in
163          * contact provider, the value should be null.
164          *
165          * <P>Type:  INTEGER</P>
166          */
167         public static final String DATA_ID = "data_id";
168     }
169 
170     /**
171      * The common table's columns. The eab_contact_id should refer to the id of contact table.
172      */
173     public static class EabCommonColumns implements BaseColumns {
174 
175         /**
176          * A reference to the {@link ContactColumns#_ID} that this data belongs to.
177          * <P>Type:  INTEGER</P>
178          */
179         public static final String EAB_CONTACT_ID = "eab_contact_id";
180 
181         /**
182          * The mechanism of querying UCE capability. Possible values are
183          * {@link android.telephony.ims.RcsContactUceCapability#CAPABILITY_MECHANISM_OPTIONS }
184          * and
185          * {@link android.telephony.ims.RcsContactUceCapability#CAPABILITY_MECHANISM_PRESENCE }
186          * <P>Type: INTEGER</P>
187          */
188         public static final String MECHANISM = "mechanism";
189 
190         /**
191          * The result of querying UCE capability. Possible values are
192          * {@link android.telephony.ims.RcsContactUceCapability#REQUEST_RESULT_NOT_ONLINE }
193          * and
194          * {@link android.telephony.ims.RcsContactUceCapability#REQUEST_RESULT_NOT_FOUND }
195          * and
196          * {@link android.telephony.ims.RcsContactUceCapability#REQUEST_RESULT_FOUND }
197          * and
198          * {@link android.telephony.ims.RcsContactUceCapability#REQUEST_RESULT_UNKNOWN }
199          * <P>Type: INTEGER</P>
200          */
201         public static final String REQUEST_RESULT = "request_result";
202 
203         /**
204          * The subscription id.
205          * <P>Type:  INTEGER</P>
206          */
207         public static final String SUBSCRIPTION_ID = "subscription_id";
208 
209         /**
210          * The value of the 'entity' attribute is the 'pres' URL of the PRESENTITY publishing
211          * presence document
212          * <P>Type:  TEXT</P>
213          */
214         public static final String ENTITY_URI = "entity_uri";
215     }
216 
217     /**
218      * This is used to generate a instance of {@link android.telephony.ims.RcsContactPresenceTuple}.
219      * See that class for more information on each of these parameters.
220      */
221     public static class PresenceTupleColumns implements BaseColumns {
222 
223         /**
224          * A reference to the {@link ContactColumns#_ID} that this data belongs to.
225          * <P>Type:  INTEGER</P>
226          */
227         public static final String EAB_COMMON_ID = "eab_common_id";
228 
229         /**
230          * The basic status of service capabilities. Possible values are
231          * {@link android.telephony.ims.RcsContactPresenceTuple#TUPLE_BASIC_STATUS_OPEN}
232          * and
233          * {@link android.telephony.ims.RcsContactPresenceTuple#TUPLE_BASIC_STATUS_CLOSED}
234          * <P>Type:  TEXT</P>
235          */
236         public static final String BASIC_STATUS = "basic_status";
237 
238         /**
239          * The OMA Presence service-id associated with this capability. See the OMA Presence SIMPLE
240          * specification v1.1, section 10.5.1.
241          * <P>Type:  TEXT</P>
242          */
243         public static final String SERVICE_ID = "service_id";
244 
245         /**
246          * The contact uri of service capabilities.
247          * <P>Type:  TEXT</P>
248          */
249         public static final String CONTACT_URI = "contact_uri";
250 
251         /**
252          * The service version of service capabilities.
253          * <P>Type:  TEXT</P>
254          */
255         public static final String SERVICE_VERSION = "service_version";
256 
257         /**
258          * The description of service capabilities.
259          * <P>Type:  TEXT</P>
260          */
261         public static final String DESCRIPTION = "description";
262 
263         /**
264          * The supported duplex mode of service capabilities. Possible values are
265          * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_FULL}
266          * and
267          * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_HALF}
268          * and
269          * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_RECEIVE_ONLY}
270          * and
271          * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_SEND_ONLY}
272          * <P>Type:  TEXT</P>
273          */
274         public static final String DUPLEX_MODE = "duplex_mode";
275 
276         /**
277          * The unsupported duplex mode of service capabilities. Possible values are
278          * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_FULL}
279          * and
280          * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_HALF}
281          * and
282          * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_RECEIVE_ONLY}
283          * and
284          * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_SEND_ONLY}
285          * <P>Type:  TEXT</P>
286          */
287         public static final String UNSUPPORTED_DUPLEX_MODE = "unsupported_duplex_mode";
288 
289         /**
290          * The presence request timestamp. Represents seconds of UTC time since Unix epoch
291          * 1970-01-01 00:00:00.
292          * <P>Type:  LONG</P>
293          */
294         public static final String REQUEST_TIMESTAMP = "presence_request_timestamp";
295 
296         /**
297          * The audio capable.
298          * <P>Type:  BOOLEAN </P>
299          */
300         public static final String AUDIO_CAPABLE = "audio_capable";
301 
302         /**
303          * The video capable.
304          * <P>Type:  BOOLEAN </P>
305          */
306         public static final String VIDEO_CAPABLE = "video_capable";
307     }
308 
309     /**
310      * This is used to generate a instance of {@link android.telephony.ims.RcsContactPresenceTuple}.
311      * See that class for more information on each of these parameters.
312      */
313     public static class OptionsColumns implements BaseColumns {
314 
315         /**
316          * A reference to the {@link ContactColumns#_ID} that this data belongs to.
317          * <P>Type:  INTEGER</P>
318          */
319         public static final String EAB_COMMON_ID = "eab_common_id";
320 
321         /**
322          * An IMS feature tag indicating the capabilities of the contact. See RFC3840 #section-9.
323          * <P>Type:  TEXT</P>
324          */
325         public static final String FEATURE_TAG = "feature_tag";
326 
327         /**
328          * The request timestamp of options capabilities.
329          * <P>Type:  LONG</P>
330          */
331         public static final String REQUEST_TIMESTAMP = "options_request_timestamp";
332     }
333 
334     @VisibleForTesting
335     public static final class EabDatabaseHelper extends SQLiteOpenHelper {
336         private static final String DB_NAME = "EabDatabase";
337         private static final List<String> CONTACT_UNIQUE_FIELDS = new ArrayList<>();
338         private static final List<String> COMMON_UNIQUE_FIELDS = new ArrayList<>();
339 
340         static {
341             CONTACT_UNIQUE_FIELDS.add(ContactColumns.PHONE_NUMBER);
342         }
343 
344         @VisibleForTesting
345         public static final String SQL_CREATE_CONTACT_TABLE = "CREATE TABLE "
346                 + EAB_CONTACT_TABLE_NAME
347                 + " ("
348                 + ContactColumns._ID + " INTEGER PRIMARY KEY, "
349                 + ContactColumns.PHONE_NUMBER + " Text DEFAULT NULL, "
350                 + ContactColumns.CONTACT_ID + " INTEGER DEFAULT -1, "
351                 + ContactColumns.RAW_CONTACT_ID + " INTEGER DEFAULT -1, "
352                 + ContactColumns.DATA_ID + " INTEGER DEFAULT -1, "
353                 + "UNIQUE (" + TextUtils.join(", ", CONTACT_UNIQUE_FIELDS) + ")"
354                 + ");";
355 
356         @VisibleForTesting
357         public static final String SQL_CREATE_COMMON_TABLE = "CREATE TABLE "
358                 + EAB_COMMON_TABLE_NAME
359                 + " ("
360                 + EabCommonColumns._ID + " INTEGER PRIMARY KEY, "
361                 + EabCommonColumns.EAB_CONTACT_ID + " INTEGER DEFAULT -1, "
362                 + EabCommonColumns.MECHANISM + " INTEGER DEFAULT NULL, "
363                 + EabCommonColumns.REQUEST_RESULT + " INTEGER DEFAULT -1, "
364                 + EabCommonColumns.SUBSCRIPTION_ID + " INTEGER DEFAULT -1, "
365                 + EabCommonColumns.ENTITY_URI + " TEXT DEFAULT NULL "
366                 + ");";
367 
368         @VisibleForTesting
369         public static final String SQL_CREATE_PRESENCE_TUPLE_TABLE = "CREATE TABLE "
370                 + EAB_PRESENCE_TUPLE_TABLE_NAME
371                 + " ("
372                 + PresenceTupleColumns._ID + " INTEGER PRIMARY KEY, "
373                 + PresenceTupleColumns.EAB_COMMON_ID + " INTEGER DEFAULT -1, "
374                 + PresenceTupleColumns.BASIC_STATUS + " TEXT DEFAULT NULL, "
375                 + PresenceTupleColumns.SERVICE_ID + " TEXT DEFAULT NULL, "
376                 + PresenceTupleColumns.SERVICE_VERSION + " TEXT DEFAULT NULL, "
377                 + PresenceTupleColumns.DESCRIPTION + " TEXT DEFAULT NULL, "
378                 + PresenceTupleColumns.REQUEST_TIMESTAMP + " LONG DEFAULT NULL, "
379                 + PresenceTupleColumns.CONTACT_URI + " TEXT DEFAULT NULL, "
380 
381                 // For ServiceCapabilities
382                 + PresenceTupleColumns.DUPLEX_MODE + " TEXT DEFAULT NULL, "
383                 + PresenceTupleColumns.UNSUPPORTED_DUPLEX_MODE + " TEXT DEFAULT NULL, "
384                 + PresenceTupleColumns.AUDIO_CAPABLE + " BOOLEAN DEFAULT NULL, "
385                 + PresenceTupleColumns.VIDEO_CAPABLE + " BOOLEAN DEFAULT NULL"
386                 + ");";
387 
388         @VisibleForTesting
389         public static final String SQL_CREATE_OPTIONS_TABLE = "CREATE TABLE "
390                 + EAB_OPTIONS_TABLE_NAME
391                 + " ("
392                 + OptionsColumns._ID + " INTEGER PRIMARY KEY, "
393                 + OptionsColumns.EAB_COMMON_ID + " INTEGER DEFAULT -1, "
394                 + OptionsColumns.REQUEST_TIMESTAMP + " LONG DEFAULT NULL, "
395                 + OptionsColumns.FEATURE_TAG + " TEXT DEFAULT NULL "
396                 + ");";
397 
EabDatabaseHelper(Context context)398         EabDatabaseHelper(Context context) {
399             super(context, DB_NAME, null, DATABASE_VERSION);
400         }
401 
onCreate(SQLiteDatabase db)402         public void onCreate(SQLiteDatabase db) {
403             db.execSQL(SQL_CREATE_CONTACT_TABLE);
404             db.execSQL(SQL_CREATE_COMMON_TABLE);
405             db.execSQL(SQL_CREATE_PRESENCE_TUPLE_TABLE);
406             db.execSQL(SQL_CREATE_OPTIONS_TABLE);
407         }
408 
409         @Override
onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion)410         public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
411             Log.d(TAG, "DB upgrade from " + oldVersion + " to " + newVersion);
412 
413             if (oldVersion < 2) {
414                 sqLiteDatabase.execSQL("ALTER TABLE " + EAB_CONTACT_TABLE_NAME + " ADD COLUMN "
415                         + ContactColumns.CONTACT_ID + " INTEGER DEFAULT -1;");
416                 oldVersion = 2;
417             }
418 
419             if (oldVersion < 3) {
420                 // Drop UNIQUE constraint in EAB_COMMON_TABLE, SQLite didn't support DROP
421                 // constraint, so drop the old one and migrate data to new one
422 
423                 // Create temp table
424                 final String createTempTableCommand = "CREATE TABLE temp"
425                         + " ("
426                         + EabCommonColumns._ID + " INTEGER PRIMARY KEY, "
427                         + EabCommonColumns.EAB_CONTACT_ID + " INTEGER DEFAULT -1, "
428                         + EabCommonColumns.MECHANISM + " INTEGER DEFAULT NULL, "
429                         + EabCommonColumns.REQUEST_RESULT + " INTEGER DEFAULT -1, "
430                         + EabCommonColumns.SUBSCRIPTION_ID + " INTEGER DEFAULT -1 "
431                         + ");";
432                 sqLiteDatabase.execSQL(createTempTableCommand);
433 
434                 // Migrate data to temp table
435                 sqLiteDatabase.execSQL("INSERT INTO temp ("
436                         + EabCommonColumns._ID + ", "
437                         + EabCommonColumns.EAB_CONTACT_ID + ", "
438                         + EabCommonColumns.MECHANISM + ", "
439                         + EabCommonColumns.REQUEST_RESULT + ", "
440                         + EabCommonColumns.SUBSCRIPTION_ID + ") "
441                         + " SELECT "
442                         + EabCommonColumns._ID + ", "
443                         + EabCommonColumns.EAB_CONTACT_ID + ", "
444                         + EabCommonColumns.MECHANISM + ", "
445                         + EabCommonColumns.REQUEST_RESULT + ", "
446                         + EabCommonColumns.SUBSCRIPTION_ID + " "
447                         + " FROM "
448                         + EAB_COMMON_TABLE_NAME
449                         +";");
450 
451                 // Drop old one
452                 sqLiteDatabase.execSQL("DROP TABLE " + EAB_COMMON_TABLE_NAME + ";");
453 
454                 // Rename temp to eab_common
455                 sqLiteDatabase.execSQL("ALTER TABLE temp RENAME TO " + EAB_COMMON_TABLE_NAME + ";");
456                 oldVersion = 3;
457             }
458 
459             if (oldVersion < 4) {
460                 sqLiteDatabase.execSQL("ALTER TABLE " + EAB_COMMON_TABLE_NAME + " ADD COLUMN "
461                         + EabCommonColumns.ENTITY_URI + " Text DEFAULT NULL;");
462                 oldVersion = 4;
463             }
464         }
465     }
466 
467     private EabDatabaseHelper mOpenHelper;
468 
469     @Override
onCreate()470     public boolean onCreate() {
471         mOpenHelper = new EabDatabaseHelper(getContext());
472         return true;
473     }
474 
475     /**
476      * Support 6 URLs for querying:
477      *
478      * <ul>
479      * <li>{@link #URL_CONTACT}: query contact table.
480      *
481      * <li>{@link #URL_COMMON}: query common table.
482      *
483      * <li>{@link #URL_PRESENCE}: query presence capability table.
484      *
485      * <li>{@link #URL_OPTIONS}: query options capability table.
486      *
487      * <li>{@link #URL_ALL_WITH_SUB_ID_AND_PHONE_NUMBER}: To provide more efficient query way,
488      * filter by the {@link ContactColumns#PHONE_NUMBER} first and join with others tables. The
489      * format is like content://eab/all/[sub_id]/[phone_number]
490      *
491      * <li> {@link #URL_ALL}: Join all of tables at once
492      * </ul>
493      */
494     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)495     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
496             String sortOrder) {
497         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
498         SQLiteDatabase db = getReadableDatabase();
499         int match = URI_MATCHER.match(uri);
500         int subId;
501         String subIdString;
502 
503         Log.d(TAG, "Query URI: " + match);
504 
505         switch (match) {
506             case URL_CONTACT:
507                 qb.setTables(EAB_CONTACT_TABLE_NAME);
508                 break;
509 
510             case URL_COMMON:
511                 qb.setTables(EAB_COMMON_TABLE_NAME);
512                 break;
513 
514             case URL_PRESENCE:
515                 qb.setTables(EAB_PRESENCE_TUPLE_TABLE_NAME);
516                 break;
517 
518             case URL_OPTIONS:
519                 qb.setTables(EAB_OPTIONS_TABLE_NAME);
520                 break;
521 
522             case URL_ALL_WITH_SUB_ID_AND_PHONE_NUMBER:
523                 List<String> pathSegment = uri.getPathSegments();
524 
525                 subIdString = pathSegment.get(1);
526                 try {
527                     subId = Integer.parseInt(subIdString);
528                 } catch (NumberFormatException e) {
529                     Log.e(TAG, "NumberFormatException" + e);
530                     return null;
531                 }
532                 qb.appendWhereStandalone(EabCommonColumns.SUBSCRIPTION_ID + "=" + subId);
533 
534                 String phoneNumber = pathSegment.get(2);
535                 String whereClause;
536                 if (TextUtils.isEmpty(phoneNumber)) {
537                     Log.e(TAG, "phone number is null");
538                     return null;
539                 }
540                 whereClause = " where " + ContactColumns.PHONE_NUMBER + "='" + phoneNumber + "' ";
541                 qb.setTables(
542                         "((" + QUERY_CONTACT_TABLE + whereClause + ") AS " + EAB_CONTACT_TABLE_NAME
543                                 + JOIN_ALL_TABLES + ")");
544                 break;
545 
546             case URL_ALL:
547                 qb.setTables("(" + QUERY_CONTACT_TABLE + JOIN_ALL_TABLES + ")");
548                 break;
549 
550             default:
551                 Log.d(TAG, "Query failed. Not support URL.");
552                 return null;
553         }
554         return qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
555     }
556 
557     @Override
insert(Uri uri, ContentValues contentValues)558     public Uri insert(Uri uri, ContentValues contentValues) {
559         SQLiteDatabase db = getWritableDatabase();
560         int match = URI_MATCHER.match(uri);
561         long result = 0;
562         String tableName = "";
563         switch (match) {
564             case URL_CONTACT:
565                 tableName = EAB_CONTACT_TABLE_NAME;
566                 break;
567             case URL_COMMON:
568                 tableName = EAB_COMMON_TABLE_NAME;
569                 break;
570             case URL_PRESENCE:
571                 tableName = EAB_PRESENCE_TUPLE_TABLE_NAME;
572                 break;
573             case URL_OPTIONS:
574                 tableName = EAB_OPTIONS_TABLE_NAME;
575                 break;
576         }
577         if (!TextUtils.isEmpty(tableName)) {
578             result = db.insertWithOnConflict(tableName, null, contentValues,
579                     SQLiteDatabase.CONFLICT_REPLACE);
580             Log.d(TAG, "Insert uri: " + match + " ID: " + result);
581             if (result > 0) {
582                 getContext().getContentResolver().notifyChange(uri, null, NOTIFY_INSERT);
583             }
584         } else {
585             Log.d(TAG, "Insert. Not support URI.");
586         }
587 
588         return Uri.withAppendedPath(uri, String.valueOf(result));
589     }
590 
591     @Override
bulkInsert(Uri uri, ContentValues[] values)592     public int bulkInsert(Uri uri, ContentValues[] values) {
593         SQLiteDatabase db = getWritableDatabase();
594         int match = URI_MATCHER.match(uri);
595         int result = 0;
596         String tableName = "";
597         switch (match) {
598             case URL_CONTACT:
599                 tableName = EAB_CONTACT_TABLE_NAME;
600                 break;
601             case URL_COMMON:
602                 tableName = EAB_COMMON_TABLE_NAME;
603                 break;
604             case URL_PRESENCE:
605                 tableName = EAB_PRESENCE_TUPLE_TABLE_NAME;
606                 break;
607             case URL_OPTIONS:
608                 tableName = EAB_OPTIONS_TABLE_NAME;
609                 break;
610         }
611 
612         if (TextUtils.isEmpty(tableName)) {
613             Log.d(TAG, "bulkInsert. Not support URI.");
614             return 0;
615         }
616 
617         try {
618             // Batch all insertions in a single transaction to improve efficiency.
619             db.beginTransaction();
620             for (ContentValues contentValue : values) {
621                 if (contentValue != null) {
622                     db.insertWithOnConflict(tableName, null, contentValue,
623                             SQLiteDatabase.CONFLICT_REPLACE);
624                     result++;
625                 }
626             }
627             db.setTransactionSuccessful();
628         } finally {
629             db.endTransaction();
630         }
631         if (result > 0) {
632             getContext().getContentResolver().notifyChange(uri, null, NOTIFY_INSERT);
633         }
634         Log.d(TAG, "bulkInsert uri: " + match + " count: " + result);
635         return result;
636     }
637 
638     @Override
delete(Uri uri, String selection, String[] selectionArgs)639     public int delete(Uri uri, String selection, String[] selectionArgs) {
640         SQLiteDatabase db = getWritableDatabase();
641         int match = URI_MATCHER.match(uri);
642         int result = 0;
643         String tableName = "";
644         switch (match) {
645             case URL_CONTACT:
646                 tableName = EAB_CONTACT_TABLE_NAME;
647                 break;
648             case URL_COMMON:
649                 tableName = EAB_COMMON_TABLE_NAME;
650                 break;
651             case URL_PRESENCE:
652                 tableName = EAB_PRESENCE_TUPLE_TABLE_NAME;
653                 break;
654             case URL_OPTIONS:
655                 tableName = EAB_OPTIONS_TABLE_NAME;
656                 break;
657         }
658         if (!TextUtils.isEmpty(tableName)) {
659             result = db.delete(tableName, selection, selectionArgs);
660             if (result > 0) {
661                 getContext().getContentResolver().notifyChange(uri, null, NOTIFY_DELETE);
662             }
663             Log.d(TAG, "Delete uri: " + match + " result: " + result);
664         } else {
665             Log.d(TAG, "Delete. Not support URI.");
666         }
667         return result;
668     }
669 
670     @Override
update(Uri uri, ContentValues contentValues, String selection, String[] selectionArgs)671     public int update(Uri uri, ContentValues contentValues, String selection,
672             String[] selectionArgs) {
673         SQLiteDatabase db = getWritableDatabase();
674         int match = URI_MATCHER.match(uri);
675         int result = 0;
676         String tableName = "";
677         switch (match) {
678             case URL_CONTACT:
679                 tableName = EAB_CONTACT_TABLE_NAME;
680                 break;
681             case URL_COMMON:
682                 tableName = EAB_COMMON_TABLE_NAME;
683                 break;
684             case URL_PRESENCE:
685                 tableName = EAB_PRESENCE_TUPLE_TABLE_NAME;
686                 break;
687             case URL_OPTIONS:
688                 tableName = EAB_OPTIONS_TABLE_NAME;
689                 break;
690         }
691         if (!TextUtils.isEmpty(tableName)) {
692             result = db.updateWithOnConflict(tableName, contentValues,
693                     selection, selectionArgs, SQLiteDatabase.CONFLICT_REPLACE);
694             if (result > 0) {
695                 getContext().getContentResolver().notifyChange(uri, null, NOTIFY_UPDATE);
696             }
697             Log.d(TAG, "Update uri: " + match + " result: " + result);
698         } else {
699             Log.d(TAG, "Update. Not support URI.");
700         }
701         return result;
702     }
703 
704     @Override
getType(Uri uri)705     public String getType(Uri uri) {
706         return null;
707     }
708 
709     @VisibleForTesting
getWritableDatabase()710     public SQLiteDatabase getWritableDatabase() {
711         return mOpenHelper.getWritableDatabase();
712     }
713 
714     @VisibleForTesting
getReadableDatabase()715     public SQLiteDatabase getReadableDatabase() {
716         return mOpenHelper.getReadableDatabase();
717     }
718 }
719