Merge "[People Service] Add throttling to DataManager add / change listener" into tm-qpr-dev am: 83693640c3

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21546169

Change-Id: I1e485c9d36900709cbe22a28b189d2ffff8e8fcc
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Jeff Nainaparampil
2023-02-23 19:01:05 +00:00
committed by Automerger Merge Worker
5 changed files with 214 additions and 36 deletions

View File

@@ -113,6 +113,7 @@ public class DataManager {
private static final long USAGE_STATS_QUERY_INTERVAL_SEC = 120L; private static final long USAGE_STATS_QUERY_INTERVAL_SEC = 120L;
@VisibleForTesting @VisibleForTesting
static final int MAX_CACHED_RECENT_SHORTCUTS = 30; static final int MAX_CACHED_RECENT_SHORTCUTS = 30;
private static final int DEBOUNCE_LENGTH_MS = 500;
private final Context mContext; private final Context mContext;
private final Injector mInjector; private final Injector mInjector;
@@ -129,6 +130,7 @@ public class DataManager {
private final List<PeopleService.ConversationsListener> mConversationsListeners = private final List<PeopleService.ConversationsListener> mConversationsListeners =
new ArrayList<>(1); new ArrayList<>(1);
private final Handler mHandler; private final Handler mHandler;
private final PerPackageThrottler mShortcutsThrottler;
private ContentObserver mCallLogContentObserver; private ContentObserver mCallLogContentObserver;
private ContentObserver mMmsSmsContentObserver; private ContentObserver mMmsSmsContentObserver;
@@ -140,14 +142,17 @@ public class DataManager {
private ConversationStatusExpirationBroadcastReceiver mStatusExpReceiver; private ConversationStatusExpirationBroadcastReceiver mStatusExpReceiver;
public DataManager(Context context) { public DataManager(Context context) {
this(context, new Injector(), BackgroundThread.get().getLooper()); this(context, new Injector(), BackgroundThread.get().getLooper(),
new PerPackageThrottlerImpl(BackgroundThread.getHandler(), DEBOUNCE_LENGTH_MS));
} }
DataManager(Context context, Injector injector, Looper looper) { DataManager(Context context, Injector injector, Looper looper,
PerPackageThrottler shortcutsThrottler) {
mContext = context; mContext = context;
mInjector = injector; mInjector = injector;
mScheduledExecutor = mInjector.createScheduledExecutor(); mScheduledExecutor = mInjector.createScheduledExecutor();
mHandler = new Handler(looper); mHandler = new Handler(looper);
mShortcutsThrottler = shortcutsThrottler;
} }
/** Initialization. Called when the system services are up running. */ /** Initialization. Called when the system services are up running. */
@@ -1104,25 +1109,34 @@ public class DataManager {
@Override @Override
public void onShortcutsAddedOrUpdated(@NonNull String packageName, public void onShortcutsAddedOrUpdated(@NonNull String packageName,
@NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user) { @NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user) {
mInjector.getBackgroundExecutor().execute(() -> { mShortcutsThrottler.scheduleDebounced(
new Pair<>(packageName, user.getIdentifier()),
() -> {
PackageData packageData = getPackage(packageName, user.getIdentifier()); PackageData packageData = getPackage(packageName, user.getIdentifier());
for (ShortcutInfo shortcut : shortcuts) { List<ShortcutInfo> queriedShortcuts = getShortcuts(packageName,
user.getIdentifier(), null);
boolean hasCachedShortcut = false;
for (ShortcutInfo shortcut : queriedShortcuts) {
if (ShortcutHelper.isConversationShortcut( if (ShortcutHelper.isConversationShortcut(
shortcut, mShortcutServiceInternal, user.getIdentifier())) { shortcut, mShortcutServiceInternal, user.getIdentifier())) {
if (shortcut.isCached()) { if (shortcut.isCached()) {
ConversationInfo conversationInfo = packageData != null ConversationInfo info = packageData != null
? packageData.getConversationInfo(shortcut.getId()) : null; ? packageData.getConversationInfo(shortcut.getId())
if (conversationInfo == null : null;
|| !conversationInfo.isShortcutCachedForNotification()) { if (info == null
// This is a newly cached shortcut. Clean up the existing cached || !info.isShortcutCachedForNotification()) {
// shortcuts to ensure the cache size is under the limit. hasCachedShortcut = true;
cleanupCachedShortcuts(user.getIdentifier(),
MAX_CACHED_RECENT_SHORTCUTS - 1);
} }
} }
addOrUpdateConversationInfo(shortcut); addOrUpdateConversationInfo(shortcut);
} }
} }
// Added at least one new conversation. Uncache older existing cached
// shortcuts to ensure the cache size is under the limit.
if (hasCachedShortcut) {
cleanupCachedShortcuts(user.getIdentifier(),
MAX_CACHED_RECENT_SHORTCUTS);
}
}); });
} }

View File

@@ -0,0 +1,28 @@
/*
* Copyright (C) 2023 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.util.Pair;
/** The interface for throttling expensive runnables per package. */
interface PerPackageThrottler {
/**
* Schedule a runnable to run in the future, and debounce runnables for same {@code pkgUserId}
* that occur until that future has run.
*/
void scheduleDebounced(Pair<String, Integer> pkgUserId, Runnable runnable);
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (C) 2023 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.os.Handler;
import android.util.Pair;
import java.util.HashSet;
/**
* A class that implements a per-package throttler that prevents a runnable from executing more than
* once every {@code debounceTime}.
*/
public class PerPackageThrottlerImpl implements PerPackageThrottler {
private final Handler mBackgroundHandler;
private final int mDebounceTime;
private final HashSet<Pair<String, Integer>> mPkgScheduledTasks = new HashSet<>();
PerPackageThrottlerImpl(Handler backgroundHandler, int debounceTime) {
mBackgroundHandler = backgroundHandler;
mDebounceTime = debounceTime;
}
@Override
public synchronized void scheduleDebounced(
Pair<String, Integer> pkgUserId, Runnable runnable) {
if (mPkgScheduledTasks.contains(pkgUserId)) {
return;
}
mPkgScheduledTasks.add(pkgUserId);
mBackgroundHandler.postDelayed(() -> {
synchronized (this) {
mPkgScheduledTasks.remove(pkgUserId);
runnable.run();
}
}, mDebounceTime);
}
}

View File

@@ -86,6 +86,7 @@ import android.service.notification.StatusBarNotification;
import android.telecom.TelecomManager; import android.telecom.TelecomManager;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.util.Pair;
import android.util.Range; import android.util.Range;
import com.android.internal.app.ChooserActivity; import com.android.internal.app.ChooserActivity;
@@ -186,6 +187,7 @@ public final class DataManagerTest {
private ShortcutInfo mShortcutInfo; private ShortcutInfo mShortcutInfo;
private TestInjector mInjector; private TestInjector mInjector;
private TestLooper mLooper; private TestLooper mLooper;
private TestPerPackageThrottler mShortcutThrottler;
@Before @Before
public void setUp() throws PackageManager.NameNotFoundException { public void setUp() throws PackageManager.NameNotFoundException {
@@ -275,7 +277,9 @@ public final class DataManagerTest {
mInjector = new TestInjector(); mInjector = new TestInjector();
mLooper = new TestLooper(); mLooper = new TestLooper();
mDataManager = new DataManager(mContext, mInjector, mLooper.getLooper()); mShortcutThrottler = new TestPerPackageThrottler();
mDataManager = new DataManager(mContext, mInjector, mLooper.getLooper(),
mShortcutThrottler);
mDataManager.initialize(); mDataManager.initialize();
when(mShortcutServiceInternal.isSharingShortcut(anyInt(), anyString(), anyString(), when(mShortcutServiceInternal.isSharingShortcut(anyInt(), anyString(), anyString(),
@@ -283,10 +287,7 @@ public final class DataManagerTest {
mShortcutInfo = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID, mShortcutInfo = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson()); buildPerson());
when(mShortcutServiceInternal.getShortcuts( mockGetShortcuts(Collections.singletonList(mShortcutInfo));
anyInt(), anyString(), anyLong(), anyString(), anyList(), any(), any(),
anyInt(), anyInt(), anyInt(), anyInt()))
.thenReturn(Collections.singletonList(mShortcutInfo));
verify(mShortcutServiceInternal).addShortcutChangeCallback( verify(mShortcutServiceInternal).addShortcutChangeCallback(
mShortcutChangeCallbackCaptor.capture()); mShortcutChangeCallbackCaptor.capture());
mShortcutChangeCallback = mShortcutChangeCallbackCaptor.getValue(); mShortcutChangeCallback = mShortcutChangeCallbackCaptor.getValue();
@@ -972,6 +973,7 @@ public final class DataManagerTest {
buildPerson()); buildPerson());
ShortcutInfo shortcut3 = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, "sc3", ShortcutInfo shortcut3 = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, "sc3",
buildPerson()); buildPerson());
mockGetShortcuts(List.of(shortcut1, shortcut2, shortcut3));
mShortcutChangeCallback.onShortcutsAddedOrUpdated(TEST_PKG_NAME, mShortcutChangeCallback.onShortcutsAddedOrUpdated(TEST_PKG_NAME,
Arrays.asList(shortcut1, shortcut2, shortcut3), UserHandle.of(USER_ID_PRIMARY)); Arrays.asList(shortcut1, shortcut2, shortcut3), UserHandle.of(USER_ID_PRIMARY));
mShortcutChangeCallback.onShortcutsRemoved(TEST_PKG_NAME, mShortcutChangeCallback.onShortcutsRemoved(TEST_PKG_NAME,
@@ -1223,7 +1225,6 @@ public final class DataManagerTest {
eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS)); eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
} }
} }
@Test @Test
public void testUncacheOldestCachedShortcut_missingNotificationEvents() { public void testUncacheOldestCachedShortcut_missingNotificationEvents() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY); mDataManager.onUserUnlocked(USER_ID_PRIMARY);
@@ -1233,6 +1234,7 @@ public final class DataManagerTest {
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, shortcutId, ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, shortcutId,
buildPerson()); buildPerson());
shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS); shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
mockGetShortcuts(Collections.singletonList(shortcut));
mShortcutChangeCallback.onShortcutsAddedOrUpdated( mShortcutChangeCallback.onShortcutsAddedOrUpdated(
TEST_PKG_NAME, TEST_PKG_NAME,
Collections.singletonList(shortcut), Collections.singletonList(shortcut),
@@ -1252,7 +1254,6 @@ public final class DataManagerTest {
eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS)); eq(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS));
} }
} }
@Test @Test
public void testUncacheOldestCachedShortcut_legacyConversation() { public void testUncacheOldestCachedShortcut_legacyConversation() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY); mDataManager.onUserUnlocked(USER_ID_PRIMARY);
@@ -1274,6 +1275,7 @@ public final class DataManagerTest {
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, shortcutId, ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, shortcutId,
buildPerson()); buildPerson());
shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS); shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
mockGetShortcuts(Collections.singletonList(shortcut));
mShortcutChangeCallback.onShortcutsAddedOrUpdated( mShortcutChangeCallback.onShortcutsAddedOrUpdated(
TEST_PKG_NAME, TEST_PKG_NAME,
Collections.singletonList(shortcut), Collections.singletonList(shortcut),
@@ -1311,7 +1313,8 @@ public final class DataManagerTest {
mDataManager.reportShareTargetEvent(appTargetEvent, intentFilter); mDataManager.reportShareTargetEvent(appTargetEvent, intentFilter);
byte[] payload = mDataManager.getBackupPayload(USER_ID_PRIMARY); byte[] payload = mDataManager.getBackupPayload(USER_ID_PRIMARY);
DataManager dataManager = new DataManager(mContext, mInjector, mLooper.getLooper()); DataManager dataManager = new DataManager(
mContext, mInjector, mLooper.getLooper(), mShortcutThrottler);
dataManager.onUserUnlocked(USER_ID_PRIMARY); dataManager.onUserUnlocked(USER_ID_PRIMARY);
dataManager.restore(USER_ID_PRIMARY, payload); dataManager.restore(USER_ID_PRIMARY, payload);
ConversationInfo conversationInfo = dataManager.getPackage(TEST_PKG_NAME, USER_ID_PRIMARY) ConversationInfo conversationInfo = dataManager.getPackage(TEST_PKG_NAME, USER_ID_PRIMARY)
@@ -1723,6 +1726,13 @@ public final class DataManagerTest {
return (queryFlags & flag) != 0; return (queryFlags & flag) != 0;
} }
private void mockGetShortcuts(List<ShortcutInfo> shortcutInfoList) {
when(mShortcutServiceInternal.getShortcuts(
anyInt(), anyString(), anyLong(), anyString(), any(), any(), any(),
anyInt(), anyInt(), anyInt(), anyInt()))
.thenReturn(shortcutInfoList);
}
// "Sends" a notification to a non-customized notification channel - the notification channel // "Sends" a notification to a non-customized notification channel - the notification channel
// is something generic like "messages" and the notification has a shortcut id // is something generic like "messages" and the notification has a shortcut id
private void sendGenericNotification() { private void sendGenericNotification() {
@@ -1948,4 +1958,11 @@ public final class DataManagerTest {
return mUsageStatsQueryHelper; return mUsageStatsQueryHelper;
} }
} }
private static class TestPerPackageThrottler implements PerPackageThrottler {
@Override
public void scheduleDebounced(Pair<String, Integer> pkgUserId, Runnable runnable) {
runnable.run();
}
}
} }

