diff --git a/services/people/java/com/android/server/people/PeopleService.java b/services/people/java/com/android/server/people/PeopleService.java index 16b9165915e29..091e688743ddd 100644 --- a/services/people/java/com/android/server/people/PeopleService.java +++ b/services/people/java/com/android/server/people/PeopleService.java @@ -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); } diff --git a/services/people/java/com/android/server/people/data/ConversationStatusExpirationBroadcastReceiver.java b/services/people/java/com/android/server/people/data/ConversationStatusExpirationBroadcastReceiver.java new file mode 100644 index 0000000000000..c631026f43891 --- /dev/null +++ b/services/people/java/com/android/server/people/data/ConversationStatusExpirationBroadcastReceiver.java @@ -0,0 +1,98 @@ +/* + * 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(); + } + } +} diff --git a/services/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java index e04e287d80f63..752141598c9d1 100644 --- a/services/people/java/com/android/server/people/data/DataManager.java +++ b/services/people/java/com/android/server/people/data/DataManager.java @@ -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 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); }); diff --git a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java index be8a99c5ce363..63330d5182977 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java @@ -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 mShortcutChangeCallbackCaptor; @Captor private ArgumentCaptor 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