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

Commit 44cb400f authored by Vania Januar's avatar Vania Januar
Browse files

USI Stylus settings Fragment.

The USI stylus settings fragment shows specific preferences
for USI stylus devices only.

There is currently no way to access this fragment from the UI.

Bug: 251201006
DD: go/stylus-connected-devices-doc
Test: StylusUsiHeaderControllerTest
Change-Id: I18223abc8113dce977f57f930ba701da0a34cc18
parent 82789d15
Loading
Loading
Loading
Loading
+33 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ 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.
  -->

<PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:settings="http://schemas.android.com/apk/res-auto"
    android:title="@string/stylus_device_details_title">

    <com.android.settingslib.widget.LayoutPreference
        android:key="stylus_usi_header"
        android:layout="@layout/settings_entity_header"
        android:selectable="false"
        settings:allowDividerBelow="true"
        settings:searchable="false"/>

    <PreferenceCategory
        android:key="device_stylus"/>

</PreferenceScreen>
 No newline at end of file
+84 −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.settings.connecteddevice.stylus;

import android.app.settings.SettingsEnums;
import android.content.Context;
import android.hardware.input.InputManager;
import android.view.InputDevice;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.lifecycle.Lifecycle;

import java.util.ArrayList;
import java.util.List;

/** Controls the USI stylus details and provides updates to individual controllers. */
public class StylusUsiDetailsFragment extends DashboardFragment {
    private static final String TAG = StylusUsiDetailsFragment.class.getSimpleName();
    private static final String KEY_DEVICE_INPUT_ID = "device_input_id";

    @VisibleForTesting
    @Nullable
    InputDevice mInputDevice;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        int inputDeviceId = getArguments().getInt(KEY_DEVICE_INPUT_ID);
        InputManager im = context.getSystemService(InputManager.class);
        mInputDevice = im.getInputDevice(inputDeviceId);

        super.onAttach(context);
        if (mInputDevice == null) {
            finish();
        }
    }


    @Override
    public int getMetricsCategory() {
        // TODO(b/261988317): for new SettingsEnum for this page
        return SettingsEnums.BLUETOOTH_DEVICE_DETAILS;
    }

    @Override
    protected String getLogTag() {
        return TAG;
    }

    @Override
    protected int getPreferenceScreenResId() {
        return R.xml.stylus_usi_details_fragment;
    }

    @Override
    protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
        ArrayList<AbstractPreferenceController> controllers = new ArrayList<>();
        if (mInputDevice != null) {
            Lifecycle lifecycle = getSettingsLifecycle();
            controllers.add(new StylusUsiHeaderController(context, mInputDevice));
            controllers.add(new StylusDevicesController(context, mInputDevice, lifecycle));
        }
        return controllers;
    }
}
+145 −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.settings.connecteddevice.stylus;

import android.content.Context;
import android.hardware.BatteryState;
import android.hardware.input.InputManager;
import android.os.Bundle;
import android.view.InputDevice;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;

import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnCreate;
import com.android.settingslib.core.lifecycle.events.OnDestroy;
import com.android.settingslib.widget.LayoutPreference;

import java.text.NumberFormat;

/**
 * This class adds a header for USI stylus devices with a heading, icon, and battery level.
 * As opposed to the bluetooth device headers, this USI header gets its battery values
 * from {@link InputManager} APIs, rather than the bluetooth battery levels.
 */
public class StylusUsiHeaderController extends BasePreferenceController implements
        InputManager.InputDeviceBatteryListener, LifecycleObserver, OnCreate, OnDestroy {

    private static final String KEY_STYLUS_USI_HEADER = "stylus_usi_header";
    private static final String TAG = StylusUsiHeaderController.class.getSimpleName();

    private final InputManager mInputManager;
    private final InputDevice mInputDevice;

    private LayoutPreference mHeaderPreference;


    public StylusUsiHeaderController(Context context, InputDevice inputDevice) {
        super(context, KEY_STYLUS_USI_HEADER);
        mInputDevice = inputDevice;
        mInputManager = context.getSystemService(InputManager.class);
    }

    @Override
    public void displayPreference(PreferenceScreen screen) {
        mHeaderPreference = screen.findPreference(getPreferenceKey());
        View view = mHeaderPreference.findViewById(R.id.entity_header);
        TextView titleView = view.findViewById(R.id.entity_header_title);
        titleView.setText(R.string.stylus_usi_header_title);

        ImageView iconView = mHeaderPreference.findViewById(R.id.entity_header_icon);
        if (iconView != null) {
            // TODO(b/250909304): get proper icon once VisD ready
            iconView.setImageResource(R.drawable.circle);
            iconView.setContentDescription("Icon for stylus");
        }
        refresh();
        super.displayPreference(screen);
    }

    @Override
    public void updateState(Preference preference) {
        refresh();
    }

    private void refresh() {
        BatteryState batteryState = mInputDevice.getBatteryState();
        View view = mHeaderPreference.findViewById(R.id.entity_header);
        TextView summaryView = view.findViewById(R.id.entity_header_summary);

        if (isValidBatteryState(batteryState)) {
            summaryView.setVisibility(View.VISIBLE);
            summaryView.setText(
                    NumberFormat.getPercentInstance().format(batteryState.getCapacity()));
        } else {
            summaryView.setVisibility(View.INVISIBLE);
        }
    }

    /**
     * This determines if a battery state is 'stale', as indicated by the presence of
     * battery values.
     *
     * A USI battery state is valid (and present) if a USI battery value has been pulled
     * within the last 1 hour of a stylus touching/hovering on the screen. The header shows
     * battery values in this case, Conversely, a stale battery state means no USI battery
     * value has been detected within the last 1 hour. Thus, the USI stylus preference will
     * not be shown in Settings, and accordingly, the USI battery state won't surface.
     *
     * @param batteryState Latest battery state pulled from the kernel
     */
    private boolean isValidBatteryState(BatteryState batteryState) {
        return batteryState != null
                && batteryState.isPresent()
                && batteryState.getCapacity() > 0f;
    }

    @Override
    public int getAvailabilityStatus() {
        return AVAILABLE;
    }

    @Override
    public String getPreferenceKey() {
        return KEY_STYLUS_USI_HEADER;
    }

    @Override
    public void onBatteryStateChanged(int deviceId, long eventTimeMillis,
            @NonNull BatteryState batteryState) {
        refresh();
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        mInputManager.addInputDeviceBatteryListener(mInputDevice.getId(),
                mContext.getMainExecutor(), this);
    }

    @Override
    public void onDestroy() {
        mInputManager.removeInputDeviceBatteryListener(mInputDevice.getId(),
                this);
    }
}
+158 −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.settings.connecteddevice.stylus;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.hardware.BatteryState;
import android.hardware.input.InputManager;
import android.os.Bundle;
import android.view.InputDevice;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;

