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

Commit bb434258 authored by Lorenzo Lucena Maguire's avatar Lorenzo Lucena Maguire
Browse files

Create Double Tap Power Gesture Lockscreen Shortcut Tip

Shows a suggestion card if the double tap power gesture is currently a
lockscreen shortcut. Upon tapping the card, the user will be directed to
the wallpaper settings page, where they can modify their current
lockscreen shortcut selections.

Android Settings Feature Request: b/380287172

Test: atest
tests/robotests/src/com/android/settings/gestures/DoubleTapPowerLockscreenTipPreferenceControllerTest
Test: Manually Tested.
Flag: android.service.quickaccesswallet.launch_wallet_option_on_power_double_tap
Bug: 406311198
Change-Id: Ifbb08d0461c97459fa3c278a282db19841955472

Change-Id: Ic032b97d6d8946f56df273e42b2cb7ce6707b7b8
parent 23a80da5
Loading
Loading
Loading
Loading
+29 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
    Copyright (C) 2025 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
    android:width="40dp"
    android:height="40dp"
    android:viewportWidth="40.0"
    android:viewportHeight="40.0">
    <path android:pathData="M0 20C0 8.95431 8.95431 0 20 0C31.0457 0 40 8.95431 40 20C40 31.0457 31.0457 40 20 40C8.95431 40 0 31.0457 0 20Z"
        android:fillColor="@androidprv:color/materialColorSecondary"/>

    <path
        android:pathData="M25.7203 29.6492L19.6703 23.5492C19.337 23.6826 18.9953 23.7826 18.6453 23.8492C18.312 23.9159 17.9536 23.9492 17.5703 23.9492C15.9036 23.9492 14.487 23.3659 13.3203 22.1992C12.1536 21.0326 11.5703 19.6159 11.5703 17.9492C11.5703 17.3492 11.6536 16.7826 11.8203 16.2492C11.987 15.6992 12.2203 15.1826 12.5203 14.6992L16.1703 18.3492L17.9703 16.5492L14.3203 12.8992C14.8036 12.5992 15.312 12.3659 15.8453 12.1992C16.3953 12.0326 16.9703 11.9492 17.5703 11.9492C19.237 11.9492 20.6536 12.5326 21.8203 13.6992C22.987 14.8659 23.5703 16.2826 23.5703 17.9492C23.5703 18.3326 23.537 18.6992 23.4703 19.0492C23.4036 19.3826 23.3036 19.7159 23.1703 20.0492L29.2703 26.0992C29.4703 26.2992 29.5703 26.5409 29.5703 26.8242C29.5703 27.1076 29.4703 27.3492 29.2703 27.5492L27.1703 29.6492C26.9703 29.8492 26.7286 29.9492 26.4453 29.9492C26.162 29.9492 25.9203 29.8492 25.7203 29.6492Z"
        android:fillColor="@androidprv:color/materialColorOnSecondary"/>
</vector>
+5 −0
Original line number Diff line number Diff line
@@ -11576,6 +11576,11 @@ Data usage charges may apply.</string>
    <string name="double_tap_power_wallet_action_title">Wallet</string>
    <!-- Setting summary to describe double tap power button will open wallet. [CHAR LIMIT=NONE] -->
    <string name="double_tap_power_wallet_action_summary">Open Wallet</string>
    <!-- Title text for tip suggesting user to modify lockscreen shortcut. [CHAR LIMIT=100] -->
    <string name="double_tap_power_lockscreen_shortcut_tip_title">Change lock screen shortcut?</string>
    <!-- Description text for tip suggesting user to modify lockscreen shortcut. [CHAR LIMIT=200]-->
    <string name="double_tap_power_lockscreen_shortcut_tip_description"><xliff:g id="double_tap_power_target_action" example="Camera">%1$s</xliff:g> is also your lock screen shortcut. Want to change it?</string>
    <!-- Title text for double twist for camera mode [CHAR LIMIT=60]-->
    <string name="double_twist_for_camera_mode_title">Flip camera for selfie</string>
