diff options
7 files changed, 284 insertions, 20 deletions
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java index 2dcc58564f82..66b78f38cc0d 100644 --- a/core/java/com/android/internal/jank/InteractionJankMonitor.java +++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java @@ -20,6 +20,7 @@ import static com.android.internal.jank.FrameTracker.REASON_CANCEL_NORMAL; import static com.android.internal.jank.FrameTracker.REASON_CANCEL_TIMEOUT; import static com.android.internal.jank.FrameTracker.REASON_END_NORMAL; import static com.android.internal.jank.FrameTracker.REASON_END_UNKNOWN; +import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__BIOMETRIC_PROMPT_TRANSITION; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_ALL_APPS_SCROLL; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_CLOSE_TO_HOME; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_CLOSE_TO_PIP; @@ -46,6 +47,7 @@ import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_IN import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SCREEN_OFF_SHOW_AOD; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SETTINGS_PAGE_SCROLL; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SETTINGS_SLIDER; +import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SETTINGS_TOGGLE; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_APP_LAUNCH; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER; @@ -198,6 +200,7 @@ public class InteractionJankMonitor { public static final int CUJ_SETTINGS_SLIDER = 53; public static final int CUJ_TAKE_SCREENSHOT = 54; public static final int CUJ_VOLUME_CONTROL = 55; + public static final int CUJ_SETTINGS_TOGGLE = 57; private static final int NO_STATSD_LOGGING = -1; @@ -262,6 +265,8 @@ public class InteractionJankMonitor { UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SETTINGS_SLIDER, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TAKE_SCREENSHOT, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__VOLUME_CONTROL, + UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__BIOMETRIC_PROMPT_TRANSITION, + UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SETTINGS_TOGGLE, }; private static volatile InteractionJankMonitor sInstance; @@ -338,6 +343,7 @@ public class InteractionJankMonitor { CUJ_SETTINGS_SLIDER, CUJ_TAKE_SCREENSHOT, CUJ_VOLUME_CONTROL, + CUJ_SETTINGS_TOGGLE, }) @Retention(RetentionPolicy.SOURCE) public @interface CujType { @@ -768,6 +774,8 @@ public class InteractionJankMonitor { return "TAKE_SCREENSHOT"; case CUJ_VOLUME_CONTROL: return "VOLUME_CONTROL"; + case CUJ_SETTINGS_TOGGLE: + return "SETTINGS_TOGGLE"; } return "UNKNOWN"; } diff --git a/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java index b43b44421fb8..fb06976ebfe3 100644 --- a/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java @@ -19,8 +19,6 @@ package com.android.settingslib; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnClickListener; import android.widget.Switch; import androidx.annotation.Keep; @@ -28,6 +26,7 @@ import androidx.annotation.Nullable; import androidx.preference.PreferenceViewHolder; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.core.instrumentation.SettingsJankMonitor; /** * A custom preference that provides inline switch toggle. It has a mandatory field for title, and @@ -65,31 +64,25 @@ public class PrimarySwitchPreference extends RestrictedPreference { @Override public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); - final View switchWidget = holder.findViewById(R.id.switchWidget); - if (switchWidget != null) { - switchWidget.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - if (mSwitch != null && !mSwitch.isEnabled()) { - return; - } - setChecked(!mChecked); - if (!callChangeListener(mChecked)) { - setChecked(!mChecked); - } else { - persistBoolean(mChecked); - } + mSwitch = (Switch) holder.findViewById(R.id.switchWidget); + if (mSwitch != null) { + mSwitch.setOnClickListener(v -> { + if (mSwitch != null && !mSwitch.isEnabled()) { + return; + } + final boolean newChecked = !mChecked; + if (callChangeListener(newChecked)) { + SettingsJankMonitor.detectToggleJank(getKey(), mSwitch); + setChecked(newChecked); + persistBoolean(newChecked); } }); // Consumes move events to ignore drag actions. - switchWidget.setOnTouchListener((v, event) -> { + mSwitch.setOnTouchListener((v, event) -> { return event.getActionMasked() == MotionEvent.ACTION_MOVE; }); - } - mSwitch = (Switch) holder.findViewById(R.id.switchWidget); - if (mSwitch != null) { mSwitch.setContentDescription(getTitle()); mSwitch.setChecked(mChecked); mSwitch.setEnabled(mEnableSwitch); diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SettingsJankMonitor.kt b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SettingsJankMonitor.kt new file mode 100644 index 000000000000..a5f69ffec4b4 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SettingsJankMonitor.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2022 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.settingslib.core.instrumentation + +import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.preference.PreferenceGroupAdapter +import androidx.preference.SwitchPreference +import androidx.recyclerview.widget.RecyclerView +import com.android.internal.jank.InteractionJankMonitor +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +/** + * Helper class for Settings library to trace jank. + */ +object SettingsJankMonitor { + private val jankMonitor = InteractionJankMonitor.getInstance() + private val scheduledExecutorService = Executors.newSingleThreadScheduledExecutor() + + // Switch toggle animation duration is 250ms, and there is also a ripple effect animation when + // clicks, which duration is variable. Use 300ms here to cover. + @VisibleForTesting + const val MONITORED_ANIMATION_DURATION_MS = 300L + + /** + * Detects the jank when click on a SwitchPreference. + * + * @param recyclerView the recyclerView contains the preference + * @param preference the clicked preference + */ + @JvmStatic + fun detectSwitchPreferenceClickJank(recyclerView: RecyclerView, preference: SwitchPreference) { + val adapter = recyclerView.adapter as? PreferenceGroupAdapter ?: return + val adapterPosition = adapter.getPreferenceAdapterPosition(preference) + val viewHolder = recyclerView.findViewHolderForAdapterPosition(adapterPosition) ?: return + detectToggleJank(preference.key, viewHolder.itemView) + } + + /** + * Detects the animation jank on the given view. + * + * @param tag the tag for jank monitor + * @param view the instrumented view + */ + @JvmStatic + fun detectToggleJank(tag: String?, view: View) { + val builder = InteractionJankMonitor.Configuration.Builder.withView( + InteractionJankMonitor.CUJ_SETTINGS_TOGGLE, + view + ) + if (tag != null) { + builder.setTag(tag) + } + if (jankMonitor.begin(builder)) { + scheduledExecutorService.schedule({ + jankMonitor.end(InteractionJankMonitor.CUJ_SETTINGS_TOGGLE) + }, MONITORED_ANIMATION_DURATION_MS, TimeUnit.MILLISECONDS) + } + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/tests/robotests/Android.bp b/packages/SettingsLib/tests/robotests/Android.bp index 2d1a516f4f11..5c55a435b463 100644 --- a/packages/SettingsLib/tests/robotests/Android.bp +++ b/packages/SettingsLib/tests/robotests/Android.bp @@ -63,6 +63,7 @@ java_library { libs: [ "Robolectric_all-target", + "mockito-robolectric-prebuilt", "truth-prebuilt", ], } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/PrimarySwitchPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/PrimarySwitchPreferenceTest.java index 9c16740061fe..74c2fc8cce4c 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/PrimarySwitchPreferenceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/PrimarySwitchPreferenceTest.java @@ -30,14 +30,17 @@ import androidx.preference.Preference.OnPreferenceChangeListener; import androidx.preference.PreferenceViewHolder; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.testutils.shadow.ShadowInteractionJankMonitor; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowInteractionJankMonitor.class}) public class PrimarySwitchPreferenceTest { private Context mContext; diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SettingsJankMonitorTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SettingsJankMonitorTest.java new file mode 100644 index 000000000000..d67d44b9035d --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SettingsJankMonitorTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2022 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.settingslib.core.instrumentation; + +import static com.android.internal.jank.InteractionJankMonitor.CUJ_SETTINGS_TOGGLE; +import static com.android.settingslib.core.instrumentation.SettingsJankMonitor.MONITORED_ANIMATION_DURATION_MS; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.annotation.NonNull; +import android.view.View; + +import androidx.preference.PreferenceGroupAdapter; +import androidx.preference.SwitchPreference; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.internal.jank.InteractionJankMonitor; +import com.android.internal.jank.InteractionJankMonitor.CujType; +import com.android.settingslib.testutils.shadow.ShadowInteractionJankMonitor; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; +import org.robolectric.util.ReflectionHelpers; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowInteractionJankMonitor.class, SettingsJankMonitorTest.ShadowBuilder.class}) +public class SettingsJankMonitorTest { + private static final String TEST_KEY = "key"; + + @Rule + public MockitoRule mocks = MockitoJUnit.rule(); + + @Mock + private View mView; + + @Mock + private RecyclerView mRecyclerView; + + @Mock + private PreferenceGroupAdapter mPreferenceGroupAdapter; + + @Mock + private SwitchPreference mSwitchPreference; + + @Mock + private ScheduledExecutorService mScheduledExecutorService; + + @Before + public void setUp() { + ShadowInteractionJankMonitor.reset(); + when(ShadowInteractionJankMonitor.MOCK_INSTANCE.begin(any())).thenReturn(true); + ReflectionHelpers.setStaticField(SettingsJankMonitor.class, "scheduledExecutorService", + mScheduledExecutorService); + } + + @Test + public void detectToggleJank() { + SettingsJankMonitor.detectToggleJank(TEST_KEY, mView); + + verifyToggleJankMonitored(); + } + + @Test + public void detectSwitchPreferenceClickJank() { + int adapterPosition = 7; + when(mRecyclerView.getAdapter()).thenReturn(mPreferenceGroupAdapter); + when(mPreferenceGroupAdapter.getPreferenceAdapterPosition(mSwitchPreference)) + .thenReturn(adapterPosition); + when(mRecyclerView.findViewHolderForAdapterPosition(adapterPosition)) + .thenReturn(new RecyclerView.ViewHolder(mView) { + }); + when(mSwitchPreference.getKey()).thenReturn(TEST_KEY); + + SettingsJankMonitor.detectSwitchPreferenceClickJank(mRecyclerView, mSwitchPreference); + + verifyToggleJankMonitored(); + } + + private void verifyToggleJankMonitored() { + verify(ShadowInteractionJankMonitor.MOCK_INSTANCE).begin(ShadowBuilder.sBuilder); + assertThat(ShadowBuilder.sView).isSameInstanceAs(mView); + ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mScheduledExecutorService).schedule(runnableCaptor.capture(), + eq(MONITORED_ANIMATION_DURATION_MS), eq(TimeUnit.MILLISECONDS)); + runnableCaptor.getValue().run(); + verify(ShadowInteractionJankMonitor.MOCK_INSTANCE).end(CUJ_SETTINGS_TOGGLE); + } + + @Implements(InteractionJankMonitor.Configuration.Builder.class) + static class ShadowBuilder { + private static InteractionJankMonitor.Configuration.Builder sBuilder; + private static View sView; + + @Resetter + public static void reset() { + sBuilder = null; + sView = null; + } + + @Implementation + public static InteractionJankMonitor.Configuration.Builder withView( + @CujType int cuj, @NonNull View view) { + assertThat(cuj).isEqualTo(CUJ_SETTINGS_TOGGLE); + sView = view; + sBuilder = mock(InteractionJankMonitor.Configuration.Builder.class); + when(sBuilder.setTag(TEST_KEY)).thenReturn(sBuilder); + return sBuilder; + } + } +} diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowInteractionJankMonitor.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowInteractionJankMonitor.java new file mode 100644 index 000000000000..855da16b1dfa --- /dev/null +++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowInteractionJankMonitor.java @@ -0,0 +1,41 @@ +/* + * 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.settingslib.testutils.shadow; + +import static org.mockito.Mockito.mock; + +import com.android.internal.jank.InteractionJankMonitor; + +import org.mockito.Mockito; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; + +@Implements(InteractionJankMonitor.class) +public class ShadowInteractionJankMonitor { + public static final InteractionJankMonitor MOCK_INSTANCE = mock(InteractionJankMonitor.class); + + @Resetter + public static void reset() { + Mockito.reset(MOCK_INSTANCE); + } + + @Implementation + public static InteractionJankMonitor getInstance() { + return MOCK_INSTANCE; + } +} |