Open links settings
These changes show the list of applications that handle domain links in
the "Opening links" setting. Currently it only shows a summary of the
links that can be opened. These preferences will redirect to an
application specific settings page in subsequent changes.
Bug: 130735722
Test: manual and robolectric
Change-Id: Ia94e71044efcb4645c1eeda208a17c89653bef21
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 0f19a5d..7c94d41 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -185,6 +185,11 @@
</intent-filter>
<intent-filter android:priority="100">
+ <action android:name="android.settings.MANAGE_DOMAIN_URLS" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+
+ <intent-filter android:priority="100">
<action android:name="android.settings.DATA_USAGE_SETTINGS" />
<action android:name="android.settings.MOBILE_DATA_USAGE" />
<category android:name="android.intent.category.DEFAULT" />
diff --git a/res/values/preference_keys.xml b/res/values/preference_keys.xml
index 2424446..59817bb 100644
--- a/res/values/preference_keys.xml
+++ b/res/values/preference_keys.xml
@@ -151,6 +151,8 @@
</string>
<string name="pk_default_autofill_options" translatable="false">default_autofill_options
</string>
+ <string name="pk_opening_links_entry" translatable="false">opening_links_entry</string>
+ <string name="pk_opening_links_options" translatable="false">opening_links_options</string>
<!-- DateTime Settings -->
<string name="pk_auto_datetime_switch" translatable="false">auto_datetime_switch</string>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 2561c19..a2f9566 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -441,6 +441,16 @@
</string>
<!-- Title of preference to add new autofill services. [CHAR_LIMIT=30] -->
<string name="autofill_add_service">Add service</string>
+ <!-- Title of setting to change the handled domain urls [CHAR LIMIT=60]-->
+ <string name="app_launch_domain_links_title">Opening links</string>
+ <!-- Section title for the Domain URL app preference list [CHAR LIMIT=60]-->
+ <string name="domain_url_section_title">Installed apps</string>
+ <!-- Summary for an app that doesn't open any domain URLs [CHAR LIMIT=45] -->
+ <string name="domain_urls_summary_none">Don\u2019t open supported links</string>
+ <!-- Summary of an app that can open URLs for exactly one domain [CHAR LIMIT=45] -->
+ <string name="domain_urls_summary_one">Open <xliff:g id="domain" example="mail.google.com">%s</xliff:g></string>
+ <!-- Summary of an app that can open several domain's URLs [CHAR LIMIT=45] -->
+ <string name="domain_urls_summary_some">Open <xliff:g id="domain" example="mail.google.com">%s</xliff:g> and other URLs</string>
<!-- Label for screen where user can grant applications special access to various systems. [CHAR_LIMIT=60] -->
<string name="special_access">Special app access</string>
diff --git a/res/xml/manage_domain_urls_fragment.xml b/res/xml/manage_domain_urls_fragment.xml
new file mode 100644
index 0000000..525fdd5
--- /dev/null
+++ b/res/xml/manage_domain_urls_fragment.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 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.
+-->
+
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:settings="http://schemas.android.com/apk/res-auto"
+ android:title="@string/app_launch_domain_links_title">
+ <PreferenceCategory
+ android:key="@string/pk_opening_links_options"
+ android:title="@string/domain_url_section_title"
+ settings:controller="com.android.car.settings.applications.managedomainurls.DomainAppPreferenceController"/>
+</PreferenceScreen>
diff --git a/src/com/android/car/settings/applications/managedomainurls/DomainAppPreferenceController.java b/src/com/android/car/settings/applications/managedomainurls/DomainAppPreferenceController.java
new file mode 100644
index 0000000..6feaede
--- /dev/null
+++ b/src/com/android/car/settings/applications/managedomainurls/DomainAppPreferenceController.java
@@ -0,0 +1,199 @@
+/*
+ * 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.car.settings.applications.managedomainurls;
+
+import android.app.Application;
+import android.car.drivingstate.CarUxRestrictions;
+import android.car.userlib.CarUserManagerHelper;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.IntentFilterVerificationInfo;
+import android.content.pm.PackageManager;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.IconDrawableFactory;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.Lifecycle;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceGroup;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+import com.android.settingslib.applications.ApplicationsState;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Business logic to populate the list of apps that deal with domain urls. */
+public class DomainAppPreferenceController extends PreferenceController<PreferenceGroup> {
+
+ private final ApplicationsState mApplicationsState;
+ private final PackageManager mPm;
+ private final CarUserManagerHelper mCarUserManagerHelper;
+
+ @VisibleForTesting
+ final ApplicationsState.Callbacks mApplicationStateCallbacks =
+ new ApplicationsState.Callbacks() {
+ @Override
+ public void onRunningStateChanged(boolean running) {
+ }
+
+ @Override
+ public void onPackageListChanged() {
+ }
+
+ @Override
+ public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
+ rebuildAppList(apps);
+ }
+
+ @Override
+ public void onPackageIconChanged() {
+ }
+
+ @Override
+ public void onPackageSizeChanged(String packageName) {
+ }
+
+ @Override
+ public void onAllSizesComputed() {
+ }
+
+ @Override
+ public void onLauncherInfoChanged() {
+ }
+
+ @Override
+ public void onLoadEntriesCompleted() {
+ mSession.rebuild(ApplicationsState.FILTER_WITH_DOMAIN_URLS,
+ ApplicationsState.ALPHA_COMPARATOR);
+ }
+ };
+
+ private ApplicationsState.Session mSession;
+ private ArrayMap<String, Preference> mPreferenceCache;
+
+ public DomainAppPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ mApplicationsState = ApplicationsState.getInstance(
+ (Application) context.getApplicationContext());
+ mPm = context.getPackageManager();
+ mCarUserManagerHelper = new CarUserManagerHelper(context);
+ }
+
+ @Override
+ protected Class<PreferenceGroup> getPreferenceType() {
+ return PreferenceGroup.class;
+ }
+
+ @Override
+ protected void checkInitialized() {
+ if (mSession == null) {
+ throw new IllegalStateException("session should be non null by this point");
+ }
+ }
+
+ /** Sets the lifecycle to create a new session. */
+ public void setLifecycle(Lifecycle lifecycle) {
+ mSession = mApplicationsState.newSession(mApplicationStateCallbacks, lifecycle);
+ }
+
+ @Override
+ protected void onStartInternal() {
+ // Resume the session earlier than the lifecycle so that cached information is updated
+ // even if settings is not resumed (for example in multi-display).
+ mSession.onResume();
+ }
+
+ @Override
+ protected void onStopInternal() {
+ // Since we resume early in onStart, make sure we clean up even if we don't receive onPause.
+ mSession.onPause();
+ }
+
+ private void rebuildAppList(ArrayList<ApplicationsState.AppEntry> apps) {
+ PreferenceGroup preferenceGroup = getPreference();
+ preferenceGroup.removeAll();
+ for (int i = 0; i < apps.size(); i++) {
+ ApplicationsState.AppEntry entry = apps.get(i);
+ preferenceGroup.addPreference(createPreference(entry));
+ }
+ }
+
+ private Preference createPreference(ApplicationsState.AppEntry entry) {
+ String key = entry.info.packageName + "|" + entry.info.uid;
+ IconDrawableFactory iconDrawableFactory = IconDrawableFactory.newInstance(getContext());
+ Preference preference = new Preference(getContext());
+ preference.setKey(key);
+ preference.setTitle(entry.label);
+ preference.setSummary(getDomainsSummary(entry.info.packageName));
+ preference.setIcon(iconDrawableFactory.getBadgedIcon(entry.info));
+ preference.setOnPreferenceClickListener(pref -> {
+ // TODO: Create AppLaunchSettings.
+ return true;
+ });
+ return preference;
+ }
+
+ private CharSequence getDomainsSummary(String packageName) {
+ // If the user has explicitly said "no" for this package, that's the
+ // string we should show.
+ int domainStatus = mPm.getIntentVerificationStatusAsUser(packageName,
+ mCarUserManagerHelper.getCurrentProcessUserId());
+ if (domainStatus == PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER) {
+ return getContext().getText(R.string.domain_urls_summary_none);
+ }
+ // Otherwise, ask package manager for the domains for this package,
+ // and show the first one (or none if there aren't any).
+ ArraySet<String> result = getHandledDomains(mPm, packageName);
+ if (result.isEmpty()) {
+ return getContext().getText(R.string.domain_urls_summary_none);
+ } else if (result.size() == 1) {
+ return getContext().getString(R.string.domain_urls_summary_one, result.valueAt(0));
+ } else {
+ return getContext().getString(R.string.domain_urls_summary_some, result.valueAt(0));
+ }
+ }
+
+ private ArraySet<String> getHandledDomains(PackageManager pm, String packageName) {
+ List<IntentFilterVerificationInfo> iviList = pm.getIntentFilterVerifications(packageName);
+ List<IntentFilter> filters = pm.getAllIntentFilters(packageName);
+
+ ArraySet<String> result = new ArraySet<>();
+ if (iviList != null && iviList.size() > 0) {
+ for (IntentFilterVerificationInfo ivi : iviList) {
+ for (String host : ivi.getDomains()) {
+ result.add(host);
+ }
+ }
+ }
+ if (filters != null && filters.size() > 0) {
+ for (IntentFilter filter : filters) {
+ if (filter.hasCategory(Intent.CATEGORY_BROWSABLE)
+ && (filter.hasDataScheme(IntentFilter.SCHEME_HTTP)
+ || filter.hasDataScheme(IntentFilter.SCHEME_HTTPS))) {
+ result.addAll(filter.getHostsList());
+ }
+ }
+ }
+ return result;
+ }
+}
diff --git a/src/com/android/car/settings/applications/managedomainurls/ManageDomainUrlsFragment.java b/src/com/android/car/settings/applications/managedomainurls/ManageDomainUrlsFragment.java
new file mode 100644
index 0000000..7aacc16
--- /dev/null
+++ b/src/com/android/car/settings/applications/managedomainurls/ManageDomainUrlsFragment.java
@@ -0,0 +1,42 @@
+/*
+ * 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.car.settings.applications.managedomainurls;
+
+import android.content.Context;
+
+import androidx.annotation.XmlRes;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.SettingsFragment;
+
+/** Fragment which shows a list of applications to manage handled domain urls. */
+public class ManageDomainUrlsFragment extends SettingsFragment {
+
+ @Override
+ @XmlRes
+ protected int getPreferenceScreenResId() {
+ return R.xml.manage_domain_urls_fragment;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+
+ use(DomainAppPreferenceController.class, R.string.pk_opening_links_options).setLifecycle(
+ getLifecycle());
+ }
+}
diff --git a/src/com/android/car/settings/common/FragmentResolver.java b/src/com/android/car/settings/common/FragmentResolver.java
index bf0aa3d..cc24020 100644
--- a/src/com/android/car/settings/common/FragmentResolver.java
+++ b/src/com/android/car/settings/common/FragmentResolver.java
@@ -30,6 +30,7 @@
import com.android.car.settings.applications.ApplicationsSettingsFragment;
import com.android.car.settings.applications.assist.ManageAssistFragment;
import com.android.car.settings.applications.defaultapps.DefaultAutofillPickerFragment;
+import com.android.car.settings.applications.managedomainurls.ManageDomainUrlsFragment;
import com.android.car.settings.bluetooth.BluetoothSettingsFragment;
import com.android.car.settings.datausage.DataUsageFragment;
import com.android.car.settings.datetime.DatetimeSettingsFragment;
@@ -141,6 +142,9 @@
}
return ApplicationDetailsFragment.getInstance(uri.getSchemeSpecificPart());
+ case Settings.ACTION_MANAGE_DOMAIN_URLS:
+ return new ManageDomainUrlsFragment();
+
case Settings.ACTION_SYNC_SETTINGS:
return new AccountSettingsFragment();
diff --git a/tests/robotests/src/com/android/car/settings/applications/managedomainurls/DomainAppPreferenceControllerTest.java b/tests/robotests/src/com/android/car/settings/applications/managedomainurls/DomainAppPreferenceControllerTest.java
new file mode 100644
index 0000000..71e5746
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/applications/managedomainurls/DomainAppPreferenceControllerTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.car.settings.applications.managedomainurls;
+
+import static android.content.pm.UserInfo.FLAG_ADMIN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.car.userlib.CarUserManagerHelper;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.os.UserManager;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.PreferenceGroup;
+
+import com.android.car.settings.CarSettingsRobolectricTestRunner;
+import com.android.car.settings.common.LogicalPreferenceGroup;
+import com.android.car.settings.common.PreferenceControllerTestHelper;
+import com.android.car.settings.testutils.ShadowApplicationsState;
+import com.android.car.settings.testutils.ShadowCarUserManagerHelper;
+import com.android.car.settings.testutils.ShadowIconDrawableFactory;
+import com.android.car.settings.testutils.ShadowUserManager;
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+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.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+import java.util.ArrayList;
+
+@RunWith(CarSettingsRobolectricTestRunner.class)
+@Config(shadows = {ShadowUserManager.class, ShadowCarUserManagerHelper.class,
+ ShadowIconDrawableFactory.class, ShadowApplicationsState.class})
+public class DomainAppPreferenceControllerTest {
+
+ private static final int USER_ID = 10;
+ private static final String TEST_PACKAGE_NAME = "com.android.test.package";
+ private static final int TEST_PACKAGE_ID = 1;
+ private static final String TEST_LABEL = "Test App";
+ private static final String TEST_PATH = "TEST_PATH";
+
+ private Context mContext;
+ private PreferenceGroup mPreferenceGroup;
+ private PreferenceControllerTestHelper<DomainAppPreferenceController> mControllerHelper;
+ private DomainAppPreferenceController mController;
+ private Lifecycle mLifecycle;
+ @Mock
+ private CarUserManagerHelper mCarUserManagerHelper;
+ @Mock
+ private ApplicationsState mApplicationsState;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ ShadowApplicationsState.setInstance(mApplicationsState);
+ ShadowCarUserManagerHelper.setMockInstance(mCarUserManagerHelper);
+ when(mCarUserManagerHelper.getCurrentProcessUserId()).thenReturn(USER_ID);
+
+ mContext = RuntimeEnvironment.application;
+ getShadowUserManager().addProfile(USER_ID, USER_ID, "Test Name", /* profileFlags= */
+ FLAG_ADMIN);
+
+ when(mApplicationsState.newSession(any(), any())).thenReturn(
+ mock(ApplicationsState.Session.class));
+
+ mPreferenceGroup = new LogicalPreferenceGroup(mContext);
+ mControllerHelper = new PreferenceControllerTestHelper<>(mContext,
+ DomainAppPreferenceController.class);
+ mController = mControllerHelper.getController();
+
+ LifecycleOwner lifecycleOwner = () -> mLifecycle;
+ mLifecycle = new Lifecycle(lifecycleOwner);
+ mController.setLifecycle(mLifecycle);
+
+ mControllerHelper.setPreference(mPreferenceGroup);
+ }
+
+ @After
+ public void tearDown() {
+ ShadowApplicationsState.reset();
+ ShadowCarUserManagerHelper.reset();
+ ShadowUserManager.reset();
+ }
+
+ @Test
+ public void checkInitialized_noLifecycle_throwsError() {
+ mControllerHelper = new PreferenceControllerTestHelper<>(mContext,
+ DomainAppPreferenceController.class);
+
+ assertThrows(IllegalStateException.class,
+ () -> mControllerHelper.setPreference(mPreferenceGroup));
+ }
+
+ @Test
+ public void onRebuildComplete_sessionLoadsValues_preferenceGroupHasValues() {
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+ ArrayList<ApplicationsState.AppEntry> apps = new ArrayList<>();
+ ApplicationInfo info = new ApplicationInfo();
+ info.packageName = TEST_PACKAGE_NAME;
+ info.uid = TEST_PACKAGE_ID;
+ info.sourceDir = TEST_PATH;
+ ApplicationsState.AppEntry entry = new ApplicationsState.AppEntry(mContext, info,
+ TEST_PACKAGE_ID);
+ entry.label = TEST_LABEL;
+ apps.add(entry);
+ mController.mApplicationStateCallbacks.onRebuildComplete(apps);
+
+ assertThat(mPreferenceGroup.getPreferenceCount()).isEqualTo(1);
+ }
+
+ private ShadowUserManager getShadowUserManager() {
+ return Shadow.extract(UserManager.get(mContext));
+ }
+}
diff --git a/tests/robotests/src/com/android/car/settings/testutils/ShadowApplicationsState.java b/tests/robotests/src/com/android/car/settings/testutils/ShadowApplicationsState.java
new file mode 100644
index 0000000..383a245
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/testutils/ShadowApplicationsState.java
@@ -0,0 +1,45 @@
+/*
+ * 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.car.settings.testutils;
+
+import android.app.Application;
+
+import com.android.settingslib.applications.ApplicationsState;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
+
+@Implements(ApplicationsState.class)
+public class ShadowApplicationsState {
+
+ private static ApplicationsState sApplicationsState;
+
+ public static void setInstance(ApplicationsState applicationsState) {
+ sApplicationsState = applicationsState;
+ }
+
+ @Resetter
+ public static void reset() {
+ sApplicationsState = null;
+ }
+
+ @Implementation
+ protected static ApplicationsState getInstance(Application app) {
+ return sApplicationsState;
+ }
+}