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.NumberAttributesBuilder; 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 RealtimeRowProcessor( @pplicationContext Context appContext, @Ui ListeningExecutorService uiExecutor, @BackgroundExecutor ListeningExecutorService backgroundExecutor, CompositePhoneLookup compositePhoneLookup)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 applyRealtimeProcessing(final CoalescedRow row)98 ListenableFuture<CoalescedRow> applyRealtimeProcessing(final CoalescedRow row) { 99 // Cp2DefaultDirectoryPhoneLookup can not always efficiently process all rows. 100 if (!row.getNumberAttributes().getIsCp2InfoIncomplete()) { 101 return Futures.immediateFuture(row); 102 } 103 104 PhoneLookupInfo cachedPhoneLookupInfo = cache.get(row.getNumber()); 105 if (cachedPhoneLookupInfo != null) { 106 return Futures.immediateFuture(applyPhoneLookupInfoToRow(cachedPhoneLookupInfo, row)); 107 } 108 109 ListenableFuture<PhoneLookupInfo> phoneLookupInfoFuture = 110 compositePhoneLookup.lookup(row.getNumber()); 111 return Futures.transform( 112 phoneLookupInfoFuture, 113 phoneLookupInfo -> { 114 queuePhoneLookupHistoryWrite(row.getNumber(), phoneLookupInfo); 115 cache.put(row.getNumber(), 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 clearCache()123 public void clearCache() { 124 Assert.isMainThread(); 125 cache.clear(); 126 } 127 128 @MainThread queuePhoneLookupHistoryWrite( DialerPhoneNumber dialerPhoneNumber, PhoneLookupInfo phoneLookupInfo)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 writePhoneLookupHistory()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 applyPhoneLookupInfoToRow( PhoneLookupInfo phoneLookupInfo, CoalescedRow row)199 private CoalescedRow applyPhoneLookupInfoToRow( 200 PhoneLookupInfo phoneLookupInfo, CoalescedRow row) { 201 // Force the "cp2_info_incomplete" value to the original value so that it is not used when 202 // comparing the original row to the updated row. 203 // TODO(linyuh): Improve the comparison instead. 204 return row.toBuilder() 205 .setNumberAttributes( 206 NumberAttributesBuilder.fromPhoneLookupInfo(phoneLookupInfo) 207 .setIsCp2InfoIncomplete(row.getNumberAttributes().getIsCp2InfoIncomplete()) 208 .build()) 209 .build(); 210 } 211 } 212