+5 −0
Original line number Diff line number Diff line
@@ -42,4 +42,9 @@
            android:title="@string/double_tap_power_wallet_action_title"
            settings:controller="com.android.settings.gestures.DoubleTapPowerForWalletPreferenceController"/>
    </PreferenceCategory>
    <com.android.settingslib.widget.CardPreference
        android:key="gesture_double_tap_power_lockscreen_shortcut_tip"
        android:title="@string/double_tap_power_lockscreen_shortcut_tip_title"
        android:icon="@drawable/ic_double_tap_power_lockscreen_shortcut_tip_icon"
        settings:controller="com.android.settings.gestures.DoubleTapPowerLockscreenTipPreferenceController" />
</PreferenceScreen>
+199 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.gestures;

import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
import com.android.settingslib.widget.CardPreference;

public class DoubleTapPowerLockscreenTipPreferenceController extends BasePreferenceController
        implements LifecycleObserver, OnStart, OnStop {

    private static final String TAG = "DoubleTapPowerLockscreenTip";

    /** URI to query current Keyguard Quick Affordance selections. */
    static final Uri KEYGUARD_QUICK_AFFORDANCE_SELECTIONS_URI = new Uri.Builder()
            .scheme(ContentResolver.SCHEME_CONTENT)
            .authority("com.android.systemui.customization")
            .appendPath("lockscreen_quickaffordance")
            .appendPath("selections")
            .build();

    /** Name of Cursor column containing Keyguard Quick Affordance selections. */
    static final String AFFORDANCE_NAME_COLUMN = "affordance_name";
    static final String CAMERA_KEYGUARD_QUICK_AFFORDANCE_NAME = "Camera";
    static final String WALLET_KEYGUARD_QUICK_AFFORDANCE_NAME = "Wallet";

    @Nullable private CardPreference mPreference;

    private final Handler mHandler = new Handler(Looper.getMainLooper());
    private final ContentObserver mSettingsObserver =
            new ContentObserver(mHandler) {
                @Override
                public void onChange(boolean selfChange, @Nullable Uri uri) {
                    if (mPreference == null || uri == null) {
                        return;
                    }
                    if (uri.equals(
                            DoubleTapPowerSettingsUtils
                                    .DOUBLE_TAP_POWER_BUTTON_GESTURE_ENABLED_URI)) {
                        if (DoubleTapPowerSettingsUtils.isDoubleTapPowerButtonGestureEnabled(
                                mContext)) {
                            // If the gesture is enabled, check whether tip needs to shown
                            updateState(mPreference);
                        } else {
                            // If the gesture is disabled, hide tip
                            mPreference.setVisible(false);
                        }
                    } else if (uri.equals(
                            DoubleTapPowerSettingsUtils
                                    .DOUBLE_TAP_POWER_BUTTON_GESTURE_TARGET_ACTION_URI)) {
                        updateState(mPreference);
                    }
                }
            };

    public DoubleTapPowerLockscreenTipPreferenceController(
            @NonNull Context context,
            @NonNull String preferenceKey) {
        super(context, preferenceKey);
    }

    @Override
    public void onStart() {
        DoubleTapPowerSettingsUtils.registerObserver(mContext, mSettingsObserver);
    }

    @Override
    public void onStop() {
        DoubleTapPowerSettingsUtils.unregisterObserver(mContext, mSettingsObserver);
    }


    @Override
    public int getAvailabilityStatus() {
        return DoubleTapPowerSettingsUtils
                .isMultiTargetDoubleTapPowerButtonGestureAvailable(mContext)
                ? AVAILABLE_UNSEARCHABLE
                : UNSUPPORTED_ON_DEVICE;
    }

    @Override
    public void displayPreference(@NonNull PreferenceScreen screen) {
        super.displayPreference(screen);
        mPreference = screen.findPreference(getPreferenceKey());
        if (mPreference != null) {
            mPreference.useDismissAction();
            mPreference.setOnPreferenceClickListener(preference -> {
                final Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER);
                intent.putExtra("destination", "quick_affordances");
                // Informs destination it was launched within Settings
                intent.putExtra(
                        "com.android.wallpaper.LAUNCH_SOURCE",
                        "app_launched_settings"
                );
                final String packageName =
                        mContext.getString(R.string.config_wallpaper_picker_package);
                if (!TextUtils.isEmpty(packageName)) {
                    intent.setPackage(packageName);
                }
                mContext.startActivity(intent);
                return true;
            });
        }
    }

    @Override
    public void updateState(@NonNull Preference preference) {
        super.updateState(preference);

        if (!DoubleTapPowerSettingsUtils.isDoubleTapPowerButtonGestureEnabled(mContext)) {
            preference.setVisible(false);
            return;
        }

        String targetActionInLockscreenShortcut = getTargetActionIfLockScreenShortcut(mContext);
        if (targetActionInLockscreenShortcut == null) {
            preference.setVisible(false);
            return;
        }
        if (preference instanceof CardPreference) {
            Log.i(TAG, "Target action is also lockscreen shorcut. Showing suggestion card.");
            preference.setSummary(mContext.getString(
                            R.string.double_tap_power_lockscreen_shortcut_tip_description,
                            targetActionInLockscreenShortcut
                    ));
            preference.setVisible(true);
        }
    }

    @Nullable
    private static String getTargetActionIfLockScreenShortcut(@NonNull Context context) {
        String currentTargetActionName =
                DoubleTapPowerSettingsUtils
                        .isDoubleTapPowerButtonGestureForCameraLaunchEnabled(context)
                ? CAMERA_KEYGUARD_QUICK_AFFORDANCE_NAME : WALLET_KEYGUARD_QUICK_AFFORDANCE_NAME;
        Log.i(TAG, "Current target action name " + currentTargetActionName);
        try (Cursor cursor = context.getContentResolver().query(
                KEYGUARD_QUICK_AFFORDANCE_SELECTIONS_URI,
                null,
                null,
                null)) {
            if (cursor == null) {
                Log.w(TAG, "Keyguard Quick Affordance Cursor was null!");
                return null;
            }

            final int columnIndex = cursor.getColumnIndex(AFFORDANCE_NAME_COLUMN);
            if (columnIndex == -1) {
                Log.w(TAG, "Keyguard Quick Affordance Cursor doesn't contain \""
                        + AFFORDANCE_NAME_COLUMN + "\" column!");
                return null;
            }

            while (cursor.moveToNext()) {
                final String affordanceName = cursor.getString(columnIndex);
                if (TextUtils.equals(affordanceName, currentTargetActionName)) {
                    return affordanceName;
                }
            }
            return null;
        } catch (Exception e) {
            Log.e(TAG, "Exception while querying Keyguard Quick Affordance content provider", e);
            return null;
        }
    }
}
+165 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.gestures;

