Merge "DO NOT MERGE Open links settings" into pi-car-dev
diff --git a/res/values/preference_keys.xml b/res/values/preference_keys.xml
index b59df95..5e8e8a8 100644
--- a/res/values/preference_keys.xml
+++ b/res/values/preference_keys.xml
@@ -150,6 +150,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 a341da4..3be4fea 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -416,6 +416,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/default_applications_settings_fragment.xml b/res/xml/default_applications_settings_fragment.xml
index c3681d3..4282fc4 100644
--- a/res/xml/default_applications_settings_fragment.xml
+++ b/res/xml/default_applications_settings_fragment.xml
@@ -25,4 +25,10 @@
android:title="@string/assist_and_voice_input_settings"
settings:controller="com.android.car.settings.applications.assist.ManageAssistEntryPreferenceController"
settings:iconSpaceReserved="true"/>
+ <Preference
+ android:fragment="com.android.car.settings.applications.managedomainurls.ManageDomainUrlsFragment"
+ android:key="@string/pk_opening_links_entry"
+ android:title="@string/app_launch_domain_links_title"
+ settings:controller="com.android.car.settings.common.DefaultRestrictionsPreferenceController"
+ settings:iconSpaceReserved="true"/>
</PreferenceScreen>
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/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..1820638
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/applications/managedomainurls/DomainAppPreferenceControllerTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.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.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})
+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;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ ShadowCarUserManagerHelper.setMockInstance(mCarUserManagerHelper);
+ when(mCarUserManagerHelper.getCurrentProcessUserId()).thenReturn(USER_ID);
+
+ mContext = RuntimeEnvironment.application;
+ getShadowUserManager().addProfile(USER_ID, USER_ID, "Test Name", /* profileFlags= */
+ FLAG_ADMIN);
+
+ 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() {
+ 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));
+ }
+}