1 /*
2  * Copyright (C) 2017 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 package com.android.dialer.calllog.database;
17 
18 import android.content.ContentProviderOperation;
19 import android.content.ContentUris;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.content.OperationApplicationException;
23 import android.os.RemoteException;
24 import android.support.annotation.WorkerThread;
25 import android.text.TextUtils;
26 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract;
27 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
28 import com.android.dialer.calllog.datasources.CallLogMutations;
29 import com.android.dialer.common.Assert;
30 import com.android.dialer.common.LogUtil;
31 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
32 import com.google.common.collect.Iterables;
33 import com.google.common.util.concurrent.Futures;
34 import com.google.common.util.concurrent.ListenableFuture;
35 import com.google.common.util.concurrent.ListeningExecutorService;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.List;
39 import java.util.Map.Entry;
40 import javax.inject.Inject;
41 
42 /** Applies {@link CallLogMutations} to the annotated call log. */
43 public class MutationApplier {
44 
45   private final ListeningExecutorService backgroundExecutorService;
46 
47   @Inject
MutationApplier(@ackgroundExecutor ListeningExecutorService backgroundExecutorService)48   public MutationApplier(@BackgroundExecutor ListeningExecutorService backgroundExecutorService) {
49     this.backgroundExecutorService = backgroundExecutorService;
50   }
51 
52   /** Applies the provided {@link CallLogMutations} to the annotated call log. */
applyToDatabase(CallLogMutations mutations, Context appContext)53   public ListenableFuture<Void> applyToDatabase(CallLogMutations mutations, Context appContext) {
54     if (mutations.isEmpty()) {
55       return Futures.immediateFuture(null);
56     }
57     return backgroundExecutorService.submit(
58         () -> {
59           applyToDatabaseInternal(mutations, appContext);
60           return null;
61         });
62   }
63 
64   @WorkerThread
applyToDatabaseInternal(CallLogMutations mutations, Context appContext)65   private void applyToDatabaseInternal(CallLogMutations mutations, Context appContext)
66       throws RemoteException, OperationApplicationException {
67     Assert.isWorkerThread();
68 
69     ArrayList<ContentProviderOperation> operations = new ArrayList<>();
70 
71     if (!mutations.getInserts().isEmpty()) {
72       LogUtil.i(
73           "MutationApplier.applyToDatabase", "inserting %d rows", mutations.getInserts().size());
74       for (Entry<Long, ContentValues> entry : mutations.getInserts().entrySet()) {
75         long id = entry.getKey();
76         ContentValues contentValues = entry.getValue();
77         operations.add(
78             ContentProviderOperation.newInsert(
79                     ContentUris.withAppendedId(AnnotatedCallLog.CONTENT_URI, id))
80                 .withValues(contentValues)
81                 .build());
82       }
83     }
84 
85     if (!mutations.getUpdates().isEmpty()) {
86       LogUtil.i(
87           "MutationApplier.applyToDatabase", "updating %d rows", mutations.getUpdates().size());
88       for (Entry<Long, ContentValues> entry : mutations.getUpdates().entrySet()) {
89         long id = entry.getKey();
90         ContentValues contentValues = entry.getValue();
91         operations.add(
92             ContentProviderOperation.newUpdate(
93                     ContentUris.withAppendedId(AnnotatedCallLog.CONTENT_URI, id))
94                 .withValues(contentValues)
95                 .build());
96       }
97     }
98 
99     if (!mutations.getDeletes().isEmpty()) {
100       LogUtil.i(
101           "MutationApplier.applyToDatabase", "deleting %d rows", mutations.getDeletes().size());
102 
103       // Batch the deletes into chunks of 999, the maximum size for SQLite selection args.
104       Iterable<List<Long>> batches = Iterables.partition(mutations.getDeletes(), 999);
105       for (List<Long> idsInBatch : batches) {
106         String[] questionMarks = new String[idsInBatch.size()];
107         Arrays.fill(questionMarks, "?");
108 
109         String whereClause =
110             (AnnotatedCallLog._ID + " in (") + TextUtils.join(",", questionMarks) + ")";
111 
112         String[] whereArgs = new String[idsInBatch.size()];
113         int i = 0;
114         for (long id : idsInBatch) {
115           whereArgs[i++] = String.valueOf(id);
116         }
117 
118         operations.add(
119             ContentProviderOperation.newDelete(AnnotatedCallLog.CONTENT_URI)
120                 .withSelection(whereClause, whereArgs)
121                 .build());
122       }
123     }
124 
125     appContext.getContentResolver().applyBatch(AnnotatedCallLogContract.AUTHORITY, operations);
126   }
127 }
128