/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.dialer.phonelookup.composite; import android.content.Context; import android.support.annotation.MainThread; import android.support.annotation.VisibleForTesting; import android.telecom.Call; import com.android.dialer.DialerPhoneNumber; import com.android.dialer.calllog.CallLogState; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.Annotations.LightweightExecutor; import com.android.dialer.common.concurrent.DialerFutures; import com.android.dialer.inject.ApplicationContext; import com.android.dialer.metrics.FutureTimer; import com.android.dialer.metrics.FutureTimer.LogCatMode; import com.android.dialer.metrics.Metrics; import com.android.dialer.phonelookup.PhoneLookup; import com.android.dialer.phonelookup.PhoneLookupInfo; import com.android.dialer.phonelookup.PhoneLookupInfo.Builder; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.inject.Inject; /** * {@link PhoneLookup} which delegates to a configured set of {@link PhoneLookup PhoneLookups}, * iterating, prioritizing, and coalescing data as necessary. * *

TODO(zachh): Consider renaming and moving this file since it does not implement PhoneLookup. */ public final class CompositePhoneLookup { private final Context appContext; private final ImmutableList phoneLookups; private final FutureTimer futureTimer; private final CallLogState callLogState; private final ListeningExecutorService lightweightExecutorService; @VisibleForTesting @Inject public CompositePhoneLookup( @ApplicationContext Context appContext, ImmutableList phoneLookups, FutureTimer futureTimer, CallLogState callLogState, @LightweightExecutor ListeningExecutorService lightweightExecutorService) { this.appContext = appContext; this.phoneLookups = phoneLookups; this.futureTimer = futureTimer; this.callLogState = callLogState; this.lightweightExecutorService = lightweightExecutorService; } /** * Delegates to a set of dependent lookups to build a complete {@link PhoneLookupInfo} for the * number associated with the provided call. * *

Note: If any of the dependent lookups fails, the returned future will also fail. If any of * the dependent lookups does not complete, the returned future will also not complete. */ public ListenableFuture lookup(Call call) { // TODO(zachh): Add short-circuiting logic so that this call is not blocked on low-priority // lookups finishing when a higher-priority one has already finished. List> futures = new ArrayList<>(); for (PhoneLookup phoneLookup : phoneLookups) { ListenableFuture lookupFuture = phoneLookup.lookup(appContext, call); String eventName = String.format(Metrics.LOOKUP_FOR_CALL_TEMPLATE, phoneLookup.getClass().getSimpleName()); futureTimer.applyTiming(lookupFuture, eventName); futures.add(lookupFuture); } ListenableFuture combinedFuture = combineSubMessageFutures(futures); String eventName = String.format(Metrics.LOOKUP_FOR_CALL_TEMPLATE, CompositePhoneLookup.class.getSimpleName()); futureTimer.applyTiming(combinedFuture, eventName); return combinedFuture; } /** * Delegates to a set of dependent lookups to build a complete {@link PhoneLookupInfo} for the * provided number. * *

Note: If any of the dependent lookups fails, the returned future will also fail. If any of * the dependent lookups does not complete, the returned future will also not complete. */ public ListenableFuture lookup(DialerPhoneNumber dialerPhoneNumber) { // TODO(zachh): Add short-circuiting logic so that this call is not blocked on low-priority // lookups finishing when a higher-priority one has already finished. List> futures = new ArrayList<>(); for (PhoneLookup phoneLookup : phoneLookups) { ListenableFuture lookupFuture = phoneLookup.lookup(dialerPhoneNumber); String eventName = String.format(Metrics.LOOKUP_FOR_NUMBER_TEMPLATE, phoneLookup.getClass().getSimpleName()); futureTimer.applyTiming(lookupFuture, eventName); futures.add(lookupFuture); } ListenableFuture combinedFuture = combineSubMessageFutures(futures); String eventName = String.format( Metrics.LOOKUP_FOR_NUMBER_TEMPLATE, CompositePhoneLookup.class.getSimpleName()); futureTimer.applyTiming(combinedFuture, eventName); return combinedFuture; } /** Combines a list of sub-message futures into a future for {@link PhoneLookupInfo}. */ @SuppressWarnings({"unchecked", "rawtype"}) private ListenableFuture combineSubMessageFutures( List> subMessageFutures) { return Futures.transform( Futures.allAsList(subMessageFutures), subMessages -> { Builder mergedInfo = PhoneLookupInfo.newBuilder(); for (int i = 0; i < subMessages.size(); i++) { PhoneLookup phoneLookup = phoneLookups.get(i); phoneLookup.setSubMessage(mergedInfo, subMessages.get(i)); } return mergedInfo.build(); }, lightweightExecutorService); } /** * Delegates to sub-lookups' {@link PhoneLookup#isDirty(ImmutableSet)} completing when the first * sub-lookup which returns true completes. */ public ListenableFuture isDirty(ImmutableSet phoneNumbers) { List> futures = new ArrayList<>(); for (PhoneLookup phoneLookup : phoneLookups) { ListenableFuture isDirtyFuture = phoneLookup.isDirty(phoneNumbers); futures.add(isDirtyFuture); String eventName = String.format(Metrics.IS_DIRTY_TEMPLATE, phoneLookup.getClass().getSimpleName()); futureTimer.applyTiming(isDirtyFuture, eventName, LogCatMode.LOG_VALUES); } // Executes all child lookups (possibly in parallel), completing when the first composite lookup // which returns "true" completes, and cancels the others. ListenableFuture firstMatching = DialerFutures.firstMatching(futures, Preconditions::checkNotNull, false /* defaultValue */); String eventName = String.format(Metrics.IS_DIRTY_TEMPLATE, CompositePhoneLookup.class.getSimpleName()); futureTimer.applyTiming(firstMatching, eventName, LogCatMode.LOG_VALUES); return firstMatching; } /** * Delegates to a set of dependent lookups and combines results. * *

Note: If any of the dependent lookups fails, the returned future will also fail. If any of * the dependent lookups does not complete, the returned future will also not complete. */ @SuppressWarnings("unchecked") public ListenableFuture> getMostRecentInfo( ImmutableMap existingInfoMap) { return Futures.transformAsync( callLogState.isBuilt(), isBuilt -> { List>> futures = new ArrayList<>(); for (PhoneLookup phoneLookup : phoneLookups) { futures.add(buildSubmapAndGetMostRecentInfo(existingInfoMap, phoneLookup, isBuilt)); } ListenableFuture> combinedFuture = Futures.transform( Futures.allAsList(futures), (allMaps) -> { ImmutableMap.Builder combinedMap = ImmutableMap.builder(); for (DialerPhoneNumber dialerPhoneNumber : existingInfoMap.keySet()) { PhoneLookupInfo.Builder combinedInfo = PhoneLookupInfo.newBuilder(); for (int i = 0; i < allMaps.size(); i++) { ImmutableMap map = allMaps.get(i); Object subInfo = map.get(dialerPhoneNumber); if (subInfo == null) { throw new IllegalStateException( "A sublookup didn't return an info for number: " + LogUtil.sanitizePhoneNumber( dialerPhoneNumber.getNormalizedNumber())); } phoneLookups.get(i).setSubMessage(combinedInfo, subInfo); } combinedMap.put(dialerPhoneNumber, combinedInfo.build()); } return combinedMap.build(); }, lightweightExecutorService); String eventName = getMostRecentInfoEventName(this, isBuilt); futureTimer.applyTiming(combinedFuture, eventName); return combinedFuture; }, MoreExecutors.directExecutor()); } private ListenableFuture> buildSubmapAndGetMostRecentInfo( ImmutableMap existingInfoMap, PhoneLookup phoneLookup, boolean isBuilt) { Map submap = Maps.transformEntries( existingInfoMap, (dialerPhoneNumber, phoneLookupInfo) -> phoneLookup.getSubMessage(existingInfoMap.get(dialerPhoneNumber))); ListenableFuture> mostRecentInfoFuture = phoneLookup.getMostRecentInfo(ImmutableMap.copyOf(submap)); String eventName = getMostRecentInfoEventName(phoneLookup, isBuilt); futureTimer.applyTiming(mostRecentInfoFuture, eventName); return mostRecentInfoFuture; } /** Delegates to sub-lookups' {@link PhoneLookup#onSuccessfulBulkUpdate()}. */ public ListenableFuture onSuccessfulBulkUpdate() { return Futures.transformAsync( callLogState.isBuilt(), isBuilt -> { List> futures = new ArrayList<>(); for (PhoneLookup phoneLookup : phoneLookups) { ListenableFuture phoneLookupFuture = phoneLookup.onSuccessfulBulkUpdate(); futures.add(phoneLookupFuture); String eventName = onSuccessfulBulkUpdatedEventName(phoneLookup, isBuilt); futureTimer.applyTiming(phoneLookupFuture, eventName); } ListenableFuture combinedFuture = Futures.transform( Futures.allAsList(futures), unused -> null, lightweightExecutorService); String eventName = onSuccessfulBulkUpdatedEventName(this, isBuilt); futureTimer.applyTiming(combinedFuture, eventName); return combinedFuture; }, MoreExecutors.directExecutor()); } /** Delegates to sub-lookups' {@link PhoneLookup#registerContentObservers()}. */ @MainThread public void registerContentObservers() { for (PhoneLookup phoneLookup : phoneLookups) { phoneLookup.registerContentObservers(); } } /** Delegates to sub-lookups' {@link PhoneLookup#unregisterContentObservers()}. */ @MainThread public void unregisterContentObservers() { for (PhoneLookup phoneLookup : phoneLookups) { phoneLookup.unregisterContentObservers(); } } /** Delegates to sub-lookups' {@link PhoneLookup#clearData()}. */ public ListenableFuture clearData() { List> futures = new ArrayList<>(); for (PhoneLookup phoneLookup : phoneLookups) { ListenableFuture phoneLookupFuture = phoneLookup.clearData(); futures.add(phoneLookupFuture); } return Futures.transform( Futures.allAsList(futures), unused -> null, lightweightExecutorService); } private static String getMostRecentInfoEventName(Object classNameSource, boolean isBuilt) { return String.format( !isBuilt ? Metrics.INITIAL_GET_MOST_RECENT_INFO_TEMPLATE : Metrics.GET_MOST_RECENT_INFO_TEMPLATE, classNameSource.getClass().getSimpleName()); } private static String onSuccessfulBulkUpdatedEventName(Object classNameSource, boolean isBuilt) { return String.format( !isBuilt ? Metrics.INITIAL_ON_SUCCESSFUL_BULK_UPDATE_TEMPLATE : Metrics.ON_SUCCESSFUL_BULK_UPDATE_TEMPLATE, classNameSource.getClass().getSimpleName()); } }