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