Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2018 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.calllog.ui;
     18 
     19 import android.content.ContentProviderOperation;
     20 import android.content.ContentValues;
     21 import android.content.Context;
     22 import android.support.annotation.MainThread;
     23 import android.support.annotation.VisibleForTesting;
     24 import android.util.ArrayMap;
     25 import com.android.dialer.DialerPhoneNumber;
     26 import com.android.dialer.calllog.model.CoalescedRow;
     27 import com.android.dialer.calllogutils.NumberAttributesConverter;
     28 import com.android.dialer.common.Assert;
     29 import com.android.dialer.common.LogUtil;
     30 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
     31 import com.android.dialer.common.concurrent.Annotations.Ui;
     32 import com.android.dialer.common.concurrent.ThreadUtil;
     33 import com.android.dialer.inject.ApplicationContext;
     34 import com.android.dialer.phonelookup.PhoneLookupInfo;
     35 import com.android.dialer.phonelookup.composite.CompositePhoneLookup;
     36 import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract;
     37 import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory;
     38 import com.google.common.collect.ImmutableMap;
     39 import com.google.common.util.concurrent.FutureCallback;
     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 java.util.ArrayList;
     44 import java.util.LinkedHashMap;
     45 import java.util.Map;
     46 import java.util.Map.Entry;
     47 import java.util.concurrent.TimeUnit;
     48 import javax.inject.Inject;
     49 
     50 /**
     51  * Does work necessary to update a {@link CoalescedRow} when it is requested to be displayed.
     52  *
     53  * <p>In most cases this is a no-op as most AnnotatedCallLog rows can be displayed immediately
     54  * as-is. However, there are certain times that a row from the AnnotatedCallLog cannot be displayed
     55  * without further work being performed.
     56  *
     57  * <p>For example, when there are many invalid numbers in the call log, we cannot efficiently update
     58  * the CP2 information for all of them at once, and so information for those rows must be retrieved
     59  * at display time.
     60  *
     61  * <p>This class also updates {@link PhoneLookupHistory} with the results that it fetches.
     62  */
     63 public final class RealtimeRowProcessor {
     64 
     65   /*
     66    * The time to wait between writing batches of records to PhoneLookupHistory.
     67    */
     68   @VisibleForTesting static final long BATCH_WAIT_MILLIS = TimeUnit.SECONDS.toMillis(3);
     69 
     70   private final Context appContext;
     71   private final CompositePhoneLookup compositePhoneLookup;
     72   private final ListeningExecutorService uiExecutor;
     73   private final ListeningExecutorService backgroundExecutor;
     74 
     75   private final Map<DialerPhoneNumber, PhoneLookupInfo> cache = new ArrayMap<>();
     76 
     77   private final Map<DialerPhoneNumber, PhoneLookupInfo> queuedPhoneLookupHistoryWrites =
     78       new LinkedHashMap<>(); // Keep the order so the most recent looked up value always wins
     79   private final Runnable writePhoneLookupHistoryRunnable = this::writePhoneLookupHistory;
     80 
     81   @Inject
     82   RealtimeRowProcessor(
     83       @ApplicationContext Context appContext,
     84       @Ui ListeningExecutorService uiExecutor,
     85       @BackgroundExecutor ListeningExecutorService backgroundExecutor,
     86       CompositePhoneLookup compositePhoneLookup) {
     87     this.appContext = appContext;
     88     this.uiExecutor = uiExecutor;
     89     this.backgroundExecutor = backgroundExecutor;
     90     this.compositePhoneLookup = compositePhoneLookup;
     91   }
     92 
     93   /**
     94    * Converts a {@link CoalescedRow} to a future which is the result of performing additional work
     95    * on the row. May simply return the original row if no modifications were necessary.
     96    */
     97   @MainThread
     98   ListenableFuture<CoalescedRow> applyRealtimeProcessing(final CoalescedRow row) {
     99     // Cp2DefaultDirectoryPhoneLookup can not always efficiently process all rows.
    100     if (!row.numberAttributes().getIsCp2InfoIncomplete()) {
    101       return Futures.immediateFuture(row);
    102     }
    103 
    104     PhoneLookupInfo cachedPhoneLookupInfo = cache.get(row.number());
    105     if (cachedPhoneLookupInfo != null) {
    106       return Futures.immediateFuture(applyPhoneLookupInfoToRow(cachedPhoneLookupInfo, row));
    107     }
    108 
    109     ListenableFuture<PhoneLookupInfo> phoneLookupInfoFuture =
    110         compositePhoneLookup.lookup(row.number());
    111     return Futures.transform(
    112         phoneLookupInfoFuture,
    113         phoneLookupInfo -> {
    114           queuePhoneLookupHistoryWrite(row.number(), phoneLookupInfo);
    115           cache.put(row.number(), phoneLookupInfo);
    116           return applyPhoneLookupInfoToRow(phoneLookupInfo, row);
    117         },
    118         uiExecutor /* ensures the cache is updated on a single thread */);
    119   }
    120 
    121   /** Clears the internal cache. */
    122   @MainThread
    123   public void clearCache() {
    124     Assert.isMainThread();
    125     cache.clear();
    126   }
    127 
    128   @MainThread
    129   private void queuePhoneLookupHistoryWrite(
    130       DialerPhoneNumber dialerPhoneNumber, PhoneLookupInfo phoneLookupInfo) {
    131     Assert.isMainThread();
    132     queuedPhoneLookupHistoryWrites.put(dialerPhoneNumber, phoneLookupInfo);
    133     ThreadUtil.getUiThreadHandler().removeCallbacks(writePhoneLookupHistoryRunnable);
    134     ThreadUtil.getUiThreadHandler().postDelayed(writePhoneLookupHistoryRunnable, BATCH_WAIT_MILLIS);
    135   }
    136 
    137   @MainThread
    138   private void writePhoneLookupHistory() {
    139     Assert.isMainThread();
    140 
    141     // Copy the batch to a new collection that be safely processed on a background thread.
    142     ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> currentBatch =
    143         ImmutableMap.copyOf(queuedPhoneLookupHistoryWrites);
    144 
    145     // Clear the queue, handing responsibility for its items to the background task.
    146     queuedPhoneLookupHistoryWrites.clear();
    147 
    148     // Returns the number of rows updated.
    149     ListenableFuture<Integer> applyBatchFuture =
    150         backgroundExecutor.submit(
    151             () -> {
    152               ArrayList<ContentProviderOperation> operations = new ArrayList<>();
    153               long currentTimestamp = System.currentTimeMillis();
    154               for (Entry<DialerPhoneNumber, PhoneLookupInfo> entry : currentBatch.entrySet()) {
    155                 DialerPhoneNumber dialerPhoneNumber = entry.getKey();
    156                 PhoneLookupInfo phoneLookupInfo = entry.getValue();
    157 
    158                 // Note: Multiple DialerPhoneNumbers can map to the same normalized number but we
    159                 // just write them all and the value for the last one will arbitrarily win.
    160                 // Note: This loses country info when number is not valid.
    161                 String normalizedNumber = dialerPhoneNumber.getNormalizedNumber();
    162 
    163                 ContentValues contentValues = new ContentValues();
    164                 contentValues.put(
    165                     PhoneLookupHistory.PHONE_LOOKUP_INFO, phoneLookupInfo.toByteArray());
    166                 contentValues.put(PhoneLookupHistory.LAST_MODIFIED, currentTimestamp);
    167                 operations.add(
    168                     ContentProviderOperation.newUpdate(
    169                             PhoneLookupHistory.contentUriForNumber(normalizedNumber))
    170                         .withValues(contentValues)
    171                         .build());
    172               }
    173               return Assert.isNotNull(
    174                       appContext
    175                           .getContentResolver()
    176                           .applyBatch(PhoneLookupHistoryContract.AUTHORITY, operations))
    177                   .length;
    178             });
    179 
    180     Futures.addCallback(
    181         applyBatchFuture,
    182         new FutureCallback<Integer>() {
    183           @Override
    184           public void onSuccess(Integer rowsAffected) {
    185             LogUtil.i(
    186                 "RealtimeRowProcessor.onSuccess",
    187                 "wrote %d rows to PhoneLookupHistory",
    188                 rowsAffected);
    189           }
    190 
    191           @Override
    192           public void onFailure(Throwable throwable) {
    193             throw new RuntimeException(throwable);
    194           }
    195         },
    196         uiExecutor);
    197   }
    198 
    199   private CoalescedRow applyPhoneLookupInfoToRow(
    200       PhoneLookupInfo phoneLookupInfo, CoalescedRow row) {
    201     return row.toBuilder()
    202         .setNumberAttributes(NumberAttributesConverter.fromPhoneLookupInfo(phoneLookupInfo).build())
    203         .build();
    204   }
    205 }
    206