Merge changes from topic "jank-toggle" into tm-dev

* changes:
  Add SettingsJankMonitor
  Add SETTINGS_TOGGLE CUJ to InteractionJankMonitor
This commit is contained in:
Chaohui Wang
2022-05-20 09:22:46 +00:00
committed by Android (Google) Code Review
7 changed files with 284 additions and 20 deletions

View File

@@ -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";
}

View File

@@ -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);

View File

@@ -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)
}
}
}

View File

@@ -63,6 +63,7 @@ java_library {
libs: [
"Robolectric_all-target",
"mockito-robolectric-prebuilt",
"truth-prebuilt",
],
}

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}