--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2019 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
+ -->
+<!-- This file is copied from preference_radio.xml with modification to
+ support an extra clickable icon on the opposite side horizontally -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:settings="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:gravity="center_vertical"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+ <LinearLayout
+ android:id="@android:id/widget_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:minWidth="56dp"
+ android:layout_marginEnd="16dp"
+ android:orientation="vertical" />
+
+ <LinearLayout
+ android:id="@+id/icon_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:minWidth="32dp"
+ android:orientation="horizontal"
+ android:layout_marginEnd="16dp"
+ android:paddingTop="4dp"
+ android:paddingBottom="4dp">
+ <androidx.preference.internal.PreferenceImageView
+ android:id="@android:id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ settings:maxWidth="@dimen/secondary_app_icon_size"
+ settings:maxHeight="@dimen/secondary_app_icon_size" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp">
+
+ <TextView android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textAppearance="@style/TextAppearance.TileTitle"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal" />
+
+ <TextView android:id="@android:id/summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:textAppearance="@style/TextAppearance.Small"
+ android:textAlignment="viewStart"
+ android:textColor="?android:attr/textColorSecondary" />
+ </LinearLayout>
+ <LinearLayout
+ android:id="@+id/radio_extra_widget_container"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+ <View
+ android:id="@+id/radio_extra_widget_divider"
+ android:layout_width="@dimen/vertical_divider_width"
+ android:layout_height="match_parent"
+ android:layout_marginTop="36dp"
+ android:layout_marginBottom="36dp"
+ android:layout_marginStart="8dp"
+ android:background="?android:attr/dividerVertical" />
+ <ImageView
+ android:id="@+id/radio_extra_widget"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:src="@drawable/ic_settings_about"
+ android:contentDescription="@string/information_label"
+ android:layout_gravity="center"
+ android:background="?android:attr/selectableItemBackground" />
+ </LinearLayout>
+
+</LinearLayout>
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY;
+import static com.android.settings.widget.RadioButtonPreferenceWithExtraWidget.EXTRA_WIDGET_VISIBILITY_GONE;
+import static com.android.settings.widget.RadioButtonPreferenceWithExtraWidget.EXTRA_WIDGET_VISIBILITY_INFO;
+
+import android.app.AlertDialog;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.provider.SearchIndexableResource;
-import android.view.View;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceScreen;
import com.android.settings.search.Indexable;
import com.android.settings.widget.RadioButtonPickerFragment;
import com.android.settings.widget.RadioButtonPreference;
+import com.android.settings.widget.RadioButtonPreferenceWithExtraWidget;
import com.android.settings.widget.VideoPreference;
import com.android.settingslib.search.SearchIndexable;
import com.android.settingslib.widget.CandidateInfo;
}
@Override
- protected void addStaticPreferences(PreferenceScreen screen) {
+ public void updateCandidates() {
+ final String defaultKey = getDefaultKey();
+ final String systemDefaultKey = getSystemDefaultKey();
+ final PreferenceScreen screen = getPreferenceScreen();
+ screen.removeAll();
screen.addPreference(mVideoPreference);
+
+ final List<? extends CandidateInfo> candidateList = getCandidates();
+ if (candidateList == null) {
+ return;
+ }
+ for (CandidateInfo info : candidateList) {
+ RadioButtonPreferenceWithExtraWidget pref =
+ new RadioButtonPreferenceWithExtraWidget(getPrefContext());
+ bindPreference(pref, info.getKey(), info, defaultKey);
+ bindPreferenceExtra(pref, info.getKey(), info, defaultKey, systemDefaultKey);
+ screen.addPreference(pref);
+ }
+ mayCheckOnlyRadioButton();
}
@Override
@Override
protected boolean setDefaultKey(String key) {
+ final Context c = getContext();
+ if (key == KEY_SYSTEM_NAV_GESTURAL &&
+ !SystemNavigationPreferenceController.isGestureNavSupportedByDefaultLauncher(c)) {
+ // This should not happen since the preference is disabled. Return to be safe.
+ return false;
+ }
+
setCurrentSystemNavigationMode(mOverlayManager, key);
setIllustrationVideo(mVideoPreference, key);
return true;
@Override
public void bindPreferenceExtra(RadioButtonPreference pref,
String key, CandidateInfo info, String defaultKey, String systemDefaultKey) {
- if (info instanceof NavModeCandidateInfo) {
- pref.setSummary(((NavModeCandidateInfo) info).loadSummary());
- pref.setAppendixVisibility(View.GONE);
+ if (!(info instanceof NavModeCandidateInfo)
+ || !(pref instanceof RadioButtonPreferenceWithExtraWidget)) {
+ return;
+ }
+
+ pref.setSummary(((NavModeCandidateInfo) info).loadSummary());
+
+ RadioButtonPreferenceWithExtraWidget p = (RadioButtonPreferenceWithExtraWidget) pref;
+ if (info.getKey() == KEY_SYSTEM_NAV_GESTURAL
+ && !SystemNavigationPreferenceController.isGestureNavSupportedByDefaultLauncher(
+ getContext())) {
+ p.setEnabled(false);
+ p.setExtraWidgetVisibility(EXTRA_WIDGET_VISIBILITY_INFO);
+ p.setExtraWidgetOnClickListener((v) -> {
+ showGestureNavDisabledDialog();
+ });
+ } else {
+ p.setExtraWidgetVisibility(EXTRA_WIDGET_VISIBILITY_GONE);
}
}
+ private void showGestureNavDisabledDialog() {
+ final String defaultHomeAppName = SystemNavigationPreferenceController
+ .getDefaultHomeAppName(getContext());
+ final String message = getString(R.string.gesture_not_supported_dialog_message,
+ defaultHomeAppName);
+ AlertDialog d = new AlertDialog.Builder(getContext())
+ .setMessage(message)
+ .setPositiveButton(R.string.okay, null)
+ .show();
+ }
+
static class NavModeCandidateInfo extends CandidateInfo {
private final CharSequence mLabel;
private final CharSequence mSummary;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
+import java.util.ArrayList;
+
public class SystemNavigationPreferenceController extends BasePreferenceController {
static final String PREF_KEY_SYSTEM_NAVIGATION = "gesture_system_navigation";
return NAV_BAR_MODE_GESTURAL == context.getResources().getInteger(
com.android.internal.R.integer.config_navBarInteractionMode);
}
+
+ static boolean isGestureNavSupportedByDefaultLauncher(Context context) {
+ final ComponentName cn = context.getPackageManager().getHomeActivities(new ArrayList<>());
+ if (cn == null) {
+ // There is no default home app set for the current user, don't make any changes yet.
+ return true;
+ }
+ ComponentName recentsComponentName = ComponentName.unflattenFromString(context.getString(
+ com.android.internal.R.string.config_recentsComponentName));
+ return recentsComponentName.getPackageName().equals(cn.getPackageName());
+ }
+
+ static String getDefaultHomeAppName(Context context) {
+ final PackageManager pm = context.getPackageManager();
+ final ComponentName cn = pm.getHomeActivities(new ArrayList<>());
+ if (cn != null) {
+ try {
+ ApplicationInfo ai = pm.getApplicationInfo(cn.getPackageName(), 0);
+ if (ai != null) {
+ return pm.getApplicationLabel(ai).toString();
+ }
+ } catch (final PackageManager.NameNotFoundException e) {
+ // Do nothing
+ }
+ }
+ return "";
+ }
}
--- /dev/null
+/*
+ * Copyright (C) 2019 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.widget;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.ImageView;
+
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.settings.R;
+
+public class RadioButtonPreferenceWithExtraWidget extends RadioButtonPreference {
+ public static final int EXTRA_WIDGET_VISIBILITY_GONE = 0;
+ public static final int EXTRA_WIDGET_VISIBILITY_INFO = 1;
+
+ private View mExtraWidgetDivider;
+ private ImageView mExtraWidget;
+
+ private int mExtraWidgetVisibility = EXTRA_WIDGET_VISIBILITY_GONE;
+ private View.OnClickListener mExtraWidgetOnClickListener;
+
+ public RadioButtonPreferenceWithExtraWidget(Context context) {
+ super(context, null);
+ setLayoutResource(R.layout.preference_radio_with_extra_widget);
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder view) {
+ super.onBindViewHolder(view);
+
+ mExtraWidget = (ImageView) view.findViewById(R.id.radio_extra_widget);
+ mExtraWidgetDivider = view.findViewById(R.id.radio_extra_widget_divider);
+ setExtraWidgetVisibility(mExtraWidgetVisibility);
+
+ if (mExtraWidgetOnClickListener != null) {
+ setExtraWidgetOnClickListener(mExtraWidgetOnClickListener);
+ }
+ }
+
+ public void setExtraWidgetVisibility(int visibility) {
+ mExtraWidgetVisibility = visibility;
+ if (mExtraWidget == null || mExtraWidgetDivider == null) {
+ return;
+ }
+
+ if (visibility == EXTRA_WIDGET_VISIBILITY_GONE) {
+ mExtraWidget.setClickable(false);
+ mExtraWidget.setVisibility(View.GONE);
+ mExtraWidgetDivider.setVisibility(View.GONE);
+ } else {
+ mExtraWidget.setClickable(true);
+ mExtraWidget.setVisibility(View.VISIBLE);
+ mExtraWidgetDivider.setVisibility(View.VISIBLE);
+ }
+ }
+
+ public void setExtraWidgetOnClickListener(View.OnClickListener listener) {
+ mExtraWidgetOnClickListener = listener;
+ if (mExtraWidget != null) {
+ mExtraWidget.setEnabled(true);
+ mExtraWidget.setOnClickListener(listener);
+ }
+ }
+}
\ No newline at end of file
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.text.TextUtils;
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;
import org.robolectric.RuntimeEnvironment;
private Context mContext;
private ShadowPackageManager mPackageManager;
+ @Mock
+ private Context mMockContext;
+ @Mock
+ private PackageManager mMockPackageManager;
+
private SystemNavigationPreferenceController mController;
private static final String ACTION_QUICKSTEP = "android.intent.action.QUICKSTEP_SERVICE";
+ private static final String TEST_RECENTS_COMPONENT_NAME = "test.component.name/.testActivity";
@Before
public void setUp() {
mController = new SystemNavigationPreferenceController(mContext,
PREF_KEY_SYSTEM_NAVIGATION);
+
+ when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+ when(mMockContext.getString(com.android.internal.R.string.config_recentsComponentName))
+ .thenReturn(TEST_RECENTS_COMPONENT_NAME);
}
@After
assertThat(TextUtils.equals(mController.getSummary(), mContext.getText(
com.android.settings.R.string.swipe_up_to_switch_apps_title))).isTrue();
}
+
+ @Test
+ public void testIsGestureNavSupportedByDefaultLauncher_noDefaultLauncher() {
+ when(mMockPackageManager.getHomeActivities(any())).thenReturn(null);
+ assertThat(SystemNavigationPreferenceController
+ .isGestureNavSupportedByDefaultLauncher(mMockContext)).isTrue();
+ }
+
+ @Test
+ public void testIsGestureNavSupportedByDefaultLauncher_supported() {
+ when(mMockPackageManager.getHomeActivities(any())).thenReturn(
+ ComponentName.unflattenFromString(TEST_RECENTS_COMPONENT_NAME));
+ assertThat(SystemNavigationPreferenceController
+ .isGestureNavSupportedByDefaultLauncher(mMockContext)).isTrue();
+ }
+
+ @Test
+ public void testIsGestureNavSupportedByDefaultLauncher_notSupported() {
+ when(mMockPackageManager.getHomeActivities(any())).thenReturn(
+ new ComponentName("unsupported", "launcher"));
+ assertThat(SystemNavigationPreferenceController
+ .isGestureNavSupportedByDefaultLauncher(mMockContext)).isFalse();
+ }
+
+ @Test
+ public void testGetDefaultHomeAppName_noDefaultLauncher() {
+ when(mMockPackageManager.getHomeActivities(any())).thenReturn(null);
+ assertThat(SystemNavigationPreferenceController
+ .getDefaultHomeAppName(mMockContext)).isEqualTo("");
+ }
+
+ @Test
+ public void testGetDefaultHomeAppName_defaultLauncherExists() throws Exception {
+ when(mMockPackageManager.getHomeActivities(any())).thenReturn(
+ new ComponentName("supported", "launcher"));
+ ApplicationInfo info = new ApplicationInfo();
+ when(mMockPackageManager.getApplicationInfo("supported", 0)).thenReturn(info);
+ when(mMockPackageManager.getApplicationLabel(info)).thenReturn("Test Home App");
+
+ assertThat(SystemNavigationPreferenceController
+ .getDefaultHomeAppName(mMockContext)).isEqualTo("Test Home App");
+ }
}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (C) 2019 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.widget;
+
+import static com.android.settings.widget.RadioButtonPreferenceWithExtraWidget.EXTRA_WIDGET_VISIBILITY_GONE;
+import static com.android.settings.widget.RadioButtonPreferenceWithExtraWidget.EXTRA_WIDGET_VISIBILITY_INFO;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+
+import android.app.Application;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.settings.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class RadioButtonPreferenceWithExtraWidgetTest {
+
+ private Application mContext;
+ private RadioButtonPreferenceWithExtraWidget mPreference;
+
+ private TextView mSummary;
+ private ImageView mExtraWidget;
+ private View mExtraWidgetDivider;
+
+ private boolean mIsClickListenerCalled = false;
+ private View.OnClickListener mClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mIsClickListenerCalled = true;
+ }
+ };
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application;
+ mPreference = new RadioButtonPreferenceWithExtraWidget(mContext);
+ mPreference.setSummary("test summary");
+
+ View view = LayoutInflater.from(mContext)
+ .inflate(R.layout.preference_radio_with_extra_widget, null);
+ PreferenceViewHolder preferenceViewHolder =
+ PreferenceViewHolder.createInstanceForTests(view);
+ mPreference.onBindViewHolder(preferenceViewHolder);
+
+ mSummary = view.findViewById(android.R.id.summary);
+ mExtraWidget = view.findViewById(R.id.radio_extra_widget);
+ mExtraWidgetDivider = view.findViewById(R.id.radio_extra_widget_divider);
+ }
+
+ @Test
+ public void shouldHaveRadioPreferenceWithExtraWidgetLayout() {
+ assertThat(mPreference.getLayoutResource())
+ .isEqualTo(R.layout.preference_radio_with_extra_widget);
+ }
+
+ @Test
+ public void iconSpaceReservedShouldBeFalse() {
+ assertThat(mPreference.isIconSpaceReserved()).isFalse();
+ }
+
+ @Test
+ public void summaryShouldBeVisible() {
+ assertEquals(View.VISIBLE, mSummary.getVisibility());
+ }
+
+ @Test
+ public void testSetExtraWidgetVisibility_gone() {
+ mPreference.setExtraWidgetVisibility(EXTRA_WIDGET_VISIBILITY_GONE);
+ assertEquals(View.GONE, mExtraWidget.getVisibility());
+ assertEquals(View.GONE, mExtraWidgetDivider.getVisibility());
+ assertThat(mExtraWidget.isClickable()).isFalse();
+ }
+
+ @Test
+ public void testSetExtraWidgetVisibility_info() {
+ mPreference.setExtraWidgetVisibility(EXTRA_WIDGET_VISIBILITY_INFO);
+ assertEquals(View.VISIBLE, mExtraWidget.getVisibility());
+ assertEquals(View.VISIBLE, mExtraWidgetDivider.getVisibility());
+ assertThat(mExtraWidget.isClickable()).isTrue();
+ }
+
+ @Test
+ public void testSetExtraWidgetOnClickListener() {
+ mPreference.setExtraWidgetOnClickListener(mClickListener);
+
+ assertThat(mIsClickListenerCalled).isFalse();
+ mExtraWidget.callOnClick();
+ assertThat(mIsClickListenerCalled).isTrue();
+ }
+
+ @Test
+ public void extraWidgetStaysEnabledWhenPreferenceIsDisabled() {
+ mPreference.setEnabled(false);
+ mExtraWidget.setEnabled(false);
+
+ assertThat(mExtraWidget.isEnabled()).isFalse();
+ mPreference.setExtraWidgetOnClickListener(mClickListener);
+ assertThat(mExtraWidget.isEnabled()).isTrue();
+ }
+}