Merge "Improve VibratorManagerService dumpsys" into main
diff --git a/core/java/android/os/CombinedVibration.java b/core/java/android/os/CombinedVibration.java
index db1a741..f32a1f8 100644
--- a/core/java/android/os/CombinedVibration.java
+++ b/core/java/android/os/CombinedVibration.java
@@ -24,7 +24,9 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 import java.util.Objects;
+import java.util.StringJoiner;
 
 /**
  * A CombinedVibration describes a combination of haptic effects to be performed by one or more
@@ -151,6 +153,13 @@
     public abstract boolean hasVibrator(int vibratorId);
 
     /**
+     * Returns a compact version of the {@link #toString()} result for debugging purposes.
+     *
+     * @hide
+     */
+    public abstract String toDebugString();
+
+    /**
      * Adapts a {@link VibrationEffect} to a specific device vibrator using the ID.
      *
      * <p>This can be used for adapting effects to the capabilities of the specific device vibrator
@@ -437,6 +446,13 @@
             return "Mono{mEffect=" + mEffect + '}';
         }
 
+        /** @hide */
+        @Override
+        public String toDebugString() {
+            // Simplify vibration string, use the single effect to represent it.
+            return mEffect.toDebugString();
+        }
+
         @Override
         public int describeContents() {
             return 0;
@@ -619,6 +635,17 @@
             return "Stereo{mEffects=" + mEffects + '}';
         }
 
+        /** @hide */
+        @Override
+        public String toDebugString() {
+            StringJoiner sj = new StringJoiner(",", "Stereo{", "}");
+            for (int i = 0; i < mEffects.size(); i++) {
+                sj.add(String.format(Locale.ROOT, "vibrator(id=%d): %s",
+                        mEffects.keyAt(i), mEffects.valueAt(i).toDebugString()));
+            }
+            return sj.toString();
+        }
+
         @Override
         public int describeContents() {
             return 0;
@@ -833,6 +860,17 @@
             return "Sequential{mEffects=" + mEffects + ", mDelays=" + mDelays + '}';
         }
 
