diff --git a/res/layout/preference_radio_with_extra_widget.xml b/res/layout/preference_radio_with_extra_widget.xml new file mode 100644 index 00000000000..00c428f273e --- /dev/null +++ b/res/layout/preference_radio_with_extra_widget.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/com/android/settings/gestures/SystemNavigationGestureSettings.java b/src/com/android/settings/gestures/SystemNavigationGestureSettings.java index e17b8705703..40e8831e5c2 100644 --- a/src/com/android/settings/gestures/SystemNavigationGestureSettings.java +++ b/src/com/android/settings/gestures/SystemNavigationGestureSettings.java @@ -21,6 +21,10 @@ import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON_OVE 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; @@ -29,7 +33,6 @@ import android.graphics.drawable.Drawable; import android.os.RemoteException; import android.os.ServiceManager; import android.provider.SearchIndexableResource; -import android.view.View; import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceScreen; @@ -41,6 +44,7 @@ import com.android.settings.search.BaseSearchIndexProvider; 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; @@ -94,8 +98,25 @@ public class SystemNavigationGestureSettings extends RadioButtonPickerFragment { } @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 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 @@ -135,6 +156,13 @@ public class SystemNavigationGestureSettings extends RadioButtonPickerFragment { @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; @@ -196,10 +224,36 @@ public class SystemNavigationGestureSettings extends RadioButtonPickerFragment { @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 { diff --git a/src/com/android/settings/gestures/SystemNavigationPreferenceController.java b/src/com/android/settings/gestures/SystemNavigationPreferenceController.java index d0d81552ff5..a151dc1746c 100644 --- a/src/com/android/settings/gestures/SystemNavigationPreferenceController.java +++ b/src/com/android/settings/gestures/SystemNavigationPreferenceController.java @@ -22,11 +22,14 @@ import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; 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"; @@ -98,4 +101,31 @@ public class SystemNavigationPreferenceController extends BasePreferenceControll 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 ""; + } } diff --git a/src/com/android/settings/widget/RadioButtonPreferenceWithExtraWidget.java b/src/com/android/settings/widget/RadioButtonPreferenceWithExtraWidget.java new file mode 100644 index 00000000000..6a47ce516af --- /dev/null +++ b/src/com/android/settings/widget/RadioButtonPreferenceWithExtraWidget.java @@ -0,0 +1,79 @@ +/* + * 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 diff --git a/tests/robotests/src/com/android/settings/gestures/SystemNavigationPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/gestures/SystemNavigationPreferenceControllerTest.java index 095c4d78f11..79f18502c81 100644 --- a/tests/robotests/src/com/android/settings/gestures/SystemNavigationPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/gestures/SystemNavigationPreferenceControllerTest.java @@ -24,10 +24,14 @@ import static com.android.settings.gestures.SystemNavigationPreferenceController 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; @@ -39,6 +43,7 @@ import org.junit.After; 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; @@ -53,9 +58,15 @@ public class SystemNavigationPreferenceControllerTest { 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() { @@ -69,6 +80,10 @@ public class SystemNavigationPreferenceControllerTest { 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 @@ -166,4 +181,46 @@ public class SystemNavigationPreferenceControllerTest { 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 diff --git a/tests/robotests/src/com/android/settings/widget/RadioButtonPreferenceWithExtraWidgetTest.java b/tests/robotests/src/com/android/settings/widget/RadioButtonPreferenceWithExtraWidgetTest.java new file mode 100644 index 00000000000..b84b3bb48d8 --- /dev/null +++ b/tests/robotests/src/com/android/settings/widget/RadioButtonPreferenceWithExtraWidgetTest.java @@ -0,0 +1,127 @@ +/* + * 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(); + } +}