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

Commit 88c39610 authored by Tyler Gunn's avatar Tyler Gunn
Browse files

Synchronize updates of missed call counts and add more logging.

When the missed call count stored in MissedCallNotifierImpl was updated,
there was a rare chance for the missed call count to be cleared while it
was being updated.  mMissedCallCounts is a concurrent hash map and the
references it stores are AtomicIntegers; getting an element and then
updating it can't be guaranteed to be thread safe.  Also, in the case of
updating the missed call count we'd update the value and then re-fetch it
when sending the intent to dialer; this means it could have been cleared
in the process of being updated.

Test: Ran MissedCallNotifierImpl tests.
Test: Performed missed call regression testing to verify that missed call
notifications and clearing takes place in Dialer as expected.
Test: Added unit tests for dialer-handled set/clear of missed call count.
This doesn't test concurrency per se, but it helps improve test coverage.
Fixes: 168695531

Change-Id: I76389500bf8d91f029123178060db6ff38e398eb
parent c01de52d
Loading
Loading
Loading
Loading
+36 −26
Original line number Diff line number Diff line
@@ -67,18 +67,15 @@ import android.text.TextDirectionHeuristics;
import android.text.TextUtils;

import android.telecom.CallerInfo;
import android.util.ArrayMap;

import java.lang.Override;
import java.lang.String;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;

// TODO: Needed for move to system service: import com.android.internal.R;

/**
 * Creates a notification for calls that the user missed (neither answered nor rejected).
@@ -139,8 +136,10 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements
    private final DeviceIdleControllerAdapter mDeviceIdleControllerAdapter;
    private UserHandle mCurrentUserHandle;

    // Used to guard access to mMissedCallCounts
    private final Object mMissedCallCountsLock = new Object();
    // Used to track the number of missed calls.
    private ConcurrentMap<UserHandle, AtomicInteger> mMissedCallCounts;
    private final Map<UserHandle, Integer> mMissedCallCounts;

    private List<UserHandle> mUsersToLoadAfterBootComplete = new ArrayList<>();

@@ -164,7 +163,7 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements
        mDefaultDialerCache = defaultDialerCache;

        mNotificationBuilderFactory = notificationBuilderFactory;
        mMissedCallCounts = new ConcurrentHashMap<>();
        mMissedCallCounts = new ArrayMap<>();
    }

    /** Clears missed call notification and marks the call log's missed calls as read. */
@@ -263,17 +262,16 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements
    }

    private void sendNotificationThroughDefaultDialer(String dialerPackage, CallInfo callInfo,
            UserHandle userHandle) {
        int count = mMissedCallCounts.get(userHandle).get();
            UserHandle userHandle, int missedCallCount) {
        Intent intent = getShowMissedCallIntentForDefaultDialer(dialerPackage)
            .setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
            .putExtra(TelecomManager.EXTRA_CLEAR_MISSED_CALLS_INTENT,
                    createClearMissedCallsPendingIntent(userHandle))
            .putExtra(TelecomManager.EXTRA_NOTIFICATION_COUNT, count)
            .putExtra(TelecomManager.EXTRA_NOTIFICATION_COUNT, missedCallCount)
            .putExtra(TelecomManager.EXTRA_NOTIFICATION_PHONE_NUMBER,
                    callInfo == null ? null : callInfo.getPhoneNumber());

        if (count == 1 && callInfo != null) {
        if (missedCallCount == 1 && callInfo != null) {
            final Uri handleUri = callInfo.getHandle();
            String handle = handleUri == null ? null : handleUri.getSchemeSpecificPart();

@@ -284,8 +282,8 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements
            }
        }


        Log.w(this, "Showing missed calls through default dialer.");
        Log.i(this, "sendNotificationThroughDefaultDialer; count=%d, dialerPackage=%s",
                missedCallCount, intent.getPackage());
        Bundle options = exemptFromPowerSavingTemporarily(dialerPackage, userHandle);
        mContext.sendBroadcastAsUser(intent, userHandle, READ_PHONE_STATE, options);
    }
