From 9125068a991b24d27810b6392a562b32457b3f5d Mon Sep 17 00:00:00 2001 From: Adrian Roos Date: Mon, 6 Feb 2017 14:48:15 -0800 Subject: [PATCH] SysUiLeaks: Add GarbageMonitor Adds a service for monitoring the amount of tracked garbage. If it exceeds reasonable levels, a notification with a leak report is posted. Test: runtest systemui Change-Id: Ib55281f2aac557743b97c46bc616688261c72e9c --- packages/SystemUI/AndroidManifest.xml | 10 ++ packages/SystemUI/res/xml/fileprovider.xml | 20 +++ .../src/com/android/systemui/Dependency.java | 20 +++ .../android/systemui/SystemUIApplication.java | 2 + .../systemui/util/leak/GarbageMonitor.java | 94 +++++++++++ .../systemui/util/leak/LeakDetector.java | 4 + .../systemui/util/leak/LeakReporter.java | 142 ++++++++++++++++ .../systemui/util/leak/TrackedGarbage.java | 14 ++ .../util/leak/GarbageMonitorTest.java | 153 ++++++++++++++++++ .../systemui/util/leak/LeakReporterTest.java | 96 +++++++++++ .../systemui/utils/TestableContext.java | 6 +- 11 files changed, 560 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res/xml/fileprovider.xml create mode 100644 packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java create mode 100644 packages/SystemUI/src/com/android/systemui/util/leak/LeakReporter.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakReporterTest.java diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 4b8734fd18167..b2e2a2c5f2ff9 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -559,6 +559,16 @@ + + + + diff --git a/packages/SystemUI/res/xml/fileprovider.xml b/packages/SystemUI/res/xml/fileprovider.xml new file mode 100644 index 0000000000000..4aaa90fb5c531 --- /dev/null +++ b/packages/SystemUI/res/xml/fileprovider.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 8d46b431d2c53..273b5e3644a68 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -67,7 +67,9 @@ import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.policy.ZenModeControllerImpl; import com.android.systemui.tuner.TunerService; +import com.android.systemui.util.leak.GarbageMonitor; import com.android.systemui.util.leak.LeakDetector; +import com.android.systemui.util.leak.LeakReporter; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -104,6 +106,12 @@ public class Dependency extends SystemUI { */ public static final DependencyKey MAIN_HANDLER = new DependencyKey<>("main_handler"); + /** + * An email address to send memory leak reports to by default. + */ + public static final DependencyKey LEAK_REPORT_EMAIL + = new DependencyKey<>("leak_report_email"); + private final ArrayMap mDependencies = new ArrayMap<>(); private final ArrayMap mProviders = new ArrayMap<>(); @@ -192,6 +200,18 @@ public class Dependency extends SystemUI { mProviders.put(LeakDetector.class, LeakDetector::create); + mProviders.put(LEAK_REPORT_EMAIL, () -> null); + + mProviders.put(LeakReporter.class, () -> new LeakReporter( + mContext, + getDependency(LeakDetector.class), + getDependency(LEAK_REPORT_EMAIL))); + + mProviders.put(GarbageMonitor.class, () -> new GarbageMonitor( + getDependency(BG_LOOPER), + getDependency(LeakDetector.class), + getDependency(LeakReporter.class))); + mProviders.put(TunerService.class, () -> new TunerService(mContext)); diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index e5bda7edfc589..187b5573b97c6 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -48,6 +48,7 @@ import com.android.systemui.statusbar.phone.StatusBarWindowManager; import com.android.systemui.tuner.TunerService; import com.android.systemui.usb.StorageNotification; import com.android.systemui.util.NotificationChannels; +import com.android.systemui.util.leak.GarbageMonitor; import com.android.systemui.volume.VolumeUI; import java.util.HashMap; @@ -81,6 +82,7 @@ public class SystemUIApplication extends Application implements SysUiServiceProv PipUI.class, ShortcutKeyDispatcher.class, VendorServices.class, + GarbageMonitor.Service.class, LatencyTester.class, }; diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java new file mode 100644 index 0000000000000..ef743e38d7388 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2017 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.systemui.util.leak; + + +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.VisibleForTesting; + +import com.android.systemui.Dependency; +import com.android.systemui.SystemUI; + +public class GarbageMonitor { + + private static final String TAG = "GarbageMonitor"; + + private static final long GARBAGE_INSPECTION_INTERVAL = 5 * 60 * 1000; // 5min + private static final int GARBAGE_ALLOWANCE = 5; + + private final Handler mHandler; + private final TrackedGarbage mTrackedGarbage; + private final LeakReporter mLeakReporter; + + public GarbageMonitor(Looper bgLooper, LeakDetector leakDetector, + LeakReporter leakReporter) { + mHandler = bgLooper != null ? new Handler(bgLooper): null; + mTrackedGarbage = leakDetector.getTrackedGarbage(); + mLeakReporter = leakReporter; + } + + public void start() { + if (mTrackedGarbage == null) { + return; + } + + scheduleInspectGarbage(this::inspectGarbage); + } + + @VisibleForTesting + void scheduleInspectGarbage(Runnable runnable) { + mHandler.postDelayed(runnable, GARBAGE_INSPECTION_INTERVAL); + } + + private void inspectGarbage() { + if (mTrackedGarbage.countOldGarbage() > GARBAGE_ALLOWANCE) { + Runtime.getRuntime().gc(); + + // Allow some time to for ReferenceQueue to catch up. + scheduleReinspectGarbage(this::reinspectGarbageAfterGc); + } + scheduleInspectGarbage(this::inspectGarbage); + } + + @VisibleForTesting + void scheduleReinspectGarbage(Runnable runnable) { + mHandler.postDelayed(runnable, (long) 100); + } + + private void reinspectGarbageAfterGc() { + int count = mTrackedGarbage.countOldGarbage(); + if (count > GARBAGE_ALLOWANCE) { + mLeakReporter.dumpLeak(count); + } + } + + public static class Service extends SystemUI { + + private GarbageMonitor mGarbageMonitor; + + @Override + public void start() { + if (!Build.IS_DEBUGGABLE) { + return; + } + mGarbageMonitor = Dependency.get(GarbageMonitor.class); + mGarbageMonitor.start(); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java b/packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java index a0f8659ab5d07..574fdb98caa73 100644 --- a/packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java +++ b/packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java @@ -96,6 +96,10 @@ public class LeakDetector implements Dumpable { } } + TrackedGarbage getTrackedGarbage() { + return mTrackedGarbage; + } + @Override public void dump(FileDescriptor df, PrintWriter w, String[] args) { IndentingPrintWriter pw = new IndentingPrintWriter(w, " "); diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/LeakReporter.java b/packages/SystemUI/src/com/android/systemui/util/leak/LeakReporter.java new file mode 100644 index 0000000000000..a247f3fe15dd2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/leak/LeakReporter.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2017 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.systemui.util.leak; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ClipData; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Debug; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.support.v4.content.FileProvider; +import android.util.Log; + +import com.google.android.collect.Lists; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; + +/** + * Dumps data to debug leaks and posts a notification to share the data. + */ +public class LeakReporter { + + static final String TAG = "LeakReporter"; + + public static final String FILEPROVIDER_AUTHORITY = "com.android.systemui.fileprovider"; + + static final String LEAK_DIR = "leak"; + static final String LEAK_HPROF = "leak.hprof"; + static final String LEAK_DUMP = "leak.dump"; + + private final Context mContext; + private final LeakDetector mLeakDetector; + private final String mLeakReportEmail; + + public LeakReporter(Context context, LeakDetector leakDetector, String leakReportEmail) { + mContext = context; + mLeakDetector = leakDetector; + mLeakReportEmail = leakReportEmail; + } + + public void dumpLeak(int garbageCount) { + try { + File leakDir = new File(mContext.getCacheDir(), LEAK_DIR); + leakDir.mkdir(); + + File hprofFile = new File(leakDir, LEAK_HPROF); + Debug.dumpHprofData(hprofFile.getAbsolutePath()); + + File dumpFile = new File(leakDir, LEAK_DUMP); + try (FileOutputStream fos = new FileOutputStream(dumpFile)) { + PrintWriter w = new PrintWriter(fos); + w.print("Build: "); w.println(SystemProperties.get("ro.build.description")); + w.println(); + w.flush(); + mLeakDetector.dump(fos.getFD(), w, new String[0]); + w.close(); + } + + NotificationManager notiMan = mContext.getSystemService(NotificationManager.class); + + NotificationChannel channel = new NotificationChannel("leak", "Leak Alerts", + NotificationManager.IMPORTANCE_HIGH); + channel.enableVibration(true); + + notiMan.createNotificationChannel(channel); + Notification.Builder builder = new Notification.Builder(mContext, channel.getId()) + .setAutoCancel(true) + .setShowWhen(true) + .setContentTitle("Memory Leak Detected") + .setContentText(String.format( + "SystemUI has detected %d leaked objects. Tap to send", garbageCount)) + .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb) + .setContentIntent(PendingIntent.getActivityAsUser(mContext, 0, + getIntent(hprofFile, dumpFile), + PendingIntent.FLAG_UPDATE_CURRENT, null, UserHandle.CURRENT)); + notiMan.notify(TAG, 0, builder.build()); + } catch (IOException e) { + Log.e(TAG, "Couldn't dump heap for leak", e); + } + } + + private Intent getIntent(File hprofFile, File dumpFile) { + Uri dumpUri = FileProvider.getUriForFile(mContext, FILEPROVIDER_AUTHORITY, dumpFile); + Uri hprofUri = FileProvider.getUriForFile(mContext, FILEPROVIDER_AUTHORITY, hprofFile); + + Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); + String mimeType = "application/vnd.android.leakreport"; + + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setType(mimeType); + + final String subject = "SystemUI leak report"; + intent.putExtra(Intent.EXTRA_SUBJECT, subject); + + // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String. + // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually + // create the ClipData object with the attachments URIs. + final StringBuilder messageBody = new StringBuilder("Build info: ") + .append(SystemProperties.get("ro.build.description")); + intent.putExtra(Intent.EXTRA_TEXT, messageBody.toString()); + final ClipData clipData = new ClipData(null, new String[] { mimeType }, + new ClipData.Item(null, null, null, dumpUri)); + final ArrayList attachments = Lists.newArrayList(dumpUri); + + clipData.addItem(new ClipData.Item(null, null, null, hprofUri)); + attachments.add(hprofUri); + + intent.setClipData(clipData); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments); + + String leakReportEmail = mLeakReportEmail; + if (leakReportEmail != null) { + intent.putExtra(Intent.EXTRA_EMAIL, new String[] { leakReportEmail }); + } + + return intent; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/TrackedGarbage.java b/packages/SystemUI/src/com/android/systemui/util/leak/TrackedGarbage.java index fd59aeeac2ddc..98a6450800a3b 100644 --- a/packages/SystemUI/src/com/android/systemui/util/leak/TrackedGarbage.java +++ b/packages/SystemUI/src/com/android/systemui/util/leak/TrackedGarbage.java @@ -106,6 +106,20 @@ public class TrackedGarbage { } } + public synchronized int countOldGarbage() { + cleanUp(); + + long now = SystemClock.uptimeMillis(); + + int result = 0; + for (LeakReference ref : mGarbage) { + if (isOld(ref.createdUptimeMillis, now)) { + result++; + } + } + return result; + } + private boolean isOld(long createdUptimeMillis, long now) { return createdUptimeMillis + GARBAGE_COLLECTION_DEADLINE_MILLIS < now; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java new file mode 100644 index 0000000000000..9a825c1d45430 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2017 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.systemui.util.leak; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class GarbageMonitorTest { + + private LeakReporter mLeakReporter; + private TrackedGarbage mTrackedGarbage; + private TestableGarbageMonitor mGarbageMonitor; + + @Before + public void setup() { + mTrackedGarbage = mock(TrackedGarbage.class); + mLeakReporter = mock(LeakReporter.class); + mGarbageMonitor = new TestableGarbageMonitor( + new LeakDetector(null, mTrackedGarbage, null), + mLeakReporter); + } + + @Test + public void testCallbacks_getScheduled() { + mGarbageMonitor.start(); + mGarbageMonitor.runCallbacksOnce(); + mGarbageMonitor.runCallbacksOnce(); + mGarbageMonitor.runCallbacksOnce(); + } + + @Test + public void testNoGarbage_doesntDump() { + when(mTrackedGarbage.countOldGarbage()).thenReturn(0); + + mGarbageMonitor.start(); + mGarbageMonitor.runCallbacksOnce(); + mGarbageMonitor.runCallbacksOnce(); + mGarbageMonitor.runCallbacksOnce(); + + verify(mLeakReporter, never()).dumpLeak(anyInt()); + } + + @Test + public void testALittleGarbage_doesntDump() { + when(mTrackedGarbage.countOldGarbage()).thenReturn(4); + + mGarbageMonitor.start(); + mGarbageMonitor.runCallbacksOnce(); + mGarbageMonitor.runCallbacksOnce(); + mGarbageMonitor.runCallbacksOnce(); + + verify(mLeakReporter, never()).dumpLeak(anyInt()); + } + + @Test + public void testTransientGarbage_doesntDump() { + when(mTrackedGarbage.countOldGarbage()).thenReturn(100); + + mGarbageMonitor.start(); + mGarbageMonitor.runInspectCallback(); + + when(mTrackedGarbage.countOldGarbage()).thenReturn(0); + + mGarbageMonitor.runReinspectCallback(); + + verify(mLeakReporter, never()).dumpLeak(anyInt()); + } + + @Test + public void testLotsOfPersistentGarbage_dumps() { + when(mTrackedGarbage.countOldGarbage()).thenReturn(100); + + mGarbageMonitor.start(); + mGarbageMonitor.runCallbacksOnce(); + + verify(mLeakReporter).dumpLeak(anyInt()); + } + + private static class TestableGarbageMonitor extends GarbageMonitor { + Runnable mInspectCallback; + Runnable mReinspectCallback; + + public TestableGarbageMonitor(LeakDetector leakDetector, + LeakReporter leakReporter) { + super(null /* bgLooper */, leakDetector, leakReporter); + } + + @Override + void scheduleInspectGarbage(Runnable runnable) { + assertNull("must not have more than one pending inspect callback", mInspectCallback); + mInspectCallback = runnable; + } + + void runInspectCallback() { + assertNotNull("expected an inspect callback to be scheduled", mInspectCallback); + Runnable callback = mInspectCallback; + mInspectCallback = null; + callback.run(); + } + + @Override + void scheduleReinspectGarbage(Runnable runnable) { + assertNull("must not have more than one reinspect callback", mReinspectCallback); + mReinspectCallback = runnable; + } + + void runReinspectCallback() { + assertNotNull("expected a reinspect callback to be scheduled", mInspectCallback); + maybeRunReinspectCallback(); + } + + void maybeRunReinspectCallback() { + Runnable callback = mReinspectCallback; + mReinspectCallback = null; + if (callback != null) { + callback.run(); + } + } + + void runCallbacksOnce() { + runInspectCallback(); + maybeRunReinspectCallback(); + } + } +} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakReporterTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakReporterTest.java new file mode 100644 index 0000000000000..96a9bee9584e6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakReporterTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2017 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.systemui.util.leak; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.app.NotificationManager; +import android.support.test.runner.AndroidJUnit4; + +import com.android.systemui.SysuiTestCase; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.PrintWriter; + +@RunWith(AndroidJUnit4.class) +public class LeakReporterTest extends SysuiTestCase { + + private LeakDetector mLeakDetector; + private LeakReporter mLeakReporter; + private File mLeakDir; + private File mLeakDump; + private File mLeakHprof; + private NotificationManager mNotificationManager; + + @Before + public void setup() { + mLeakDir = new File(mContext.getCacheDir(), LeakReporter.LEAK_DIR); + mLeakDump = new File(mLeakDir, LeakReporter.LEAK_DUMP); + mLeakHprof = new File(mLeakDir, LeakReporter.LEAK_HPROF); + + mNotificationManager = mock(NotificationManager.class); + mContext.addMockSystemService(NotificationManager.class, mNotificationManager); + + mLeakDetector = mock(LeakDetector.class); + doAnswer(invocation -> { + invocation.getArgument(1).println("test"); + return null; + }).when(mLeakDetector).dump(any(), any(), any()); + + mLeakReporter = new LeakReporter(mContext, mLeakDetector, "test@example.com"); + } + + @After + public void teardown() { + mLeakDump.delete(); + mLeakHprof.delete(); + mLeakDir.delete(); + } + + @Test + public void testDump_postsNotification() { + mLeakReporter.dumpLeak(5); + verify(mNotificationManager).notify(any(), anyInt(), any()); + } + + @Test + public void testDump_Repeated() { + mLeakReporter.dumpLeak(1); + mLeakReporter.dumpLeak(2); + } + + @Test + public void testDump_ProducesNonZeroFiles() { + mLeakReporter.dumpLeak(5); + + assertTrue(mLeakDump.exists()); + assertTrue(mLeakDump.length() > 0); + + assertTrue(mLeakHprof.exists()); + assertTrue(mLeakHprof.length() > 0); + } +} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/TestableContext.java b/packages/SystemUI/tests/src/com/android/systemui/utils/TestableContext.java index c4f10035a8c46..1429390d193a1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/utils/TestableContext.java +++ b/packages/SystemUI/tests/src/com/android/systemui/utils/TestableContext.java @@ -33,8 +33,8 @@ import android.util.ArrayMap; import android.view.LayoutInflater; import com.android.systemui.SysUiServiceProvider; -import com.android.systemui.utils.leaks.Tracker; import com.android.systemui.SysuiTestCase; +import com.android.systemui.utils.leaks.Tracker; public class TestableContext extends ContextWrapper implements SysUiServiceProvider { @@ -81,6 +81,10 @@ public class TestableContext extends ContextWrapper implements SysUiServiceProvi return super.getResources(); } + public void addMockSystemService(Class service, T mock) { + addMockSystemService(getSystemServiceName(service), mock); + } + public void addMockSystemService(String name, Object service) { mMockSystemServices = lazyInit(mMockSystemServices); mMockSystemServices.put(name, service);