Handle start and end times of convo statuses

Test: atest DataManagerTest
Bug: 163617224
Change-Id: Iab2139740d427001f64b5666f791ba8890ea9cf9
This commit is contained in:
Julia Reynolds
2021-01-11 16:12:11 -05:00
parent 9e4b9c84da
commit dfbdf8bc8b
4 changed files with 196 additions and 3 deletions

View File

@@ -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);
}

View File

@@ -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();
}
}
}

View File

@@ -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);
});

View File

@@ -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