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

Commit dfbdf8bc authored by Julia Reynolds's avatar Julia Reynolds
Browse files

Handle start and end times of convo statuses

Test: atest DataManagerTest
Bug: 163617224
Change-Id: Iab2139740d427001f64b5666f791ba8890ea9cf9
parent 9e4b9c84
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -188,6 +188,9 @@ public class PeopleService extends SystemService {
                ConversationStatus status) {
            handleIncomingUser(userId);
            checkCallerIsSameApp(packageName);
            if (status.getStartTimeMillis() > System.currentTimeMillis()) {
                throw new IllegalArgumentException("Start time must be in the past");
            }
            mDataManager.addOrUpdateStatus(packageName, userId, conversationId, status);
        }

+98 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.server.people.data;

import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.app.people.ConversationStatus;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.CancellationSignal;
import android.os.SystemClock;

import com.android.server.LocalServices;
import com.android.server.notification.NotificationRecord;
import com.android.server.people.PeopleServiceInternal;

import java.util.concurrent.TimeUnit;

/**
 * If a {@link ConversationStatus} is added to the system with an expiration time, remove that
 * status at that time
 */
public class ConversationStatusExpirationBroadcastReceiver extends BroadcastReceiver {

    static final String ACTION = "ConversationStatusExpiration";
    static final String EXTRA_USER_ID = "userId";
    static final int REQUEST_CODE = 10;
    static final String SCHEME = "expStatus";

