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