Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d0e9dc0d authored by Chaohui Wang's avatar Chaohui Wang
Browse files

Add SettingsJankMonitor

Put in settingslib to be accessible by PrimarySwitchPreference.

Also detect jank for PrimarySwitchPreference.

Bug: 230285829
Test: manual & robo test
Change-Id: I060ad05334d15302ed904a8ad015aa858a680dbf
parent 4921f2f2
Loading
Loading
Loading
Loading
+13 −20
Original line number Diff line number Diff line
@@ -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) {
        mSwitch = (Switch) holder.findViewById(R.id.switchWidget);
        if (mSwitch != null) {
            mSwitch.setOnClickListener(v -> {
                if (mSwitch != null && !mSwitch.isEnabled()) {
                    return;
                }
                    setChecked(!mChecked);
                    if (!callChangeListener(mChecked)) {
                        setChecked(!mChecked);
                    } else {
                        persistBoolean(mChecked);
                    }
                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);
+74 −0
Original line number Diff line number Diff line
/*
 * 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
+1 −0
Original line number Diff line number Diff line
@@ -63,6 +63,7 @@ java_library {

    libs: [
        "Robolectric_all-target",
        "mockito-robolectric-prebuilt",
        "truth-prebuilt",
    ],
}
+3 −0
Original line number Diff line number Diff line
@@ -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;
+144 −0
Original line number Diff line number Diff line
/*
 * 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;
        }
    }
}
Loading