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;
+    }
+}