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; 18 19 import android.content.ContentProviderOperation; 20 import android.content.ContentUris; 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.content.OperationApplicationException; 24 import android.os.RemoteException; 25 import android.provider.CallLog; 26 import android.provider.CallLog.Calls; 27 import android.provider.ContactsContract.CommonDataKinds.Phone; 28 import android.support.annotation.VisibleForTesting; 29 import com.android.dialer.DialerPhoneNumber; 30 import com.android.dialer.NumberAttributes; 31 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog; 32 import com.android.dialer.calllog.datasources.CallLogMutations; 33 import com.android.dialer.common.LogUtil; 34 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; 35 import com.android.dialer.inject.ApplicationContext; 36 import com.android.dialer.protos.ProtoParsers; 37 import com.google.common.util.concurrent.Futures; 38 import com.google.common.util.concurrent.ListenableFuture; 39 import com.google.common.util.concurrent.ListeningExecutorService; 40 import java.util.ArrayList; 41 import java.util.Arrays; 42 import java.util.stream.Stream; 43 import javax.inject.Inject; 44 45 /** 46 * Update {@link Calls#CACHED_NAME} and other cached columns after the annotated call log has been 47 * updated. Dialer does not read these columns but other apps relies on it. 48 */ 49 public final class CallLogCacheUpdater { 50 51 private final Context appContext; 52 private final ListeningExecutorService backgroundExecutor; 53 private final CallLogState callLogState; 54 55 /** 56 * Maximum numbers of operations the updater can do. Each transaction to the system call log will 57 * trigger a call log refresh, so the updater can only do a single batch. If there are more 58 * operations it will be truncated. Under normal circumstances there will only be 1 operation 59 */ 60 @VisibleForTesting static final int CACHE_UPDATE_LIMIT = 100; 61 62 @Inject CallLogCacheUpdater( @pplicationContext Context appContext, @BackgroundExecutor ListeningExecutorService backgroundExecutor, CallLogState callLogState)63 CallLogCacheUpdater( 64 @ApplicationContext Context appContext, 65 @BackgroundExecutor ListeningExecutorService backgroundExecutor, 66 CallLogState callLogState) { 67 this.appContext = appContext; 68 this.backgroundExecutor = backgroundExecutor; 69 this.callLogState = callLogState; 70 } 71 72 /** 73 * Extracts inserts and updates from {@code mutations} to update the 'cached' columns in the 74 * system call log. 75 * 76 * <p>If the cached columns are non-empty, it will only be updated if {@link Calls#CACHED_NAME} 77 * has changed 78 */ updateCache(CallLogMutations mutations)79 public ListenableFuture<Void> updateCache(CallLogMutations mutations) { 80 return Futures.transform( 81 callLogState.isBuilt(), 82 isBuilt -> { 83 if (!isBuilt) { 84 // Initial build might need to update 1000 caches, which may overflow the batch 85 // operation limit. The initial data was already built with the cache, there's no need 86 // to update it. 87 LogUtil.i("CallLogCacheUpdater.updateCache", "not updating cache for initial build"); 88 return null; 89 } 90 updateCacheInternal(mutations); 91 return null; 92 }, 93 backgroundExecutor); 94 } 95 96 private void updateCacheInternal(CallLogMutations mutations) { 97 ArrayList<ContentProviderOperation> operations = new ArrayList<>(); 98 Stream.concat( 99 mutations.getInserts().entrySet().stream(), mutations.getUpdates().entrySet().stream()) 100 .limit(CACHE_UPDATE_LIMIT) 101 .forEach( 102 entry -> { 103 ContentValues values = entry.getValue(); 104 if (!values.containsKey(AnnotatedCallLog.NUMBER_ATTRIBUTES) 105 || !values.containsKey(AnnotatedCallLog.NUMBER)) { 106 return; 107 } 108 DialerPhoneNumber dialerPhoneNumber = 109 ProtoParsers.getTrusted( 110 values, AnnotatedCallLog.NUMBER, DialerPhoneNumber.getDefaultInstance()); 111 NumberAttributes numberAttributes = 112 ProtoParsers.getTrusted( 113 values, 114 AnnotatedCallLog.NUMBER_ATTRIBUTES, 115 NumberAttributes.getDefaultInstance()); 116 operations.add( 117 ContentProviderOperation.newUpdate( 118 ContentUris.withAppendedId(Calls.CONTENT_URI, entry.getKey())) 119 .withValue( 120 Calls.CACHED_FORMATTED_NUMBER, 121 values.getAsString(AnnotatedCallLog.FORMATTED_NUMBER)) 122 .withValue(Calls.CACHED_LOOKUP_URI, numberAttributes.getLookupUri()) 123 // Calls.CACHED_MATCHED_NUMBER is not available. 124 .withValue(Calls.CACHED_NAME, numberAttributes.getName()) 125 .withValue( 126 Calls.CACHED_NORMALIZED_NUMBER, dialerPhoneNumber.getNormalizedNumber()) 127 .withValue(Calls.CACHED_NUMBER_LABEL, numberAttributes.getNumberTypeLabel()) 128 // NUMBER_TYPE is lost in NumberAttributes when it is converted to a string 129 // label, Use TYPE_CUSTOM so the label will be displayed. 130 .withValue(Calls.CACHED_NUMBER_TYPE, Phone.TYPE_CUSTOM) 131 .withValue(Calls.CACHED_PHOTO_ID, numberAttributes.getPhotoId()) 132 .withValue(Calls.CACHED_PHOTO_URI, numberAttributes.getPhotoUri()) 133 // Avoid writing to the call log for insignificant changes to avoid triggering 134 // other content observers such as the voicemail client. 135 .withSelection( 136 Calls.CACHED_NAME + " IS NOT ?", 137 new String[] {numberAttributes.getName()}) 138 .build()); 139 }); 140 try { 141 int count = 142 Arrays.stream(appContext.getContentResolver().applyBatch(CallLog.AUTHORITY, operations)) 143 .mapToInt(result -> result.count) 144 .sum(); 145 LogUtil.i("CallLogCacheUpdater.updateCache", "updated %d rows", count); 146 } catch (OperationApplicationException | RemoteException e) { 147 throw new IllegalStateException(e); 148 } 149 } 150 } 151