Add a JobScheduler service to prune the People Service data on a daily basis

- Refactor the class EventStore to simplify the event history access methods
- Add the prune methods to delete the data for:
  - uninstalled packages
  - event data that exceeds the retention period
  - call events data for non-dialer packages
  - SMS events data for non-SMS-app packages
  - orphan events (the annotated locus ID or phone number have  changed in the associated shortcut)
- Change the package-level events to class-level for the app share events

Bug: 146522621
Test: Manually verify the behavior on device \
    atest com.android.server.people.data.DataManagerTest \
    atest com.android.server.people.data.PackageDataTest
Change-Id: I3f17ed269fe5cb05f1d3696e44dfb0bed951678f
This commit is contained in:
Danning Chen
2020-02-07 17:49:42 -08:00
committed by Trung Lam
parent b82b55f30d
commit c4933eb5b7
15 changed files with 593 additions and 111 deletions

View File

@@ -5344,6 +5344,10 @@
android:permission="android.permission.BIND_JOB_SERVICE" >
</service>
<service android:name="com.android.server.people.data.DataMaintenanceService"
android:permission="android.permission.BIND_JOB_SERVICE" >
</service>
<service
android:name="com.android.server.autofill.AutofillCompatAccessibilityService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"

View File

@@ -17,6 +17,8 @@
package com.android.server.people;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.os.CancellationSignal;
import android.service.appprediction.IPredictionService;
/**
@@ -24,17 +26,24 @@ import android.service.appprediction.IPredictionService;
*/
public abstract class PeopleServiceInternal extends IPredictionService.Stub {
/**
* Prunes the data for the specified user. Called by {@link
* com.android.server.people.data.DataMaintenanceService} when the device is idle.
*/
public abstract void pruneDataForUser(@UserIdInt int userId,
@NonNull CancellationSignal signal);
/**
* The number conversation infos will be dynamic, based on the currently installed apps on the
* device. All of which should be combined into a single blob to be backed up.
*/
public abstract byte[] backupConversationInfos(@NonNull int userId);
public abstract byte[] backupConversationInfos(@UserIdInt int userId);
/**
* Multiple conversation infos may exist in the restore payload, child classes are required to
* manage the restoration based on how individual conversation infos were originally combined
* during backup.
*/
public abstract void restoreConversationInfos(@NonNull int userId, @NonNull String key,
public abstract void restoreConversationInfos(@UserIdInt int userId, @NonNull String key,
@NonNull byte[] payload);
}

View File

@@ -17,6 +17,7 @@
package com.android.server.people;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.prediction.AppPredictionContext;
import android.app.prediction.AppPredictionSessionId;
import android.app.prediction.AppTarget;
@@ -24,6 +25,7 @@ import android.app.prediction.AppTargetEvent;
import android.app.prediction.IPredictionCallback;
import android.content.Context;
import android.content.pm.ParceledListSlice;
import android.os.CancellationSignal;
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.Slog;
@@ -138,6 +140,21 @@ public class PeopleService extends SystemService {
});
}
@Override
public void pruneDataForUser(@UserIdInt int userId, @NonNull CancellationSignal signal) {
mDataManager.pruneDataForUser(userId, signal);
}
@Override
public byte[] backupConversationInfos(@UserIdInt int userId) {
return new byte[0];
}
@Override
public void restoreConversationInfos(@UserIdInt int userId, @NonNull String key,
@NonNull byte[] payload) {
}
@VisibleForTesting
SessionInfo getSessionInfo(AppPredictionSessionId sessionId) {
return mSessions.get(sessionId);
@@ -160,14 +177,5 @@ public class PeopleService extends SystemService {
Slog.e(TAG, "Failed to calling callback" + e);
}
}
@Override
public byte[] backupConversationInfos(int userId) {
return new byte[0];
}
@Override
public void restoreConversationInfos(int userId, String key, byte[] payload) {
}
}
}

View File

