From bcafba3a1f9294aba2cb4b10e15e72ea00fcbe46 Mon Sep 17 00:00:00 2001 From: Felipe Leme Date: Fri, 1 Oct 2021 13:25:53 -0700 Subject: Creates a generic mechanism to dump app-side information. Currently, 'dumpsys activity' can dump the state of some managers: - AutofillManager - ContentCapturemaanger - UiTranslationController But the support for these custom dumping is hardcoded into Activity itself, which makes it harder to extend. For example, automotive builds provide an app-side Car object, which currently cannot be dumped. This CL makes the mechanism more flexible by providing a couple new public / SystemAPIs that let Automotive (or other mainline modules) extend it. Examples: $ adb shell dumpsys activity com.android.car.carlauncher/.CarLauncher --list-dumpables $ adb shell dumpsys activity com.android.car.carlauncher/.CarLauncher --dump-dumpable CarUserManager $ adb shell dumpsys activity service com.android.systemui/.SystemUIService CarUserManager NOTE: this CL only adds the new APIs; a follow-up CL will change the existing managers to use them. Test: see above Test: m update-api Bug: 149254050 CTS-Coverage-Bug: 149254050 Change-Id: I6920ff3542d3d75edd667c2c7658e9d0a7af534f --- core/api/current.txt | 9 ++ core/api/module-lib-current.txt | 4 + core/java/android/app/Activity.java | 47 ++++++- core/java/android/util/Dumpable.java | 47 +++++++ core/java/android/util/DumpableContainer.java | 40 ++++++ .../internal/util/dump/DumpableContainerImpl.java | 139 +++++++++++++++++++++ .../com/android/systemui/SystemUIApplication.java | 38 +++++- .../core/java/com/android/server/Dumpable.java | 2 + 8 files changed, 322 insertions(+), 4 deletions(-) create mode 100644 core/java/android/util/Dumpable.java create mode 100644 core/java/android/util/DumpableContainer.java create mode 100644 core/java/com/android/internal/util/dump/DumpableContainerImpl.java diff --git a/core/api/current.txt b/core/api/current.txt index 4f15fa9bf3b5..1137d867b1dc 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -47166,6 +47166,15 @@ package android.util { field public float ydpi; } + public interface Dumpable { + method public void dump(@NonNull java.io.PrintWriter, @Nullable String[]); + method @NonNull public default String getDumpableName(); + } + + public interface DumpableContainer { + method public boolean addDumpable(@NonNull android.util.Dumpable); + } + public class EventLog { method public static int getTagCode(String); method public static String getTagName(int); diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index 4d8453725205..8365e5623e8f 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -9,6 +9,10 @@ package android { package android.app { + @UiContext public class Activity extends android.view.ContextThemeWrapper implements android.content.ComponentCallbacks2 android.view.KeyEvent.Callback android.view.LayoutInflater.Factory2 android.view.View.OnCreateContextMenuListener android.view.Window.Callback { + method public final boolean addDumpable(@NonNull android.util.Dumpable); + } + public class ActivityManager { method @RequiresPermission(android.Manifest.permission.SET_ACTIVITY_WATCHER) public void addHomeVisibilityListener(@NonNull java.util.concurrent.Executor, @NonNull android.app.HomeVisibilityListener); method @RequiresPermission(android.Manifest.permission.SET_ACTIVITY_WATCHER) public void removeHomeVisibilityListener(@NonNull android.app.HomeVisibilityListener); diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index cf2b7aca8e52..283345f07337 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -90,6 +90,7 @@ import android.transition.Scene; import android.transition.TransitionManager; import android.util.ArrayMap; import android.util.AttributeSet; +import android.util.Dumpable; import android.util.EventLog; import android.util.Log; import android.util.PrintWriterPrinter; @@ -145,6 +146,7 @@ import com.android.internal.app.IVoiceInteractor; import com.android.internal.app.ToolbarActionBar; import com.android.internal.app.WindowDecorActionBar; import com.android.internal.policy.PhoneWindow; +import com.android.internal.util.dump.DumpableContainerImpl; import dalvik.system.VMRuntime; @@ -954,6 +956,9 @@ public class Activity extends ContextThemeWrapper private SplashScreen mSplashScreen; + @Nullable + private DumpableContainerImpl mDumpableContainer; + private final WindowControllerCallback mWindowControllerCallback = new WindowControllerCallback() { /** @@ -7081,8 +7086,23 @@ public class Activity extends ContextThemeWrapper dumpInner(prefix, fd, writer, args); } + /** + * See {@link android.util.DumpableContainer#addDumpable(Dumpable)}. + * + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public final boolean addDumpable(@NonNull Dumpable dumpable) { + if (mDumpableContainer == null) { + mDumpableContainer = new DumpableContainerImpl(); + } + return mDumpableContainer.addDumpable(dumpable); + } + void dumpInner(@NonNull String prefix, @Nullable FileDescriptor fd, @NonNull PrintWriter writer, @Nullable String[] args) { + String innerPrefix = prefix + " "; + if (args != null && args.length > 0) { // Handle special cases switch (args[0]) { @@ -7095,12 +7115,33 @@ public class Activity extends ContextThemeWrapper case "--translation": dumpUiTranslation(prefix, writer); return; + case "--list-dumpables": + if (mDumpableContainer == null) { + writer.print(prefix); writer.println("No dumpables"); + return; + } + mDumpableContainer.listDumpables(prefix, writer); + return; + case "--dump-dumpable": + if (args.length == 1) { + writer.println("--dump-dumpable requires the dumpable name"); + return; + } + if (mDumpableContainer == null) { + writer.println("no dumpables"); + return; + } + // Strips --dump-dumpable NAME + String[] prunedArgs = new String[args.length - 2]; + System.arraycopy(args, 2, prunedArgs, 0, prunedArgs.length); + mDumpableContainer.dumpOneDumpable(prefix, writer, args[1], prunedArgs); + return; } } + writer.print(prefix); writer.print("Local Activity "); writer.print(Integer.toHexString(System.identityHashCode(this))); writer.println(" State:"); - String innerPrefix = prefix + " "; writer.print(innerPrefix); writer.print("mResumed="); writer.print(mResumed); writer.print(" mStopped="); writer.print(mStopped); writer.print(" mFinished="); @@ -7138,6 +7179,10 @@ public class Activity extends ContextThemeWrapper dumpUiTranslation(prefix, writer); ResourcesManager.getInstance().dump(prefix, writer); + + if (mDumpableContainer != null) { + mDumpableContainer.dumpAllDumpables(prefix, writer, args); + } } void dumpContentCaptureManager(String prefix, PrintWriter writer) { diff --git a/core/java/android/util/Dumpable.java b/core/java/android/util/Dumpable.java new file mode 100644 index 000000000000..79c576d08866 --- /dev/null +++ b/core/java/android/util/Dumpable.java @@ -0,0 +1,47 @@ +/* + * 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 android.util; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.io.PrintWriter; + +/** + * Represents an object whose state can be dumped into a {@link PrintWriter}. + */ +public interface Dumpable { + + /** + * Gets the name of the {@link Dumpable}. + * + * @return class name, by default. + */ + @NonNull + default String getDumpableName() { + return getClass().getName(); + } + + //TODO(b/149254050): decide whether it should take a ParcelFileDescription as well. + + /** + * Dumps the internal state into the given {@code writer}. + * + * @param writer writer to be written to + * @param args optional list of arguments + */ + void dump(@NonNull PrintWriter writer, @Nullable String[] args); +} diff --git a/core/java/android/util/DumpableContainer.java b/core/java/android/util/DumpableContainer.java new file mode 100644 index 000000000000..04d19dc41926 --- /dev/null +++ b/core/java/android/util/DumpableContainer.java @@ -0,0 +1,40 @@ +/* + * 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 android.util; + +import android.annotation.NonNull; + +/** + * Objects that contains a list of {@link Dumpable}, which will be dumped when the object itself + * is dumped. + */ +public interface DumpableContainer { + + /** + * Adds the given {@link Dumpable dumpable} to the container. + * + *

If a dumpable with the same {@link Dumpable#getDumpableName() name} was added before, this + * call is ignored. + * + * @param dumpable dumpable to be added. + * + * @throws IllegalArgumentException if the {@link Dumpable#getDumpableName() dumpable name} is + * {@code null}. + * + * @return {@code true} if the dumpable was added, {@code false} if the call was ignored. + */ + boolean addDumpable(@NonNull Dumpable dumpable); +} diff --git a/core/java/com/android/internal/util/dump/DumpableContainerImpl.java b/core/java/com/android/internal/util/dump/DumpableContainerImpl.java new file mode 100644 index 000000000000..d48b4b136f4a --- /dev/null +++ b/core/java/com/android/internal/util/dump/DumpableContainerImpl.java @@ -0,0 +1,139 @@ +/* + * 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.internal.util.dump; + +import android.annotation.Nullable; +import android.util.ArrayMap; +import android.util.Dumpable; +import android.util.DumpableContainer; +import android.util.IndentingPrintWriter; +import android.util.Log; + +import java.io.PrintWriter; +import java.util.Objects; + +// TODO(b/149254050): add unit tests +/** + * Helper class for {@link DumpableContainer} implementations - they can "implement it by + * association", i.e., by delegating the interface methods to a {@code DumpableContainerImpl}. + * + * @hide + */ +public final class DumpableContainerImpl implements DumpableContainer { + + private static final String TAG = DumpableContainerImpl.class.getSimpleName(); + + private static final boolean DEBUG = false; + + @Nullable + private final ArrayMap mDumpables = new ArrayMap<>(); + + @Override + public boolean addDumpable(Dumpable dumpable) { + Objects.requireNonNull(dumpable, "dumpable"); + String name = dumpable.getDumpableName(); + Objects.requireNonNull(name, () -> "name of" + dumpable); + + if (mDumpables.containsKey(name)) { + Log.e(TAG, "addDumpable(): ignoring " + dumpable + " as there is already a dumpable" + + " with that name (" + name + "): " + mDumpables.get(name)); + return false; + } + + if (DEBUG) { + Log.d(TAG, "Adding " + name + " -> " + dumpable); + } + mDumpables.put(name, dumpable); + return true; + } + + /** + * Dumps the number of dumpable, without a newline. + */ + private int dumpNumberDumpables(IndentingPrintWriter writer) { + int size = mDumpables == null ? 0 : mDumpables.size(); + if (size == 0) { + writer.print("No dumpables"); + } else { + writer.print(size); writer.print(" dumpables"); + } + return size; + } + + /** + * Lists the name of all dumpables to the given {@code writer}. + */ + public void listDumpables(String prefix, PrintWriter writer) { + IndentingPrintWriter ipw = new IndentingPrintWriter(writer, prefix, prefix); + + int size = dumpNumberDumpables(ipw); + if (size == 0) { + ipw.println(); + return; + } + ipw.print(": "); + for (int i = 0; i < size; i++) { + ipw.print(mDumpables.keyAt(i)); + if (i < size - 1) ipw.print(' '); + } + ipw.println(); + } + + /** + * Dumps the content of all dumpables to the given {@code writer}. + */ + public void dumpAllDumpables(String prefix, PrintWriter writer, String[] args) { + IndentingPrintWriter ipw = new IndentingPrintWriter(writer, prefix, prefix); + int size = dumpNumberDumpables(ipw); + if (size == 0) { + ipw.println(); + return; + } + ipw.println(": "); + + for (int i = 0; i < size; i++) { + String dumpableName = mDumpables.keyAt(i); + ipw.print('#'); ipw.print(i); ipw.print(": "); ipw.println(dumpableName); + Dumpable dumpable = mDumpables.valueAt(i); + indentAndDump(ipw, dumpable, args); + } + } + + private void indentAndDump(IndentingPrintWriter writer, Dumpable dumpable, String[] args) { + writer.increaseIndent(); + try { + dumpable.dump(writer, args); + } finally { + writer.decreaseIndent(); + } + } + + /** + * Dumps the content of a specific dumpable to the given {@code writer}. + */ + @SuppressWarnings("resource") // cannot close ipw as it would close writer + public void dumpOneDumpable(String prefix, PrintWriter writer, String dumpableName, + String[] args) { + IndentingPrintWriter ipw = new IndentingPrintWriter(writer, prefix, prefix); + Dumpable dumpable = mDumpables.get(dumpableName); + if (dumpable == null) { + ipw.print("No "); ipw.println(dumpableName); + return; + } + ipw.print(dumpableName); ipw.println(':'); + indentAndDump(ipw, dumpable, args); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index 63962fa6da11..daca918ec0c5 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -32,6 +32,9 @@ import android.os.RemoteException; import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; +import android.util.ArrayMap; +import android.util.Dumpable; +import android.util.DumpableContainer; import android.util.Log; import android.util.TimingsTraceLog; import android.view.SurfaceControl; @@ -53,13 +56,19 @@ import java.util.Collections; * Application class for SystemUI. */ public class SystemUIApplication extends Application implements - SystemUIAppComponentFactory.ContextInitializer { + SystemUIAppComponentFactory.ContextInitializer, DumpableContainer { public static final String TAG = "SystemUIService"; private static final boolean DEBUG = false; private ContextComponentHelper mComponentHelper; private BootCompleteCacheImpl mBootCompleteCache; + private DumpManager mDumpManager; + + /** + * Map of dumpables added externally. + */ + private final ArrayMap mDumpables = new ArrayMap<>(); /** * Hold a reference on the stuff we start. @@ -214,7 +223,7 @@ public class SystemUIApplication extends Application implements } } - final DumpManager dumpManager = mSysUIComponent.createDumpManager(); + mDumpManager = mSysUIComponent.createDumpManager(); Log.v(TAG, "Starting SystemUI services for user " + Process.myUserHandle().getIdentifier() + "."); @@ -255,7 +264,7 @@ public class SystemUIApplication extends Application implements mServices[i].onBootCompleted(); } - dumpManager.registerDumpable(mServices[i].getClass().getName(), mServices[i]); + mDumpManager.registerDumpable(mServices[i].getClass().getName(), mServices[i]); } mSysUIComponent.getInitController().executePostInitTasks(); log.traceEnd(); @@ -263,6 +272,29 @@ public class SystemUIApplication extends Application implements mServicesStarted = true; } + // TODO(b/149254050): add unit tests? There doesn't seem to be a SystemUiApplicationTest... + @Override + public boolean addDumpable(Dumpable dumpable) { + String name = dumpable.getDumpableName(); + if (mDumpables.containsKey(name)) { + // This is normal because SystemUIApplication is an application context that is shared + // among multiple components + if (DEBUG) { + Log.d(TAG, "addDumpable(): ignoring " + dumpable + " as there is already a dumpable" + + " with that name (" + name + "): " + mDumpables.get(name)); + } + return false; + } + if (DEBUG) Log.d(TAG, "addDumpable(): adding '" + name + "' = " + dumpable); + mDumpables.put(name, dumpable); + + // TODO(b/149254050): replace com.android.systemui.dump.Dumpable by + // com.android.util.Dumpable and get rid of the intermediate lambda + mDumpManager.registerDumpable(dumpable.getDumpableName(), + (fd, pw, args) -> dumpable.dump(pw, args)); + return true; + } + @Override public void onConfigurationChanged(Configuration newConfig) { if (mServicesStarted) { diff --git a/services/core/java/com/android/server/Dumpable.java b/services/core/java/com/android/server/Dumpable.java index 866f81c1c5c6..004f923774e1 100644 --- a/services/core/java/com/android/server/Dumpable.java +++ b/services/core/java/com/android/server/Dumpable.java @@ -24,6 +24,8 @@ import android.util.IndentingPrintWriter; * *

See {@link SystemServer.SystemServerDumper} for usage example. */ +// TODO(b/149254050): replace / merge with package android.util.Dumpable (it would require +// exporting IndentingPrintWriter as @SystemApi) and/or changing the method to use a prefix public interface Dumpable { /** -- cgit v1.2.3-59-g8ed1b