Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 39009b4a authored by zachh's avatar zachh Committed by Copybara-Service
Browse files

Mark calls as read in new call log.

Playing with the existing app, the missed call becomes unbolded when:

1) Expanding the row. The closest analog of this is in the new UI is opening the bottom sheet, I've done that.

2) Swiping away from the call history tab. This can't be done in NewCallLogFragment because it doesn't know if the user is selected a new tab or pressed Home. So, I implemented this in NewMainActivityPeer.

3) After viewing the call log for 3(ish) seconds and leaving the activity pressing Home/Back. This is best done in NewCallLogFragment#onResume since MainActivity doesn't always know when the fragment is being displayed (it could be done after the user comes back to the app after pressing Home for example).

Note that the notification is also removed in all of these cases.

Also note that dismissing the notification makes the call unbolded (but this case already appears to be handled via the system call log content observer).

Also, as part of writing tests for this, I made TestCallLogProvider more realistic.

Bug: 70989622
Test: manual
PiperOrigin-RevId: 185457438
Change-Id: Ib360d3bc73083bd1a018ed98e2b7d9a69fb7fafb
parent c266566d
Loading
Loading
Loading
Loading
+3 −10
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.concurrent.DialerExecutor.Worker;
import com.android.dialer.common.concurrent.DialerExecutorComponent;
import com.android.dialer.notification.missedcalls.MissedCallNotificationCanceller;
import com.android.dialer.telecom.TelecomUtil;
import com.android.dialer.util.PermissionsUtil;

