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.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.IntentFilter;
23 import android.support.annotation.Nullable;
24 import com.android.dialer.calllog.RefreshAnnotatedCallLogWorker.RefreshResult;
25 import com.android.dialer.calllog.constants.IntentNames;
26 import com.android.dialer.common.LogUtil;
27 import com.android.dialer.common.concurrent.ThreadUtil;
28 import com.android.dialer.logging.DialerImpression;
29 import com.android.dialer.logging.Logger;
30 import com.android.dialer.logging.LoggingBindings;
31 import com.android.dialer.metrics.FutureTimer;
32 import com.android.dialer.metrics.Metrics;
33 import com.android.dialer.metrics.MetricsComponent;
34 import com.google.common.base.Function;
35 import com.google.common.util.concurrent.FutureCallback;
36 import com.google.common.util.concurrent.Futures;
37 import com.google.common.util.concurrent.ListenableFuture;
38 import com.google.common.util.concurrent.MoreExecutors;
39 
40 /**
41  * A {@link BroadcastReceiver} that starts/cancels refreshing the annotated call log when notified.
42  */
43 public final class RefreshAnnotatedCallLogReceiver extends BroadcastReceiver {
44 
45   /**
46    * This is a reasonable time that it might take between related call log writes, that also
47    * shouldn't slow down single-writes too much. For example, when populating the database using the
48    * simulator, using this value results in ~6 refresh cycles (on a release build) to write 120 call
49    * log entries.
50    */
51   private static final long REFRESH_ANNOTATED_CALL_LOG_WAIT_MILLIS = 100L;
52 
53   private final RefreshAnnotatedCallLogWorker refreshAnnotatedCallLogWorker;
54   private final FutureTimer futureTimer;
55   private final LoggingBindings logger;
56 
57   @Nullable private Runnable refreshAnnotatedCallLogRunnable;
58 
59   /** Returns an {@link IntentFilter} containing all actions accepted by this broadcast receiver. */
getIntentFilter()60   public static IntentFilter getIntentFilter() {
61     IntentFilter intentFilter = new IntentFilter();
62     intentFilter.addAction(IntentNames.ACTION_REFRESH_ANNOTATED_CALL_LOG);
63     intentFilter.addAction(IntentNames.ACTION_CANCEL_REFRESHING_ANNOTATED_CALL_LOG);
64     return intentFilter;
65   }
66 
RefreshAnnotatedCallLogReceiver(Context context)67   public RefreshAnnotatedCallLogReceiver(Context context) {
68     refreshAnnotatedCallLogWorker =
69         CallLogComponent.get(context).getRefreshAnnotatedCallLogWorker();
70     futureTimer = MetricsComponent.get(context).futureTimer();
71     logger = Logger.get(context);
72   }
73 
74   @Override
onReceive(Context context, Intent intent)75   public void onReceive(Context context, Intent intent) {
76     LogUtil.enterBlock("RefreshAnnotatedCallLogReceiver.onReceive");
77 
78     String action = intent.getAction();
79 
80     if (IntentNames.ACTION_REFRESH_ANNOTATED_CALL_LOG.equals(action)) {
81       boolean checkDirty = intent.getBooleanExtra(IntentNames.EXTRA_CHECK_DIRTY, false);
82       refreshAnnotatedCallLog(checkDirty);
83     } else if (IntentNames.ACTION_CANCEL_REFRESHING_ANNOTATED_CALL_LOG.equals(action)) {
84       cancelRefreshingAnnotatedCallLog();
85     }
86   }
87 
88   /**
89    * Request a refresh of the annotated call log.
90    *
91    * <p>Note that the execution will be delayed by {@link #REFRESH_ANNOTATED_CALL_LOG_WAIT_MILLIS}.
92    * Once the work begins, it can't be cancelled.
93    *
94    * @see #cancelRefreshingAnnotatedCallLog()
95    */
refreshAnnotatedCallLog(boolean checkDirty)96   private void refreshAnnotatedCallLog(boolean checkDirty) {
97     LogUtil.enterBlock("RefreshAnnotatedCallLogReceiver.refreshAnnotatedCallLog");
98 
99     // If we already scheduled a refresh, cancel it and schedule a new one so that repeated requests
100     // in quick succession don't result in too much work. For example, if we get 10 requests in
101     // 10ms, and a complete refresh takes a constant 200ms, the refresh will take 300ms (100ms wait
102     // and 1 iteration @200ms) instead of 2 seconds (10 iterations @ 200ms) since the work requests
103     // are serialized in RefreshAnnotatedCallLogWorker.
104     //
105     // We might get many requests in quick succession, for example, when the simulator inserts
106     // hundreds of rows into the system call log, or when the data for a new call is incrementally
107     // written to different columns as it becomes available.
108     ThreadUtil.getUiThreadHandler().removeCallbacks(refreshAnnotatedCallLogRunnable);
109 
110     refreshAnnotatedCallLogRunnable =
111         () -> {
112           ListenableFuture<RefreshResult> future =
113               checkDirty
114                   ? refreshAnnotatedCallLogWorker.refreshWithDirtyCheck()
115                   : refreshAnnotatedCallLogWorker.refreshWithoutDirtyCheck();
116           Futures.addCallback(
117               future,
118               new FutureCallback<RefreshResult>() {
119                 @Override
120                 public void onSuccess(RefreshResult refreshResult) {
121                   logger.logImpression(getImpressionType(checkDirty, refreshResult));
122                 }
123 
124                 @Override
125                 public void onFailure(Throwable throwable) {
126                   ThreadUtil.getUiThreadHandler()
127                       .post(
128                           () -> {
129                             throw new RuntimeException(throwable);
130                           });
131                 }
132               },
133               MoreExecutors.directExecutor());
134           futureTimer.applyTiming(future, new EventNameFromResultFunction(checkDirty));
135         };
136 
137     ThreadUtil.getUiThreadHandler()
138         .postDelayed(refreshAnnotatedCallLogRunnable, REFRESH_ANNOTATED_CALL_LOG_WAIT_MILLIS);
139   }
140 
141   /**
142    * When a refresh is requested, its execution is delayed (see {@link
143    * #refreshAnnotatedCallLog(boolean)}). This method only cancels the refresh if it hasn't started.
144    */
cancelRefreshingAnnotatedCallLog()145   private void cancelRefreshingAnnotatedCallLog() {
146     LogUtil.enterBlock("RefreshAnnotatedCallLogReceiver.cancelRefreshingAnnotatedCallLog");
147 
148     ThreadUtil.getUiThreadHandler().removeCallbacks(refreshAnnotatedCallLogRunnable);
149   }
150 
151   private static class EventNameFromResultFunction implements Function<RefreshResult, String> {
152 
153     private final boolean checkDirty;
154 
EventNameFromResultFunction(boolean checkDirty)155     private EventNameFromResultFunction(boolean checkDirty) {
156       this.checkDirty = checkDirty;
157     }
158 
159     @Override
apply(RefreshResult refreshResult)160     public String apply(RefreshResult refreshResult) {
161       switch (refreshResult) {
162         case NOT_DIRTY:
163           return Metrics.ANNOTATED_CALL_LOG_NOT_DIRTY; // NOT_DIRTY implies forceRefresh is false
164         case REBUILT_BUT_NO_CHANGES_NEEDED:
165           return checkDirty
166               ? Metrics.ANNOTATED_LOG_NO_CHANGES_NEEDED
167               : Metrics.NEW_CALL_LOG_FORCE_REFRESH_NO_CHANGES_NEEDED;
168         case REBUILT_AND_CHANGES_NEEDED:
169           return checkDirty
170               ? Metrics.ANNOTATED_CALL_LOG_CHANGES_NEEDED
171               : Metrics.ANNOTATED_CALL_LOG_FORCE_REFRESH_CHANGES_NEEDED;
172         default:
173           throw new IllegalStateException("Unsupported result: " + refreshResult);
174       }
175     }
176   }
177 
getImpressionType( boolean checkDirty, RefreshResult refreshResult)178   private static DialerImpression.Type getImpressionType(
179       boolean checkDirty, RefreshResult refreshResult) {
180     switch (refreshResult) {
181       case NOT_DIRTY:
182         return DialerImpression.Type.ANNOTATED_CALL_LOG_NOT_DIRTY;
183       case REBUILT_BUT_NO_CHANGES_NEEDED:
184         return checkDirty
185             ? DialerImpression.Type.ANNOTATED_CALL_LOG_NO_CHANGES_NEEDED
186             : DialerImpression.Type.ANNOTATED_CALL_LOG_FORCE_REFRESH_NO_CHANGES_NEEDED;
187       case REBUILT_AND_CHANGES_NEEDED:
188         return checkDirty
189             ? DialerImpression.Type.ANNOTATED_CALL_LOG_CHANGES_NEEDED
190             : DialerImpression.Type.ANNOTATED_CALL_LOG_FORCE_REFRESH_CHANGES_NEEDED;
191       default:
192         throw new IllegalStateException("Unsupported result: " + refreshResult);
193     }
194   }
195 }
196