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