@@ -87,14 +88,6 @@ public class CallLogNotificationsService extends IntentService {
    context.startService(serviceIntent);
  }

  public static void markSingleNewVoicemailAsOld(Context context, @Nullable Uri voicemailUri) {
    LogUtil.enterBlock("CallLogNotificationsService.markSingleNewVoicemailAsOld");
    Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
    serviceIntent.setAction(CallLogNotificationsService.ACTION_MARK_SINGLE_NEW_VOICEMAIL_AS_OLD);
    serviceIntent.setData(voicemailUri);
    context.startService(serviceIntent);
  }

  public static void cancelAllMissedCalls(Context context) {
    LogUtil.enterBlock("CallLogNotificationsService.cancelAllMissedCalls");
    DialerExecutorComponent.get(context)
@@ -175,7 +168,7 @@ public class CallLogNotificationsService extends IntentService {
      case ACTION_CANCEL_SINGLE_MISSED_CALL:
        Uri callUri = intent.getData();
        CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead(this, callUri);
        MissedCallNotifier.cancelSingleMissedCallNotification(this, callUri);
        MissedCallNotificationCanceller.cancelSingle(this, callUri);
        TelecomUtil.cancelMissedCallsNotification(this);
        break;
      case ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION:
@@ -196,7 +189,7 @@ public class CallLogNotificationsService extends IntentService {
    LogUtil.enterBlock("CallLogNotificationsService.cancelAllMissedCallsBackground");
    Assert.isWorkerThread();
    CallLogNotificationsQueryHelper.markAllMissedCallsInCallLogAsRead(context);
    MissedCallNotifier.cancelAllMissedCallNotifications(context);
    MissedCallNotificationCanceller.cancelAll(context);
    TelecomUtil.cancelMissedCallsNotification(context);
  }

+17 −42
Original line number Diff line number Diff line
@@ -58,7 +58,9 @@ import com.android.dialer.duo.DuoConstants;
import com.android.dialer.enrichedcall.FuzzyPhoneNumberMatcher;
import com.android.dialer.notification.DialerNotificationManager;
import com.android.dialer.notification.NotificationChannelId;
import com.android.dialer.notification.NotificationManagerUtils;
import com.android.dialer.notification.missedcalls.MissedCallConstants;
import com.android.dialer.notification.missedcalls.MissedCallNotificationCanceller;
import com.android.dialer.notification.missedcalls.MissedCallNotificationTags;
import com.android.dialer.phonenumbercache.ContactInfo;
import com.android.dialer.phonenumberutil.PhoneNumberHelper;
import com.android.dialer.precall.PreCall;
@@ -71,18 +73,6 @@ import java.util.Set;
/** Creates a notification for calls that the user missed (neither answered nor rejected). */
public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> {

  /** Prefix used to generate a unique tag for each missed call notification. */
  private static final String NOTIFICATION_TAG_PREFIX = "MissedCall_";
  /** Common ID for all missed call notifications. */
  private static final int NOTIFICATION_ID = 1;
  /** Tag for the group summary notification. */
  private static final String GROUP_SUMMARY_NOTIFICATION_TAG = "GroupSummary_MissedCall";
  /**
   * Key used to associate all missed call notifications and the summary as belonging to a single
   * group.
   */
  private static final String GROUP_KEY = "MissedCallGroup";

  private final Context context;
  private final CallLogNotificationsQueryHelper callLogNotificationsQueryHelper;

@@ -126,7 +116,7 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> {
    if ((newCalls != null && newCalls.isEmpty()) || count == 0) {
      // No calls to notify about: clear the notification.
      CallLogNotificationsQueryHelper.markAllMissedCallsInCallLogAsRead(context);
      cancelAllMissedCallNotifications(context);
      MissedCallNotificationCanceller.cancelAll(context);
      return;
    }

@@ -226,7 +216,10 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> {

    LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "adding missed call notification");
    DialerNotificationManager.notify(
        context, GROUP_SUMMARY_NOTIFICATION_TAG, NOTIFICATION_ID, notification);
        context,
        MissedCallConstants.GROUP_SUMMARY_NOTIFICATION_TAG,
        MissedCallConstants.NOTIFICATION_ID,
        notification);

    if (useCallList) {
      // Do not repost active notifications to prevent erasing post call notes.
@@ -240,7 +233,10 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> {
        String callTag = getNotificationTagForCall(call);
        if (!activeTags.contains(callTag)) {
          DialerNotificationManager.notify(
              context, callTag, NOTIFICATION_ID, getNotificationForCall(call, null));
              context,
              callTag,
              MissedCallConstants.NOTIFICATION_ID,
              getNotificationForCall(call, null));
        }
      }
    }
@@ -286,29 +282,8 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> {
    }
  }

  public static void cancelAllMissedCallNotifications(@NonNull Context context) {
    NotificationManagerUtils.cancelAllInGroup(context, GROUP_KEY);
  }

  public static void cancelSingleMissedCallNotification(
      @NonNull Context context, @Nullable Uri callUri) {
    if (callUri == null) {
      LogUtil.e(
          "MissedCallNotifier.cancelSingleMissedCallNotification",
          "unable to cancel notification, uri is null");
      return;
    }
    // This will also dismiss the group summary if there are no more missed call notifications.
    DialerNotificationManager.cancel(
        context, getNotificationTagForCallUri(callUri), NOTIFICATION_ID);
  }

  private static String getNotificationTagForCall(@NonNull NewCall call) {
    return getNotificationTagForCallUri(call.callsUri);
  }

  private static String getNotificationTagForCallUri(@NonNull Uri callUri) {
    return NOTIFICATION_TAG_PREFIX + callUri;
    return MissedCallNotificationTags.getNotificationTagForCallUri(call.callsUri);
  }

  @WorkerThread
@@ -324,7 +299,7 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> {
          DialerNotificationManager.notify(
              context,
              getNotificationTagForCall(call),
              NOTIFICATION_ID,
              MissedCallConstants.NOTIFICATION_ID,
              getNotificationForCall(call, note));
          return;
        }
@@ -408,7 +383,7 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> {

  private Notification.Builder createNotificationBuilder() {
    return new Notification.Builder(context)
        .setGroup(GROUP_KEY)
        .setGroup(MissedCallConstants.GROUP_KEY)
        .setSmallIcon(android.R.drawable.stat_notify_missed_call)
        .setColor(context.getResources().getColor(R.color.dialer_theme_color, null))
        .setAutoCancel(true)
@@ -437,7 +412,7 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> {
  public void callBackFromMissedCall(String number, Uri callUri) {
    closeSystemDialogs(context);
    CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead(context, callUri);
    cancelSingleMissedCallNotification(context, callUri);
    MissedCallNotificationCanceller.cancelSingle(context, callUri);
    DialerUtils.startActivityWithErrorToast(
        context,
        PreCall.getIntent(
@@ -450,7 +425,7 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> {
  public void sendSmsFromMissedCall(String number, Uri callUri) {
    closeSystemDialogs(context);
    CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead(context, callUri);
    cancelSingleMissedCallNotification(context, callUri);
    MissedCallNotificationCanceller.cancelSingle(context, callUri);
    DialerUtils.startActivityWithErrorToast(
        context, IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
  }
+2 −0
Original line number Diff line number Diff line
@@ -27,6 +27,8 @@ public abstract class CallLogComponent {

  public abstract RefreshAnnotatedCallLogWorker getRefreshAnnotatedCallLogWorker();

  public abstract ClearMissedCalls getClearMissedCalls();

  public static CallLogComponent get(Context context) {
    return ((HasComponent) ((HasRootComponent) context.getApplicationContext()).component())
        .callLogComponent();
+166 −0
Original line number Diff line number Diff line
/*
 * 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.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.provider.CallLog.Calls;
import android.support.v4.os.UserManagerCompat;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
import com.android.dialer.common.concurrent.Annotations.Ui;
import com.android.dialer.common.database.Selection;
import com.android.dialer.inject.ApplicationContext;
import com.android.dialer.notification.missedcalls.MissedCallNotificationCanceller;
import com.android.dialer.util.PermissionsUtil;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.Collection;
import javax.inject.Inject;

/**
 * Clears missed calls. This includes cancelling notifications and updating the "NEW" status in the
 * system call log.
 */
public final class ClearMissedCalls {

  private final Context appContext;
  private final ListeningExecutorService backgroundExecutor;
  private final ListeningExecutorService uiThreadExecutor;

  @Inject
  ClearMissedCalls(
      @ApplicationContext Context appContext,
      @BackgroundExecutor ListeningExecutorService backgroundExecutor,
      @Ui ListeningExecutorService uiThreadExecutor) {
    this.appContext = appContext;
    this.backgroundExecutor = backgroundExecutor;
    this.uiThreadExecutor = uiThreadExecutor;
  }

  /**
   * Cancels all missed call notifications and marks all "new" missed calls in the system call log
   * as "not new".
   */
  public ListenableFuture<Void> clearAll() {
    ListenableFuture<Void> markNewFuture = markNotNew(ImmutableSet.of());
    ListenableFuture<Void> cancelNotificationsFuture =
        uiThreadExecutor.submit(
            () -> {
              MissedCallNotificationCanceller.cancelAll(appContext);
              return null;
            });

    // Note on this usage of whenAllComplete:
    //   -The returned future completes when all sub-futures complete (whether they fail or not)
    //   -The returned future fails if any sub-future fails
    return Futures.whenAllComplete(markNewFuture, cancelNotificationsFuture)
        .call(
            () -> {
              // Calling get() is necessary to propagate failures.
              markNewFuture.get();
              cancelNotificationsFuture.get();
              return null;
            },
            MoreExecutors.directExecutor());
  }

  /**
   * For the provided set of IDs from the system call log, cancels their missed call notifications
   * and marks them "not new".
   *
   * @param ids IDs from the system call log (see {@link Calls#_ID}}.
   */
  public ListenableFuture<Void> clearBySystemCallLogId(Collection<Long> ids) {
    ListenableFuture<Void> markNewFuture = markNotNew(ids);
    ListenableFuture<Void> cancelNotificationsFuture =
        uiThreadExecutor.submit(
            () -> {
              for (long id : ids) {
                Uri callUri = Calls.CONTENT_URI.buildUpon().appendPath(Long.toString(id)).build();
                MissedCallNotificationCanceller.cancelSingle(appContext, callUri);
              }
              return null;
            });

    // Note on this usage of whenAllComplete:
    //   -The returned future completes when all sub-futures complete (whether they fail or not)
    //   -The returned future fails if any sub-future fails
    return Futures.whenAllComplete(markNewFuture, cancelNotificationsFuture)
        .call(
            () -> {
              // Calling get() is necessary to propagate failures.
              markNewFuture.get();
              cancelNotificationsFuture.get();
              return null;
            },
            MoreExecutors.directExecutor());
  }

  /**
   * Marks all provided system call log IDs as not new, or if the provided collection is empty,
   * marks all calls as not new.
   */
  @SuppressLint("MissingPermission")
  private ListenableFuture<Void> markNotNew(Collection<Long> ids) {
    return backgroundExecutor.submit(
        () -> {
          if (!UserManagerCompat.isUserUnlocked(appContext)) {
            LogUtil.e("ClearMissedCalls.markNotNew", "locked");
            return null;
          }
          if (!PermissionsUtil.hasCallLogWritePermissions(appContext)) {
            LogUtil.e("ClearMissedCalls.markNotNew", "no permission");
            return null;
          }

          ContentValues values = new ContentValues();
          values.put(Calls.NEW, 0);

          Selection.Builder selectionBuilder =
              Selection.builder()
                  .and(Selection.column(Calls.NEW).is("=", 1))
                  .and(Selection.column(Calls.TYPE).is("=", Calls.MISSED_TYPE));
          if (!ids.isEmpty()) {
            selectionBuilder.and(Selection.column(Calls._ID).in(toStrings(ids)));
          }
          Selection selection = selectionBuilder.build();
          appContext
              .getContentResolver()
              .update(
                  Calls.CONTENT_URI,
                  values,
                  selection.getSelection(),
                  selection.getSelectionArgs());
          return null;
        });
  }

  private static String[] toStrings(Collection<Long> longs) {
    String[] strings = new String[longs.size()];
    int i = 0;
    for (long value : longs) {
      strings[i++] = Long.toString(value);
    }
    return strings;
  }
}
+29 −2
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.dialer.calllog.ui;
import android.database.Cursor;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
@@ -31,10 +32,14 @@ import com.android.dialer.calllog.CallLogFramework;
import com.android.dialer.calllog.CallLogFramework.CallLogUi;
import com.android.dialer.calllog.RefreshAnnotatedCallLogWorker;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.concurrent.DefaultFutureCallback;
import com.android.dialer.common.concurrent.DialerExecutorComponent;
import com.android.dialer.common.concurrent.ThreadUtil;
import com.android.dialer.common.concurrent.UiListener;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.concurrent.TimeUnit;

/** The "new" call log fragment implementation, which is built on top of the annotated call log. */
public final class NewCallLogFragment extends Fragment
@@ -46,13 +51,19 @@ public final class NewCallLogFragment extends Fragment
   * the simulator, using this value results in ~6 refresh cycles (on a release build) to write 120
   * call log entries.
   */
  private static final long WAIT_MILLIS = 100L;
  private static final long REFRESH_ANNOTATED_CALL_LOG_WAIT_MILLIS = 100L;

  @VisibleForTesting
  static final long MARK_ALL_CALLS_READ_WAIT_MILLIS = TimeUnit.SECONDS.toMillis(3);

  private RefreshAnnotatedCallLogWorker refreshAnnotatedCallLogWorker;
  private UiListener<Void> refreshAnnotatedCallLogListener;
  private RecyclerView recyclerView;
  @Nullable private Runnable refreshAnnotatedCallLogRunnable;

  private boolean shouldMarkCallsRead = false;
  private final Runnable setShouldMarkCallsReadTrue = () -> shouldMarkCallsRead = true;

  public NewCallLogFragment() {
    LogUtil.enterBlock("NewCallLogFragment.NewCallLogFragment");
  }
@@ -103,6 +114,13 @@ public final class NewCallLogFragment extends Fragment
      ((NewCallLogAdapter) recyclerView.getAdapter()).clearCache();
      recyclerView.getAdapter().notifyDataSetChanged();
    }

    // We shouldn't mark the calls as read immediately when the 3 second timer expires because we
    // don't want to disrupt the UI; instead we set a bit indicating to mark them read when the user
    // leaves the fragment (in onPause).
    shouldMarkCallsRead = false;
    ThreadUtil.getUiThreadHandler()
        .postDelayed(setShouldMarkCallsReadTrue, MARK_ALL_CALLS_READ_WAIT_MILLIS);
  }

  @Override
@@ -113,9 +131,17 @@ public final class NewCallLogFragment extends Fragment

    // This is pending work that we don't actually need to follow through with.
    ThreadUtil.getUiThreadHandler().removeCallbacks(refreshAnnotatedCallLogRunnable);
    ThreadUtil.getUiThreadHandler().removeCallbacks(setShouldMarkCallsReadTrue);

    CallLogFramework callLogFramework = CallLogComponent.get(getContext()).callLogFramework();
    callLogFramework.detachUi();

    if (shouldMarkCallsRead) {
      Futures.addCallback(
          CallLogComponent.get(getContext()).getClearMissedCalls().clearAll(),
          new DefaultFutureCallback<>(),
          MoreExecutors.directExecutor());
    }
  }

  @Override
@@ -159,7 +185,8 @@ public final class NewCallLogFragment extends Fragment
                throw new RuntimeException(throwable);
              });
        };
    ThreadUtil.getUiThreadHandler().postDelayed(refreshAnnotatedCallLogRunnable, WAIT_MILLIS);
    ThreadUtil.getUiThreadHandler()
        .postDelayed(refreshAnnotatedCallLogRunnable, REFRESH_ANNOTATED_CALL_LOG_WAIT_MILLIS);
  }

  @Override
Loading