1 /*
2  * Copyright (C) 2013 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.dialer.calllog;
18 
19 import com.google.common.base.Strings;
20 
21 import android.Manifest;
22 import android.content.ContentResolver;
23 import android.content.ContentUris;
24 import android.content.Context;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.provider.CallLog.Calls;
28 import android.provider.ContactsContract.PhoneLookup;
29 import android.support.annotation.NonNull;
30 import android.support.annotation.Nullable;
31 import android.telephony.PhoneNumberUtils;
32 import android.text.TextUtils;
33 import android.util.Log;
34 
35 import com.android.contacts.common.GeoUtil;
36 import com.android.contacts.common.util.PermissionsUtil;
37 import com.android.dialer.R;
38 import com.android.dialer.util.TelecomUtil;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 
43 /**
44  * Helper class operating on call log notifications.
45  */
46 public class CallLogNotificationsHelper {
47     private static final String TAG = "CallLogNotifHelper";
48     private static CallLogNotificationsHelper sInstance;
49 
50     /** Returns the singleton instance of the {@link CallLogNotificationsHelper}. */
getInstance(Context context)51     public static CallLogNotificationsHelper getInstance(Context context) {
52         if (sInstance == null) {
53             ContentResolver contentResolver = context.getContentResolver();
54             String countryIso = GeoUtil.getCurrentCountryIso(context);
55             sInstance = new CallLogNotificationsHelper(context,
56                     createNewCallsQuery(context, contentResolver),
57                     createNameLookupQuery(context, contentResolver),
58                     new ContactInfoHelper(context, countryIso),
59                     countryIso);
60         }
61         return sInstance;
62     }
63 
64     private final Context mContext;
65     private final NewCallsQuery mNewCallsQuery;
66     private final NameLookupQuery mNameLookupQuery;
67     private final ContactInfoHelper mContactInfoHelper;
68     private final String mCurrentCountryIso;
69 
CallLogNotificationsHelper(Context context, NewCallsQuery newCallsQuery, NameLookupQuery nameLookupQuery, ContactInfoHelper contactInfoHelper, String countryIso)70     CallLogNotificationsHelper(Context context, NewCallsQuery newCallsQuery,
71             NameLookupQuery nameLookupQuery, ContactInfoHelper contactInfoHelper,
72             String countryIso) {
73         mContext = context;
74         mNewCallsQuery = newCallsQuery;
75         mNameLookupQuery = nameLookupQuery;
76         mContactInfoHelper = contactInfoHelper;
77         mCurrentCountryIso = countryIso;
78     }
79 
80     /**
81      * Get all voicemails with the "new" flag set to 1.
82      *
83      * @return A list of NewCall objects where each object represents a new voicemail.
84      */
85     @Nullable
getNewVoicemails()86     public List<NewCall> getNewVoicemails() {
87         return mNewCallsQuery.query(Calls.VOICEMAIL_TYPE);
88     }
89 
90     /**
91      * Get all missed calls with the "new" flag set to 1.
92      *
93      * @return A list of NewCall objects where each object represents a new missed call.
94      */
95     @Nullable
getNewMissedCalls()96     public List<NewCall> getNewMissedCalls() {
97         return mNewCallsQuery.query(Calls.MISSED_TYPE);
98     }
99 
100     /**
101      * Given a number and number information (presentation and country ISO), get the best name
102      * for display. If the name is empty but we have a special presentation, display that.
103      * Otherwise attempt to look it up in the database or the cache.
104      * If that fails, fall back to displaying the number.
105      */
getName(@ullable String number, int numberPresentation, @Nullable String countryIso)106     public String getName(@Nullable String number, int numberPresentation,
107                           @Nullable String countryIso) {
108         return getContactInfo(number, numberPresentation, countryIso).name;
109     }
110 
111     /**
112      * Given a number and number information (presentation and country ISO), get
113      * {@link ContactInfo}. If the name is empty but we have a special presentation, display that.
114      * Otherwise attempt to look it up in the cache.
115      * If that fails, fall back to displaying the number.
116      */
getContactInfo(@ullable String number, int numberPresentation, @Nullable String countryIso)117     public ContactInfo getContactInfo(@Nullable String number, int numberPresentation,
118                           @Nullable String countryIso) {
119         if (countryIso == null) {
120             countryIso = mCurrentCountryIso;
121         }
122 
123         number = Strings.nullToEmpty(number);
124         ContactInfo contactInfo = new ContactInfo();
125         contactInfo.number = number;
126         contactInfo.formattedNumber = PhoneNumberUtils.formatNumber(number, countryIso);
127         // contactInfo.normalizedNumber is not PhoneNumberUtils.normalizeNumber. Read ContactInfo.
128         contactInfo.normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
129 
130         // 1. Special number representation.
131         contactInfo.name = PhoneNumberDisplayUtil.getDisplayName(
132                 mContext,
133                 number,
134                 numberPresentation,
135                 false).toString();
136         if (!TextUtils.isEmpty(contactInfo.name)) {
137             return contactInfo;
138         }
139 
140         // 2. Look it up in the cache.
141         ContactInfo cachedContactInfo = mContactInfoHelper.lookupNumber(number, countryIso);
142 
143         if (cachedContactInfo != null && !TextUtils.isEmpty(cachedContactInfo.name)) {
144             return cachedContactInfo;
145         }
146 
147         if (!TextUtils.isEmpty(contactInfo.formattedNumber)) {
148             // 3. If we cannot lookup the contact, use the formatted number instead.
149             contactInfo.name = contactInfo.formattedNumber;
150         } else if (!TextUtils.isEmpty(number)) {
151             // 4. If number can't be formatted, use number.
152             contactInfo.name = number;
153         } else {
154             // 5. Otherwise, it's unknown number.
155             contactInfo.name = mContext.getResources().getString(R.string.unknown);
156         }
157         return contactInfo;
158     }
159 
160     /** Removes the missed call notifications. */
removeMissedCallNotifications(Context context)161     public static void removeMissedCallNotifications(Context context) {
162         TelecomUtil.cancelMissedCallsNotification(context);
163     }
164 
165     /** Update the voice mail notifications. */
updateVoicemailNotifications(Context context)166     public static void updateVoicemailNotifications(Context context) {
167         CallLogNotificationsService.updateVoicemailNotifications(context, null);
168     }
169 
170     /** Information about a new voicemail. */
171     public static final class NewCall {
172         public final Uri callsUri;
173         public final Uri voicemailUri;
174         public final String number;
175         public final int numberPresentation;
176         public final String accountComponentName;
177         public final String accountId;
178         public final String transcription;
179         public final String countryIso;
180         public final long dateMs;
181 
NewCall( Uri callsUri, Uri voicemailUri, String number, int numberPresentation, String accountComponentName, String accountId, String transcription, String countryIso, long dateMs)182         public NewCall(
183                 Uri callsUri,
184                 Uri voicemailUri,
185                 String number,
186                 int numberPresentation,
187                 String accountComponentName,
188                 String accountId,
189                 String transcription,
190                 String countryIso,
191                 long dateMs) {
192             this.callsUri = callsUri;
193             this.voicemailUri = voicemailUri;
194             this.number = number;
195             this.numberPresentation = numberPresentation;
196             this.accountComponentName = accountComponentName;
197             this.accountId = accountId;
198             this.transcription = transcription;
199             this.countryIso = countryIso;
200             this.dateMs = dateMs;
201         }
202     }
203 
204     /** Allows determining the new calls for which a notification should be generated. */
205     public interface NewCallsQuery {
206         /**
207          * Returns the new calls of a certain type for which a notification should be generated.
208          */
209         @Nullable
query(int type)210         public List<NewCall> query(int type);
211     }
212 
213     /** Create a new instance of {@link NewCallsQuery}. */
createNewCallsQuery(Context context, ContentResolver contentResolver)214     public static NewCallsQuery createNewCallsQuery(Context context,
215             ContentResolver contentResolver) {
216 
217         return new DefaultNewCallsQuery(context.getApplicationContext(), contentResolver);
218     }
219 
220     /**
221      * Default implementation of {@link NewCallsQuery} that looks up the list of new calls to
222      * notify about in the call log.
223      */
224     private static final class DefaultNewCallsQuery implements NewCallsQuery {
225         private static final String[] PROJECTION = {
226             Calls._ID,
227             Calls.NUMBER,
228             Calls.VOICEMAIL_URI,
229             Calls.NUMBER_PRESENTATION,
230             Calls.PHONE_ACCOUNT_COMPONENT_NAME,
231             Calls.PHONE_ACCOUNT_ID,
232             Calls.TRANSCRIPTION,
233             Calls.COUNTRY_ISO,
234             Calls.DATE
235         };
236         private static final int ID_COLUMN_INDEX = 0;
237         private static final int NUMBER_COLUMN_INDEX = 1;
238         private static final int VOICEMAIL_URI_COLUMN_INDEX = 2;
239         private static final int NUMBER_PRESENTATION_COLUMN_INDEX = 3;
240         private static final int PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX = 4;
241         private static final int PHONE_ACCOUNT_ID_COLUMN_INDEX = 5;
242         private static final int TRANSCRIPTION_COLUMN_INDEX = 6;
243         private static final int COUNTRY_ISO_COLUMN_INDEX = 7;
244         private static final int DATE_COLUMN_INDEX = 8;
245 
246         private final ContentResolver mContentResolver;
247         private final Context mContext;
248 
DefaultNewCallsQuery(Context context, ContentResolver contentResolver)249         private DefaultNewCallsQuery(Context context, ContentResolver contentResolver) {
250             mContext = context;
251             mContentResolver = contentResolver;
252         }
253 
254         @Override
255         @Nullable
query(int type)256         public List<NewCall> query(int type) {
257             if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CALL_LOG)) {
258                 Log.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup.");
259                 return null;
260             }
261             final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE);
262             final String[] selectionArgs = new String[]{ Integer.toString(type) };
263             try (Cursor cursor = mContentResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL,
264                     PROJECTION, selection, selectionArgs, Calls.DEFAULT_SORT_ORDER)) {
265                 if (cursor == null) {
266                     return null;
267                 }
268                 List<NewCall> newCalls = new ArrayList<>();
269                 while (cursor.moveToNext()) {
270                     newCalls.add(createNewCallsFromCursor(cursor));
271                 }
272                 return newCalls;
273             } catch (RuntimeException e) {
274                 Log.w(TAG, "Exception when querying Contacts Provider for calls lookup");
275                 return null;
276             }
277         }
278 
279         /** Returns an instance of {@link NewCall} created by using the values of the cursor. */
createNewCallsFromCursor(Cursor cursor)280         private NewCall createNewCallsFromCursor(Cursor cursor) {
281             String voicemailUriString = cursor.getString(VOICEMAIL_URI_COLUMN_INDEX);
282             Uri callsUri = ContentUris.withAppendedId(
283                     Calls.CONTENT_URI_WITH_VOICEMAIL, cursor.getLong(ID_COLUMN_INDEX));
284             Uri voicemailUri = voicemailUriString == null ? null : Uri.parse(voicemailUriString);
285             return new NewCall(
286                     callsUri,
287                     voicemailUri,
288                     cursor.getString(NUMBER_COLUMN_INDEX),
289                     cursor.getInt(NUMBER_PRESENTATION_COLUMN_INDEX),
290                     cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX),
291                     cursor.getString(PHONE_ACCOUNT_ID_COLUMN_INDEX),
292                     cursor.getString(TRANSCRIPTION_COLUMN_INDEX),
293                     cursor.getString(COUNTRY_ISO_COLUMN_INDEX),
294                     cursor.getLong(DATE_COLUMN_INDEX));
295         }
296     }
297 
298     /** Allows determining the name associated with a given phone number. */
299     public interface NameLookupQuery {
300         /**
301          * Returns the name associated with the given number in the contacts database, or null if
302          * the number does not correspond to any of the contacts.
303          * <p>
304          * If there are multiple contacts with the same phone number, it will return the name of one
305          * of the matching contacts.
306          */
307         @Nullable
query(@ullable String number)308         public String query(@Nullable String number);
309     }
310 
311     /** Create a new instance of {@link NameLookupQuery}. */
createNameLookupQuery(Context context, ContentResolver contentResolver)312     public static NameLookupQuery createNameLookupQuery(Context context,
313             ContentResolver contentResolver) {
314         return new DefaultNameLookupQuery(context.getApplicationContext(), contentResolver);
315     }
316 
317     /**
318      * Default implementation of {@link NameLookupQuery} that looks up the name of a contact in the
319      * contacts database.
320      */
321     private static final class DefaultNameLookupQuery implements NameLookupQuery {
322         private static final String[] PROJECTION = { PhoneLookup.DISPLAY_NAME };
323         private static final int DISPLAY_NAME_COLUMN_INDEX = 0;
324 
325         private final ContentResolver mContentResolver;
326         private final Context mContext;
327 
DefaultNameLookupQuery(Context context, ContentResolver contentResolver)328         private DefaultNameLookupQuery(Context context, ContentResolver contentResolver) {
329             mContext = context;
330             mContentResolver = contentResolver;
331         }
332 
333         @Override
334         @Nullable
query(@ullable String number)335         public String query(@Nullable String number) {
336             if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CONTACTS)) {
337                 Log.w(TAG, "No READ_CONTACTS permission, returning null for name lookup.");
338                 return null;
339             }
340             try (Cursor cursor =  mContentResolver.query(
341                     Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
342                     PROJECTION, null, null, null)) {
343                 if (cursor == null || !cursor.moveToFirst()) {
344                     return null;
345                 }
346                 return cursor.getString(DISPLAY_NAME_COLUMN_INDEX);
347             } catch (RuntimeException e) {
348                 Log.w(TAG, "Exception when querying Contacts Provider for name lookup");
349                 return null;
350             }
351         }
352     }
353 }
354