import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.test.core.app.ApplicationProvider;

import com.android.settings.R;
import com.android.settingslib.widget.LayoutPreference;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;

@RunWith(RobolectricTestRunner.class)
public class StylusUsiHeaderControllerTest {

    private Context mContext;
    private StylusUsiHeaderController mController;
    private LayoutPreference mLayoutPreference;
    private PreferenceScreen mScreen;
    private InputDevice mInputDevice;

    @Mock
    private InputManager mInputManager;
    @Mock
    private BatteryState mBatteryState;
    @Mock
    private Bundle mBundle;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);

        InputDevice device = new InputDevice.Builder().setId(1).setSources(
                InputDevice.SOURCE_BLUETOOTH_STYLUS).build();
        mInputDevice = spy(device);
        when(mInputDevice.getBatteryState()).thenReturn(mBatteryState);
        when(mBatteryState.getCapacity()).thenReturn(1f);
        when(mBatteryState.isPresent()).thenReturn(true);

        mContext = spy(ApplicationProvider.getApplicationContext());
        when(mContext.getSystemService(InputManager.class)).thenReturn(mInputManager);
        mController = new StylusUsiHeaderController(mContext, mInputDevice);

        PreferenceManager preferenceManager = new PreferenceManager(mContext);
        mLayoutPreference = new LayoutPreference(mContext,
                LayoutInflater.from(mContext).inflate(R.layout.advanced_bt_entity_header, null));
        mLayoutPreference.setKey(mController.getPreferenceKey());

        mScreen = preferenceManager.createPreferenceScreen(mContext);
        mScreen.addPreference(mLayoutPreference);

    }

    @Test
    public void onCreate_registersBatteryListener() {
        mController.onCreate(mBundle);

        verify(mInputManager).addInputDeviceBatteryListener(mInputDevice.getId(),
                mContext.getMainExecutor(),
                mController);
    }

    @Test
    public void onDestroy_unregistersBatteryListener() {
        mController.onDestroy();

        verify(mInputManager).removeInputDeviceBatteryListener(mInputDevice.getId(),
                mController);
    }

    @Test
    public void displayPreference_showsCorrectTitle() {
        mController.displayPreference(mScreen);

        assertThat(((TextView) mLayoutPreference.findViewById(
                R.id.entity_header_title)).getText().toString()).isEqualTo(
                mContext.getString(R.string.stylus_usi_header_title));
    }

    @Test
    public void displayPreference_hasBattery_showsCorrectBatterySummary() {
        mController.displayPreference(mScreen);

        assertThat(mLayoutPreference.findViewById(
                R.id.entity_header_summary).getVisibility()).isEqualTo(View.VISIBLE);
        assertThat(((TextView) mLayoutPreference.findViewById(
                R.id.entity_header_summary)).getText().toString()).isEqualTo(
                "100%");
    }

    @Test
    public void displayPreference_noBattery_showsEmptySummary() {
        when(mBatteryState.isPresent()).thenReturn(false);

        mController.displayPreference(mScreen);

        assertThat(mLayoutPreference.findViewById(
                R.id.entity_header_summary).getVisibility()).isEqualTo(View.INVISIBLE);
    }

    @Test
    public void displayPreference_invalidCapacity_showsEmptySummary() {
        when(mBatteryState.getCapacity()).thenReturn(-1f);

        mController.displayPreference(mScreen);

        assertThat(mLayoutPreference.findViewById(
                R.id.entity_header_summary).getVisibility()).isEqualTo(View.INVISIBLE);
    }

    @Test
    public void onBatteryStateChanged_updatesSummary() {
        mController.displayPreference(mScreen);

        when(mBatteryState.getCapacity()).thenReturn(0.2f);
        mController.onBatteryStateChanged(mInputDevice.getId(),
                System.currentTimeMillis(), mBatteryState);

        assertThat(((TextView) mLayoutPreference.findViewById(
                R.id.entity_header_summary)).getText().toString()).isEqualTo(
                "20%");
    }
}