• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 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.content.Context;
20 import android.net.Uri;
21 import android.service.notification.StatusBarNotification;
22 import android.support.annotation.NonNull;
23 import android.support.annotation.Nullable;
24 import android.support.annotation.WorkerThread;
25 import android.text.TextUtils;
26 import android.util.ArrayMap;
27 import com.android.dialer.app.R;
28 import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall;
29 import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
30 import com.android.dialer.blocking.FilteredNumbersUtil;
31 import com.android.dialer.common.Assert;
32 import com.android.dialer.common.LogUtil;
33 import com.android.dialer.common.concurrent.DialerExecutor.Worker;
34 import com.android.dialer.common.concurrent.DialerExecutorComponent;
35 import com.android.dialer.logging.DialerImpression;
36 import com.android.dialer.logging.Logger;
37 import com.android.dialer.notification.DialerNotificationManager;
38 import com.android.dialer.phonenumbercache.ContactInfo;
39 import com.android.dialer.spam.SpamComponent;
40 import com.android.dialer.telecom.TelecomUtil;
41 import java.util.ArrayList;
42 import java.util.List;
43 import java.util.Map;
44 
45 /** Updates voicemail notifications in the background. */
46 class VisualVoicemailUpdateTask implements Worker<VisualVoicemailUpdateTask.Input, Void> {
47   @Nullable
48   @Override
doInBackground(@onNull Input input)49   public Void doInBackground(@NonNull Input input) throws Throwable {
50     updateNotification(input.context, input.queryHelper, input.queryHandler);
51     return null;
52   }
53 
54   /**
55    * Updates the notification and notifies of the call with the given URI.
56    *
57    * <p>Clears the notification if there are no new voicemails, and notifies if the given URI
58    * corresponds to a new voicemail.
59    */
60   @WorkerThread
updateNotification( Context context, CallLogNotificationsQueryHelper queryHelper, FilteredNumberAsyncQueryHandler queryHandler)61   private static void updateNotification(
62       Context context,
63       CallLogNotificationsQueryHelper queryHelper,
64       FilteredNumberAsyncQueryHandler queryHandler) {
65     Assert.isWorkerThread();
66     LogUtil.enterBlock("VisualVoicemailUpdateTask.updateNotification");
67 
68     List<NewCall> voicemailsToNotify = queryHelper.getNewVoicemails();
69     if (voicemailsToNotify == null) {
70       // Query failed, just return
71       return;
72     }
73 
74     if (FilteredNumbersUtil.hasRecentEmergencyCall(context)) {
75       LogUtil.i(
76           "VisualVoicemailUpdateTask.updateNotification",
77           "not filtering due to recent emergency call");
78     } else {
79       voicemailsToNotify = filterBlockedNumbers(context, queryHandler, voicemailsToNotify);
80       voicemailsToNotify = filterSpamNumbers(context, voicemailsToNotify);
81     }
82     boolean shouldAlert =
83         !voicemailsToNotify.isEmpty()
84             && voicemailsToNotify.size() > getExistingNotificationCount(context);
85     voicemailsToNotify.addAll(getAndUpdateVoicemailsWithExistingNotification(context, queryHelper));
86     if (voicemailsToNotify.isEmpty()) {
87       LogUtil.i("VisualVoicemailUpdateTask.updateNotification", "no voicemails to notify about");
88       VisualVoicemailNotifier.cancelAllVoicemailNotifications(context);
89       VoicemailNotificationJobService.cancelJob(context);
90       return;
91     }
92 
93     // This represents a list of names to include in the notification.
94     String callers = null;
95 
96     // Maps each number into a name: if a number is in the map, it has already left a more
97     // recent voicemail.
98     Map<String, ContactInfo> contactInfos = new ArrayMap<>();
99     for (NewCall newCall : voicemailsToNotify) {
100       if (!contactInfos.containsKey(newCall.number)) {
101         ContactInfo contactInfo =
102             queryHelper.getContactInfo(
103                 newCall.number, newCall.numberPresentation, newCall.countryIso);
104         contactInfos.put(newCall.number, contactInfo);
105 
106         // This is a new caller. Add it to the back of the list of callers.
107         if (TextUtils.isEmpty(callers)) {
108           callers = contactInfo.name;
109         } else {
110           callers =
111               context.getString(
112                   R.string.notification_voicemail_callers_list, callers, contactInfo.name);
113         }
114       }
115     }
116     VisualVoicemailNotifier.showNotifications(
117         context, voicemailsToNotify, contactInfos, callers, shouldAlert);
118 
119     // Set trigger to update notifications when database changes.
120     VoicemailNotificationJobService.scheduleJob(context);
121   }
122 
123   @WorkerThread
124   @NonNull
getExistingNotificationCount(Context context)125   private static int getExistingNotificationCount(Context context) {
126     Assert.isWorkerThread();
127     int result = 0;
128     for (StatusBarNotification notification :
129         DialerNotificationManager.getActiveNotifications(context)) {
130       if (notification.getId() != VisualVoicemailNotifier.NOTIFICATION_ID) {
131         continue;
132       }
133       if (TextUtils.isEmpty(notification.getTag())
134           || !notification.getTag().startsWith(VisualVoicemailNotifier.NOTIFICATION_TAG_PREFIX)) {
135         continue;
136       }
137       result++;
138     }
139     return result;
140   }
141 
142   /**
143    * Cancel notification for voicemail that is already deleted. Returns a list of voicemails that
144    * already has notifications posted and should be updated.
145    */
146   @WorkerThread
147   @NonNull
getAndUpdateVoicemailsWithExistingNotification( Context context, CallLogNotificationsQueryHelper queryHelper)148   private static List<NewCall> getAndUpdateVoicemailsWithExistingNotification(
149       Context context, CallLogNotificationsQueryHelper queryHelper) {
150     Assert.isWorkerThread();
151     List<NewCall> result = new ArrayList<>();
152     for (StatusBarNotification notification :
153         DialerNotificationManager.getActiveNotifications(context)) {
154       if (notification.getId() != VisualVoicemailNotifier.NOTIFICATION_ID) {
155         continue;
156       }
157       if (TextUtils.isEmpty(notification.getTag())
158           || !notification.getTag().startsWith(VisualVoicemailNotifier.NOTIFICATION_TAG_PREFIX)) {
159         continue;
160       }
161       String uri =
162           notification.getTag().replace(VisualVoicemailNotifier.NOTIFICATION_TAG_PREFIX, "");
163       NewCall existingCall = queryHelper.getNewCallsQuery().queryUnreadVoicemail(Uri.parse(uri));
164       if (existingCall != null) {
165         result.add(existingCall);
166       } else {
167         LogUtil.i(
168             "VisualVoicemailUpdateTask.getVoicemailsWithExistingNotification",
169             "voicemail deleted, removing notification");
170         DialerNotificationManager.cancel(context, notification.getTag(), notification.getId());
171       }
172     }
173     return result;
174   }
175 
176   @WorkerThread
filterBlockedNumbers( Context context, FilteredNumberAsyncQueryHandler queryHandler, List<NewCall> newCalls)177   private static List<NewCall> filterBlockedNumbers(
178       Context context, FilteredNumberAsyncQueryHandler queryHandler, List<NewCall> newCalls) {
179     Assert.isWorkerThread();
180     List<NewCall> result = new ArrayList<>();
181     for (NewCall newCall : newCalls) {
182       if (queryHandler.getBlockedIdSynchronous(newCall.number, newCall.countryIso) != null) {
183         LogUtil.i(
184             "VisualVoicemailUpdateTask.filterBlockedNumbers",
185             "found voicemail from blocked number, deleting");
186         if (newCall.voicemailUri != null) {
187           // Delete the voicemail.
188           CallLogAsyncTaskUtil.deleteVoicemailSynchronous(context, newCall.voicemailUri);
189         }
190       } else {
191         result.add(newCall);
192       }
193     }
194     return result;
195   }
196 
197   @WorkerThread
filterSpamNumbers(Context context, List<NewCall> newCalls)198   private static List<NewCall> filterSpamNumbers(Context context, List<NewCall> newCalls) {
199     Assert.isWorkerThread();
200     if (!SpamComponent.get(context).spamSettings().isSpamBlockingEnabled()) {
201       return newCalls;
202     }
203 
204     List<NewCall> result = new ArrayList<>();
205     for (NewCall newCall : newCalls) {
206       Logger.get(context).logImpression(DialerImpression.Type.INCOMING_VOICEMAIL_SCREENED);
207       if (SpamComponent.get(context)
208           .spam()
209           .checkSpamStatusSynchronous(newCall.number, newCall.countryIso)) {
210         LogUtil.i(
211             "VisualVoicemailUpdateTask.filterSpamNumbers",
212             "found voicemail from spam number, suppressing notification");
213         Logger.get(context)
214             .logImpression(DialerImpression.Type.INCOMING_VOICEMAIL_AUTO_BLOCKED_AS_SPAM);
215         if (newCall.voicemailUri != null) {
216           // Mark auto blocked voicemail as old so that we don't process it again.
217           VoicemailQueryHandler.markSingleNewVoicemailAsOld(context, newCall.voicemailUri);
218         }
219       } else {
220         result.add(newCall);
221       }
222     }
223     return result;
224   }
225 
226   /** Updates the voicemail notifications displayed. */
scheduleTask(@onNull Context context, @NonNull Runnable callback)227   static void scheduleTask(@NonNull Context context, @NonNull Runnable callback) {
228     Assert.isNotNull(context);
229     Assert.isNotNull(callback);
230     if (!TelecomUtil.isDefaultDialer(context)) {
231       LogUtil.i("VisualVoicemailUpdateTask.scheduleTask", "not default dialer, not running");
232       callback.run();
233       return;
234     }
235 
236     Input input =
237         new Input(
238             context,
239             CallLogNotificationsQueryHelper.getInstance(context),
240             new FilteredNumberAsyncQueryHandler(context));
241     DialerExecutorComponent.get(context)
242         .dialerExecutorFactory()
243         .createNonUiTaskBuilder(new VisualVoicemailUpdateTask())
244         .onSuccess(
245             output -> {
246               LogUtil.i("VisualVoicemailUpdateTask.scheduleTask", "update successful");
247               callback.run();
248             })
249         .onFailure(
250             throwable -> {
251               LogUtil.i("VisualVoicemailUpdateTask.scheduleTask", "update failed: " + throwable);
252               callback.run();
253             })
254         .build()
255         .executeParallel(input);
256   }
257 
258   static class Input {
259     @NonNull final Context context;
260     @NonNull final CallLogNotificationsQueryHelper queryHelper;
261     @NonNull final FilteredNumberAsyncQueryHandler queryHandler;
262 
Input( Context context, CallLogNotificationsQueryHelper queryHelper, FilteredNumberAsyncQueryHandler queryHandler)263     Input(
264         Context context,
265         CallLogNotificationsQueryHelper queryHelper,
266         FilteredNumberAsyncQueryHandler queryHandler) {
267       this.context = context;
268       this.queryHelper = queryHelper;
269       this.queryHandler = queryHandler;
270     }
271   }
272 }
273