    void scheduleExpiration(Context context, @UserIdInt int userId, String pkg,
            String conversationId, ConversationStatus status) {

        final PendingIntent pi = PendingIntent.getBroadcast(context,
                REQUEST_CODE,
                new Intent(ACTION)
                        .setData(new Uri.Builder().scheme(SCHEME)
                                .appendPath(getKey(userId, pkg, conversationId, status))
                                .build())
                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
                        .putExtra(EXTRA_USER_ID, userId),
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
        context.getSystemService(AlarmManager.class).setExactAndAllowWhileIdle(
                AlarmManager.RTC_WAKEUP, status.getEndTimeMillis(), pi);
    }

    private static String getKey(@UserIdInt int userId, String pkg,
            String conversationId, ConversationStatus status) {
        return userId + pkg + conversationId + status.getId();
    }

    static IntentFilter getFilter() {
        IntentFilter conversationStatusFilter =
                new IntentFilter(ConversationStatusExpirationBroadcastReceiver.ACTION);
        conversationStatusFilter.addDataScheme(
                ConversationStatusExpirationBroadcastReceiver.SCHEME);
        return conversationStatusFilter;
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (action == null) {
            return;
        }
        if (ACTION.equals(action)) {
            new Thread(() -> {
                PeopleServiceInternal peopleServiceInternal =
                        LocalServices.getService(PeopleServiceInternal.class);
                peopleServiceInternal.pruneDataForUser(intent.getIntExtra(EXTRA_USER_ID,
                        ActivityManager.getCurrentUser()), new CancellationSignal());
            }).start();
        }
    }
}
+33 −0
Original line number Diff line number Diff line
@@ -124,6 +124,7 @@ public class DataManager {
    private PackageManagerInternal mPackageManagerInternal;
    private NotificationManagerInternal mNotificationManagerInternal;
    private UserManager mUserManager;
    private ConversationStatusExpirationBroadcastReceiver mStatusExpReceiver;

    public DataManager(Context context) {
        this(context, new Injector());
@@ -145,6 +146,10 @@ public class DataManager {

        mShortcutServiceInternal.addShortcutChangeCallback(new ShortcutServiceCallback());

        mStatusExpReceiver = new ConversationStatusExpirationBroadcastReceiver();
        mContext.registerReceiver(mStatusExpReceiver,
                ConversationStatusExpirationBroadcastReceiver.getFilter());

        IntentFilter shutdownIntentFilter = new IntentFilter(Intent.ACTION_SHUTDOWN);
        BroadcastReceiver shutdownBroadcastReceiver = new ShutdownBroadcastReceiver();
        mContext.registerReceiver(shutdownBroadcastReceiver, shutdownIntentFilter);
@@ -295,6 +300,27 @@ public class DataManager {
        });
    }

    /**
     * Removes any status with an expiration time in the past.
     */
    public void pruneExpiredConversationStatuses(@UserIdInt int callingUserId, long currentTimeMs) {
        forPackagesInProfile(callingUserId, packageData -> {
            final ConversationStore cs = packageData.getConversationStore();
            packageData.forAllConversations(conversationInfo -> {
                ConversationInfo.Builder builder = new ConversationInfo.Builder(conversationInfo);
                List<ConversationStatus> newStatuses = new ArrayList<>();
                for (ConversationStatus status : conversationInfo.getStatuses()) {
                    if (status.getEndTimeMillis() < 0
                            || currentTimeMs < status.getEndTimeMillis()) {
                        newStatuses.add(status);
                    }
                }
                builder.setStatuses(newStatuses);
                cs.addOrUpdate(builder.build());
            });
        });
    }

    /**
     * Returns the last notification interaction with the specified conversation. If the
     * conversation can't be found or no interactions have been recorded, returns 0L.
@@ -317,6 +343,12 @@ public class DataManager {
        ConversationInfo.Builder builder = new ConversationInfo.Builder(convToModify);
        builder.addOrUpdateStatus(status);
        cs.addOrUpdate(builder.build());

        if (status.getEndTimeMillis() >= 0) {
            mStatusExpReceiver.scheduleExpiration(
                    mContext, userId, packageName, conversationId, status);
        }

    }

    public void clearStatus(String packageName, int userId, String conversationId,
@@ -458,6 +490,7 @@ public class DataManager {
                packageData.getEventStore().deleteEventHistories(EventStore.CATEGORY_SMS);
            }
            packageData.pruneOrphanEvents();
            pruneExpiredConversationStatuses(userId, System.currentTimeMillis());
            pruneOldRecentConversations(userId, System.currentTimeMillis());
            cleanupCachedShortcuts(userId, MAX_CACHED_RECENT_SHORTCUTS);
        });
+62 −3
Original line number Diff line number Diff line
@@ -42,12 +42,14 @@ import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@@ -141,6 +143,7 @@ public final class DataManagerTest {
    @Mock private JobScheduler mJobScheduler;
    @Mock private StatusBarNotification mStatusBarNotification;
    @Mock private Notification mNotification;
    @Mock private AlarmManager mAlarmManager;

    @Captor private ArgumentCaptor<ShortcutChangeCallback> mShortcutChangeCallbackCaptor;
    @Captor private ArgumentCaptor<BroadcastReceiver> mBroadcastReceiverCaptor;
@@ -152,7 +155,6 @@ public final class DataManagerTest {
    private DataManager mDataManager;
    private CancellationSignal mCancellationSignal;
    private ShortcutChangeCallback mShortcutChangeCallback;
    private BroadcastReceiver mShutdownBroadcastReceiver;
    private ShortcutInfo mShortcutInfo;
    private TestInjector mInjector;

@@ -187,10 +189,15 @@ public final class DataManagerTest {

        Context originalContext = getInstrumentation().getTargetContext();
        when(mContext.getApplicationInfo()).thenReturn(originalContext.getApplicationInfo());
        when(mContext.getUser()).thenReturn(originalContext.getUser());
        when(mContext.getPackageName()).thenReturn(originalContext.getPackageName());

        when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
        when(mContext.getSystemServiceName(UserManager.class)).thenReturn(
                Context.USER_SERVICE);
        when(mContext.getSystemService(Context.ALARM_SERVICE)).thenReturn(mAlarmManager);
        when(mContext.getSystemServiceName(AlarmManager.class)).thenReturn(
                Context.ALARM_SERVICE);

        when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephonyManager);

@@ -246,8 +253,7 @@ public final class DataManagerTest {
                mShortcutChangeCallbackCaptor.capture());
        mShortcutChangeCallback = mShortcutChangeCallbackCaptor.getValue();

        verify(mContext).registerReceiver(mBroadcastReceiverCaptor.capture(), any());
        mShutdownBroadcastReceiver = mBroadcastReceiverCaptor.getValue();
        verify(mContext, times(2)).registerReceiver(any(), any());
    }

    @After
@@ -766,6 +772,36 @@ public final class DataManagerTest {
        assertTrue(activeTimeSlots.isEmpty());
    }

    @Test
    public void testPruneExpiredConversationStatuses() {
        mDataManager.onUserUnlocked(USER_ID_PRIMARY);

        ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
                buildPerson());
        mDataManager.addOrUpdateConversationInfo(shortcut);

        ConversationStatus cs1 = new ConversationStatus.Builder("cs1", 9)
                .setEndTimeMillis(System.currentTimeMillis())
                .build();
        ConversationStatus cs2 = new ConversationStatus.Builder("cs2", 10)
                .build();
        ConversationStatus cs3 = new ConversationStatus.Builder("cs3", 1)
                .setEndTimeMillis(Long.MAX_VALUE)
                .build();
        mDataManager.addOrUpdateStatus(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID, cs1);
        mDataManager.addOrUpdateStatus(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID, cs2);
        mDataManager.addOrUpdateStatus(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID, cs3);

        mDataManager.pruneDataForUser(USER_ID_PRIMARY, mCancellationSignal);

        assertThat(mDataManager.getStatuses(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID))
                .doesNotContain(cs1);
        assertThat(mDataManager.getStatuses(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID))
                .contains(cs2);
        assertThat(mDataManager.getStatuses(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID))
                .contains(cs3);
    }

    @Test
    public void testDoNotUncacheShortcutWithActiveNotifications() {
        mDataManager.onUserUnlocked(USER_ID_PRIMARY);
@@ -976,6 +1012,29 @@ public final class DataManagerTest {
                .contains(cs);
        assertThat(mDataManager.getStatuses(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID))
                .contains(cs2);

        verify(mAlarmManager, never()).setExactAndAllowWhileIdle(anyInt(), anyLong(), any());
    }

    @Test
    public void testAddOrUpdateStatus_schedulesJob() {
        mDataManager.onUserUnlocked(USER_ID_PRIMARY);

        ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
                buildPerson());
        mDataManager.addOrUpdateConversationInfo(shortcut);

        ConversationStatus cs = new ConversationStatus.Builder("id", ACTIVITY_ANNIVERSARY)
                .setEndTimeMillis(1000)
                .build();
        mDataManager.addOrUpdateStatus(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID, cs);

        ConversationStatus cs2 = new ConversationStatus.Builder("id2", ACTIVITY_GAME)
                .setEndTimeMillis(3000)
                .build();
        mDataManager.addOrUpdateStatus(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID, cs2);

        verify(mAlarmManager, times(2)).setExactAndAllowWhileIdle(anyInt(), anyLong(), any());
    }

    @Test