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.annotation.SuppressLint;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.net.Uri;
23 import android.provider.CallLog.Calls;
24 import android.support.v4.os.UserManagerCompat;
25 import com.android.dialer.common.LogUtil;
26 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
27 import com.android.dialer.common.concurrent.Annotations.Ui;
28 import com.android.dialer.common.database.Selection;
29 import com.android.dialer.inject.ApplicationContext;
30 import com.android.dialer.notification.missedcalls.MissedCallNotificationCanceller;
31 import com.android.dialer.util.PermissionsUtil;
32 import com.google.common.collect.ImmutableSet;
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 com.google.common.util.concurrent.MoreExecutors;
37 import java.util.Collection;
38 import javax.inject.Inject;
39 
40 /**
41  * Clears missed calls. This includes cancelling notifications and updating the "IS_READ" status in
42  * the system call log.
43  */
44 public final class ClearMissedCalls {
45 
46   private final Context appContext;
47   private final ListeningExecutorService backgroundExecutor;
48   private final ListeningExecutorService uiThreadExecutor;
49 
50   @Inject
ClearMissedCalls( @pplicationContext Context appContext, @BackgroundExecutor ListeningExecutorService backgroundExecutor, @Ui ListeningExecutorService uiThreadExecutor)51   ClearMissedCalls(
52       @ApplicationContext Context appContext,
53       @BackgroundExecutor ListeningExecutorService backgroundExecutor,
54       @Ui ListeningExecutorService uiThreadExecutor) {
55     this.appContext = appContext;
56     this.backgroundExecutor = backgroundExecutor;
57     this.uiThreadExecutor = uiThreadExecutor;
58   }
59 
60   /**
61    * Cancels all missed call notifications and marks all "unread" missed calls in the system call
62    * log as "read".
63    */
clearAll()64   public ListenableFuture<Void> clearAll() {
65     ListenableFuture<Void> markReadFuture = markRead(ImmutableSet.of());
66     ListenableFuture<Void> cancelNotificationsFuture =
67         uiThreadExecutor.submit(
68             () -> {
69               MissedCallNotificationCanceller.cancelAll(appContext);
70               return null;
71             });
72 
73     // Note on this usage of whenAllComplete:
74     //   -The returned future completes when all sub-futures complete (whether they fail or not)
75     //   -The returned future fails if any sub-future fails
76     return Futures.whenAllComplete(markReadFuture, cancelNotificationsFuture)
77         .call(
78             () -> {
79               // Calling get() is necessary to propagate failures.
80               markReadFuture.get();
81               cancelNotificationsFuture.get();
82               return null;
83             },
84             MoreExecutors.directExecutor());
85   }
86 
87   /**
88    * For the provided set of IDs from the system call log, cancels their missed call notifications
89    * and marks them "read".
90    *
91    * @param ids IDs from the system call log (see {@link Calls#_ID}}.
92    */
clearBySystemCallLogId(Collection<Long> ids)93   public ListenableFuture<Void> clearBySystemCallLogId(Collection<Long> ids) {
94     ListenableFuture<Void> markReadFuture = markRead(ids);
95     ListenableFuture<Void> cancelNotificationsFuture =
96         uiThreadExecutor.submit(
97             () -> {
98               for (long id : ids) {
99                 Uri callUri = Calls.CONTENT_URI.buildUpon().appendPath(Long.toString(id)).build();
100                 MissedCallNotificationCanceller.cancelSingle(appContext, callUri);
101               }
102               return null;
103             });
104 
105     // Note on this usage of whenAllComplete:
106     //   -The returned future completes when all sub-futures complete (whether they fail or not)
107     //   -The returned future fails if any sub-future fails
108     return Futures.whenAllComplete(markReadFuture, cancelNotificationsFuture)
109         .call(
110             () -> {
111               // Calling get() is necessary to propagate failures.
112               markReadFuture.get();
113               cancelNotificationsFuture.get();
114               return null;
115             },
116             MoreExecutors.directExecutor());
117   }
118 
119   /**
120    * Marks all provided system call log IDs as read, or if the provided collection is empty, marks
121    * all calls as read.
122    */
123   @SuppressLint("MissingPermission")
124   private ListenableFuture<Void> markRead(Collection<Long> ids) {
125     return backgroundExecutor.submit(
126         () -> {
127           if (!UserManagerCompat.isUserUnlocked(appContext)) {
128             LogUtil.e("ClearMissedCalls.markRead", "locked");
129             return null;
130           }
131           if (!PermissionsUtil.hasCallLogWritePermissions(appContext)) {
132             LogUtil.e("ClearMissedCalls.markRead", "no permission");
133             return null;
134           }
135 
136           ContentValues values = new ContentValues();
137           values.put(Calls.IS_READ, 1);
138 
139           Selection.Builder selectionBuilder =
140               Selection.builder()
141                   .and(
142                       Selection.column(Calls.IS_READ)
143                           .is("=", 0)
144                           .buildUpon()
145                           .or(Selection.column(Calls.IS_READ).is("IS NULL"))
146                           .build())
147                   .and(Selection.column(Calls.TYPE).is("=", Calls.MISSED_TYPE));
148           if (!ids.isEmpty()) {
149             selectionBuilder.and(Selection.column(Calls._ID).in(toStrings(ids)));
150           }
151           Selection selection = selectionBuilder.build();
152           appContext
153               .getContentResolver()
154               .update(
155                   Calls.CONTENT_URI,
156                   values,
157                   selection.getSelection(),
158                   selection.getSelectionArgs());
159           return null;
160         });
161   }
162 
163   private static String[] toStrings(Collection<Long> longs) {
164     String[] strings = new String[longs.size()];
165     int i = 0;
166     for (long value : longs) {
167       strings[i++] = Long.toString(value);
168     }
169     return strings;
170   }
171 }
172