summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/current.txt41
-rw-r--r--core/java/android/app/Activity.java58
-rw-r--r--core/java/android/app/jank/AppJankStats.java253
-rw-r--r--core/java/android/app/jank/FrameOverrunHistogram.java107
-rw-r--r--core/java/android/app/jank/JankDataProcessor.java8
-rw-r--r--core/java/android/app/jank/JankTracker.java8
-rw-r--r--core/java/android/view/View.java19
-rw-r--r--core/java/com/android/internal/policy/DecorView.java39
-rw-r--r--tests/AppJankTest/AndroidManifest.xml2
-rw-r--r--tests/AppJankTest/src/android/app/jank/tests/JankTrackerTest.java15
10 files changed, 549 insertions, 1 deletions
diff --git a/core/api/current.txt b/core/api/current.txt
index aa58fe42fa26..653f907635ba 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -9094,6 +9094,46 @@ package android.app.blob {
}
+package android.app.jank {
+
+ @FlaggedApi("android.app.jank.detailed_app_jank_metrics_api") public final class AppJankStats {
+ ctor public AppJankStats(int, @NonNull String, @Nullable String, @Nullable String, long, long, @NonNull android.app.jank.FrameOverrunHistogram);
+ method @NonNull public android.app.jank.FrameOverrunHistogram getFrameOverrunHistogram();
+ method public long getJankyFrameCount();
+ method public long getTotalFrameCount();
+ method public int getUid();
+ method @NonNull public String getWidgetCategory();
+ method @NonNull public String getWidgetId();
+ method @NonNull public String getWidgetState();
+ field public static final String ANIMATING = "animating";
+ field public static final String ANIMATION = "animation";
+ field public static final String DRAGGING = "dragging";
+ field public static final String FLINGING = "flinging";
+ field public static final String KEYBOARD = "keyboard";
+ field public static final String MEDIA = "media";
+ field public static final String NAVIGATION = "navigation";
+ field public static final String NONE = "none";
+ field public static final String OTHER = "other";
+ field public static final String PLAYBACK = "playback";
+ field public static final String PREDICTIVE_BACK = "predictive_back";
+ field public static final String SCROLL = "scroll";
+ field public static final String SCROLLING = "scrolling";
+ field public static final String SWIPING = "swiping";
+ field public static final String TAPPING = "tapping";
+ field public static final String WIDGET_CATEGORY_UNSPECIFIED = "widget_category_unspecified";
+ field public static final String WIDGET_STATE_UNSPECIFIED = "widget_state_unspecified";
+ field public static final String ZOOMING = "zooming";
+ }
+
+ @FlaggedApi("android.app.jank.detailed_app_jank_metrics_api") public class FrameOverrunHistogram {
+ ctor public FrameOverrunHistogram();
+ method public void addFrameOverrunMillis(int);
+ method @NonNull public int[] getBucketCounters();
+ method @NonNull public int[] getBucketEndpointsMillis();
+ }
+
+}
+
package android.app.job {
public class JobInfo implements android.os.Parcelable {
@@ -53459,6 +53499,7 @@ package android.view {
method public void removeOnAttachStateChangeListener(android.view.View.OnAttachStateChangeListener);
method public void removeOnLayoutChangeListener(android.view.View.OnLayoutChangeListener);
method public void removeOnUnhandledKeyEventListener(android.view.View.OnUnhandledKeyEventListener);
+ method @FlaggedApi("android.app.jank.detailed_app_jank_metrics_api") public void reportAppJankStats(@NonNull android.app.jank.AppJankStats);
method public void requestApplyInsets();
method @Deprecated public void requestFitSystemWindows();
method public final boolean requestFocus();
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 7a1c759a3ec4..198d9ea4d129 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -53,6 +53,7 @@ import android.app.VoiceInteractor.Request;
import android.app.admin.DevicePolicyManager;
import android.app.assist.AssistContent;
import android.app.compat.CompatChanges;
+import android.app.jank.JankTracker;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
@@ -123,6 +124,7 @@ import android.util.Slog;
import android.util.SparseArray;
import android.util.SuperNotCalledException;
import android.view.ActionMode;
+import android.view.Choreographer;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.ContextThemeWrapper;
@@ -174,6 +176,7 @@ import com.android.internal.app.IVoiceInteractionManagerService;
import com.android.internal.app.IVoiceInteractor;
import com.android.internal.app.ToolbarActionBar;
import com.android.internal.app.WindowDecorActionBar;
+import com.android.internal.policy.DecorView;
import com.android.internal.policy.PhoneWindow;
import com.android.internal.util.dump.DumpableContainerImpl;
@@ -1144,6 +1147,9 @@ public class Activity extends ContextThemeWrapper
};
+ @Nullable
+ private JankTracker mJankTracker;
+
private static native String getDlWarning();
/**
@@ -2244,6 +2250,10 @@ public class Activity extends ContextThemeWrapper
// Notify autofill
getAutofillClientController().onActivityPostResumed();
+ if (android.app.jank.Flags.detailedAppJankMetricsApi()) {
+ startAppJankTracking();
+ }
+
mCalled = true;
}
@@ -9246,6 +9256,9 @@ public class Activity extends ContextThemeWrapper
dispatchActivityPrePaused();
mDoReportFullyDrawn = false;
mFragments.dispatchPause();
+ if (android.app.jank.Flags.detailedAppJankMetricsApi()) {
+ stopAppJankTracking();
+ }
mCalled = false;
final long startTime = SystemClock.uptimeMillis();
onPause();
@@ -9924,4 +9937,49 @@ public class Activity extends ContextThemeWrapper
mScreenCaptureCallbackHandler.unregisterScreenCaptureCallback(callback);
}
}
+
+ /**
+ * Enabling jank tracking for this activity but only if certain conditions are met. The
+ * application must have an app category other than undefined and a visible view.
+ */
+ private void startAppJankTracking() {
+ if (!android.app.jank.Flags.detailedAppJankMetricsLoggingEnabled()) {
+ return;
+ }
+ if (mApplication.getApplicationInfo().category == ApplicationInfo.CATEGORY_UNDEFINED) {
+ return;
+ }
+ if (getWindow() != null && getWindow().peekDecorView() != null) {
+ DecorView decorView = (DecorView) getWindow().peekDecorView();
+ if (decorView.getVisibility() == View.VISIBLE) {
+ decorView.setAppJankStatsCallback(new DecorView.AppJankStatsCallback() {
+ @Override
+ public JankTracker getAppJankTracker() {
+ return mJankTracker;
+ }
+ });
+ if (mJankTracker == null) {
+ // TODO b/377960907 use the Choreographer attached to the ViewRootImpl instead.
+ mJankTracker = new JankTracker(Choreographer.getInstance(),
+ decorView);
+ }
+ // TODO b/377674765 confirm this is the string we want logged.
+ mJankTracker.setActivityName(getComponentName().getClassName());
+ mJankTracker.setAppUid(myUid());
+ mJankTracker.enableAppJankTracking();
+ }
+ }
+ }
+
+ /**
+ * Call to disable jank tracking for this activity.
+ */
+ private void stopAppJankTracking() {
+ if (!android.app.jank.Flags.detailedAppJankMetricsLoggingEnabled()) {
+ return;
+ }
+ if (mJankTracker != null) {
+ mJankTracker.disableAppJankTracking();
+ }
+ }
}
diff --git a/core/java/android/app/jank/AppJankStats.java b/core/java/android/app/jank/AppJankStats.java
new file mode 100644
index 000000000000..eea1d2ba5b9e
--- /dev/null
+++ b/core/java/android/app/jank/AppJankStats.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2024 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.app.jank;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringDef;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This class stores detailed jank statistics for an individual UI widget. These statistics
+ * provide performance insights for specific UI widget states by correlating the number of
+ * "Janky frames" with the total frames rendered while the widget is in that state. This class
+ * can be used by library widgets to provide the system with more detailed information about
+ * where jank is happening for diagnostic purposes.
+ */
+@FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API)
+public final class AppJankStats {
+ // UID of the app
+ private int mUid;
+
+ // The id that has been set for the widget.
+ private String mWidgetId;
+
+ // A general category that the widget applies to.
+ private String mWidgetCategory;
+
+ // The states that the UI elements can report
+ private String mWidgetState;
+
+ // The number of frames reported during this state.
+ private long mTotalFrames;
+
+ // Total number of frames determined to be janky during the reported state.
+ private long mJankyFrames;
+
+ // Histogram of frame duration overruns encoded in predetermined buckets.
+ private FrameOverrunHistogram mFrameOverrunHistogram;
+
+
+ /** Used to indicate no widget category has been set. */
+ public static final String WIDGET_CATEGORY_UNSPECIFIED =
+ "widget_category_unspecified";
+
+ /** UI elements that facilitate scrolling. */
+ public static final String SCROLL = "scroll";
+
+ /** UI elements that facilitate playing animations. */
+ public static final String ANIMATION = "animation";
+
+ /** UI elements that facilitate media playback. */
+ public static final String MEDIA = "media";
+
+ /** UI elements that facilitate in-app navigation. */
+ public static final String NAVIGATION = "navigation";
+
+ /** UI elements that facilitate displaying, hiding or interacting with keyboard. */
+ public static final String KEYBOARD = "keyboard";
+
+ /** UI elements that facilitate predictive back gesture navigation. */
+ public static final String PREDICTIVE_BACK = "predictive_back";
+
+ /** UI elements that don't fall in one or any of the other categories. */
+ public static final String OTHER = "other";
+
+ /** Used to indicate no widget state has been set. */
+ public static final String WIDGET_STATE_UNSPECIFIED = "widget_state_unspecified";
+
+ /** Used to indicate the UI element currently has no state and is idle. */
+ public static final String NONE = "none";
+
+ /** Used to indicate the UI element is currently scrolling. */
+ public static final String SCROLLING = "scrolling";
+
+ /** Used to indicate the UI element is currently being flung. */
+ public static final String FLINGING = "flinging";
+
+ /** Used to indicate the UI element is currently being swiped. */
+ public static final String SWIPING = "swiping";
+
+ /** Used to indicate the UI element is currently being dragged. */
+ public static final String DRAGGING = "dragging";
+
+ /** Used to indicate the UI element is currently zooming. */
+ public static final String ZOOMING = "zooming";
+
+ /** Used to indicate the UI element is currently animating. */
+ public static final String ANIMATING = "animating";
+
+ /** Used to indicate the UI element is currently playing media. */
+ public static final String PLAYBACK = "playback";
+
+ /** Used to indicate the UI element is currently being tapped on, for example on a keyboard. */
+ public static final String TAPPING = "tapping";
+
+
+ /**
+ * @hide
+ */
+ @StringDef(value = {
+ WIDGET_CATEGORY_UNSPECIFIED,
+ SCROLL,
+ ANIMATION,
+ MEDIA,
+ NAVIGATION,
+ KEYBOARD,
+ PREDICTIVE_BACK,
+ OTHER
+ })
+ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface WidgetCategory {
+ }
+ /**
+ * @hide
+ */
+ @StringDef(value = {
+ WIDGET_STATE_UNSPECIFIED,
+ NONE,
+ SCROLLING,
+ FLINGING,
+ SWIPING,
+ DRAGGING,
+ ZOOMING,
+ ANIMATING,
+ PLAYBACK,
+ TAPPING,
+ })
+ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface WidgetState {
+ }
+
+
+ /**
+ * Creates a new AppJankStats object.
+ *
+ * @param appUid the Uid of the App that is collecting jank stats.
+ * @param widgetId the widget id that frames will be associated to.
+ * @param widgetCategory a general functionality category that the widget falls into. Must be
+ * one of the following: SCROLL, ANIMATION, MEDIA, NAVIGATION, KEYBOARD,
+ * PREDICTIVE_BACK, OTHER or will be set to WIDGET_CATEGORY_UNSPECIFIED
+ * if no value is passed.
+ * @param widgetState the state the widget was in while frames were counted. Must be one of
+ * the following: NONE, SCROLLING, FLINGING, SWIPING, DRAGGING, ZOOMING,
+ * ANIMATING, PLAYBACK, TAPPING or will be set to WIDGET_STATE_UNSPECIFIED
+ * if no value is passed.
+ * @param totalFrames the total number of frames that were counted for this stat.
+ * @param jankyFrames the total number of janky frames that were counted for this stat.
+ * @param frameOverrunHistogram the histogram with predefined buckets. See
+ * {@link #getFrameOverrunHistogram()} for details.
+ *
+ */
+ public AppJankStats(int appUid, @NonNull String widgetId,
+ @Nullable @WidgetCategory String widgetCategory,
+ @Nullable @WidgetState String widgetState, long totalFrames, long jankyFrames,
+ @NonNull FrameOverrunHistogram frameOverrunHistogram) {
+ mUid = appUid;
+ mWidgetId = widgetId;
+ mWidgetCategory = widgetCategory != null ? widgetCategory : WIDGET_CATEGORY_UNSPECIFIED;
+ mWidgetState = widgetState != null ? widgetState : WIDGET_STATE_UNSPECIFIED;
+ mTotalFrames = totalFrames;
+ mJankyFrames = jankyFrames;
+ mFrameOverrunHistogram = frameOverrunHistogram;
+ }
+
+ /**
+ * Returns the app uid.
+ *
+ * @return the app uid.
+ */
+ public int getUid() {
+ return mUid;
+ }
+
+ /**
+ * Returns the id of the widget that reported state changes.
+ *
+ * @return the id of the widget that reported state changes. This value cannot be null.
+ */
+ public @NonNull String getWidgetId() {
+ return mWidgetId;
+ }
+
+ /**
+ * Returns the category that the widget's functionality generally falls into, or
+ * widget_category_unspecified {@link #WIDGET_CATEGORY_UNSPECIFIED} if no value was passed in.
+ *
+ * @return the category that the widget's functionality generally falls into, this value cannot
+ * be null.
+ */
+ public @NonNull @WidgetCategory String getWidgetCategory() {
+ return mWidgetCategory;
+ }
+
+ /**
+ * Returns the widget's state that was reported for this stat, or widget_state_unspecified
+ * {@link #WIDGET_STATE_UNSPECIFIED} if no value was passed in.
+ *
+ * @return the widget's state that was reported for this stat. This value cannot be null.
+ */
+ public @NonNull @WidgetState String getWidgetState() {
+ return mWidgetState;
+ }
+
+ /**
+ * Returns the number of frames that were determined to be janky for this stat.
+ *
+ * @return the number of frames that were determined to be janky for this stat.
+ */
+ public long getJankyFrameCount() {
+ return mJankyFrames;
+ }
+
+ /**
+ * Returns the total number of frames counted for this stat.
+ *
+ * @return the total number of frames counted for this stat.
+ */
+ public long getTotalFrameCount() {
+ return mTotalFrames;
+ }
+
+ /**
+ * Returns a Histogram containing frame overrun times in millis grouped into predefined buckets.
+ * See {@link FrameOverrunHistogram} for more information.
+ *
+ * @return Histogram containing frame overrun times in predefined buckets. This value cannot
+ * be null.
+ */
+ public @NonNull FrameOverrunHistogram getFrameOverrunHistogram() {
+ return mFrameOverrunHistogram;
+ }
+}
diff --git a/core/java/android/app/jank/FrameOverrunHistogram.java b/core/java/android/app/jank/FrameOverrunHistogram.java
new file mode 100644
index 000000000000..e28ac126a90a
--- /dev/null
+++ b/core/java/android/app/jank/FrameOverrunHistogram.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2024 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.app.jank;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+
+import java.util.Arrays;
+
+/**
+ * This class is intended to be used when reporting {@link AppJankStats} back to the system. It's
+ * intended to be used by library widgets to help facilitate the reporting of frame overrun times
+ * by adding those times into predefined buckets.
+ */
+@FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API)
+public class FrameOverrunHistogram {
+ private static int[] sBucketEndpoints = new int[]{
+ Integer.MIN_VALUE, -200, -150, -100, -90, -80, -70, -60, -50, -40, -30, -25, -20, -18,
+ -16, -14, -12, -10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 25, 30, 40,
+ 50, 60, 70, 80, 90, 100, 150, 200, 300, 400, 500, 600, 700, 800, 900, 1000
+ };
+ private int[] mBucketCounts;
+
+ /**
+ * Create a new instance of FrameOverrunHistogram.
+ */
+ public FrameOverrunHistogram() {
+ mBucketCounts = new int[sBucketEndpoints.length - 1];
+ }
+
+ /**
+ * Increases the count by one for the bucket representing the frame overrun duration.
+ *
+ * @param frameOverrunMillis frame overrun duration in millis, frame overrun is the difference
+ * between a frames deadline and when it was rendered.
+ */
+ public void addFrameOverrunMillis(int frameOverrunMillis) {
+ int countsIndex = getIndexForCountsFromOverrunTime(frameOverrunMillis);
+ mBucketCounts[countsIndex]++;
+ }
+
+ /**
+ * Returns the counts for the all the frame overrun buckets.
+ *
+ * @return an array of integers representing the counts of frame overrun times. This value
+ * cannot be null.
+ */
+ public @NonNull int[] getBucketCounters() {
+ return Arrays.copyOf(mBucketCounts, mBucketCounts.length);
+ }
+
+ /**
+ * Returns the predefined endpoints for the histogram.
+ *
+ * @return array of integers representing the endpoints for the predefined histogram count
+ * buckets. This value cannot be null.
+ */
+ public @NonNull int[] getBucketEndpointsMillis() {
+ return Arrays.copyOf(sBucketEndpoints, sBucketEndpoints.length);
+ }
+
+ // This takes the overrun time and returns what bucket it belongs to in the counters array.
+ private int getIndexForCountsFromOverrunTime(int overrunTime) {
+ if (overrunTime < 20) {
+ if (overrunTime >= -20) {
+ return (overrunTime + 20) / 2 + 12;
+ }
+ if (overrunTime >= -30) {
+ return (overrunTime + 30) / 5 + 10;
+ }
+ if (overrunTime >= -100) {
+ return (overrunTime + 100) / 10 + 3;
+ }
+ if (overrunTime >= -200) {
+ return (overrunTime + 200) / 50 + 1;
+ }
+ return 0;
+ }
+ if (overrunTime < 30) {
+ return (overrunTime - 20) / 5 + 32;
+ }
+ if (overrunTime < 100) {
+ return (overrunTime - 30) / 10 + 34;
+ }
+ if (overrunTime < 200) {
+ return (overrunTime - 50) / 100 + 41;
+ }
+ if (overrunTime < 1000) {
+ return (overrunTime - 200) / 100 + 43;
+ }
+ return sBucketEndpoints.length - 1;
+ }
+}
diff --git a/core/java/android/app/jank/JankDataProcessor.java b/core/java/android/app/jank/JankDataProcessor.java
index 981a9167c2da..3783a5f9e829 100644
--- a/core/java/android/app/jank/JankDataProcessor.java
+++ b/core/java/android/app/jank/JankDataProcessor.java
@@ -87,6 +87,14 @@ public class JankDataProcessor {
}
/**
+ * Merges app jank stats reported by components outside the platform to the current pending
+ * stats
+ */
+ public void mergeJankStats(AppJankStats jankStats, String activityName) {
+ // TODO b/377572463 Add Merging Logic
+ }
+
+ /**
* Returns the aggregate map of different pending jank stats.
*/
@VisibleForTesting
diff --git a/core/java/android/app/jank/JankTracker.java b/core/java/android/app/jank/JankTracker.java
index df422e0069c5..202281f98c97 100644
--- a/core/java/android/app/jank/JankTracker.java
+++ b/core/java/android/app/jank/JankTracker.java
@@ -84,6 +84,14 @@ public class JankTracker {
registerWindowListeners();
}
+ /**
+ * Merges app jank stats reported by components outside the platform to the current pending
+ * stats
+ */
+ public void mergeAppJankStats(AppJankStats appJankStats) {
+ mJankDataProcessor.mergeJankStats(appJankStats, mActivityName);
+ }
+
public void setActivityName(@NonNull String activityName) {
mActivityName = activityName;
}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 5ee229f87a1e..beb9ca886ae0 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -90,6 +90,8 @@ import android.annotation.TestApi;
import android.annotation.UiContext;
import android.annotation.UiThread;
import android.app.PendingIntent;
+import android.app.jank.AppJankStats;
+import android.app.jank.JankTracker;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.AutofillOptions;
import android.content.ClipData;
@@ -34415,4 +34417,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
boolean getSelfRequestedFrameRateFlag() {
return (mPrivateFlags4 & PFLAG4_SELF_REQUESTED_FRAME_RATE) != 0;
}
+
+ /**
+ * Called from apps when they want to report jank stats to the system.
+ * @param appJankStats the stats that will be merged with the stats collected by the system.
+ */
+ @FlaggedApi(android.app.jank.Flags.FLAG_DETAILED_APP_JANK_METRICS_API)
+ public void reportAppJankStats(@NonNull AppJankStats appJankStats) {
+ getRootView().reportAppJankStats(appJankStats);
+ }
+
+ /**
+ * Called by widgets to get a reference to JankTracker in order to update states.
+ * @hide
+ */
+ public @Nullable JankTracker getJankTracker() {
+ return getRootView().getJankTracker();
+ }
}
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index bd746d5ecf04..270cf085b06f 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -46,9 +46,13 @@ import static com.android.internal.policy.PhoneWindow.FEATURE_OPTIONS_PANEL;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
import android.app.WindowConfiguration;
+import android.app.jank.AppJankStats;
+import android.app.jank.JankTracker;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.res.Configuration;
@@ -283,6 +287,9 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
private final WearGestureInterceptionDetector mWearGestureInterceptionDetector;
+ @Nullable
+ private AppJankStatsCallback mAppJankStatsCallback;
+
DecorView(Context context, int featureId, PhoneWindow window,
WindowManager.LayoutParams params) {
super(context);
@@ -2336,6 +2343,38 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
}
}
+ public interface AppJankStatsCallback {
+ /**
+ * Called when app jank stats are being reported to the platform or when a widget needs
+ * to obtain a reference to the JankTracker instance to update states.
+ */
+ JankTracker getAppJankTracker();
+ }
+
+ public void setAppJankStatsCallback(AppJankStatsCallback
+ jankStatsReportedCallback) {
+ mAppJankStatsCallback = jankStatsReportedCallback;
+ }
+
+ @Override
+ @FlaggedApi(android.app.jank.Flags.FLAG_DETAILED_APP_JANK_METRICS_API)
+ public void reportAppJankStats(@NonNull AppJankStats appJankStats) {
+ if (mAppJankStatsCallback != null) {
+ JankTracker jankTracker = mAppJankStatsCallback.getAppJankTracker();
+ if (jankTracker != null) {
+ jankTracker.mergeAppJankStats(appJankStats);
+ }
+ }
+ }
+
+ @Override
+ public @Nullable JankTracker getJankTracker() {
+ if (mAppJankStatsCallback != null) {
+ return mAppJankStatsCallback.getAppJankTracker();
+ }
+ return null;
+ }
+
@Override
public String toString() {
return super.toString() + "[" + getTitleSuffix(mWindow.getAttributes()) + "]";
diff --git a/tests/AppJankTest/AndroidManifest.xml b/tests/AppJankTest/AndroidManifest.xml
index ae973393b90e..abed1798c47c 100644
--- a/tests/AppJankTest/AndroidManifest.xml
+++ b/tests/AppJankTest/AndroidManifest.xml
@@ -18,7 +18,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="android.app.jank.tests">
- <application>
+ <application android:appCategory="news">
<uses-library android:name="android.test.runner" />
<activity android:name=".EmptyActivity"
android:label="EmptyActivity"
diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankTrackerTest.java b/tests/AppJankTest/src/android/app/jank/tests/JankTrackerTest.java
index a3e5533599bc..1bdf019d6c42 100644
--- a/tests/AppJankTest/src/android/app/jank/tests/JankTrackerTest.java
+++ b/tests/AppJankTest/src/android/app/jank/tests/JankTrackerTest.java
@@ -17,6 +17,7 @@
package android.app.jank.tests;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import android.app.jank.Flags;
import android.app.jank.JankTracker;
@@ -31,6 +32,8 @@ import androidx.test.annotation.UiThreadTest;
import androidx.test.core.app.ActivityScenario;
import androidx.test.runner.AndroidJUnit4;
+import com.android.internal.policy.DecorView;
+
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
@@ -153,4 +156,16 @@ public class JankTrackerTest {
assertEquals(1, stateData.size());
}
+
+ /**
+ * Test confirms a JankTracker object is retrieved from the activity.
+ */
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_LOGGING_ENABLED)
+ public void jankTracker_NotNull_WhenRetrievedFromDecorView() {
+ DecorView decorView = (DecorView) sActivityDecorView;
+ JankTracker jankTracker = decorView.getJankTracker();
+
+ assertNotNull(jankTracker);
+ }
}