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