import static com.android.settings.gestures.DoubleTapPowerLockscreenTipPreferenceController.AFFORDANCE_NAME_COLUMN;
import static com.android.settings.gestures.DoubleTapPowerLockscreenTipPreferenceController.CAMERA_KEYGUARD_QUICK_AFFORDANCE_NAME;
import static com.android.settings.gestures.DoubleTapPowerLockscreenTipPreferenceController.KEYGUARD_QUICK_AFFORDANCE_SELECTIONS_URI;
import static com.android.settings.gestures.DoubleTapPowerSettingsUtils.DOUBLE_TAP_POWER_DISABLED_MODE;
import static com.android.settings.gestures.DoubleTapPowerSettingsUtils.DOUBLE_TAP_POWER_MULTI_TARGET_MODE;

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.spy;
import static org.mockito.Mockito.when;

import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.database.MatrixCursor;
import android.text.TextUtils;

import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.widget.CardPreference;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)
public class DoubleTapPowerLockscreenTipPreferenceControllerTest {

    private static final String KEY = "gesture_double_tap_power_lockscreen_shortcut_tip";
    private Context mContext;
    private Resources mResources;
    private DoubleTapPowerLockscreenTipPreferenceController mController;
    private CardPreference mPreference;
    private ContentResolver mContentResolver;

