| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.dialer.calllog; |
| |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.support.annotation.Nullable; |
| import com.android.dialer.calllog.RefreshAnnotatedCallLogWorker.RefreshResult; |
| import com.android.dialer.calllog.constants.IntentNames; |
| import com.android.dialer.common.LogUtil; |
| import com.android.dialer.common.concurrent.ThreadUtil; |
| import com.android.dialer.logging.DialerImpression; |
| import com.android.dialer.logging.Logger; |
| import com.android.dialer.logging.LoggingBindings; |
| import com.android.dialer.metrics.FutureTimer; |
| import com.android.dialer.metrics.Metrics; |
| import com.android.dialer.metrics.MetricsComponent; |
| import com.google.common.base.Function; |
| import com.google.common.util.concurrent.FutureCallback; |
| import com.google.common.util.concurrent.Futures; |
| import com.google.common.util.concurrent.ListenableFuture; |
| import com.google.common.util.concurrent.MoreExecutors; |
| |
| /** |
| * A {@link BroadcastReceiver} that starts/cancels refreshing the annotated call log when notified. |
| */ |
| public final class RefreshAnnotatedCallLogReceiver extends BroadcastReceiver { |
| |
| /** |
| * This is a reasonable time that it might take between related call log writes, that also |
| * shouldn't slow down single-writes too much. For example, when populating the database using the |
| * simulator, using this value results in ~6 refresh cycles (on a release build) to write 120 call |
| * log entries. |
| */ |
| private static final long REFRESH_ANNOTATED_CALL_LOG_WAIT_MILLIS = 100L; |
| |
| private final RefreshAnnotatedCallLogWorker refreshAnnotatedCallLogWorker; |
| private final FutureTimer futureTimer; |
| private final LoggingBindings logger; |
| |
| @Nullable private Runnable refreshAnnotatedCallLogRunnable; |
| |
| /** Returns an {@link IntentFilter} containing all actions accepted by this broadcast receiver. */ |
| public static IntentFilter getIntentFilter() { |
| IntentFilter intentFilter = new IntentFilter(); |
| intentFilter.addAction(IntentNames.ACTION_REFRESH_ANNOTATED_CALL_LOG); |
| intentFilter.addAction(IntentNames.ACTION_CANCEL_REFRESHING_ANNOTATED_CALL_LOG); |
| return intentFilter; |
| } |
| |
| public RefreshAnnotatedCallLogReceiver(Context context) { |
| refreshAnnotatedCallLogWorker = |
| CallLogComponent.get(context).getRefreshAnnotatedCallLogWorker(); |
| futureTimer = MetricsComponent.get(context).futureTimer(); |
| logger = Logger.get(context); |
| } |
| |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| LogUtil.enterBlock("RefreshAnnotatedCallLogReceiver.onReceive"); |
| |
| String action = intent.getAction(); |
| |
| if (IntentNames.ACTION_REFRESH_ANNOTATED_CALL_LOG.equals(action)) { |
| boolean checkDirty = intent.getBooleanExtra(IntentNames.EXTRA_CHECK_DIRTY, false); |
| refreshAnnotatedCallLog(checkDirty); |
| } else if (IntentNames.ACTION_CANCEL_REFRESHING_ANNOTATED_CALL_LOG.equals(action)) { |
| cancelRefreshingAnnotatedCallLog(); |
| } |
| } |
| |
| /** |
| * Request a refresh of the annotated call log. |
| * |
| * <p>Note that the execution will be delayed by {@link #REFRESH_ANNOTATED_CALL_LOG_WAIT_MILLIS}. |
| * Once the work begins, it can't be cancelled. |
| * |
| * @see #cancelRefreshingAnnotatedCallLog() |
| */ |
| private void refreshAnnotatedCallLog(boolean checkDirty) { |
| LogUtil.enterBlock("RefreshAnnotatedCallLogReceiver.refreshAnnotatedCallLog"); |
| |
| // If we already scheduled a refresh, cancel it and schedule a new one so that repeated requests |
| // in quick succession don't result in too much work. For example, if we get 10 requests in |
| // 10ms, and a complete refresh takes a constant 200ms, the refresh will take 300ms (100ms wait |
| // and 1 iteration @200ms) instead of 2 seconds (10 iterations @ 200ms) since the work requests |
| // are serialized in RefreshAnnotatedCallLogWorker. |
| // |
| // We might get many requests in quick succession, for example, when the simulator inserts |
| // hundreds of rows into the system call log, or when the data for a new call is incrementally |
| // written to different columns as it becomes available. |
| ThreadUtil.getUiThreadHandler().removeCallbacks(refreshAnnotatedCallLogRunnable); |
| |
| refreshAnnotatedCallLogRunnable = |
| () -> { |
| ListenableFuture<RefreshResult> future = |
| checkDirty |
| ? refreshAnnotatedCallLogWorker.refreshWithDirtyCheck() |
| : refreshAnnotatedCallLogWorker.refreshWithoutDirtyCheck(); |
| Futures.addCallback( |
| future, |
| new FutureCallback<RefreshResult>() { |
| @Override |
| public void onSuccess(RefreshResult refreshResult) { |
| logger.logImpression(getImpressionType(checkDirty, refreshResult)); |
| } |
| |
| @Override |
| public void onFailure(Throwable throwable) { |
| ThreadUtil.getUiThreadHandler() |
| .post( |
| () -> { |
| throw new RuntimeException(throwable); |
| }); |
| } |
| }, |
| MoreExecutors.directExecutor()); |
| futureTimer.applyTiming(future, new EventNameFromResultFunction(checkDirty)); |
| }; |
| |
| ThreadUtil.getUiThreadHandler() |
| .postDelayed(refreshAnnotatedCallLogRunnable, REFRESH_ANNOTATED_CALL_LOG_WAIT_MILLIS); |
| } |
| |
| /** |
| * When a refresh is requested, its execution is delayed (see {@link |
| * #refreshAnnotatedCallLog(boolean)}). This method only cancels the refresh if it hasn't started. |
| */ |
| private void cancelRefreshingAnnotatedCallLog() { |
| LogUtil.enterBlock("RefreshAnnotatedCallLogReceiver.cancelRefreshingAnnotatedCallLog"); |
| |
| ThreadUtil.getUiThreadHandler().removeCallbacks(refreshAnnotatedCallLogRunnable); |
| } |
| |
| private static class EventNameFromResultFunction implements Function<RefreshResult, String> { |
| |
| private final boolean checkDirty; |
| |
| private EventNameFromResultFunction(boolean checkDirty) { |
| this.checkDirty = checkDirty; |
| } |
| |
| @Override |
| public String apply(RefreshResult refreshResult) { |
| switch (refreshResult) { |
| case NOT_DIRTY: |
| return Metrics.ANNOTATED_CALL_LOG_NOT_DIRTY; // NOT_DIRTY implies forceRefresh is false |
| case REBUILT_BUT_NO_CHANGES_NEEDED: |
| return checkDirty |
| ? Metrics.ANNOTATED_LOG_NO_CHANGES_NEEDED |
| : Metrics.NEW_CALL_LOG_FORCE_REFRESH_NO_CHANGES_NEEDED; |
| case REBUILT_AND_CHANGES_NEEDED: |
| return checkDirty |
| ? Metrics.ANNOTATED_CALL_LOG_CHANGES_NEEDED |
| : Metrics.ANNOTATED_CALL_LOG_FORCE_REFRESH_CHANGES_NEEDED; |
| default: |
| throw new IllegalStateException("Unsupported result: " + refreshResult); |
| } |
| } |
| } |
| |
| private static DialerImpression.Type getImpressionType( |
| boolean checkDirty, RefreshResult refreshResult) { |
| switch (refreshResult) { |
| case NOT_DIRTY: |
| return DialerImpression.Type.ANNOTATED_CALL_LOG_NOT_DIRTY; |
| case REBUILT_BUT_NO_CHANGES_NEEDED: |
| return checkDirty |
| ? DialerImpression.Type.ANNOTATED_CALL_LOG_NO_CHANGES_NEEDED |
| : DialerImpression.Type.ANNOTATED_CALL_LOG_FORCE_REFRESH_NO_CHANGES_NEEDED; |
| case REBUILT_AND_CHANGES_NEEDED: |
| return checkDirty |
| ? DialerImpression.Type.ANNOTATED_CALL_LOG_CHANGES_NEEDED |
| : DialerImpression.Type.ANNOTATED_CALL_LOG_FORCE_REFRESH_CHANGES_NEEDED; |
| default: |
| throw new IllegalStateException("Unsupported result: " + refreshResult); |
| } |
| } |
| } |