1 /* 2 * Copyright (C) 2019 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.car.dialer.storage; 18 19 import android.bluetooth.BluetoothDevice; 20 import android.content.ContentResolver; 21 import android.content.ContentUris; 22 import android.content.Context; 23 import android.database.Cursor; 24 import android.net.Uri; 25 import android.provider.ContactsContract; 26 import android.text.TextUtils; 27 28 import androidx.annotation.WorkerThread; 29 import androidx.lifecycle.LiveData; 30 import androidx.lifecycle.MediatorLiveData; 31 import androidx.lifecycle.MutableLiveData; 32 33 import com.android.car.dialer.log.L; 34 import com.android.car.telephony.common.Contact; 35 import com.android.car.telephony.common.I18nPhoneNumberWrapper; 36 import com.android.car.telephony.common.InMemoryPhoneBook; 37 import com.android.car.telephony.common.PhoneNumber; 38 39 import java.util.ArrayList; 40 import java.util.Collections; 41 import java.util.List; 42 import java.util.Set; 43 import java.util.concurrent.ExecutorService; 44 import java.util.concurrent.Executors; 45 import java.util.concurrent.Future; 46 47 /** 48 * Repository for favorite numbers.It supports the operation to convert the favorite entities to 49 * {@link Contact}s and add or delete entry. 50 */ 51 public class FavoriteNumberRepository { 52 private static final String TAG = "CD.FavRepository"; 53 private static ExecutorService sSerializedExecutor; 54 55 static { 56 sSerializedExecutor = Executors.newSingleThreadExecutor(); 57 } 58 59 private static volatile FavoriteNumberRepository sFavoriteNumberRepository; 60 61 /** 62 * Returns the single instance of the {@link FavoriteNumberRepository}. 63 */ getRepository(final Context context)64 public static FavoriteNumberRepository getRepository(final Context context) { 65 if (sFavoriteNumberRepository == null) { 66 synchronized (FavoriteNumberRepository.class) { 67 if (sFavoriteNumberRepository == null) { 68 sFavoriteNumberRepository = new FavoriteNumberRepository(context); 69 } 70 } 71 } 72 return sFavoriteNumberRepository; 73 } 74 75 private final Context mContext; 76 private final FavoriteNumberDao mFavoriteNumberDao; 77 private final LiveData<List<FavoriteNumberEntity>> mFavoriteNumbers; 78 private final LiveData<List<Contact>> mFavoriteContacts; 79 private Future<?> mConvertAllRunnableFuture; 80 FavoriteNumberRepository(Context context)81 private FavoriteNumberRepository(Context context) { 82 mContext = context.getApplicationContext(); 83 84 FavoriteNumberDatabase db = FavoriteNumberDatabase.getDatabase(mContext); 85 mFavoriteNumberDao = db.favoriteNumberDao(); 86 mFavoriteNumbers = mFavoriteNumberDao.loadAll(); 87 88 mFavoriteContacts = new FavoriteContactLiveData(mContext); 89 } 90 91 /** 92 * Returns the favorite number list. 93 */ getFavoriteNumbers()94 public LiveData<List<FavoriteNumberEntity>> getFavoriteNumbers() { 95 return mFavoriteNumbers; 96 } 97 98 /** 99 * Returns the favorite contact list. 100 */ getFavoriteContacts()101 public LiveData<List<Contact>> getFavoriteContacts() { 102 return mFavoriteContacts; 103 } 104 105 /** 106 * Add a phone number to favorite. 107 */ addToFavorite(Contact contact, PhoneNumber phoneNumber)108 public void addToFavorite(Contact contact, PhoneNumber phoneNumber) { 109 FavoriteNumberEntity favoriteNumber = new FavoriteNumberEntity(); 110 favoriteNumber.setContactId(contact.getId()); 111 favoriteNumber.setContactLookupKey(contact.getLookupKey()); 112 favoriteNumber.setPhoneNumber(new CipherWrapper<>( 113 phoneNumber.getRawNumber())); 114 favoriteNumber.setAccountName(phoneNumber.getAccountName()); 115 favoriteNumber.setAccountType(phoneNumber.getAccountType()); 116 sSerializedExecutor.execute(() -> mFavoriteNumberDao.insert(favoriteNumber)); 117 } 118 119 /** 120 * Remove a phone number from favorite. 121 */ removeFromFavorite(Contact contact, PhoneNumber phoneNumber)122 public void removeFromFavorite(Contact contact, PhoneNumber phoneNumber) { 123 List<FavoriteNumberEntity> favoriteNumbers = mFavoriteNumbers.getValue(); 124 if (favoriteNumbers == null) { 125 return; 126 } 127 for (FavoriteNumberEntity favoriteNumberEntity : favoriteNumbers) { 128 if (matches(favoriteNumberEntity, contact, phoneNumber)) { 129 sSerializedExecutor.execute(() -> mFavoriteNumberDao.delete(favoriteNumberEntity)); 130 } 131 } 132 } 133 134 /** 135 * Remove favorite entries for devices that has been unpaired. 136 */ cleanup(Set<BluetoothDevice> pairedDevices)137 public void cleanup(Set<BluetoothDevice> pairedDevices) { 138 L.d(TAG, "remove entries for unpaired devices except %s", pairedDevices); 139 sSerializedExecutor.execute(() -> { 140 List<String> pairedDeviceAddresses = new ArrayList<>(); 141 for (BluetoothDevice device : pairedDevices) { 142 pairedDeviceAddresses.add(device.getAddress()); 143 } 144 mFavoriteNumberDao.cleanup(pairedDeviceAddresses); 145 }); 146 } 147 148 /** 149 * Convert the {@link FavoriteNumberEntity}s to {@link Contact}s and update contact id and 150 * contact lookup key for all the entities that are out of date. 151 */ convertToContacts(Context context, final MutableLiveData<List<Contact>> results)152 private void convertToContacts(Context context, final MutableLiveData<List<Contact>> results) { 153 if (mConvertAllRunnableFuture != null) { 154 mConvertAllRunnableFuture.cancel(false); 155 } 156 157 mConvertAllRunnableFuture = sSerializedExecutor.submit(() -> { 158 // Don't set null value to trigger unnecessary update when results are null. 159 if (mFavoriteNumbers.getValue() == null) { 160 if (results.getValue() != null) { 161 results.postValue(Collections.emptyList()); 162 } 163 return; 164 } 165 166 ContentResolver cr = context.getContentResolver(); 167 List<FavoriteNumberEntity> outOfDateList = new ArrayList<>(); 168 List<Contact> favoriteContacts = new ArrayList<>(); 169 List<FavoriteNumberEntity> favoriteNumbers = mFavoriteNumbers.getValue(); 170 for (FavoriteNumberEntity favoriteNumber : favoriteNumbers) { 171 Contact contact = lookupContact(cr, favoriteNumber); 172 if (contact != null) { 173 favoriteContacts.add(contact); 174 if (favoriteNumber.getContactId() != contact.getId() 175 || !TextUtils.equals(favoriteNumber.getContactLookupKey(), 176 contact.getLookupKey())) { 177 favoriteNumber.setContactLookupKey(contact.getLookupKey()); 178 favoriteNumber.setContactId(contact.getId()); 179 outOfDateList.add(favoriteNumber); 180 } 181 } 182 } 183 results.postValue(favoriteContacts); 184 if (!outOfDateList.isEmpty()) { 185 mFavoriteNumberDao.updateAll(outOfDateList); 186 } 187 }); 188 } 189 190 @WorkerThread lookupContact(ContentResolver cr, FavoriteNumberEntity favoriteNumber)191 private Contact lookupContact(ContentResolver cr, FavoriteNumberEntity favoriteNumber) { 192 Uri lookupUri = ContactsContract.Contacts.getLookupUri( 193 favoriteNumber.getContactId(), favoriteNumber.getContactLookupKey()); 194 Uri refreshedUri = ContactsContract.Contacts.lookupContact( 195 mContext.getContentResolver(), lookupUri); 196 if (refreshedUri == null) { 197 return null; 198 } 199 long contactId = ContentUris.parseId(refreshedUri); 200 201 try (Cursor cursor = cr.query( 202 ContactsContract.CommonDataKinds.Phone.CONTENT_URI, 203 /* projection= */null, 204 /* selection= */ ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?", 205 new String[]{String.valueOf(contactId)}, 206 /* orderBy= */null)) { 207 if (cursor != null) { 208 if (cursor.moveToFirst()) { 209 Contact contact = Contact.fromCursor(mContext, cursor); 210 contact.getNumbers().clear(); 211 Contact inMemoryContact = InMemoryPhoneBook.get().lookupContactByKey( 212 contact.getLookupKey(), contact.getAccountName()); 213 for (PhoneNumber inMemoryPhoneNumber : inMemoryContact.getNumbers()) { 214 if (numberMatches(favoriteNumber, inMemoryPhoneNumber)) { 215 contact.getNumbers().add(inMemoryPhoneNumber); 216 } 217 } 218 if (!contact.getNumbers().isEmpty()) { 219 return contact; 220 } 221 } 222 } 223 } 224 return null; 225 } 226 matches(FavoriteNumberEntity favoriteNumber, Contact contact, PhoneNumber phoneNumber)227 private boolean matches(FavoriteNumberEntity favoriteNumber, Contact contact, 228 PhoneNumber phoneNumber) { 229 if (TextUtils.equals(favoriteNumber.getContactLookupKey(), contact.getLookupKey())) { 230 return numberMatches(favoriteNumber, phoneNumber); 231 } 232 233 return false; 234 } 235 numberMatches(FavoriteNumberEntity favoriteNumber, PhoneNumber phoneNumber)236 private boolean numberMatches(FavoriteNumberEntity favoriteNumber, PhoneNumber phoneNumber) { 237 if (favoriteNumber.getPhoneNumber() == null) { 238 return false; 239 } 240 241 if (!TextUtils.equals(favoriteNumber.getAccountName(), phoneNumber.getAccountName()) 242 || !TextUtils.equals(favoriteNumber.getAccountType(), 243 phoneNumber.getAccountType())) { 244 return false; 245 } 246 247 I18nPhoneNumberWrapper i18nPhoneNumberWrapper = I18nPhoneNumberWrapper.Factory.INSTANCE.get( 248 mContext, favoriteNumber.getPhoneNumber().get()); 249 return i18nPhoneNumberWrapper.equals(phoneNumber.getI18nPhoneNumberWrapper()); 250 } 251 252 private class FavoriteContactLiveData extends MediatorLiveData<List<Contact>> { FavoriteContactLiveData(Context context)253 private FavoriteContactLiveData(Context context) { 254 super(); 255 addSource(InMemoryPhoneBook.get().getContactsLiveData(), 256 contacts -> convertToContacts(context, this)); 257 addSource(mFavoriteNumbers, favorites -> convertToContacts(context, this)); 258 observeForever(favoriteContacts -> L.d(TAG, "%d favorite contacts loaded.", 259 favoriteContacts.size())); 260 } 261 262 @Override setValue(List<Contact> contacts)263 public void setValue(List<Contact> contacts) { 264 // Clean up the old favorite bit and update the new favorite bit. 265 List<Contact> currentList = getValue(); 266 if (currentList != null) { 267 for (Contact contact : currentList) { 268 for (PhoneNumber phoneNumber : contact.getNumbers()) { 269 phoneNumber.setIsFavorite(false); 270 } 271 } 272 } 273 274 for (Contact contact : contacts) { 275 for (PhoneNumber phoneNumber : contact.getNumbers()) { 276 phoneNumber.setIsFavorite(true); 277 } 278 } 279 280 super.setValue(contacts); 281 } 282 } 283 } 284