• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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