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