• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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