@@ -293,7 +291,7 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements
    /**
     * Create a system notification for the missed call.
     *
     * @param call The missed call.
     * @param callInfo The missed call.
     */
    @Override
    public void showMissedCallNotification(@NonNull CallInfo callInfo) {
@@ -311,13 +309,21 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements
    }

    private void showMissedCallNotification(@NonNull CallInfo callInfo, UserHandle userHandle) {
        Log.i(this, "showMissedCallNotification: userHandle=%d", userHandle.getIdentifier());
        mMissedCallCounts.putIfAbsent(userHandle, new AtomicInteger(0));
        int missCallCounts = mMissedCallCounts.get(userHandle).incrementAndGet();
        int missedCallCounts;
        synchronized (mMissedCallCountsLock) {
            Integer currentCount = mMissedCallCounts.get(userHandle);
            missedCallCounts = currentCount == null ? 0 : currentCount;
            missedCallCounts++;
            mMissedCallCounts.put(userHandle, missedCallCounts);
        }

        Log.i(this, "showMissedCallNotification: userHandle=%d, missedCallCount=%d",
                userHandle.getIdentifier(), missedCallCounts);

        String dialerPackage = getDefaultDialerPackage(userHandle);
        if (shouldManageNotificationThroughDefaultDialer(dialerPackage, userHandle)) {
            sendNotificationThroughDefaultDialer(dialerPackage, callInfo, userHandle);
            sendNotificationThroughDefaultDialer(dialerPackage, callInfo, userHandle,
                    missedCallCounts);
            return;
        }

@@ -327,7 +333,7 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements
        // Display the first line of the notification:
        // 1 missed call: <caller name || handle>
        // More than 1 missed call: <number of calls> + "missed calls"
        if (missCallCounts == 1) {
        if (missedCallCounts == 1) {
            expandedText = getNameForMissedCallNotification(callInfo);

            CallerInfo ci = callInfo.getCallerInfo();
@@ -339,7 +345,7 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements
        } else {
            titleResId = R.string.notification_missedCallsTitle;
            expandedText =
                    mContext.getString(R.string.notification_missedCallsMsg, missCallCounts);
                    mContext.getString(R.string.notification_missedCallsMsg, missedCallCounts);
        }

        // Create a public viewable version of the notification, suitable for display when sensitive
@@ -381,7 +387,7 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements
        String handle = callInfo.getHandleSchemeSpecificPart();

        // Add additional actions when there is only 1 missed call, like call-back and SMS.
        if (missCallCounts == 1) {
        if (missedCallCounts == 1) {
            Log.d(this, "Add actions with number %s.", Log.piiHandle(handle));

            if (!TextUtils.isEmpty(handle)
@@ -410,7 +416,7 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements
            }
        } else {
            Log.d(this, "Suppress actions. handle: %s, missedCalls: %d.", Log.piiHandle(handle),
                    missCallCounts);
                    missedCallCounts);
        }

        Notification notification = builder.build();
@@ -430,12 +436,14 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements
    /** Cancels the "missed call" notification. */
    private void cancelMissedCallNotification(UserHandle userHandle) {
        // Reset the number of missed calls to 0.
        mMissedCallCounts.putIfAbsent(userHandle, new AtomicInteger(0));
        mMissedCallCounts.get(userHandle).set(0);
        synchronized(mMissedCallCountsLock) {
            mMissedCallCounts.put(userHandle, 0);
        }

        String dialerPackage = getDefaultDialerPackage(userHandle);
        if (shouldManageNotificationThroughDefaultDialer(dialerPackage, userHandle)) {
            sendNotificationThroughDefaultDialer(dialerPackage, null, userHandle);
            sendNotificationThroughDefaultDialer(dialerPackage, null, userHandle,
                    0 /* missedCallCount */);
            return;
        }

@@ -612,7 +620,9 @@ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements
                Log.d(MissedCallNotifierImpl.this, "onQueryComplete()...");
                if (cursor != null) {
                    try {
                        synchronized(mMissedCallCountsLock) {
                            mMissedCallCounts.remove(userHandle);
                        }
                        while (cursor.moveToNext()) {
                            // Get data about the missed call from the cursor
                            final String handleString = cursor.getString(CALL_LOG_COLUMN_NUMBER);
+50 −0
Original line number Diff line number Diff line
@@ -29,6 +29,8 @@ import android.content.Context;
import android.content.IContentProvider;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
@@ -65,11 +67,13 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -79,6 +83,7 @@ import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isNull;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -132,6 +137,7 @@ public class MissedCallNotifierImplTest extends TelecomTestCase {
        }
    }

    private static final long TIMEOUT_DELAY = 5000;
    private static final Uri TEL_CALL_HANDLE = Uri.parse("tel:+11915552620");
    private static final Uri SIP_CALL_HANDLE = Uri.parse("sip:testaddress@testdomain.com");
    private static final String CALLER_NAME = "Fake Name";
@@ -139,6 +145,7 @@ public class MissedCallNotifierImplTest extends TelecomTestCase {
    private static final String MISSED_CALLS_TITLE = "Missed Calls";
    private static final String MISSED_CALLS_MSG = "%s missed calls";
    private static final String USER_CALL_ACTIVITY_LABEL = "Phone";
    private static final String DEFAULT_DIALER_PACKAGE = "com.android.server.telecom.test";

    private static final int REQUEST_ID = 0;
    private static final long CALL_TIMESTAMP;
@@ -213,6 +220,49 @@ public class MissedCallNotifierImplTest extends TelecomTestCase {
        cancelNotificationTestInternal(SECONARY_USER);
    }

    @SmallTest
    @Test
    public void testDefaultDialerClear() {
        MissedCallNotifier missedCallNotifier = setupMissedCallNotificationThroughDefaultDialer();
        missedCallNotifier.clearMissedCalls(PRIMARY_USER);

        ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
        verify(mContext).sendBroadcastAsUser(intentArgumentCaptor.capture(), any(),
                anyString(), any());
        Intent sentIntent = intentArgumentCaptor.getValue();
        assertEquals(0, sentIntent.getIntExtra(TelecomManager.EXTRA_NOTIFICATION_COUNT, -1));
    }

    @SmallTest
    @Test
    public void testDefaultDialerIncrement() {
        MissedCallNotifier missedCallNotifier = setupMissedCallNotificationThroughDefaultDialer();
        PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER, NO_CAPABILITY);
        MissedCallNotifier.CallInfo fakeCall = makeFakeCallInfo(TEL_CALL_HANDLE, CALLER_NAME,
                CALL_TIMESTAMP, phoneAccount.getAccountHandle());

        missedCallNotifier.showMissedCallNotification(fakeCall);
        ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
        verify(mContext).sendBroadcastAsUser(intentArgumentCaptor.capture(), any(),
                anyString(), any());

        Intent sentIntent = intentArgumentCaptor.getValue();
        assertEquals(1, sentIntent.getIntExtra(TelecomManager.EXTRA_NOTIFICATION_COUNT, -1));
    }

    private MissedCallNotifier setupMissedCallNotificationThroughDefaultDialer() {
        mComponentContextFixture.addIntentReceiver(
                TelecomManager.ACTION_SHOW_MISSED_CALLS_NOTIFICATION, COMPONENT_NAME);
        when(mDefaultDialerCache.getDefaultDialerApplication(anyInt())).thenReturn(
                DEFAULT_DIALER_PACKAGE);

        Notification.Builder builder1 = makeNotificationBuilder("builder1");
        Notification.Builder builder2 = makeNotificationBuilder("builder2");
        MissedCallNotifierImpl.NotificationBuilderFactory fakeBuilderFactory =
                makeNotificationBuilderFactory(builder1, builder1, builder2, builder2);
        return makeMissedCallNotifier(fakeBuilderFactory, PRIMARY_USER);
    }

    private void cancelNotificationTestInternal(UserHandle userHandle) {
        Notification.Builder builder1 = makeNotificationBuilder("builder1");
        Notification.Builder builder2 = makeNotificationBuilder("builder2");