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

Commit 9032a61e authored by Julia Reynolds's avatar Julia Reynolds Committed by Android (Google) Code Review
Browse files

Merge "Handle start and end times of convo statuses"

parents e2abb20e dfbdf8bc
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