+        /** @hide */
+        @Override
+        public String toDebugString() {
+            StringJoiner sj = new StringJoiner(",", "Sequential{", "}");
+            for (int i = 0; i < mEffects.size(); i++) {
+                sj.add(String.format(Locale.ROOT, "delayMs=%d, effect=%s",
+                        mDelays.get(i), mEffects.get(i).toDebugString()));
+            }
+            return sj.toString();
+        }
+
         @Override
         public int describeContents() {
             return 0;
diff --git a/core/java/android/os/VibrationAttributes.java b/core/java/android/os/VibrationAttributes.java
index b2d62eb..98f9dff 100644
--- a/core/java/android/os/VibrationAttributes.java
+++ b/core/java/android/os/VibrationAttributes.java
@@ -336,10 +336,11 @@
 
     @Override
     public String toString() {
-        return "VibrationAttributes:"
-                + " Usage=" + usageToString()
-                + " Audio Usage= " + AudioAttributes.usageToString(mOriginalAudioUsage)
-                + " Flags=" + mFlags;
+        return "VibrationAttributes{"
+                + "mUsage=" + usageToString()
+                + ", mAudioUsage= " + AudioAttributes.usageToString(mOriginalAudioUsage)
+                + ", mFlags=" + mFlags
+                + '}';
     }
 
     /** @hide */
diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index c3d7540..b24b45d 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -44,7 +44,9 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Locale;
 import java.util.Objects;
+import java.util.StringJoiner;
 
 /**
  * A VibrationEffect describes a haptic effect to be performed by a {@link Vibrator}.
@@ -629,6 +631,13 @@
         return MathUtils.constrain(a * fx, 0f, 1f);
     }
 
+    /**
+     * Returns a compact version of the {@link #toString()} result for debugging purposes.
+     *
+     * @hide
+     */
+    public abstract String toDebugString();
+
     /** @hide */
     public static String effectIdToString(int effectId) {
         switch (effectId) {
@@ -925,6 +934,23 @@
                     + "}";
         }
 
+        /** @hide */
+        @Override
+        public String toDebugString() {
+            if (mSegments.size() == 1 && mRepeatIndex < 0) {
+                // Simplify effect string, use the single segment to represent it.
+                return mSegments.get(0).toDebugString();
+            }
+            StringJoiner sj = new StringJoiner(",", "[", "]");
+            for (int i = 0; i < mSegments.size(); i++) {
+                sj.add(mSegments.get(i).toDebugString());
+            }
+            if (mRepeatIndex >= 0) {
+                return String.format(Locale.ROOT, "%s, repeat=%d", sj, mRepeatIndex);
+            }
+            return sj.toString();
+        }
+
         @Override
         public int describeContents() {
             return 0;
@@ -1250,23 +1276,23 @@
         public static String primitiveToString(@PrimitiveType int id) {
             switch (id) {
                 case PRIMITIVE_NOOP:
-                    return "PRIMITIVE_NOOP";
+                    return "NOOP";
                 case PRIMITIVE_CLICK:
-                    return "PRIMITIVE_CLICK";
+                    return "CLICK";
                 case PRIMITIVE_THUD:
-                    return "PRIMITIVE_THUD";
+                    return "THUD";
                 case PRIMITIVE_SPIN:
-                    return "PRIMITIVE_SPIN";
+                    return "SPIN";
                 case PRIMITIVE_QUICK_RISE:
-                    return "PRIMITIVE_QUICK_RISE";
+                    return "QUICK_RISE";
                 case PRIMITIVE_SLOW_RISE:
-                    return "PRIMITIVE_SLOW_RISE";
+                    return "SLOW_RISE";
                 case PRIMITIVE_QUICK_FALL:
-                    return "PRIMITIVE_QUICK_FALL";
+                    return "QUICK_FALL";
                 case PRIMITIVE_TICK:
-                    return "PRIMITIVE_TICK";
+                    return "TICK";
                 case PRIMITIVE_LOW_TICK:
-                    return "PRIMITIVE_LOW_TICK";
+                    return "LOW_TICK";
                 default:
                     return Integer.toString(id);
             }
diff --git a/core/java/android/os/VibratorInfo.java b/core/java/android/os/VibratorInfo.java
index 71ec096..02e6856 100644
--- a/core/java/android/os/VibratorInfo.java
+++ b/core/java/android/os/VibratorInfo.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.hardware.vibrator.Braking;
 import android.hardware.vibrator.IVibrator;
+import android.util.IndentingPrintWriter;
 import android.util.MathUtils;
 import android.util.Range;
 import android.util.SparseBooleanArray;
@@ -207,6 +208,25 @@
                 + '}';
     }
 
+    /** @hide */
+    public void dump(IndentingPrintWriter pw) {
+        pw.println("VibratorInfo:");
+        pw.increaseIndent();
+        pw.println("id = " + mId);
+        pw.println("capabilities = " + Arrays.toString(getCapabilitiesNames()));
+        pw.println("capabilitiesFlags = " + Long.toBinaryString(mCapabilities));
+        pw.println("supportedEffects = " + Arrays.toString(getSupportedEffectsNames()));
+        pw.println("supportedPrimitives = " + Arrays.toString(getSupportedPrimitivesNames()));
+        pw.println("supportedBraking = " + Arrays.toString(getSupportedBrakingNames()));
+        pw.println("primitiveDelayMax = " + mPrimitiveDelayMax);
+        pw.println("compositionSizeMax = " + mCompositionSizeMax);
+        pw.println("pwlePrimitiveDurationMax = " + mPwlePrimitiveDurationMax);
+        pw.println("pwleSizeMax = " + mPwleSizeMax);
+        pw.println("q-factor = " + mQFactor);
+        pw.println("frequencyProfile = " + mFrequencyProfile);
+        pw.decreaseIndent();
+    }
+
     /** Return the id of this vibrator. */
     public int getId() {
         return mId;
@@ -370,7 +390,7 @@
      * Gets the resonant frequency of the vibrator.
      *
      * @return the resonant frequency of the vibrator, or {@link Float#NaN NaN} if it's unknown or
-     *         this vibrator is a composite of multiple physical devices.
+     * this vibrator is a composite of multiple physical devices.
      */
     public float getResonantFrequencyHz() {
         return mFrequencyProfile.mResonantFrequencyHz;
@@ -380,7 +400,7 @@
      * Gets the <a href="https://en.wikipedia.org/wiki/Q_factor">Q factor</a> of the vibrator.
      *
      * @return the Q factor of the vibrator, or {@link Float#NaN NaN} if it's unknown or
-     *         this vibrator is a composite of multiple physical devices.
+     * this vibrator is a composite of multiple physical devices.
      */
     public float getQFactor() {
         return mQFactor;
diff --git a/core/java/android/os/vibrator/PrebakedSegment.java b/core/java/android/os/vibrator/PrebakedSegment.java
index 462e5ad..42b6c2da 100644
--- a/core/java/android/os/vibrator/PrebakedSegment.java
+++ b/core/java/android/os/vibrator/PrebakedSegment.java
@@ -203,6 +203,15 @@
                 + "}";
     }
 
+    /** @hide */
+    @Override
+    public String toDebugString() {
+        return String.format("Prebaked=%s(%s, %s fallback)",
+                VibrationEffect.effectIdToString(mEffectId),
+                VibrationEffect.effectStrengthToString(mEffectStrength),
+                mFallback ? "with" : "no");
+    }
+
     @Override
     public int describeContents() {
         return 0;
diff --git a/core/java/android/os/vibrator/PrimitiveSegment.java b/core/java/android/os/vibrator/PrimitiveSegment.java
index 815a1dc..c52a09c 100644
--- a/core/java/android/os/vibrator/PrimitiveSegment.java
+++ b/core/java/android/os/vibrator/PrimitiveSegment.java
@@ -140,6 +140,13 @@
                 + '}';
     }
 
+    /** @hide */
+    @Override
+    public String toDebugString() {
+        return String.format("Primitive=%s(scale=%.2f, delay=%dms)",
+                VibrationEffect.Composition.primitiveToString(mPrimitiveId), mScale, mDelay);
+    }
+
     @Override
     public boolean equals(@Nullable Object o) {
         if (this == o) return true;
diff --git a/core/java/android/os/vibrator/RampSegment.java b/core/java/android/os/vibrator/RampSegment.java
index a5dee6c..e997bcd 100644
--- a/core/java/android/os/vibrator/RampSegment.java
+++ b/core/java/android/os/vibrator/RampSegment.java
@@ -179,6 +179,17 @@
                 + "}";
     }
 
+    /** @hide */
+    @Override
+    public String toDebugString() {
+        return String.format("Ramp=%dms(amplitude=%.2f%s to %.2f%s)",
+                mDuration,
+                mStartAmplitude,
+                Float.compare(mStartFrequencyHz, 0) == 0 ? "" : " @ " + mStartFrequencyHz + "Hz",
+                mEndAmplitude,
+                Float.compare(mEndFrequencyHz, 0) == 0 ? "" : " @ " + mEndFrequencyHz + "Hz");
+    }
+
     @Override
     public int describeContents() {
         return 0;
diff --git a/core/java/android/os/vibrator/StepSegment.java b/core/java/android/os/vibrator/StepSegment.java
index 54a44a8..a585aa8 100644
--- a/core/java/android/os/vibrator/StepSegment.java
+++ b/core/java/android/os/vibrator/StepSegment.java
@@ -161,6 +161,13 @@
                 + "}";
     }
 
+    /** @hide */
+    @Override
+    public String toDebugString() {
+        return String.format("Step=%dms(amplitude=%.2f%s)", mDuration, mAmplitude,
+                Float.compare(mFrequencyHz, 0) == 0 ? "" : " @ " + mFrequencyHz + "Hz");
+    }
+
     @Override
     public int describeContents() {
         return 0;
diff --git a/core/java/android/os/vibrator/VibrationConfig.java b/core/java/android/os/vibrator/VibrationConfig.java
index 4790d81..bde334a 100644
--- a/core/java/android/os/vibrator/VibrationConfig.java
+++ b/core/java/android/os/vibrator/VibrationConfig.java
@@ -32,6 +32,9 @@
 import android.os.VibrationAttributes;
 import android.os.Vibrator;
 import android.os.Vibrator.VibrationIntensity;
+import android.util.IndentingPrintWriter;
+
+import java.io.PrintWriter;
 
 /**
  * List of device-specific internal vibration configuration loaded from platform config.xml.
@@ -191,4 +194,18 @@
                 + ", mDefaultRingIntensity=" + mDefaultRingVibrationIntensity
                 + "}";
     }
+
+    /**
+     * Write current settings into given {@link PrintWriter}, skipping the default settings.
+     *
+     * @hide
+     */
+    public void dumpWithoutDefaultSettings(IndentingPrintWriter pw) {
+        pw.println("VibrationConfig:");
+        pw.increaseIndent();
+        pw.println("hapticChannelMaxAmplitude = " + mHapticChannelMaxVibrationAmplitude);
+        pw.println("rampStepDurationMs = " + mRampStepDurationMs);
+        pw.println("rampDownDurationMs = " + mRampDownDurationMs);
+        pw.decreaseIndent();
+    }
 }
diff --git a/core/java/android/os/vibrator/VibrationEffectSegment.java b/core/java/android/os/vibrator/VibrationEffectSegment.java
index 9a19ed4..3b286a7 100644
--- a/core/java/android/os/vibrator/VibrationEffectSegment.java
+++ b/core/java/android/os/vibrator/VibrationEffectSegment.java
@@ -116,6 +116,13 @@
     public abstract <T extends VibrationEffectSegment> T applyEffectStrength(int effectStrength);
 
     /**
+     * Returns a compact version of the {@link #toString()} result for debugging purposes.
+     *
+     * @hide
+     */
+    public abstract String toDebugString();
+
+    /**
      * Checks the given frequency argument is valid to represent a vibration effect frequency in
      * hertz, i.e. a finite non-negative value.
      *
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 54dff08..64be87b 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3938,8 +3938,14 @@
     <!-- Flag indicating device support for EAP SIM, AKA, AKA' -->
     <bool name="config_eap_sim_based_auth_supported">true</bool>
 
+    <!-- How long history of recent vibrations should be kept for the dumpsys. -->
+    <integer name="config_recentVibrationsDumpSizeLimit">20</integer>
+
     <!-- How long history of previous vibrations should be kept for the dumpsys. -->
-    <integer name="config_previousVibrationsDumpLimit">50</integer>
+    <integer name="config_previousVibrationsDumpSizeLimit">50</integer>
+
+    <!-- How close vibration request should be when they're aggregated for dumpsys, in ms. -->
+    <integer name="config_previousVibrationsDumpAggregationTimeMillisLimit">1000</integer>
 
     <!-- The default vibration strength, must be between 1 and 255 inclusive. -->
     <integer name="config_defaultVibrationAmplitude">255</integer>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 9da0f17..3aae3ca 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2038,7 +2038,9 @@
   <java-symbol type="integer" name="config_notificationsBatteryNearlyFullLevel" />
   <java-symbol type="integer" name="config_notificationServiceArchiveSize" />
   <java-symbol type="dimen" name="config_rotaryEncoderAxisScrollTickInterval" />
-  <java-symbol type="integer" name="config_previousVibrationsDumpLimit" />
+  <java-symbol type="integer" name="config_recentVibrationsDumpSizeLimit" />
+  <java-symbol type="integer" name="config_previousVibrationsDumpSizeLimit" />
+  <java-symbol type="integer" name="config_previousVibrationsDumpAggregationTimeMillisLimit" />
   <java-symbol type="integer" name="config_defaultVibrationAmplitude" />
   <java-symbol type="dimen" name="config_hapticChannelMaxVibrationAmplitude" />
   <java-symbol type="integer" name="config_vibrationWaveformRampStepDuration" />
diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java
index 4f7f13e..fed6e7e 100644
--- a/services/core/java/com/android/server/vibrator/Vibration.java
+++ b/services/core/java/com/android/server/vibrator/Vibration.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.media.AudioAttributes;
 import android.os.CombinedVibration;
 import android.os.IBinder;
 import android.os.VibrationAttributes;
@@ -27,8 +28,10 @@
 import android.os.vibrator.RampSegment;
 import android.os.vibrator.StepSegment;
 import android.os.vibrator.VibrationEffectSegment;
+import android.util.IndentingPrintWriter;
 import android.util.proto.ProtoOutputStream;
 
+import java.io.PrintWriter;
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.Locale;
@@ -39,7 +42,9 @@
  * The base class for all vibrations.
  */
 abstract class Vibration {
-    private static final SimpleDateFormat DEBUG_DATE_FORMAT =
+    private static final SimpleDateFormat DEBUG_TIME_FORMAT =
+            new SimpleDateFormat("HH:mm:ss.SSS");
+    private static final SimpleDateFormat DEBUG_DATE_TIME_FORMAT =
             new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
     // Used to generate globally unique vibration ids.
     private static final AtomicInteger sNextVibrationId = new AtomicInteger(1); // 0 = no callback
@@ -146,10 +151,10 @@
         @Override
         public String toString() {
             return "CallerInfo{"
-                    + " attrs=" + attrs
-                    + ", uid=" + uid
-                    + ", displayId=" + displayId
+                    + " uid=" + uid
                     + ", opPkg=" + opPkg
+                    + ", displayId=" + displayId
+                    + ", attrs=" + attrs
                     + ", reason=" + reason
                     + '}';
         }
@@ -203,14 +208,17 @@
      * potentially expensive or resource-linked objects, such as {@link IBinder}.
      */
     static final class DebugInfo {
-        private final long mCreateTime;
+        final long mCreateTime;
+        final CallerInfo mCallerInfo;
+        @Nullable
+        final CombinedVibration mPlayedEffect;
+
         private final long mStartTime;
         private final long mEndTime;
         private final long mDurationMs;
-        @Nullable private final CombinedVibration mOriginalEffect;
-        @Nullable private final CombinedVibration mPlayedEffect;
+        @Nullable
+        private final CombinedVibration mOriginalEffect;
         private final float mScale;
-        private final CallerInfo mCallerInfo;
         private final Status mStatus;
 
         DebugInfo(Status status, VibrationStats stats, @Nullable CombinedVibration playedEffect,
@@ -230,10 +238,10 @@
 
         @Override
         public String toString() {
-            return "createTime: " + DEBUG_DATE_FORMAT.format(new Date(mCreateTime))
-                    + ", startTime: " + DEBUG_DATE_FORMAT.format(new Date(mStartTime))
+            return "createTime: " + DEBUG_DATE_TIME_FORMAT.format(new Date(mCreateTime))
+                    + ", startTime: " + DEBUG_DATE_TIME_FORMAT.format(new Date(mStartTime))
                     + ", endTime: "
-                    + (mEndTime == 0 ? null : DEBUG_DATE_FORMAT.format(new Date(mEndTime)))
+                    + (mEndTime == 0 ? null : DEBUG_DATE_TIME_FORMAT.format(new Date(mEndTime)))
                     + ", durationMs: " + mDurationMs
                     + ", status: " + mStatus.name().toLowerCase(Locale.ROOT)
                     + ", playedEffect: " + mPlayedEffect
@@ -242,8 +250,56 @@
                     + ", callerInfo: " + mCallerInfo;
         }
 
+        /**
+         * Write this info in a compact way into given {@link PrintWriter}.
+         *
+         * <p>This is used by dumpsys to log multiple vibration records in single lines that are
+         * easy to skim through by the sorted created time.
+         */
+        void dumpCompact(IndentingPrintWriter pw) {
+            boolean isExternalVibration = mPlayedEffect == null;
+            String timingsStr = String.format(Locale.ROOT,
+                    "%s | %8s | %20s | duration: %5dms | start: %12s | end: %10s",
+                    DEBUG_DATE_TIME_FORMAT.format(new Date(mCreateTime)),
+                    isExternalVibration ? "external" : "effect",
+                    mStatus.name().toLowerCase(Locale.ROOT),
+                    mDurationMs,
+                    mStartTime == 0 ? "" : DEBUG_TIME_FORMAT.format(new Date(mStartTime)),
+                    mEndTime == 0 ? "" : DEBUG_TIME_FORMAT.format(new Date(mEndTime)));
+            String callerInfoStr = String.format(Locale.ROOT,
+                    " | %s (uid=%d, displayId=%d) | usage: %s (audio=%s) | flags: %s | reason: %s",
+                    mCallerInfo.opPkg, mCallerInfo.uid, mCallerInfo.displayId,
+                    mCallerInfo.attrs.usageToString(),
+                    AudioAttributes.usageToString(mCallerInfo.attrs.getAudioUsage()),
+                    Long.toBinaryString(mCallerInfo.attrs.getFlags()),
+                    mCallerInfo.reason);
+            String effectStr = String.format(Locale.ROOT,
+                    " | played: %s | original: %s | scale: %.2f",
+                    mPlayedEffect == null ? null : mPlayedEffect.toDebugString(),
+                    mOriginalEffect == null ? null : mOriginalEffect.toDebugString(),
+                    mScale);
+            pw.println(timingsStr + callerInfoStr + effectStr);
+        }
+
+        /** Write this info into given {@link PrintWriter}. */
+        void dump(IndentingPrintWriter pw) {
+            pw.println("Vibration:");
+            pw.increaseIndent();
+            pw.println("status = " + mStatus.name().toLowerCase(Locale.ROOT));
+            pw.println("durationMs = " + mDurationMs);
+            pw.println("createTime = " + DEBUG_DATE_TIME_FORMAT.format(new Date(mCreateTime)));
+            pw.println("startTime = " + DEBUG_DATE_TIME_FORMAT.format(new Date(mStartTime)));
+            pw.println("endTime = "
+                    + (mEndTime == 0 ? null : DEBUG_DATE_TIME_FORMAT.format(new Date(mEndTime))));
+            pw.println("playedEffect = " + mPlayedEffect);
+            pw.println("originalEffect = " + mOriginalEffect);
+            pw.println("scale = " + String.format(Locale.ROOT, "%.2f", mScale));
+            pw.println("callerInfo = " + mCallerInfo);
+            pw.decreaseIndent();
+        }
+
         /** Write this info into given {@code fieldId} on {@link ProtoOutputStream}. */
-        public void dumpProto(ProtoOutputStream proto, long fieldId) {
+        void dump(ProtoOutputStream proto, long fieldId) {
             final long token = proto.start(fieldId);
             proto.write(VibrationProto.START_TIME, mStartTime);
             proto.write(VibrationProto.END_TIME, mEndTime);
diff --git a/services/core/java/com/android/server/vibrator/VibrationSettings.java b/services/core/java/com/android/server/vibrator/VibrationSettings.java
index dbd6bf4..db8a9ae 100644
--- a/services/core/java/com/android/server/vibrator/VibrationSettings.java
+++ b/services/core/java/com/android/server/vibrator/VibrationSettings.java
@@ -54,6 +54,7 @@
 import android.os.Vibrator.VibrationIntensity;
 import android.os.vibrator.VibrationConfig;
 import android.provider.Settings;
+import android.util.IndentingPrintWriter;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
@@ -65,6 +66,7 @@
 import com.android.server.LocalServices;
 import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
@@ -601,8 +603,32 @@
         }
     }
 
+    /** Write current settings into given {@link PrintWriter}. */
+    void dump(IndentingPrintWriter pw) {
+        pw.println("VibrationSettings:");
+        pw.increaseIndent();
+        pw.println("vibrateOn = " + mVibrateOn);
+        pw.println("vibrateInputDevices = " + mVibrateInputDevices);
+        pw.println("batterySaverMode = " + mBatterySaverMode);
+        pw.println("VibrationIntensities:");
+
+        pw.increaseIndent();
+        for (int i = 0; i < mCurrentVibrationIntensities.size(); i++) {
+            int usage = mCurrentVibrationIntensities.keyAt(i);
+            int intensity = mCurrentVibrationIntensities.valueAt(i);
+            pw.println(VibrationAttributes.usageToString(usage) + " = "
+                    + intensityToString(intensity)
+                    + ", default: " + intensityToString(getDefaultIntensity(usage)));
+        }
+        pw.decreaseIndent();
+
+        mVibrationConfig.dumpWithoutDefaultSettings(pw);
+        pw.println("processStateCache = " + mUidObserver.mProcStatesCache);
+        pw.decreaseIndent();
+    }
+
     /** Write current settings into given {@link ProtoOutputStream}. */
-    public void dumpProto(ProtoOutputStream proto) {
+    void dump(ProtoOutputStream proto) {
         synchronized (mLock) {
             proto.write(VibratorManagerServiceDumpProto.VIBRATE_ON, mVibrateOn);
             proto.write(VibratorManagerServiceDumpProto.LOW_POWER_MODE, mBatterySaverMode);
diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java
index 47b3e1a..f5d4d1e 100644
--- a/services/core/java/com/android/server/vibrator/VibratorController.java
+++ b/services/core/java/com/android/server/vibrator/VibratorController.java
@@ -17,6 +17,7 @@
 package com.android.server.vibrator;
 
 import android.annotation.Nullable;
+import android.annotation.Nullable;
 import android.hardware.vibrator.IVibrator;
 import android.os.Binder;
 import android.os.IVibratorStateListener;
@@ -26,6 +27,7 @@
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.PrimitiveSegment;
 import android.os.vibrator.RampSegment;
+import android.util.IndentingPrintWriter;
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
@@ -351,6 +353,19 @@
                 + '}';
     }
 
+    void dump(IndentingPrintWriter pw) {
+        pw.println("VibratorController:");
+        pw.increaseIndent();
+        pw.println("isVibrating = " + mIsVibrating);
+        pw.println("isUnderExternalControl = " + mIsUnderExternalControl);
+        pw.println("currentAmplitude = " + mCurrentAmplitude);
+        pw.println("vibratorInfoLoadSuccessful = " + mVibratorInfoLoadSuccessful);
+        pw.println("vibratorStateListenerCount = "
+                + mVibratorStateListeners.getRegisteredCallbackCount());
+        mVibratorInfo.dump(pw);
+        pw.decreaseIndent();
+    }
+
     @GuardedBy("mLock")
     private void notifyListenerOnVibrating(boolean isVibrating) {
         if (mIsVibrating != isVibrating) {
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 6fdb1db..270f7f0c 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -56,6 +56,7 @@
 import android.os.vibrator.VibrationEffectSegment;
 import android.os.vibrator.persistence.VibrationXmlParser;
 import android.text.TextUtils;
+import android.util.IndentingPrintWriter;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.proto.ProtoOutputStream;
@@ -80,6 +81,7 @@
 import java.util.Arrays;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.Consumer;
 import java.util.function.Function;
 
@@ -204,9 +206,15 @@
         mNativeWrapper = injector.getNativeWrapper();
         mNativeWrapper.init(listener);
 
-        int dumpLimit = mContext.getResources().getInteger(
-                com.android.internal.R.integer.config_previousVibrationsDumpLimit);
-        mVibratorManagerRecords = new VibratorManagerRecords(dumpLimit);
+        int recentDumpSizeLimit = mContext.getResources().getInteger(
+                com.android.internal.R.integer.config_recentVibrationsDumpSizeLimit);
+        int dumpSizeLimit = mContext.getResources().getInteger(
+                com.android.internal.R.integer.config_previousVibrationsDumpSizeLimit);
+        int dumpAggregationTimeLimit = mContext.getResources().getInteger(
+                com.android.internal.R.integer
+                        .config_previousVibrationsDumpAggregationTimeMillisLimit);
+        mVibratorManagerRecords = new VibratorManagerRecords(
+                recentDumpSizeLimit, dumpSizeLimit, dumpAggregationTimeLimit);
 
         mBatteryStatsService = injector.getBatteryStatsService();
         mFrameworkStatsLogger = injector.getFrameworkStatsLogger(mHandler);
@@ -544,49 +552,74 @@
         }
     }
 
-    private void dumpText(PrintWriter pw) {
+    private void dumpText(PrintWriter w) {
         if (DEBUG) {
             Slog.d(TAG, "Dumping vibrator manager service to text...");
         }
+        IndentingPrintWriter pw = new IndentingPrintWriter(w, /* singleIndent= */ "  ");
         synchronized (mLock) {
             pw.println("Vibrator Manager Service:");
-            pw.println("  mVibrationSettings:");
-            pw.println("    " + mVibrationSettings);
+            pw.increaseIndent();
+
+            mVibrationSettings.dump(pw);
             pw.println();
-            pw.println("  mVibratorControllers:");
+
+            pw.println("VibratorControllers:");
+            pw.increaseIndent();
             for (int i = 0; i < mVibrators.size(); i++) {
-                pw.println("    " + mVibrators.valueAt(i));
+                mVibrators.valueAt(i).dump(pw);
             }
+            pw.decreaseIndent();
             pw.println();
-            pw.println("  mCurrentVibration:");
-            pw.println("    " + (mCurrentVibration == null
-                    ? null : mCurrentVibration.getVibration().getDebugInfo()));
+
+            pw.println("CurrentVibration:");
+            pw.increaseIndent();
+            if (mCurrentVibration != null) {
+                mCurrentVibration.getVibration().getDebugInfo().dump(pw);
+            } else {
+                pw.println("null");
+            }
+            pw.decreaseIndent();
             pw.println();
-            pw.println("  mNextVibration:");
-            pw.println("    " + (mNextVibration == null
-                    ? null : mNextVibration.getVibration().getDebugInfo()));
+
+            pw.println("NextVibration:");
+            pw.increaseIndent();
+            if (mNextVibration != null) {
+                mNextVibration.getVibration().getDebugInfo().dump(pw);
+            } else {
+                pw.println("null");
+            }
+            pw.decreaseIndent();
             pw.println();
-            pw.println("  mCurrentExternalVibration:");
-            pw.println("    " + (mCurrentExternalVibration == null
-                    ? null : mCurrentExternalVibration.getDebugInfo()));
-            pw.println();
+
+            pw.println("CurrentExternalVibration:");
+            pw.increaseIndent();
+            if (mCurrentExternalVibration != null) {
+                mCurrentExternalVibration.getDebugInfo().dump(pw);
+            } else {
+                pw.println("null");
+            }
+            pw.decreaseIndent();
         }
-        mVibratorManagerRecords.dumpText(pw);
+
+        pw.println();
+        pw.println();
+        mVibratorManagerRecords.dump(pw);
     }
 
-    synchronized void dumpProto(FileDescriptor fd) {
+    private void dumpProto(FileDescriptor fd) {
         final ProtoOutputStream proto = new ProtoOutputStream(fd);
         if (DEBUG) {
             Slog.d(TAG, "Dumping vibrator manager service to proto...");
         }
         synchronized (mLock) {
-            mVibrationSettings.dumpProto(proto);
+            mVibrationSettings.dump(proto);
             if (mCurrentVibration != null) {
-                mCurrentVibration.getVibration().getDebugInfo().dumpProto(proto,
+                mCurrentVibration.getVibration().getDebugInfo().dump(proto,
                         VibratorManagerServiceDumpProto.CURRENT_VIBRATION);
             }
             if (mCurrentExternalVibration != null) {
-                mCurrentExternalVibration.getDebugInfo().dumpProto(proto,
+                mCurrentExternalVibration.getDebugInfo().dump(proto,
                         VibratorManagerServiceDumpProto.CURRENT_EXTERNAL_VIBRATION);
             }
 
@@ -601,7 +634,7 @@
             proto.write(VibratorManagerServiceDumpProto.VIBRATOR_UNDER_EXTERNAL_CONTROL,
                     isUnderExternalControl);
         }
-        mVibratorManagerRecords.dumpProto(proto);
+        mVibratorManagerRecords.dump(proto);
         proto.flush();
     }
 
@@ -1581,64 +1614,105 @@
 
     /** Keep records of vibrations played and provide debug information for this service. */
     private static final class VibratorManagerRecords {
-        private final SparseArray<LinkedList<Vibration.DebugInfo>> mPreviousVibrations =
-                new SparseArray<>();
-        private final LinkedList<Vibration.DebugInfo> mPreviousExternalVibrations =
-                new LinkedList<>();
-        private final int mPreviousVibrationsLimit;
+        private final VibrationRecords mAggregatedVibrationHistory;
+        private final VibrationRecords mRecentVibrations;
 
-        VibratorManagerRecords(int limit) {
-            mPreviousVibrationsLimit = limit;
+        VibratorManagerRecords(int recentVibrationSizeLimit, int aggregationSizeLimit,
+                int aggregationTimeLimit) {
+            mAggregatedVibrationHistory =
+                    new VibrationRecords(aggregationSizeLimit, aggregationTimeLimit);
+            mRecentVibrations = new VibrationRecords(
+                    recentVibrationSizeLimit, /* aggregationTimeLimit= */ 0);
         }
 
         synchronized void record(HalVibration vib) {
-            int usage = vib.callerInfo.attrs.getUsage();
-            if (!mPreviousVibrations.contains(usage)) {
-                mPreviousVibrations.put(usage, new LinkedList<>());
-            }
-            record(mPreviousVibrations.get(usage), vib.getDebugInfo());
+            record(vib.getDebugInfo());
         }
 
         synchronized void record(ExternalVibrationHolder vib) {
-            record(mPreviousExternalVibrations, vib.getDebugInfo());
+            record(vib.getDebugInfo());
         }
 
-        synchronized void record(LinkedList<Vibration.DebugInfo> records,
-                Vibration.DebugInfo info) {
-            if (records.size() > mPreviousVibrationsLimit) {
-                records.removeFirst();
+        private synchronized void record(Vibration.DebugInfo info) {
+            AggregatedVibrationRecord removedRecord = mRecentVibrations.record(info);
+            if (removedRecord != null) {
+                mAggregatedVibrationHistory.record(removedRecord.mLatestVibration);
             }
-            records.addLast(info);
         }
 
-        synchronized void dumpText(PrintWriter pw) {
-            for (int i = 0; i < mPreviousVibrations.size(); i++) {
-                pw.println();
-                pw.print("  Previous vibrations for usage ");
-                pw.print(VibrationAttributes.usageToString(mPreviousVibrations.keyAt(i)));
-                pw.println(":");
-                for (Vibration.DebugInfo info : mPreviousVibrations.valueAt(i)) {
-                    pw.println("    " + info);
+        synchronized void dump(IndentingPrintWriter pw) {
+            pw.println("Recent vibrations:");
+            pw.increaseIndent();
+            mRecentVibrations.dump(pw);
+            pw.decreaseIndent();
+            pw.println();
+            pw.println();
+
+            pw.println("Aggregated vibration history:");
+            pw.increaseIndent();
+            mAggregatedVibrationHistory.dump(pw);
+            pw.decreaseIndent();
+        }
+
+        synchronized void dump(ProtoOutputStream proto) {
+            mRecentVibrations.dump(proto);
+        }
+    }
+
+    /** Keep records of vibrations played and provide debug information for this service. */
+    private static final class VibrationRecords {
+        private final SparseArray<LinkedList<AggregatedVibrationRecord>> mVibrations =
+                new SparseArray<>();
+        private final int mSizeLimit;
+        private final int mAggregationTimeLimit;
+
+        VibrationRecords(int sizeLimit, int aggregationTimeLimit) {
+            mSizeLimit = sizeLimit;
+            mAggregationTimeLimit = aggregationTimeLimit;
+        }
+
+        synchronized AggregatedVibrationRecord record(Vibration.DebugInfo info) {
+            int usage = info.mCallerInfo.attrs.getUsage();
+            if (!mVibrations.contains(usage)) {
+                mVibrations.put(usage, new LinkedList<>());
+            }
+            LinkedList<AggregatedVibrationRecord> records = mVibrations.get(usage);
+            if (mAggregationTimeLimit > 0 && !records.isEmpty()) {
+                AggregatedVibrationRecord lastRecord = records.getLast();
+                if (lastRecord.mayAggregate(info, mAggregationTimeLimit)) {
+                    lastRecord.record(info);
+                    return null;
                 }
             }
+            AggregatedVibrationRecord removedRecord = null;
+            if (records.size() > mSizeLimit) {
+                removedRecord = records.removeFirst();
+            }
+            records.addLast(new AggregatedVibrationRecord(info));
+            return removedRecord;
+        }
 
-            pw.println();
-            pw.println("  Previous external vibrations:");
-            for (Vibration.DebugInfo info : mPreviousExternalVibrations) {
-                pw.println("    " + info);
+        synchronized void dump(IndentingPrintWriter pw) {
+            for (int i = 0; i < mVibrations.size(); i++) {
+                pw.println(VibrationAttributes.usageToString(mVibrations.keyAt(i)) + ":");
+                pw.increaseIndent();
+                for (AggregatedVibrationRecord info : mVibrations.valueAt(i)) {
+                    info.dump(pw);
+                }
+                pw.decreaseIndent();
+                pw.println();
             }
         }
 
-        synchronized void dumpProto(ProtoOutputStream proto) {
-            for (int i = 0; i < mPreviousVibrations.size(); i++) {
+        synchronized void dump(ProtoOutputStream proto) {
+            for (int i = 0; i < mVibrations.size(); i++) {
                 long fieldId;
-                switch (mPreviousVibrations.keyAt(i)) {
+                switch (mVibrations.keyAt(i)) {
                     case VibrationAttributes.USAGE_RINGTONE:
                         fieldId = VibratorManagerServiceDumpProto.PREVIOUS_RING_VIBRATIONS;
                         break;
                     case VibrationAttributes.USAGE_NOTIFICATION:
-                        fieldId = VibratorManagerServiceDumpProto
-                                .PREVIOUS_NOTIFICATION_VIBRATIONS;
+                        fieldId = VibratorManagerServiceDumpProto.PREVIOUS_NOTIFICATION_VIBRATIONS;
                         break;
                     case VibrationAttributes.USAGE_ALARM:
                         fieldId = VibratorManagerServiceDumpProto.PREVIOUS_ALARM_VIBRATIONS;
@@ -1646,18 +1720,70 @@
                     default:
                         fieldId = VibratorManagerServiceDumpProto.PREVIOUS_VIBRATIONS;
                 }
-                for (Vibration.DebugInfo info : mPreviousVibrations.valueAt(i)) {
-                    info.dumpProto(proto, fieldId);
+                for (AggregatedVibrationRecord info : mVibrations.valueAt(i)) {
+                    if (info.mLatestVibration.mPlayedEffect == null) {
+                        // External vibrations are reported separately in the dump proto
+                        info.dump(proto,
+                                VibratorManagerServiceDumpProto.PREVIOUS_EXTERNAL_VIBRATIONS);
+                    } else {
+                        info.dump(proto, fieldId);
+                    }
                 }
             }
+        }
 
-            for (Vibration.DebugInfo info : mPreviousExternalVibrations) {
-                info.dumpProto(proto,
-                        VibratorManagerServiceDumpProto.PREVIOUS_EXTERNAL_VIBRATIONS);
+        synchronized void dumpOnSingleField(ProtoOutputStream proto, long fieldId) {
+            for (int i = 0; i < mVibrations.size(); i++) {
+                for (AggregatedVibrationRecord info : mVibrations.valueAt(i)) {
+                    info.dump(proto, fieldId);
+                }
             }
         }
     }
 
+    /**
+     * Record that keeps the last {@link Vibration.DebugInfo} played, aggregating close vibrations
+     * from the same uid that have the same {@link VibrationAttributes} and {@link VibrationEffect}.
+     */
+    private static final class AggregatedVibrationRecord {
+        private final Vibration.DebugInfo mFirstVibration;
+        private Vibration.DebugInfo mLatestVibration;
+        private int mVibrationCount;
+
+        AggregatedVibrationRecord(Vibration.DebugInfo info) {
+            mLatestVibration = mFirstVibration = info;
+            mVibrationCount = 1;
+        }
+
+        synchronized boolean mayAggregate(Vibration.DebugInfo info, long timeLimit) {
+            return Objects.equals(mLatestVibration.mCallerInfo.uid, info.mCallerInfo.uid)
+                    && Objects.equals(mLatestVibration.mCallerInfo.attrs, info.mCallerInfo.attrs)
+                    && Objects.equals(mLatestVibration.mPlayedEffect, info.mPlayedEffect)
+                    && Math.abs(mLatestVibration.mCreateTime - info.mCreateTime) < timeLimit;
+        }
+
+        synchronized void record(Vibration.DebugInfo vib) {
+            mLatestVibration = vib;
+            mVibrationCount++;
+        }
+
+        synchronized void dump(IndentingPrintWriter pw) {
+            mFirstVibration.dumpCompact(pw);
+            if (mVibrationCount == 1) {
+                return;
+            }
+            if (mVibrationCount > 2) {
+                pw.println(
+                        "-> Skipping " + (mVibrationCount - 2) + " aggregated vibrations, latest:");
+            }
+            mLatestVibration.dumpCompact(pw);
+        }
+
+        synchronized void dump(ProtoOutputStream proto, long fieldId) {
+            mLatestVibration.dump(proto, fieldId);
+        }
+    }
+
     /** Clears mNextVibration if set, ending it cleanly */
     @GuardedBy("mLock")
     private void clearNextVibrationLocked(Vibration.EndInfo vibrationEndInfo) {
@@ -1676,7 +1802,7 @@
     /**
      * Ends the external vibration, and clears related service state.
      *
-     * @param vibrationEndInfo the status and related info to end the associated Vibration with
+     * @param vibrationEndInfo        the status and related info to end the associated Vibration
      * @param continueExternalControl indicates whether external control will continue. If not, the
      *                                HAL will have external control turned off.
      */