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