@@ -133,10 +133,11 @@ class ConversationStore {
}
@MainThread
synchronized void deleteConversation(@NonNull String shortcutId) {
@Nullable
synchronized ConversationInfo deleteConversation(@NonNull String shortcutId) {
ConversationInfo conversationInfo = mConversationInfoMap.remove(shortcutId);
if (conversationInfo == null) {
return;
return null;
}
LocusId locusId = conversationInfo.getLocusId();
@@ -159,6 +160,7 @@ class ConversationStore {
mNotifChannelIdToShortcutIdMap.remove(notifChannelId);
}
scheduleUpdateConversationsOnDisk();
return conversationInfo;
}
synchronized void forAllConversations(@NonNull Consumer<ConversationInfo> consumer) {

View File

@@ -0,0 +1,92 @@
/*
* Copyright (C) 2020 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.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.content.ComponentName;
import android.content.Context;
import android.os.CancellationSignal;
import com.android.server.LocalServices;
import com.android.server.people.PeopleServiceInternal;
import java.util.concurrent.TimeUnit;
/**
* This service runs periodically to ensure the data consistency and the retention policy for a
* specific user's data.
*/
public class DataMaintenanceService extends JobService {
private static final long JOB_RUN_INTERVAL = TimeUnit.HOURS.toMillis(24);
/** This job ID must be unique within the system server. */
private static final int BASE_JOB_ID = 0xC315BD7; // 204561367
static void scheduleJob(Context context, @UserIdInt int userId) {
int jobId = getJobId(userId);
JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
if (jobScheduler.getPendingJob(jobId) == null) {
ComponentName component = new ComponentName(context, DataMaintenanceService.class);
JobInfo newJob = new JobInfo.Builder(jobId, component)
.setRequiresDeviceIdle(true)
.setPeriodic(JOB_RUN_INTERVAL)
.build();
jobScheduler.schedule(newJob);
}
}
static void cancelJob(Context context, @UserIdInt int userId) {
JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
jobScheduler.cancel(getJobId(userId));
}
private CancellationSignal mSignal;
@Override
public boolean onStartJob(JobParameters params) {
int userId = getUserId(params.getJobId());
mSignal = new CancellationSignal();
new Thread(() -> {
PeopleServiceInternal peopleServiceInternal =
LocalServices.getService(PeopleServiceInternal.class);
peopleServiceInternal.pruneDataForUser(userId, mSignal);
jobFinished(params, mSignal.isCanceled());
}).start();
return true;
}
@Override
public boolean onStopJob(JobParameters params) {
if (mSignal != null) {
mSignal.cancel();
}
return false;
}
private static int getJobId(@UserIdInt int userId) {
return BASE_JOB_ID + userId;
}
private static @UserIdInt int getUserId(int jobId) {
return jobId - BASE_JOB_ID;
}
}

View File

@@ -32,6 +32,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.LauncherApps.ShortcutQuery;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.content.pm.ShortcutManager.ShareShortcutInfo;
@@ -40,6 +41,7 @@ import android.content.pm.UserInfo;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Binder;
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.Process;
import android.os.RemoteException;
@@ -53,16 +55,20 @@ import android.service.notification.StatusBarNotification;
import android.telecom.TelecomManager;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.ArraySet;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.ChooserActivity;
import com.android.internal.content.PackageMonitor;
import com.android.internal.os.BackgroundThread;
import com.android.internal.telephony.SmsApplication;
import com.android.server.LocalServices;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
@@ -94,10 +100,12 @@ public class DataManager {
private final SparseArray<ScheduledFuture<?>> mUsageStatsQueryFutures = new SparseArray<>();
private final SparseArray<NotificationListenerService> mNotificationListeners =
new SparseArray<>();
private final SparseArray<PackageMonitor> mPackageMonitors = new SparseArray<>();
private final ContentObserver mCallLogContentObserver;
private final ContentObserver mMmsSmsContentObserver;
private ShortcutServiceInternal mShortcutServiceInternal;
private PackageManagerInternal mPackageManagerInternal;
private ShortcutManager mShortcutManager;
private UserManager mUserManager;
@@ -120,6 +128,7 @@ public class DataManager {
/** Initialization. Called when the system services are up running. */
public void initialize() {
mShortcutServiceInternal = LocalServices.getService(ShortcutServiceInternal.class);
mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
mShortcutManager = mContext.getSystemService(ShortcutManager.class);
mUserManager = mContext.getSystemService(UserManager.class);
@@ -172,6 +181,10 @@ public class DataManager {
// Should never occur for local calls.
}
PackageMonitor packageMonitor = new PerUserPackageMonitor();
packageMonitor.register(mContext, null, UserHandle.of(userId), true);
mPackageMonitors.put(userId, packageMonitor);
if (userId == UserHandle.USER_SYSTEM) {
// The call log and MMS/SMS messages are shared across user profiles. So only need to
// register the content observers once for the primary user.
@@ -183,6 +196,8 @@ public class DataManager {
MmsSms.CONTENT_URI, /* notifyForDescendants= */ false,
mMmsSmsContentObserver, UserHandle.USER_SYSTEM);
}
DataMaintenanceService.scheduleJob(mContext, userId);
}
/** This method is called when a user is stopped. */
@@ -207,10 +222,15 @@ public class DataManager {
// Should never occur for local calls.
}
}
if (mPackageMonitors.indexOfKey(userId) >= 0) {
mPackageMonitors.get(userId).unregister();
}
if (userId == UserHandle.USER_SYSTEM) {
mContext.getContentResolver().unregisterContentObserver(mCallLogContentObserver);
mContext.getContentResolver().unregisterContentObserver(mMmsSmsContentObserver);
}
DataMaintenanceService.cancelJob(mContext, userId);
}
/**
@@ -274,9 +294,8 @@ public class DataManager {
|| TextUtils.isEmpty(mimeType)) {
return;
}
EventHistoryImpl eventHistory =
packageData.getEventStore().getOrCreateShortcutEventHistory(
shortcutInfo.getId());
EventHistoryImpl eventHistory = packageData.getEventStore().getOrCreateEventHistory(
EventStore.CATEGORY_SHORTCUT_BASED, shortcutInfo.getId());
@Event.EventType int eventType;
if (mimeType.startsWith("text/")) {
eventType = Event.TYPE_SHARE_TEXT;
@@ -291,6 +310,45 @@ public class DataManager {
}
}
/** Prunes the data for the specified user. */
public void pruneDataForUser(@UserIdInt int userId, @NonNull CancellationSignal signal) {
UserData userData = getUnlockedUserData(userId);
if (userData == null || signal.isCanceled()) {
return;
}
pruneUninstalledPackageData(userData);
long currentTimeMillis = System.currentTimeMillis();
userData.forAllPackages(packageData -> {
if (signal.isCanceled()) {
return;
}
packageData.getEventStore().pruneOldEvents(currentTimeMillis);
if (!packageData.isDefaultDialer()) {
packageData.getEventStore().deleteEventHistories(EventStore.CATEGORY_CALL);
}
if (!packageData.isDefaultSmsApp()) {
packageData.getEventStore().deleteEventHistories(EventStore.CATEGORY_SMS);
}
packageData.pruneOrphanEvents();
});
}
private void pruneUninstalledPackageData(@NonNull UserData userData) {
Set<String> installApps = new ArraySet<>();
mPackageManagerInternal.forEachInstalledPackage(
pkg -> installApps.add(pkg.getPackageName()), userData.getUserId());
List<String> packagesToDelete = new ArrayList<>();
userData.forAllPackages(packageData -> {
if (!installApps.contains(packageData.getPackageName())) {
packagesToDelete.add(packageData.getPackageName());
}
});
for (String packageName : packagesToDelete) {
userData.deletePackageData(packageName);
}
}
/** Gets a list of {@link ShortcutInfo}s with the given shortcut IDs. */
private List<ShortcutInfo> getShortcuts(
@NonNull String packageName, @UserIdInt int userId,
@@ -347,7 +405,8 @@ public class DataManager {
|| packageData.getConversationStore().getConversation(shortcutId) == null) {
return null;
}
return packageData.getEventStore().getOrCreateShortcutEventHistory(shortcutId);
return packageData.getEventStore().getOrCreateEventHistory(
EventStore.CATEGORY_SHORTCUT_BASED, shortcutId);
}
@VisibleForTesting
@@ -412,6 +471,11 @@ public class DataManager {
return mNotificationListeners.get(userId);
}
@VisibleForTesting
PackageMonitor getPackageMonitorForTesting(@UserIdInt int userId) {
return mPackageMonitors.get(userId);
}
@VisibleForTesting
UserData getUserDataForTesting(@UserIdInt int userId) {
return mUserDataArray.get(userId);
@@ -499,7 +563,8 @@ public class DataManager {
return;
}
EventStore eventStore = defaultDialer.getEventStore();
eventStore.getOrCreateCallEventHistory(phoneNumber).addEvent(event);
eventStore.getOrCreateEventHistory(
EventStore.CATEGORY_CALL, phoneNumber).addEvent(event);
});
}
}
@@ -544,7 +609,8 @@ public class DataManager {
return;
}
EventStore eventStore = defaultSmsApp.getEventStore();
eventStore.getOrCreateSmsEventHistory(phoneNumber).addEvent(event);
eventStore.getOrCreateEventHistory(
EventStore.CATEGORY_SMS, phoneNumber).addEvent(event);
});
}
}
@@ -670,6 +736,20 @@ public class DataManager {
}
}
private class PerUserPackageMonitor extends PackageMonitor {
@Override
public void onPackageRemoved(String packageName, int uid) {
super.onPackageRemoved(packageName, uid);
int userId = getChangingUserId();
UserData userData = getUnlockedUserData(userId);
if (userData != null) {
userData.deletePackageData(packageName);
}
}
}
private class ShutdownBroadcastReceiver extends BroadcastReceiver {
@Override

View File

@@ -78,6 +78,17 @@ class EventHistoryImpl implements EventHistory {
mRecentEvents.add(event);
}
void onDestroy() {
mEventIndexArray.clear();
mRecentEvents.clear();
// TODO: STOPSHIP: Delete the data files.
}
/** Deletes the events data that exceeds the retention period. */
void pruneOldEvents(long currentTimeMillis) {
// TODO: STOPSHIP: Delete the old events data files.
}
@VisibleForTesting
static class Injector {

View File

@@ -69,6 +69,10 @@ class EventList {
return result;
}
void clear() {
mEvents.clear();
}
/** Returns the first index whose timestamp is greater or equal to the provided timestamp. */
private int firstIndexOnOrAfter(long timestamp) {
int result = mEvents.size();

View File

@@ -16,99 +16,129 @@
package com.android.server.people.data;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.LocusId;
import android.util.ArrayMap;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
/** The store that stores and accesses the events data for a package. */
class EventStore {
private final EventHistoryImpl mPackageEventHistory = new EventHistoryImpl();
/** The events that are queryable with a shortcut ID. */
static final int CATEGORY_SHORTCUT_BASED = 0;
// Shortcut ID -> Event History
private final Map<String, EventHistoryImpl> mShortcutEventHistoryMap = new ArrayMap<>();
/** The events that are queryable with a {@link android.content.LocusId}. */
static final int CATEGORY_LOCUS_ID_BASED = 1;
// Locus ID -> Event History
private final Map<LocusId, EventHistoryImpl> mLocusEventHistoryMap = new ArrayMap<>();
/** The phone call events that are queryable with a phone number. */
static final int CATEGORY_CALL = 2;
// Phone Number -> Event History
private final Map<String, EventHistoryImpl> mCallEventHistoryMap = new ArrayMap<>();
/** The SMS or MMS events that are queryable with a phone number. */
static final int CATEGORY_SMS = 3;
// Phone Number -> Event History
private final Map<String, EventHistoryImpl> mSmsEventHistoryMap = new ArrayMap<>();
/** The events that are queryable with an {@link android.app.Activity} class name. */
static final int CATEGORY_CLASS_BASED = 4;
/** Gets the package level {@link EventHistory}. */
@NonNull
EventHistory getPackageEventHistory() {
return mPackageEventHistory;
}
@IntDef(prefix = { "CATEGORY_" }, value = {
CATEGORY_SHORTCUT_BASED,
CATEGORY_LOCUS_ID_BASED,
CATEGORY_CALL,
CATEGORY_SMS,
CATEGORY_CLASS_BASED,
})
@Retention(RetentionPolicy.SOURCE)
@interface EventCategory {}
/** Gets the {@link EventHistory} for the specified {@code shortcutId} if exists. */
@Nullable
EventHistory getShortcutEventHistory(String shortcutId) {
return mShortcutEventHistoryMap.get(shortcutId);
}
private final List<Map<String, EventHistoryImpl>> mEventHistoryMaps = new ArrayList<>();
/** Gets the {@link EventHistory} for the specified {@code locusId} if exists. */
@Nullable
EventHistory getLocusEventHistory(LocusId locusId) {
return mLocusEventHistoryMap.get(locusId);
}
/** Gets the phone call {@link EventHistory} for the specified {@code phoneNumber} if exists. */
@Nullable
EventHistory getCallEventHistory(String phoneNumber) {
return mCallEventHistoryMap.get(phoneNumber);
}
/** Gets the SMS {@link EventHistory} for the specified {@code phoneNumber} if exists. */
@Nullable
EventHistory getSmsEventHistory(String phoneNumber) {
return mSmsEventHistoryMap.get(phoneNumber);
EventStore() {
mEventHistoryMaps.add(CATEGORY_SHORTCUT_BASED, new ArrayMap<>());
mEventHistoryMaps.add(CATEGORY_LOCUS_ID_BASED, new ArrayMap<>());
mEventHistoryMaps.add(CATEGORY_CALL, new ArrayMap<>());
mEventHistoryMaps.add(CATEGORY_SMS, new ArrayMap<>());
mEventHistoryMaps.add(CATEGORY_CLASS_BASED, new ArrayMap<>());
}
/**
* Gets the {@link EventHistoryImpl} for the specified {@code shortcutId} or creates a new
* instance and put it into the store if not exists. The caller needs to verify if a
* conversation with this shortcut ID exists before calling this method.
* Gets the {@link EventHistory} for the specified key if exists.
*
* @param key Category-specific key, it can be shortcut ID, locus ID, phone number, or class
* name.
*/
@NonNull
EventHistoryImpl getOrCreateShortcutEventHistory(String shortcutId) {
return mShortcutEventHistoryMap.computeIfAbsent(shortcutId, key -> new EventHistoryImpl());
@Nullable
EventHistory getEventHistory(@EventCategory int category, String key) {
return mEventHistoryMaps.get(category).get(key);
}
/**
* Gets the {@link EventHistoryImpl} for the specified {@code locusId} or creates a new
* instance and put it into the store if not exists. The caller needs to ensure a conversation
* with this locus ID exists before calling this method.
* Gets the {@link EventHistoryImpl} for the specified ID or creates a new instance and put it
* into the store if not exists. The caller needs to verify if the associated conversation
* exists before calling this method.
*
* @param key Category-specific key, it can be shortcut ID, locus ID, phone number, or class
* name.
*/
@NonNull
EventHistoryImpl getOrCreateLocusEventHistory(LocusId locusId) {
return mLocusEventHistoryMap.computeIfAbsent(locusId, key -> new EventHistoryImpl());
EventHistoryImpl getOrCreateEventHistory(@EventCategory int category, String key) {
return mEventHistoryMaps.get(category).computeIfAbsent(key, k -> new EventHistoryImpl());
}
/**
* Gets the {@link EventHistoryImpl} for the specified {@code phoneNumber} for call events
* or creates a new instance and put it into the store if not exists. The caller needs to ensure
* a conversation with this phone number exists and this package is the default dialer
* before calling this method.
* Deletes the events and index data for the specified key.
*
* @param key Category-specific key, it can be shortcut ID, locus ID, phone number, or class
* name.
*/
@NonNull
EventHistoryImpl getOrCreateCallEventHistory(String phoneNumber) {
return mCallEventHistoryMap.computeIfAbsent(phoneNumber, key -> new EventHistoryImpl());
void deleteEventHistory(@EventCategory int category, String key) {
EventHistoryImpl eventHistory = mEventHistoryMaps.get(category).remove(key);
if (eventHistory != null) {
eventHistory.onDestroy();
}
}
/** Deletes all the events and index data for the specified category from disk. */
void deleteEventHistories(@EventCategory int category) {
mEventHistoryMaps.get(category).clear();
// TODO: Implement this method to delete the data from disk.
}
/** Deletes the events data that exceeds the retention period. */
void pruneOldEvents(long currentTimeMillis) {
for (Map<String, EventHistoryImpl> map : mEventHistoryMaps) {
for (EventHistoryImpl eventHistory : map.values()) {
eventHistory.pruneOldEvents(currentTimeMillis);
}
}
}
/**
* Gets the {@link EventHistoryImpl} for the specified {@code phoneNumber} for SMS events
* or creates a new instance and put it into the store if not exists. The caller needs to ensure
* a conversation with this phone number exists and this package is the default SMS app
* before calling this method.
* Prunes the event histories whose key (shortcut ID, locus ID or phone number) does not match
* any conversations.
*
* @param keyChecker Check whether there exists a conversation contains this key.
*/
@NonNull
EventHistoryImpl getOrCreateSmsEventHistory(String phoneNumber) {
return mSmsEventHistoryMap.computeIfAbsent(phoneNumber, key -> new EventHistoryImpl());
void pruneOrphanEventHistories(@EventCategory int category, Predicate<String> keyChecker) {
Set<String> keys = mEventHistoryMaps.get(category).keySet();
List<String> keysToDelete = new ArrayList<>();
for (String key : keys) {
if (!keyChecker.test(key)) {
keysToDelete.add(key);
}
}
Map<String, EventHistoryImpl> eventHistoryMap = mEventHistoryMaps.get(category);
for (String key : keysToDelete) {
EventHistoryImpl eventHistory = eventHistoryMap.remove(key);
if (eventHistory != null) {
eventHistory.onDestroy();
}
}
}
}

View File

@@ -16,6 +16,12 @@
package com.android.server.people.data;
import static com.android.server.people.data.EventStore.CATEGORY_CALL;
import static com.android.server.people.data.EventStore.CATEGORY_CLASS_BASED;
import static com.android.server.people.data.EventStore.CATEGORY_LOCUS_ID_BASED;
import static com.android.server.people.data.EventStore.CATEGORY_SHORTCUT_BASED;
import static com.android.server.people.data.EventStore.CATEGORY_SMS;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
@@ -89,11 +95,6 @@ public class PackageData {
mConversationStore.forAllConversations(consumer);
}
@NonNull
public EventHistory getPackageLevelEventHistory() {
return getEventStore().getPackageEventHistory();
}
/**
* Gets the {@link ConversationInfo} for a given shortcut ID. Returns null if such as {@link
* ConversationInfo} does not exist.
@@ -117,14 +118,16 @@ public class PackageData {
return result;
}
EventHistory shortcutEventHistory = getEventStore().getShortcutEventHistory(shortcutId);
EventHistory shortcutEventHistory = getEventStore().getEventHistory(
CATEGORY_SHORTCUT_BASED, shortcutId);
if (shortcutEventHistory != null) {
result.addEventHistory(shortcutEventHistory);
}
LocusId locusId = conversationInfo.getLocusId();
if (locusId != null) {
EventHistory locusEventHistory = getEventStore().getLocusEventHistory(locusId);
EventHistory locusEventHistory = getEventStore().getEventHistory(
CATEGORY_LOCUS_ID_BASED, locusId.getId());
if (locusEventHistory != null) {
result.addEventHistory(locusEventHistory);
}
@@ -135,13 +138,15 @@ public class PackageData {
return result;
}
if (isDefaultDialer()) {
EventHistory callEventHistory = getEventStore().getCallEventHistory(phoneNumber);
EventHistory callEventHistory = getEventStore().getEventHistory(
CATEGORY_CALL, phoneNumber);
if (callEventHistory != null) {
result.addEventHistory(callEventHistory);
}
}
if (isDefaultSmsApp()) {
EventHistory smsEventHistory = getEventStore().getSmsEventHistory(phoneNumber);
EventHistory smsEventHistory = getEventStore().getEventHistory(
CATEGORY_SMS, phoneNumber);
if (smsEventHistory != null) {
result.addEventHistory(smsEventHistory);
}
@@ -149,6 +154,14 @@ public class PackageData {
return result;
}
/** Gets the {@link EventHistory} for a given Activity class. */
@NonNull
public EventHistory getClassLevelEventHistory(String className) {
EventHistory eventHistory = getEventStore().getEventHistory(
CATEGORY_CLASS_BASED, className);
return eventHistory != null ? eventHistory : new AggregateEventHistoryImpl();
}
public boolean isDefaultDialer() {
return mIsDefaultDialerPredicate.test(mPackageName);
}
@@ -167,6 +180,47 @@ public class PackageData {
return mEventStore;
}
/**
* Deletes all the data (including conversation, events and index) for the specified
* conversation shortcut ID.
*/
void deleteDataForConversation(String shortcutId) {
ConversationInfo conversationInfo = mConversationStore.deleteConversation(shortcutId);
if (conversationInfo == null) {
return;
}
mEventStore.deleteEventHistory(CATEGORY_SHORTCUT_BASED, shortcutId);
if (conversationInfo.getLocusId() != null) {
mEventStore.deleteEventHistory(
CATEGORY_LOCUS_ID_BASED, conversationInfo.getLocusId().getId());
}
String phoneNumber = conversationInfo.getContactPhoneNumber();
if (!TextUtils.isEmpty(phoneNumber)) {
if (isDefaultDialer()) {
mEventStore.deleteEventHistory(CATEGORY_CALL, phoneNumber);
}
if (isDefaultSmsApp()) {
mEventStore.deleteEventHistory(CATEGORY_SMS, phoneNumber);
}
}
}
/** Prunes the events and index data that don't have a associated conversation. */
void pruneOrphanEvents() {
mEventStore.pruneOrphanEventHistories(CATEGORY_SHORTCUT_BASED,
key -> mConversationStore.getConversation(key) != null);
mEventStore.pruneOrphanEventHistories(CATEGORY_LOCUS_ID_BASED,
key -> mConversationStore.getConversationByLocusId(new LocusId(key)) != null);
if (isDefaultDialer()) {
mEventStore.pruneOrphanEventHistories(CATEGORY_CALL,
key -> mConversationStore.getConversationByPhoneNumber(key) != null);
}
if (isDefaultSmsApp()) {
mEventStore.pruneOrphanEventHistories(CATEGORY_SMS,
key -> mConversationStore.getConversationByPhoneNumber(key) != null);
}
}
void onDestroy() {
// TODO: STOPSHIP: Implements this method for the case of package being uninstalled.
}

View File

@@ -129,8 +129,8 @@ class UsageStatsQueryHelper {
if (packageData.getConversationStore().getConversation(shortcutId) == null) {
return;
}
EventHistoryImpl eventHistory = packageData.getEventStore().getOrCreateShortcutEventHistory(
shortcutId);
EventHistoryImpl eventHistory = packageData.getEventStore().getOrCreateEventHistory(
EventStore.CATEGORY_SHORTCUT_BASED, shortcutId);
eventHistory.addEvent(event);
}
@@ -138,8 +138,8 @@ class UsageStatsQueryHelper {
if (packageData.getConversationStore().getConversationByLocusId(locusId) == null) {
return;
}
EventHistoryImpl eventHistory = packageData.getEventStore().getOrCreateLocusEventHistory(
locusId);
EventHistoryImpl eventHistory = packageData.getEventStore().getOrCreateEventHistory(
EventStore.CATEGORY_LOCUS_ID_BASED, locusId.getId());
eventHistory.addEvent(event);
}
@@ -151,8 +151,8 @@ class UsageStatsQueryHelper {
if (conversationInfo == null) {
return;
}
EventHistoryImpl eventHistory = packageData.getEventStore().getOrCreateShortcutEventHistory(
conversationInfo.getShortcutId());
EventHistoryImpl eventHistory = packageData.getEventStore().getOrCreateEventHistory(
EventStore.CATEGORY_SHORTCUT_BASED, conversationInfo.getShortcutId());
eventHistory.addEvent(event);
}
}

View File

@@ -104,6 +104,14 @@ class UserData {
return mPackageDataMap.get(packageName);
}
/** Deletes the specified package data. */
void deletePackageData(@NonNull String packageName) {
PackageData packageData = mPackageDataMap.remove(packageName);
if (packageData != null) {
packageData.onDestroy();
}
}
void setDefaultDialer(@Nullable String packageName) {
mDefaultDialer = packageName;
}

View File

@@ -24,12 +24,14 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -41,6 +43,7 @@ import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Person;
import android.app.job.JobScheduler;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.prediction.AppTargetId;
@@ -49,12 +52,15 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.content.pm.ShortcutServiceInternal;
import android.content.pm.UserInfo;
import android.content.pm.parsing.AndroidPackage;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.CancellationSignal;
import android.os.Looper;
import android.os.UserHandle;
import android.os.UserManager;
@@ -66,6 +72,7 @@ import android.telephony.TelephonyManager;
import android.util.Range;
import com.android.internal.app.ChooserActivity;
import com.android.internal.content.PackageMonitor;
import com.android.server.LocalServices;
import org.junit.After;
@@ -84,6 +91,7 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@RunWith(JUnit4.class)
public final class DataManagerTest {
@@ -101,12 +109,14 @@ public final class DataManagerTest {
@Mock private Context mContext;
@Mock private ShortcutServiceInternal mShortcutServiceInternal;
@Mock private UsageStatsManagerInternal mUsageStatsManagerInternal;
@Mock private PackageManagerInternal mPackageManagerInternal;
@Mock private ShortcutManager mShortcutManager;
@Mock private UserManager mUserManager;
@Mock private TelephonyManager mTelephonyManager;
@Mock private TelecomManager mTelecomManager;
@Mock private ContentResolver mContentResolver;
@Mock private ScheduledExecutorService mExecutorService;
@Mock private JobScheduler mJobScheduler;
@Mock private ScheduledFuture mScheduledFuture;
@Mock private StatusBarNotification mStatusBarNotification;
@Mock private Notification mNotification;
@@ -114,6 +124,7 @@ public final class DataManagerTest {
private NotificationChannel mNotificationChannel;
private DataManager mDataManager;
private int mCallingUserId;
private CancellationSignal mCancellationSignal;
private TestInjector mInjector;
@Before
@@ -124,6 +135,15 @@ public final class DataManagerTest {
addLocalServiceMock(UsageStatsManagerInternal.class, mUsageStatsManagerInternal);
addLocalServiceMock(PackageManagerInternal.class, mPackageManagerInternal);
AndroidPackage androidPackage = mock(AndroidPackage.class);
when(androidPackage.getPackageName()).thenReturn(TEST_PKG_NAME);
doAnswer(ans -> {
Consumer<AndroidPackage> callback = (Consumer<AndroidPackage>) ans.getArguments()[0];
callback.accept(androidPackage);
return null;
}).when(mPackageManagerInternal).forEachInstalledPackage(any(Consumer.class), anyInt());
when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper());
Context originalContext = getInstrumentation().getTargetContext();
@@ -145,6 +165,10 @@ public final class DataManagerTest {
when(mTelecomManager.getDefaultDialerPackage(any(UserHandle.class)))
.thenReturn(TEST_PKG_NAME);
when(mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE)).thenReturn(mJobScheduler);
when(mContext.getSystemServiceName(JobScheduler.class)).thenReturn(
Context.JOB_SCHEDULER_SERVICE);
when(mExecutorService.scheduleAtFixedRate(any(Runnable.class), anyLong(), anyLong(), any(
TimeUnit.class))).thenReturn(mScheduledFuture);
@@ -168,6 +192,8 @@ public final class DataManagerTest {
mCallingUserId = USER_ID_PRIMARY;
mCancellationSignal = new CancellationSignal();
mInjector = new TestInjector();
mDataManager = new DataManager(mContext, mInjector);
mDataManager.initialize();
@@ -177,6 +203,7 @@ public final class DataManagerTest {
public void tearDown() {
LocalServices.removeServiceForTest(ShortcutServiceInternal.class);
LocalServices.removeServiceForTest(UsageStatsManagerInternal.class);
LocalServices.removeServiceForTest(PackageManagerInternal.class);
}
@Test
@@ -454,6 +481,101 @@ public final class DataManagerTest {
assertEquals(2, activeTimeSlots.size());
}
@Test
public void testDeleteUninstalledPackageDataOnPackageRemoved() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson());
mDataManager.onShortcutAddedOrUpdated(shortcut);
assertNotNull(mDataManager.getPackage(TEST_PKG_NAME, USER_ID_PRIMARY));
PackageMonitor packageMonitor = mDataManager.getPackageMonitorForTesting(USER_ID_PRIMARY);
Intent intent = new Intent(Intent.ACTION_PACKAGE_REMOVED);
intent.putExtra(Intent.EXTRA_USER_HANDLE, USER_ID_PRIMARY);
intent.setData(Uri.parse("package:" + TEST_PKG_NAME));
packageMonitor.onReceive(mContext, intent);
assertNull(mDataManager.getPackage(TEST_PKG_NAME, USER_ID_PRIMARY));
}
@Test
public void testPruneUninstalledPackageData() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson());
mDataManager.onShortcutAddedOrUpdated(shortcut);
assertNotNull(mDataManager.getPackage(TEST_PKG_NAME, USER_ID_PRIMARY));
doAnswer(ans -> null).when(mPackageManagerInternal)
.forEachInstalledPackage(any(Consumer.class), anyInt());
mDataManager.pruneDataForUser(USER_ID_PRIMARY, mCancellationSignal);
assertNull(mDataManager.getPackage(TEST_PKG_NAME, USER_ID_PRIMARY));
}
@Test
public void testPruneCallEventsFromNonDialer() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson());
mDataManager.onShortcutAddedOrUpdated(shortcut);
long currentTimestamp = System.currentTimeMillis();
mInjector.mCallLogQueryHelper.mEventConsumer.accept(PHONE_NUMBER,
new Event(currentTimestamp - MILLIS_PER_MINUTE, Event.TYPE_CALL_OUTGOING));
List<Range<Long>> activeTimeSlots = new ArrayList<>();
mDataManager.forAllPackages(packageData ->
activeTimeSlots.addAll(
packageData.getEventHistory(TEST_SHORTCUT_ID)
.getEventIndex(Event.CALL_EVENT_TYPES)
.getActiveTimeSlots()));
assertEquals(1, activeTimeSlots.size());
mDataManager.getUserDataForTesting(USER_ID_PRIMARY).setDefaultDialer(null);
mDataManager.pruneDataForUser(USER_ID_PRIMARY, mCancellationSignal);
activeTimeSlots.clear();
mDataManager.forAllPackages(packageData ->
activeTimeSlots.addAll(
packageData.getEventHistory(TEST_SHORTCUT_ID)
.getEventIndex(Event.CALL_EVENT_TYPES)
.getActiveTimeSlots()));
assertTrue(activeTimeSlots.isEmpty());
}
@Test
public void testPruneSmsEventsFromNonDefaultSmsApp() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson());
mDataManager.onShortcutAddedOrUpdated(shortcut);
mDataManager.getUserDataForTesting(USER_ID_PRIMARY).setDefaultSmsApp(TEST_PKG_NAME);
long currentTimestamp = System.currentTimeMillis();
mInjector.mMmsQueryHelper.mEventConsumer.accept(PHONE_NUMBER,
new Event(currentTimestamp - MILLIS_PER_MINUTE, Event.TYPE_SMS_OUTGOING));
List<Range<Long>> activeTimeSlots = new ArrayList<>();
mDataManager.forAllPackages(packageData ->
activeTimeSlots.addAll(
packageData.getEventHistory(TEST_SHORTCUT_ID)
.getEventIndex(Event.SMS_EVENT_TYPES)
.getActiveTimeSlots()));
assertEquals(1, activeTimeSlots.size());
mDataManager.getUserDataForTesting(USER_ID_PRIMARY).setDefaultSmsApp(null);
mDataManager.pruneDataForUser(USER_ID_PRIMARY, mCancellationSignal);
activeTimeSlots.clear();
mDataManager.forAllPackages(packageData ->
activeTimeSlots.addAll(
packageData.getEventHistory(TEST_SHORTCUT_ID)
.getEventIndex(Event.SMS_EVENT_TYPES)
.getActiveTimeSlots()));
assertTrue(activeTimeSlots.isEmpty());
}
private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
LocalServices.removeServiceForTest(clazz);
LocalServices.addService(clazz, mock);

View File

@@ -81,8 +81,10 @@ public final class PackageDataTest {
@Test
public void testGetEventHistory() {
EventStore eventStore = mPackageData.getEventStore();
eventStore.getOrCreateShortcutEventHistory(SHORTCUT_ID).addEvent(mE1);
eventStore.getOrCreateLocusEventHistory(LOCUS_ID).addEvent(mE2);
eventStore.getOrCreateEventHistory(EventStore.CATEGORY_SHORTCUT_BASED, SHORTCUT_ID)
.addEvent(mE1);
eventStore.getOrCreateEventHistory(EventStore.CATEGORY_LOCUS_ID_BASED, LOCUS_ID.getId())
.addEvent(mE2);
EventHistory eventHistory = mPackageData.getEventHistory(SHORTCUT_ID);
List<Event> events = eventHistory.queryEvents(Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE);
@@ -96,9 +98,10 @@ public final class PackageDataTest {
mIsDefaultDialer = true;
mIsDefaultSmsApp = true;
EventStore eventStore = mPackageData.getEventStore();
eventStore.getOrCreateShortcutEventHistory(SHORTCUT_ID).addEvent(mE1);
eventStore.getOrCreateCallEventHistory(PHONE_NUMBER).addEvent(mE3);
eventStore.getOrCreateSmsEventHistory(PHONE_NUMBER).addEvent(mE4);
eventStore.getOrCreateEventHistory(EventStore.CATEGORY_SHORTCUT_BASED, SHORTCUT_ID)
.addEvent(mE1);
eventStore.getOrCreateEventHistory(EventStore.CATEGORY_CALL, PHONE_NUMBER).addEvent(mE3);
eventStore.getOrCreateEventHistory(EventStore.CATEGORY_SMS, PHONE_NUMBER).addEvent(mE4);
assertTrue(mPackageData.isDefaultDialer());
assertTrue(mPackageData.isDefaultSmsApp());
@@ -113,9 +116,10 @@ public final class PackageDataTest {
@Test
public void testGetEventHistoryNotDefaultDialerOrSmsApp() {
EventStore eventStore = mPackageData.getEventStore();
eventStore.getOrCreateShortcutEventHistory(SHORTCUT_ID).addEvent(mE1);
eventStore.getOrCreateCallEventHistory(PHONE_NUMBER).addEvent(mE3);
eventStore.getOrCreateSmsEventHistory(PHONE_NUMBER).addEvent(mE4);
eventStore.getOrCreateEventHistory(EventStore.CATEGORY_SHORTCUT_BASED, SHORTCUT_ID)
.addEvent(mE1);
eventStore.getOrCreateEventHistory(EventStore.CATEGORY_CALL, PHONE_NUMBER).addEvent(mE3);
eventStore.getOrCreateEventHistory(EventStore.CATEGORY_SMS, PHONE_NUMBER).addEvent(mE4);
assertFalse(mPackageData.isDefaultDialer());
assertFalse(mPackageData.isDefaultSmsApp());
@@ -125,6 +129,61 @@ public final class PackageDataTest {
assertEventEquals(mE1, events.get(0));
}
@Test
public void testDeleteConversationData() {
mIsDefaultDialer = true;
mIsDefaultSmsApp = true;
EventStore eventStore = mPackageData.getEventStore();
eventStore.getOrCreateEventHistory(EventStore.CATEGORY_SHORTCUT_BASED, SHORTCUT_ID)
.addEvent(mE1);
eventStore.getOrCreateEventHistory(EventStore.CATEGORY_LOCUS_ID_BASED, LOCUS_ID.getId())
.addEvent(mE2);
eventStore.getOrCreateEventHistory(EventStore.CATEGORY_CALL, PHONE_NUMBER).addEvent(mE3);
eventStore.getOrCreateEventHistory(EventStore.CATEGORY_SMS, PHONE_NUMBER).addEvent(mE4);
EventHistory eventHistory = mPackageData.getEventHistory(SHORTCUT_ID);
List<Event> events = eventHistory.queryEvents(Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE);
assertEquals(4, events.size());
mPackageData.deleteDataForConversation(SHORTCUT_ID);
eventHistory = mPackageData.getEventHistory(SHORTCUT_ID);
events = eventHistory.queryEvents(Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE);
assertTrue(events.isEmpty());
}
@Test
public void testPruneOrphanEvents() {
mIsDefaultDialer = true;
mIsDefaultSmsApp = true;
EventStore eventStore = mPackageData.getEventStore();
eventStore.getOrCreateEventHistory(EventStore.CATEGORY_SHORTCUT_BASED, SHORTCUT_ID)
.addEvent(mE1);
eventStore.getOrCreateEventHistory(EventStore.CATEGORY_LOCUS_ID_BASED, LOCUS_ID.getId())
.addEvent(mE2);
eventStore.getOrCreateEventHistory(EventStore.CATEGORY_CALL, PHONE_NUMBER).addEvent(mE3);
eventStore.getOrCreateEventHistory(EventStore.CATEGORY_SMS, PHONE_NUMBER).addEvent(mE4);
EventHistory eventHistory = mPackageData.getEventHistory(SHORTCUT_ID);
List<Event> events = eventHistory.queryEvents(Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE);
assertEquals(4, events.size());
ConversationInfo conversationInfo = new ConversationInfo.Builder()
.setShortcutId(SHORTCUT_ID)
.setLocusId(null)
.setContactUri(null)
.setContactPhoneNumber(null)
.setShortcutFlags(ShortcutInfo.FLAG_LONG_LIVED)
.build();
mPackageData.getConversationStore().addOrUpdate(conversationInfo);
mPackageData.pruneOrphanEvents();
eventHistory = mPackageData.getEventHistory(SHORTCUT_ID);
events = eventHistory.queryEvents(Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE);
assertEquals(1, events.size());
// Only the shortcut based event is kept. All the other events are deleted.
assertEventEquals(mE1, events.get(0));
}
private void assertEventEquals(Event expected, Event actual) {
assertEquals(expected.getTimestamp(), actual.getTimestamp());
assertEquals(expected.getType(), actual.getType());

View File

@@ -288,14 +288,13 @@ public final class UsageStatsQueryHelperTest {
@Override
@NonNull
EventHistoryImpl getOrCreateShortcutEventHistory(String shortcutId) {
return mShortcutEventHistory;
}
@Override
@NonNull
EventHistoryImpl getOrCreateLocusEventHistory(LocusId locusId) {
return mLocusEventHistory;
EventHistoryImpl getOrCreateEventHistory(@EventCategory int category, String key) {
if (category == EventStore.CATEGORY_SHORTCUT_BASED) {
return mShortcutEventHistory;
} else if (category == EventStore.CATEGORY_LOCUS_ID_BASED) {
return mLocusEventHistory;
}
throw new UnsupportedOperationException();
}
}