    @Before
    public void setUp() {
        mContext = spy(ApplicationProvider.getApplicationContext());
        mResources = mock(Resources.class);
        when(mContext.getResources()).thenReturn(mResources);
        mContentResolver = mock(ContentResolver.class);
        when(mContext.getContentResolver()).thenReturn(mContentResolver);

        mController = new DoubleTapPowerLockscreenTipPreferenceController(mContext, KEY);
        mPreference = new CardPreference(mContext);
    }

    @Test
    public void updateState_doubleTapPowerGestureDisabled_preferenceNotVisible() {
        DoubleTapPowerSettingsUtils.setDoubleTapPowerButtonGestureEnabled(mContext, false);

        mController.updateState(mPreference);

        assertThat(mPreference.isVisible()).isFalse();
    }

    @Test
    public void updateState_noLockscreenShortcutData_preferenceNotVisible() {
        DoubleTapPowerSettingsUtils.setDoubleTapPowerButtonGestureEnabled(mContext, true);

        mController.updateState(mPreference);

        assertThat(mPreference.isVisible()).isFalse();
    }

    @Test
    public void updateState_noLockscreenShortcutDataColumn_preferenceNotVisible() {
        DoubleTapPowerSettingsUtils.setDoubleTapPowerButtonGestureEnabled(mContext, true);
        when(mContentResolver.query(
                eq(KEYGUARD_QUICK_AFFORDANCE_SELECTIONS_URI),
                any(),
                any(),
                any())
        ).thenReturn(new MatrixCursor(new String[]{}));

        mController.updateState(mPreference);

        assertThat(mPreference.isVisible()).isFalse();
    }

    @Test
    public void updateState_noTargetActionInLockscreenShortcut_preferenceNotVisible() {
        DoubleTapPowerSettingsUtils.setDoubleTapPowerButtonGestureEnabled(mContext, true);
        setSelectedLockScreenShortcuts();

        mController.updateState(mPreference);

        assertThat(mPreference.isVisible()).isFalse();
    }

    @Test
    public void updateState_targetActionInLockscreenShortcut_preferenceVisible() {
        DoubleTapPowerSettingsUtils.setDoubleTapPowerButtonGestureEnabled(mContext, true);
        DoubleTapPowerSettingsUtils.setDoubleTapPowerButtonForCameraLaunch(mContext);
        setSelectedLockScreenShortcuts(CAMERA_KEYGUARD_QUICK_AFFORDANCE_NAME);

        mController.updateState(mPreference);

        assertThat(mPreference.isVisible()).isTrue();
        assertThat(
                TextUtils.equals(mPreference.getSummary(),
                mContext.getString(
                        R.string.double_tap_power_lockscreen_shortcut_tip_description,
                        CAMERA_KEYGUARD_QUICK_AFFORDANCE_NAME
                )
                )).isTrue();
    }

    @Test
    public void getAvailabilityStatus_setDoubleTapPowerGestureNotAvailable_preferenceUnsupported() {
        when(mResources.getInteger(
                com.android.internal.R.integer.config_doubleTapPowerGestureMode)).thenReturn(
                DOUBLE_TAP_POWER_DISABLED_MODE);

        assertThat(mController.getAvailabilityStatus())
                .isEqualTo(BasePreferenceController.UNSUPPORTED_ON_DEVICE);
    }

    @Test
    public void getAvailabilityStatus_setDoubleTapPowerGestureAvailable_preferenceAvailable() {
        when(mResources.getInteger(
                com.android.internal.R.integer.config_doubleTapPowerGestureMode)).thenReturn(
                DOUBLE_TAP_POWER_MULTI_TARGET_MODE);

        assertThat(mController.getAvailabilityStatus())
                .isEqualTo(BasePreferenceController.AVAILABLE_UNSEARCHABLE);
    }

    private void setSelectedLockScreenShortcuts(String... affordanceNames) {
        final MatrixCursor cursor = new MatrixCursor(
                new String[]{AFFORDANCE_NAME_COLUMN});
        for (final String name : affordanceNames) {
            cursor.addRow(new Object[]{name});
        }

        when(
                mContentResolver
                        .query(eq(KEYGUARD_QUICK_AFFORDANCE_SELECTIONS_URI), any(), any(), any()))
                .thenReturn(cursor);
    }
}