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