1 /* 2 * Copyright (C) 2017 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.phonelookup.composite; 18 19 import android.content.Context; 20 import android.support.annotation.MainThread; 21 import android.support.annotation.VisibleForTesting; 22 import android.telecom.Call; 23 import com.android.dialer.DialerPhoneNumber; 24 import com.android.dialer.calllog.CallLogState; 25 import com.android.dialer.common.LogUtil; 26 import com.android.dialer.common.concurrent.Annotations.LightweightExecutor; 27 import com.android.dialer.common.concurrent.DialerFutures; 28 import com.android.dialer.inject.ApplicationContext; 29 import com.android.dialer.metrics.FutureTimer; 30 import com.android.dialer.metrics.FutureTimer.LogCatMode; 31 import com.android.dialer.metrics.Metrics; 32 import com.android.dialer.phonelookup.PhoneLookup; 33 import com.android.dialer.phonelookup.PhoneLookupInfo; 34 import com.android.dialer.phonelookup.PhoneLookupInfo.Builder; 35 import com.google.common.base.Preconditions; 36 import com.google.common.collect.ImmutableList; 37 import com.google.common.collect.ImmutableMap; 38 import com.google.common.collect.ImmutableSet; 39 import com.google.common.collect.Maps; 40 import com.google.common.util.concurrent.Futures; 41 import com.google.common.util.concurrent.ListenableFuture; 42 import com.google.common.util.concurrent.ListeningExecutorService; 43 import com.google.common.util.concurrent.MoreExecutors; 44 import java.util.ArrayList; 45 import java.util.List; 46 import java.util.Map; 47 import javax.inject.Inject; 48 49 /** 50 * {@link PhoneLookup} which delegates to a configured set of {@link PhoneLookup PhoneLookups}, 51 * iterating, prioritizing, and coalescing data as necessary. 52 * 53 * <p>TODO(zachh): Consider renaming and moving this file since it does not implement PhoneLookup. 54 */ 55 public final class CompositePhoneLookup { 56 57 private final Context appContext; 58 private final ImmutableList<PhoneLookup> phoneLookups; 59 private final FutureTimer futureTimer; 60 private final CallLogState callLogState; 61 private final ListeningExecutorService lightweightExecutorService; 62 63 @VisibleForTesting 64 @Inject CompositePhoneLookup( @pplicationContext Context appContext, ImmutableList<PhoneLookup> phoneLookups, FutureTimer futureTimer, CallLogState callLogState, @LightweightExecutor ListeningExecutorService lightweightExecutorService)65 public CompositePhoneLookup( 66 @ApplicationContext Context appContext, 67 ImmutableList<PhoneLookup> phoneLookups, 68 FutureTimer futureTimer, 69 CallLogState callLogState, 70 @LightweightExecutor ListeningExecutorService lightweightExecutorService) { 71 this.appContext = appContext; 72 this.phoneLookups = phoneLookups; 73 this.futureTimer = futureTimer; 74 this.callLogState = callLogState; 75 this.lightweightExecutorService = lightweightExecutorService; 76 } 77 78 /** 79 * Delegates to a set of dependent lookups to build a complete {@link PhoneLookupInfo} for the 80 * number associated with the provided call. 81 * 82 * <p>Note: If any of the dependent lookups fails, the returned future will also fail. If any of 83 * the dependent lookups does not complete, the returned future will also not complete. 84 */ lookup(Call call)85 public ListenableFuture<PhoneLookupInfo> lookup(Call call) { 86 // TODO(zachh): Add short-circuiting logic so that this call is not blocked on low-priority 87 // lookups finishing when a higher-priority one has already finished. 88 List<ListenableFuture<?>> futures = new ArrayList<>(); 89 for (PhoneLookup<?> phoneLookup : phoneLookups) { 90 ListenableFuture<?> lookupFuture = phoneLookup.lookup(appContext, call); 91 String eventName = 92 String.format(Metrics.LOOKUP_FOR_CALL_TEMPLATE, phoneLookup.getLoggingName()); 93 futureTimer.applyTiming(lookupFuture, eventName); 94 futures.add(lookupFuture); 95 } 96 ListenableFuture<PhoneLookupInfo> combinedFuture = combineSubMessageFutures(futures); 97 String eventName = String.format(Metrics.LOOKUP_FOR_CALL_TEMPLATE, getLoggingName()); 98 futureTimer.applyTiming(combinedFuture, eventName); 99 return combinedFuture; 100 } 101 102 /** 103 * Delegates to a set of dependent lookups to build a complete {@link PhoneLookupInfo} for the 104 * provided number. 105 * 106 * <p>Note: If any of the dependent lookups fails, the returned future will also fail. If any of 107 * the dependent lookups does not complete, the returned future will also not complete. 108 */ lookup(DialerPhoneNumber dialerPhoneNumber)109 public ListenableFuture<PhoneLookupInfo> lookup(DialerPhoneNumber dialerPhoneNumber) { 110 // TODO(zachh): Add short-circuiting logic so that this call is not blocked on low-priority 111 // lookups finishing when a higher-priority one has already finished. 112 List<ListenableFuture<?>> futures = new ArrayList<>(); 113 for (PhoneLookup<?> phoneLookup : phoneLookups) { 114 ListenableFuture<?> lookupFuture = phoneLookup.lookup(dialerPhoneNumber); 115 String eventName = 116 String.format(Metrics.LOOKUP_FOR_NUMBER_TEMPLATE, phoneLookup.getLoggingName()); 117 futureTimer.applyTiming(lookupFuture, eventName); 118 futures.add(lookupFuture); 119 } 120 ListenableFuture<PhoneLookupInfo> combinedFuture = combineSubMessageFutures(futures); 121 String eventName = String.format(Metrics.LOOKUP_FOR_NUMBER_TEMPLATE, getLoggingName()); 122 futureTimer.applyTiming(combinedFuture, eventName); 123 return combinedFuture; 124 } 125 126 /** Combines a list of sub-message futures into a future for {@link PhoneLookupInfo}. */ 127 @SuppressWarnings({"unchecked", "rawtype"}) combineSubMessageFutures( List<ListenableFuture<?>> subMessageFutures)128 private ListenableFuture<PhoneLookupInfo> combineSubMessageFutures( 129 List<ListenableFuture<?>> subMessageFutures) { 130 return Futures.transform( 131 Futures.allAsList(subMessageFutures), 132 subMessages -> { 133 Preconditions.checkNotNull(subMessages); 134 Builder mergedInfo = PhoneLookupInfo.newBuilder(); 135 for (int i = 0; i < subMessages.size(); i++) { 136 PhoneLookup phoneLookup = phoneLookups.get(i); 137 phoneLookup.setSubMessage(mergedInfo, subMessages.get(i)); 138 } 139 return mergedInfo.build(); 140 }, 141 lightweightExecutorService); 142 } 143 144 /** 145 * Delegates to sub-lookups' {@link PhoneLookup#isDirty(ImmutableSet)} completing when the first 146 * sub-lookup which returns true completes. 147 */ isDirty(ImmutableSet<DialerPhoneNumber> phoneNumbers)148 public ListenableFuture<Boolean> isDirty(ImmutableSet<DialerPhoneNumber> phoneNumbers) { 149 List<ListenableFuture<Boolean>> futures = new ArrayList<>(); 150 for (PhoneLookup<?> phoneLookup : phoneLookups) { 151 ListenableFuture<Boolean> isDirtyFuture = phoneLookup.isDirty(phoneNumbers); 152 futures.add(isDirtyFuture); 153 String eventName = String.format(Metrics.IS_DIRTY_TEMPLATE, phoneLookup.getLoggingName()); 154 futureTimer.applyTiming(isDirtyFuture, eventName, LogCatMode.LOG_VALUES); 155 } 156 // Executes all child lookups (possibly in parallel), completing when the first composite lookup 157 // which returns "true" completes, and cancels the others. 158 ListenableFuture<Boolean> firstMatching = 159 DialerFutures.firstMatching(futures, Preconditions::checkNotNull, false /* defaultValue */); 160 String eventName = String.format(Metrics.IS_DIRTY_TEMPLATE, getLoggingName()); 161 futureTimer.applyTiming(firstMatching, eventName, LogCatMode.LOG_VALUES); 162 return firstMatching; 163 } 164 165 /** 166 * Delegates to a set of dependent lookups and combines results. 167 * 168 * <p>Note: If any of the dependent lookups fails, the returned future will also fail. If any of 169 * the dependent lookups does not complete, the returned future will also not complete. 170 */ 171 @SuppressWarnings("unchecked") getMostRecentInfo( ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap)172 public ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> getMostRecentInfo( 173 ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap) { 174 return Futures.transformAsync( 175 callLogState.isBuilt(), 176 isBuilt -> { 177 Preconditions.checkNotNull(isBuilt); 178 List<ListenableFuture<ImmutableMap<DialerPhoneNumber, ?>>> futures = new ArrayList<>(); 179 for (PhoneLookup phoneLookup : phoneLookups) { 180 futures.add(buildSubmapAndGetMostRecentInfo(existingInfoMap, phoneLookup, isBuilt)); 181 } 182 ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> combinedFuture = 183 Futures.transform( 184 Futures.allAsList(futures), 185 (allMaps) -> { 186 Preconditions.checkNotNull(allMaps); 187 ImmutableMap.Builder<DialerPhoneNumber, PhoneLookupInfo> combinedMap = 188 ImmutableMap.builder(); 189 for (DialerPhoneNumber dialerPhoneNumber : existingInfoMap.keySet()) { 190 PhoneLookupInfo.Builder combinedInfo = PhoneLookupInfo.newBuilder(); 191 for (int i = 0; i < allMaps.size(); i++) { 192 ImmutableMap<DialerPhoneNumber, ?> map = allMaps.get(i); 193 Object subInfo = map.get(dialerPhoneNumber); 194 if (subInfo == null) { 195 throw new IllegalStateException( 196 "A sublookup didn't return an info for number: " 197 + LogUtil.sanitizePhoneNumber( 198 dialerPhoneNumber.getNormalizedNumber())); 199 } 200 phoneLookups.get(i).setSubMessage(combinedInfo, subInfo); 201 } 202 combinedMap.put(dialerPhoneNumber, combinedInfo.build()); 203 } 204 return combinedMap.build(); 205 }, 206 lightweightExecutorService); 207 String eventName = getMostRecentInfoEventName(getLoggingName(), isBuilt); 208 futureTimer.applyTiming(combinedFuture, eventName); 209 return combinedFuture; 210 }, 211 MoreExecutors.directExecutor()); 212 } 213 214 private <T> ListenableFuture<ImmutableMap<DialerPhoneNumber, T>> buildSubmapAndGetMostRecentInfo( 215 ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, 216 PhoneLookup<T> phoneLookup, 217 boolean isBuilt) { 218 Map<DialerPhoneNumber, T> submap = 219 Maps.transformEntries( 220 existingInfoMap, 221 (dialerPhoneNumber, phoneLookupInfo) -> 222 phoneLookup.getSubMessage(existingInfoMap.get(dialerPhoneNumber))); 223 ListenableFuture<ImmutableMap<DialerPhoneNumber, T>> mostRecentInfoFuture = 224 phoneLookup.getMostRecentInfo(ImmutableMap.copyOf(submap)); 225 String eventName = getMostRecentInfoEventName(phoneLookup.getLoggingName(), isBuilt); 226 futureTimer.applyTiming(mostRecentInfoFuture, eventName); 227 return mostRecentInfoFuture; 228 } 229 230 /** Delegates to sub-lookups' {@link PhoneLookup#onSuccessfulBulkUpdate()}. */ 231 public ListenableFuture<Void> onSuccessfulBulkUpdate() { 232 return Futures.transformAsync( 233 callLogState.isBuilt(), 234 isBuilt -> { 235 Preconditions.checkNotNull(isBuilt); 236 List<ListenableFuture<Void>> futures = new ArrayList<>(); 237 for (PhoneLookup<?> phoneLookup : phoneLookups) { 238 ListenableFuture<Void> phoneLookupFuture = phoneLookup.onSuccessfulBulkUpdate(); 239 futures.add(phoneLookupFuture); 240 String eventName = 241 onSuccessfulBulkUpdatedEventName(phoneLookup.getLoggingName(), isBuilt); 242 futureTimer.applyTiming(phoneLookupFuture, eventName); 243 } 244 ListenableFuture<Void> combinedFuture = 245 Futures.transform( 246 Futures.allAsList(futures), unused -> null, lightweightExecutorService); 247 String eventName = onSuccessfulBulkUpdatedEventName(getLoggingName(), isBuilt); 248 futureTimer.applyTiming(combinedFuture, eventName); 249 return combinedFuture; 250 }, 251 MoreExecutors.directExecutor()); 252 } 253 254 /** Delegates to sub-lookups' {@link PhoneLookup#registerContentObservers()}. */ 255 @MainThread 256 public void registerContentObservers() { 257 for (PhoneLookup phoneLookup : phoneLookups) { 258 phoneLookup.registerContentObservers(); 259 } 260 } 261 262 /** Delegates to sub-lookups' {@link PhoneLookup#unregisterContentObservers()}. */ 263 @MainThread 264 public void unregisterContentObservers() { 265 for (PhoneLookup phoneLookup : phoneLookups) { 266 phoneLookup.unregisterContentObservers(); 267 } 268 } 269 270 /** Delegates to sub-lookups' {@link PhoneLookup#clearData()}. */ 271 public ListenableFuture<Void> clearData() { 272 List<ListenableFuture<Void>> futures = new ArrayList<>(); 273 for (PhoneLookup<?> phoneLookup : phoneLookups) { 274 ListenableFuture<Void> phoneLookupFuture = phoneLookup.clearData(); 275 futures.add(phoneLookupFuture); 276 } 277 return Futures.transform( 278 Futures.allAsList(futures), unused -> null, lightweightExecutorService); 279 } 280 281 private static String getMostRecentInfoEventName(String loggingName, boolean isBuilt) { 282 return String.format( 283 !isBuilt 284 ? Metrics.INITIAL_GET_MOST_RECENT_INFO_TEMPLATE 285 : Metrics.GET_MOST_RECENT_INFO_TEMPLATE, 286 loggingName); 287 } 288 289 private static String onSuccessfulBulkUpdatedEventName(String loggingName, boolean isBuilt) { 290 return String.format( 291 !isBuilt 292 ? Metrics.INITIAL_ON_SUCCESSFUL_BULK_UPDATE_TEMPLATE 293 : Metrics.ON_SUCCESSFUL_BULK_UPDATE_TEMPLATE, 294 loggingName); 295 } 296 297 private String getLoggingName() { 298 return "CompositePhoneLookup"; 299 } 300 } 301