View File

@@ -0,0 +1,67 @@
/*
* Copyright (C) 2023 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 static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.util.Pair;
import com.android.server.testutils.OffsettableClock;
import com.android.server.testutils.TestHandler;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.concurrent.atomic.AtomicBoolean;
@RunWith(JUnit4.class)
public class PerPackageThrottlerImplTest {
private static final int DEBOUNCE_INTERVAL = 500;
private static final String PKG_ONE = "pkg_one";
private static final String PKG_TWO = "pkg_two";
private static final int USER_ID = 10;
private final OffsettableClock mClock = new OffsettableClock.Stopped();
private final TestHandler mTestHandler = new TestHandler(null, mClock);
private PerPackageThrottlerImpl mThrottler;
@Before
public void setUp() {
mThrottler = new PerPackageThrottlerImpl(mTestHandler, DEBOUNCE_INTERVAL);
}
@Test
public void scheduleDebounced() {
AtomicBoolean pkgOneRan = new AtomicBoolean();
AtomicBoolean pkgTwoRan = new AtomicBoolean();
mThrottler.scheduleDebounced(new Pair<>(PKG_ONE, USER_ID), () -> pkgOneRan.set(true));
mThrottler.scheduleDebounced(new Pair<>(PKG_ONE, USER_ID), () -> pkgOneRan.set(true));
mThrottler.scheduleDebounced(new Pair<>(PKG_TWO, USER_ID), () -> pkgTwoRan.set(true));
mThrottler.scheduleDebounced(new Pair<>(PKG_TWO, USER_ID), () -> pkgTwoRan.set(true));
assertFalse(pkgOneRan.get());
assertFalse(pkgTwoRan.get());
mClock.fastForward(DEBOUNCE_INTERVAL);
mTestHandler.timeAdvance();
assertTrue(pkgOneRan.get());
assertTrue(pkgTwoRan.get());
}
}