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