1 /*
2  * Copyright (C) 2022 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.contacts.util;
17 
18 import com.android.providers.contacts.CallLogDatabaseHelper;
19 import com.android.providers.contacts.ContactsDatabaseHelper;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.content.SharedPreferences;
23 import android.provider.CallLog;
24 import android.provider.CallLog.Calls;
25 import android.database.sqlite.SQLiteDatabase;
26 import android.database.Cursor;
27 import android.database.DatabaseUtils;
28 import android.provider.ContactsContract.Data;
29 import android.preference.PreferenceManager;
30 import android.telephony.SubscriptionManager;
31 import android.telephony.SubscriptionInfo;
32 import android.text.TextUtils;
33 import android.util.Log;
34 
35 import java.util.HashMap;
36 import java.util.List;
37 import java.util.Map;
38 
39 /**
40  * Utils for PhoneAccountHandle Migration Operations in database providers.
41  *
42  * When the database is created and upgraded, PhoneAccountHandleMigrationUtils helps migrate IccId
43  * to SubId. If the PhoneAccount haven't registered yet, we set the pending status for further
44  * migration. Databases will listen to broadcast
45  * {@link android.telecom.TelecomManager#ACTION_PHONE_ACCOUNT_REGISTERED} to identify a new sim
46  * event and performing migration for pending status if possible.
47  */
48 public class PhoneAccountHandleMigrationUtils {
49     /**
50      * Indicates type of ContactsDatabase.
51      */
52     public static final int TYPE_CONTACTS = 0;
53     /**
54      * Indicates type of CallLogDatabase.
55      */
56     public static final int TYPE_CALL_LOG = 1;
57 
58     public static final String TELEPHONY_COMPONENT_NAME =
59             "com.android.phone/com.android.services.telephony.TelephonyConnectionService";
60     private static final String[] TAGS = {
61             "PhoneAccountHandleMigrationUtils_ContactsDatabaseHelper",
62                     "PhoneAccountHandleMigrationUtils_CallLogDatabaseHelper"};
63     private static final String[] TABLES = {ContactsDatabaseHelper.Tables.DATA,
64             CallLogDatabaseHelper.Tables.CALLS};
65     private static final String[] IDS = {Data._ID, Calls._ID};
66     private static final String[] PENDING_STATUS_FIELDS = {
67             Data.IS_PHONE_ACCOUNT_MIGRATION_PENDING, Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING};
68     private static final String[] COMPONENT_NAME_FIELDS = {
69             Data.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME, Calls.PHONE_ACCOUNT_COMPONENT_NAME};
70     private static final String[] PHONE_ACCOUNT_ID_FIELDS = {
71             Data.PREFERRED_PHONE_ACCOUNT_ID, Calls.PHONE_ACCOUNT_ID};
72 
73     private int mType;
74     private SubscriptionManager mSubscriptionManager;
75     private SharedPreferences mSharedPreferences;
76 
77     /**
78      *  Constructor of the util.
79      */
PhoneAccountHandleMigrationUtils(Context context, int type)80     public PhoneAccountHandleMigrationUtils(Context context, int type) {
81         mType = type;
82         mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
83         mSubscriptionManager = context.getSystemService(SubscriptionManager.class);
84     }
85 
86     /**
87      * Mark all the telephony phone account handles as pending migration.
88      */
markAllTelephonyPhoneAccountsPendingMigration(SQLiteDatabase db)89     public void markAllTelephonyPhoneAccountsPendingMigration(SQLiteDatabase db) {
90         ContentValues valuesForTelephonyPending = new ContentValues();
91         valuesForTelephonyPending.put(PENDING_STATUS_FIELDS[mType], 1);
92         String selection = COMPONENT_NAME_FIELDS[mType] + " = ?";
93         String[] selectionArgs = {TELEPHONY_COMPONENT_NAME};
94         db.beginTransaction();
95         try {
96             int count = db.update(
97                     TABLES[mType], valuesForTelephonyPending, selection, selectionArgs);
98             Log.i(TAGS[mType], "markAllTelephonyPhoneAccountsPendingMigration count: " + count);
99             db.setTransactionSuccessful();
100         } finally {
101             db.endTransaction();
102         }
103     }
104 
105     /**
106      * Set phone account migration pending status, indicating if there is any phone account handle
107      * that need to migrate. Store the value in the SharedPreference to prevent the need to query
108      * the database in the future for pending migration.
109      */
setPhoneAccountMigrationStatusPending(boolean status)110     public void setPhoneAccountMigrationStatusPending(boolean status) {
111         mSharedPreferences.edit().putBoolean(PENDING_STATUS_FIELDS[mType], status).apply();
112     }
113 
114     /**
115      * Checks phone account migration pending status, indicating if there is any phone account
116      * handle that need to migrate. Query the value in the SharedPreference to prevent the need
117      * to query the database in the future for pending migration.
118      */
isPhoneAccountMigrationPending()119     public boolean isPhoneAccountMigrationPending() {
120         return mSharedPreferences.getBoolean(PENDING_STATUS_FIELDS[mType], false);
121     }
122 
123     /**
124      * Updates phone account migration pending status, indicating if there is any phone account
125      * handle that need to migrate.
126      */
updatePhoneAccountHandleMigrationPendingStatus(SQLiteDatabase sqLiteDatabase)127     public void updatePhoneAccountHandleMigrationPendingStatus(SQLiteDatabase sqLiteDatabase) {
128         // Check to see if any entries need phone account migration pending.
129         long count = DatabaseUtils.longForQuery(sqLiteDatabase, "SELECT COUNT(DISTINCT "
130                 + IDS[mType] + ") FROM " + TABLES[mType] + " WHERE "
131                 + PENDING_STATUS_FIELDS[mType] + " == 1", null);
132         if (count > 0) {
133             Log.i(TAGS[mType], "updatePhoneAccountHandleMigrationPendingStatus true");
134             setPhoneAccountMigrationStatusPending(true);
135         } else {
136             Log.i(TAGS[mType], "updatePhoneAccountHandleMigrationPendingStatus false");
137             setPhoneAccountMigrationStatusPending(false);
138         }
139     }
140 
141     /**
142      * Migrate all the pending phone account handles based on the given iccId and subId.
143      */
migratePendingPhoneAccountHandles(String iccId, String subId, SQLiteDatabase db)144     public void migratePendingPhoneAccountHandles(String iccId, String subId, SQLiteDatabase db) {
145         ContentValues valuesForPhoneAccountId = new ContentValues();
146         valuesForPhoneAccountId.put(PHONE_ACCOUNT_ID_FIELDS[mType], subId);
147         valuesForPhoneAccountId.put(PENDING_STATUS_FIELDS[mType], 0);
148         String selection = PHONE_ACCOUNT_ID_FIELDS[mType] + " LIKE ? AND "
149                 + PENDING_STATUS_FIELDS[mType] + " = ?";
150         String[] selectionArgs = {iccId, "1"};
151         db.beginTransaction();
152         try {
153             int count = db.update(TABLES[mType], valuesForPhoneAccountId, selection, selectionArgs);
154             Log.i(TAGS[mType], "migrated pending PhoneAccountHandle subId: " + subId
155                     + " count: " + count);
156             db.setTransactionSuccessful();
157         } finally {
158             db.endTransaction();
159         }
160         updatePhoneAccountHandleMigrationPendingStatus(db);
161     }
162 
163     /**
164      * Try to migrate any PhoneAccountId to SubId from IccId.
165      */
migrateIccIdToSubId(SQLiteDatabase db)166     public void migrateIccIdToSubId(SQLiteDatabase db) {
167         final HashMap<String, String> phoneAccountIdsMigrateNow = new HashMap<>();
168         final Cursor phoneAccountIdsCursor = db.rawQuery(
169                 "SELECT DISTINCT " + PHONE_ACCOUNT_ID_FIELDS[mType] + " FROM " + TABLES[mType]
170                         + " WHERE " + PENDING_STATUS_FIELDS[mType] + " = 1", null);
171 
172         try {
173             List<SubscriptionInfo> subscriptionInfoList = mSubscriptionManager
174                     .getAllSubscriptionInfoList();
175             phoneAccountIdsCursor.moveToPosition(-1);
176             while (phoneAccountIdsCursor.moveToNext()) {
177                 if (phoneAccountIdsCursor.isNull(0)) {
178                     continue;
179                 }
180                 final String iccId = phoneAccountIdsCursor.getString(0);
181                 String subId = null;
182                 if (mSubscriptionManager != null) {
183                     subId = getSubIdForIccId(iccId, subscriptionInfoList);
184                 }
185 
186                 if (!TextUtils.isEmpty(iccId)) {
187                     if (subId != null) {
188                         // If there is already a subId that maps to the corresponding iccid
189                         // from an old phone account handle, migrate to the new phone account
190                         // handle with sub id without pending.
191                         phoneAccountIdsMigrateNow.put(iccId, subId);
192                         Log.i(TAGS[mType], "migrateIccIdToSubId(db): found subId: " + subId);
193                     }
194                 }
195             }
196         } finally {
197             phoneAccountIdsCursor.close();
198         }
199         // Migrate to the new phone account handle with its sub ID that is already available.
200         for (Map.Entry<String, String> set : phoneAccountIdsMigrateNow.entrySet()) {
201             migratePendingPhoneAccountHandles(set.getKey(), set.getValue(), db);
202         }
203     }
204 
205     // Return a subId that maps to the given iccId, or null if the subId is not available.
getSubIdForIccId(String iccId, List<SubscriptionInfo> subscriptionInfoList)206     private String getSubIdForIccId(String iccId, List<SubscriptionInfo> subscriptionInfoList) {
207         for (SubscriptionInfo subscriptionInfo : subscriptionInfoList) {
208             // Some old version callog would store phone account handle id with the IccId
209             // string plus "F", and the getIccId() returns IccId string itself without "F",
210             // so here need to use "startsWith" to match.
211             if (iccId.startsWith(subscriptionInfo.getIccId())) {
212                 Log.i(TAGS[mType], "getSubIdForIccId: Found subscription ID to migrate: "
213                         + subscriptionInfo.getSubscriptionId());
214                 return Integer.toString(subscriptionInfo.getSubscriptionId());
215             }
216         }
217         return null;
218     }
219 }