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