Add request manage credentials to Settings

Background
* This is part of the work to support
  a credential management app on
  unmanaged devices.

Changes
* Add new activity to Settings to display
  a screen to the user requesting whether
  the calling app can manage their
  KeyChain credentials.
* Display the authentication policy

Manual Testing
* Verify screen is not displayed if intent
  action is not android.security.MANAGE_CREDENTIALS
* Verify screen is not displayed if authentication
  policy is not valid
* Verify button panel is visible if all items in the
  authentication policy are displayed
* Verify button panel is not visible if not all items
  in the authentication policy are displayed. Verify
  that scrolling to the bottom of the item list, the
  button panel becomes visible.

Bug: 165641221
Test: Manual testing
      make RunSettingsRoboTests -j ROBOTEST_FILTER=com.android.settings.security.RequestManageCredentialsTest
Change-Id: Ie23b226f1a285b3de6ec3e91b8880d9144bb24a3
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index c9f4643..e917b5c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1431,6 +1431,15 @@
                   android:exported="false">
         </activity>
 
+        <activity android:name=".security.RequestManageCredentials"
+                  android:theme="@style/Theme.RequestManageCredentials"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.security.MANAGE_CREDENTIALS"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+
         <activity
             android:name="Settings$DeviceAdminSettingsActivity"
             android:exported="true"
diff --git a/res/layout/app_authentication_item.xml b/res/layout/app_authentication_item.xml
new file mode 100644
index 0000000..2df923f
--- /dev/null
+++ b/res/layout/app_authentication_item.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 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.
+  -->
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/AppAuthenticationPolicyItem"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <ImageView
+        android:id="@+id/app_icon"
+        style="@style/AppAuthenticationPolicyIcon"
+        android:layout_width="24dp"
+        android:layout_height="24dp"/>
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:layout_toRightOf="@id/app_icon"
+        android:layout_marginTop="16dp"
+        android:layout_marginBottom="16dp"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/app_name"
+            style="@style/AppAuthenticationPolicyText"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/uris"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
+
+    </LinearLayout>
+
+</RelativeLayout>
diff --git a/res/layout/app_authentication_uri_item.xml b/res/layout/app_authentication_uri_item.xml
new file mode 100644
index 0000000..202fb54
--- /dev/null
+++ b/res/layout/app_authentication_uri_item.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 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.
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/uri_name"
+        style="@style/AppUriAuthenticationPolicyText"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+
+</LinearLayout>
diff --git a/res/layout/request_manage_credentials.xml b/res/layout/request_manage_credentials.xml
new file mode 100644
index 0000000..eb4c9e8
--- /dev/null
+++ b/res/layout/request_manage_credentials.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 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.
+  -->
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/apps_list"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
+
+        <LinearLayout
+            android:id="@+id/button_panel"
+            style="@style/RequestManageCredentialsButtonPanel"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <Button
+                android:id="@+id/dont_allow_button"
+                style="@style/RequestManageCredentialsDontAllowButton"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/request_manage_credentials_dont_allow"/>
+
+            <Space
+                android:layout_width="0dp"
+                android:layout_height="0dp"
+                android:layout_weight="1"
+                android:visibility="invisible"/>
+
+            <Button
+                android:id="@+id/allow_button"
+                style="@style/RequestManageCredentialsAllowButton"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/request_manage_credentials_allow"/>
+
+        </LinearLayout>
+
+    </RelativeLayout>
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/res/layout/request_manage_credentials_header.xml b/res/layout/request_manage_credentials_header.xml
new file mode 100644
index 0000000..b22c6c9
--- /dev/null
+++ b/res/layout/request_manage_credentials_header.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 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.
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/RequestManageCredentialsHeader"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <ImageView
+        android:id="@+id/credential_management_app_icon"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:contentDescription="@null"/>
+
+    <TextView
+        android:id="@+id/credential_management_app_title"
+        style="@style/RequestManageCredentialsTitle"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+
+    <TextView
+        android:id="@+id/credential_management_app_description"
+        style="@style/RequestManageCredentialsDescription"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/request_manage_credentials_description"/>
+
+</LinearLayout>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5ae01b9..041dbdd 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -6325,6 +6325,17 @@
     <!-- Toast message that a certificate was not installed -->
     <string name="cert_not_installed">Certificate not installed</string>
 
+    <!-- Title of screen shown to the user when an app requests to manage the user's KeyChain credentials [CHAR LIMIT=NONE] -->
+    <string name="request_manage_credentials_title">Allow <xliff:g id="app_name" example="Ping">%s</xliff:g> to install certificates on this device?</string>
+    <!-- Description of screen shown to the user when an app requests to manage the user's KeyChain credentials [CHAR LIMIT=NONE] -->
+    <string name="request_manage_credentials_description">These certificates will identify you to the apps and URLs below</string>
+    <!-- Label for button to not allow an app to manage the user's KeyChain credentials [CHAR_LIMIT=50] -->
+    <string name="request_manage_credentials_dont_allow">Don\u2019t allow</string>
+    <!-- Label for button to allow an app to manage the user's KeyChain credentials [CHAR_LIMIT=50] -->
+    <string name="request_manage_credentials_allow">Allow</string>
+    <!-- Label for floating action button to scroll to the end of the authentication policy list [CHAR LIMIT=30] -->
+    <string name="request_manage_credentials_more">Show more</string>
+
     <!-- Sound settings screen, setting check box label -->
     <string name="emergency_tone_title">Emergency dialing signal</string>
     <!-- Sound settings screen, setting option summary text -->
diff --git a/res/values/styles.xml b/res/values/styles.xml
index dc69ce4..c447dec 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -781,4 +781,69 @@
            parent="@*android:style/TextAppearance.DeviceDefault.Subhead">
         <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
     </style>
+
+    <style name="RequestManageCredentialsButtonPanel">
+        <item name="android:paddingStart">12dp</item>
+        <item name="android:paddingEnd">12dp</item>
+        <item name="android:paddingTop">8dp</item>
+        <item name="android:paddingBottom">8dp</item>
+        <item name="android:orientation">horizontal</item>
+        <item name="android:layout_alignParentBottom">true</item>
+    </style>
+
+    <style name="RequestManageCredentialsAllowButton" parent="@style/ActionPrimaryButton">
+        <item name="android:fontFamily">google-sans-medium</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:textAllCaps">false</item>
+    </style>
+
+    <style name="RequestManageCredentialsDontAllowButton"
+           parent="@style/Widget.AppCompat.Button.Borderless">
+        <item name="android:fontFamily">google-sans-medium</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:textAllCaps">false</item>
+        <item name="android:textColor">?android:attr/colorAccent</item>
+    </style>
+
+    <style name="RequestManageCredentialsHeader">
+        <item name="android:paddingStart">40dp</item>
+        <item name="android:paddingEnd">24dp</item>
+        <item name="android:paddingTop">58dp</item>
+        <item name="android:paddingBottom">24dp</item>
+        <item name="android:orientation">vertical</item>
+    </style>
+
+    <style name="RequestManageCredentialsTitle">
+        <item name="android:layout_marginTop">24dp</item>
+        <item name="android:textSize">36sp</item>
+        <item name="android:textColor">#202124</item>
+    </style>
+
+    <style name="RequestManageCredentialsDescription">
+        <item name="android:layout_marginTop">24dp</item>
+        <item name="android:textSize">18sp</item>
+        <item name="android:textColor">#202124</item>
+    </style>
+
+    <style name="AppAuthenticationPolicyItem">
+        <item name="android:paddingStart">40dp</item>
+        <item name="android:paddingEnd">24dp</item>
+    </style>
+
+    <style name="AppAuthenticationPolicyIcon">
+        <item name="android:layout_marginTop">30dp</item>
+        <item name="android:layout_marginEnd">20dp</item>
+    </style>
+
+    <style name="AppAuthenticationPolicyText">
+        <item name="android:maxLines">1</item>
+        <item name="android:textSize">20sp</item>
+        <item name="android:textColor">#202124</item>
+    </style>
+
+    <style name="AppUriAuthenticationPolicyText">
+        <item name="android:maxLines">1</item>
+        <item name="android:textSize">16sp</item>
+        <item name="android:textColor">#5F6368</item>
+    </style>
 </resources>
diff --git a/res/values/themes.xml b/res/values/themes.xml
index 172a89a..01ea103 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -165,6 +165,21 @@
         <item name="android:colorAccent">@*android:color/white</item>
     </style>
 
+    <style name="Theme.RequestManageCredentials" parent="@style/Theme.MaterialComponents.Light">
+        <item name="windowActionBar">false</item>
+        <item name="windowNoTitle">true</item>
+
+        <item name="colorPrimary">@*android:color/primary_device_default_settings_light</item>
+        <item name="colorAccent">@*android:color/accent_device_default_light</item>
+        <item name="colorPrimaryDark">@*android:color/primary_dark_device_default_settings_light</item>
+
+        <item name="android:windowBackground">@android:color/white</item>
+        <item name="android:statusBarColor">@android:color/white</item>
+        <item name="android:windowLightStatusBar">true</item>
+        <item name="android:windowLightNavigationBar">true</item>
+        <item name="android:navigationBarColor">@android:color/white</item>
+    </style>
+
     <style name="FallbackHome" parent="@android:style/Theme.DeviceDefault.NoActionBar">
         <item name="android:windowBackground">@android:color/transparent</item>
         <item name="android:colorBackgroundCacheHint">@null</item>
diff --git a/src/com/android/settings/security/CredentialManagementAppAdapter.java b/src/com/android/settings/security/CredentialManagementAppAdapter.java
new file mode 100644
index 0000000..707f598
--- /dev/null
+++ b/src/com/android/settings/security/CredentialManagementAppAdapter.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2020 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.security;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.security.AppUriAuthenticationPolicy;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settings.R;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Adapter for the requesting credential management app. This adapter displays the details of the
+ * requesting app, including its authentication policy, when {@link RequestManageCredentials}
+ * is started.
+ * <p>
+ *
+ * @hide
+ * @see RequestManageCredentials
+ * @see AppUriAuthenticationPolicy
+ */
+public class CredentialManagementAppAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+
+    private static final int HEADER_VIEW = 1;
+
+    private final String mCredentialManagerPackage;
+    private final Map<String, Map<Uri, String>> mAppUriAuthentication;
+    private final List<String> mSortedAppNames;
+
+    private final Context mContext;
+    private final PackageManager mPackageManager;
+    private final RecyclerView.RecycledViewPool mViewPool;
+
+    /**
+     * View holder for the header in the request manage credentials screen.
+     */
+    public class HeaderViewHolder extends RecyclerView.ViewHolder {
+        private final ImageView mAppIconView;
+        private final TextView mTitleView;
+
+        public HeaderViewHolder(View view) {
+            super(view);
+            mAppIconView = view.findViewById(R.id.credential_management_app_icon);
+            mTitleView = view.findViewById(R.id.credential_management_app_title);
+        }
+
+        /**
+         * Bind the header view and add details on the requesting app's icon and name.
+         */
+        public void bindView() {
+            try {
+                ApplicationInfo applicationInfo =
+                        mPackageManager.getApplicationInfo(mCredentialManagerPackage, 0);
+                mAppIconView.setImageDrawable(mPackageManager.getApplicationIcon(applicationInfo));
+                mTitleView.setText(mContext.getString(R.string.request_manage_credentials_title,
+                        applicationInfo.loadLabel(mPackageManager)));
+            } catch (PackageManager.NameNotFoundException e) {
+                mAppIconView.setImageDrawable(null);
+                mTitleView.setText(mContext.getString(R.string.request_manage_credentials_title,
+                        mCredentialManagerPackage));
+            }
+        }
+    }
+
+    /**
+     * View holder for the authentication policy in the request manage credentials screen.
+     */
+    public class AppAuthenticationViewHolder extends RecyclerView.ViewHolder {
+        private final ImageView mAppIconView;
+        private final TextView mAppNameView;
+        RecyclerView mChildRecyclerView;
+
+        public AppAuthenticationViewHolder(View view) {
+            super(view);
+            mAppIconView = view.findViewById(R.id.app_icon);
+            mAppNameView = view.findViewById(R.id.app_name);
+            mChildRecyclerView = view.findViewById(R.id.uris);
+        }
+
+        /**
+         * Bind the app's authentication policy view at the given position. Add details on the
+         * app's icon, name and list of URIs.
+         */
+        public void bindView(int position) {
+            final String appName = mSortedAppNames.get(position);
+            try {
+                ApplicationInfo applicationInfo = mPackageManager.getApplicationInfo(appName, 0);
+                mAppIconView.setImageDrawable(mPackageManager.getApplicationIcon(applicationInfo));
+                mAppNameView.setText(String.valueOf(applicationInfo.loadLabel(mPackageManager)));
+            } catch (PackageManager.NameNotFoundException e) {
+                mAppIconView.setImageDrawable(null);
+                mAppNameView.setText(appName);
+            }
+            bindChildView(mAppUriAuthentication.get(appName));
+        }
+
+        /**
+         * Bind the list of URIs for an app.
+         */
+        public void bindChildView(Map<Uri, String> urisToAliases) {
+            LinearLayoutManager layoutManager = new LinearLayoutManager(
+                    mChildRecyclerView.getContext(), RecyclerView.VERTICAL, false);
+            layoutManager.setInitialPrefetchItemCount(urisToAliases.size());
+            UriAuthenticationPolicyAdapter childItemAdapter =
+                    new UriAuthenticationPolicyAdapter(new ArrayList<>(urisToAliases.keySet()));
+            mChildRecyclerView.setLayoutManager(layoutManager);
+            mChildRecyclerView.setAdapter(childItemAdapter);
+            mChildRecyclerView.setRecycledViewPool(mViewPool);
+        }
+    }
+
+    public CredentialManagementAppAdapter(Context context, String credentialManagerPackage,
+            Map<String, Map<Uri, String>> appUriAuthentication) {
+        mContext = context;
+        mCredentialManagerPackage = credentialManagerPackage;
+        mPackageManager = context.getPackageManager();
+        mAppUriAuthentication = appUriAuthentication;
+        mSortedAppNames = sortPackageNames(mAppUriAuthentication);
+        mViewPool = new RecyclerView.RecycledViewPool();
+    }
+
+    /**
+     * Sort package names in the following order:
+     * - installed apps
+     * - alphabetically
+     */
+    private List<String> sortPackageNames(Map<String, Map<Uri, String>> authenticationPolicy) {
+        List<String> packageNames = new ArrayList<>(authenticationPolicy.keySet());
+        packageNames.sort((firstPackageName, secondPackageName) -> {
+            boolean isFirstPackageInstalled = isPackageInstalled(firstPackageName);
+            boolean isSecondPackageInstalled = isPackageInstalled(secondPackageName);
+            if (isFirstPackageInstalled == isSecondPackageInstalled) {
+                return firstPackageName.compareTo(secondPackageName);
+            } else if (isFirstPackageInstalled) {
+                return -1;
+            } else {
+                return 1;
+            }
+        });
+        return packageNames;
+    }
+
+    private boolean isPackageInstalled(String packageName) {
+        try {
+            mPackageManager.getPackageInfo(packageName, 0);
+            return true;
+        } catch (PackageManager.NameNotFoundException e) {
+            return false;
+        }
+    }
+
+    @NonNull
+    @Override
+    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
+        View view;
+        if (viewType == HEADER_VIEW) {
+            view = LayoutInflater.from(viewGroup.getContext())
+                    .inflate(R.layout.request_manage_credentials_header, viewGroup, false);
+            view.setEnabled(false);
+            return new HeaderViewHolder(view);
+        } else {
+            view = LayoutInflater.from(viewGroup.getContext())
+                    .inflate(R.layout.app_authentication_item, viewGroup, false);
+            return new AppAuthenticationViewHolder(view);
+        }
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
+        if (viewHolder instanceof HeaderViewHolder) {
+            ((HeaderViewHolder) viewHolder).bindView();
+        } else if (viewHolder instanceof AppAuthenticationViewHolder) {
+            ((AppAuthenticationViewHolder) viewHolder).bindView(i - 1);
+        }
+    }
+
+    @Override
+    public int getItemCount() {
+        // Add an extra view to show the header view
+        return mAppUriAuthentication.size() + 1;
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        if (position == 0) {
+            return HEADER_VIEW;
+        }
+        return super.getItemViewType(position);
+    }
+
+}
diff --git a/src/com/android/settings/security/RequestManageCredentials.java b/src/com/android/settings/security/RequestManageCredentials.java
new file mode 100644
index 0000000..9d2d51e
--- /dev/null
+++ b/src/com/android/settings/security/RequestManageCredentials.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2020 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.security;
+
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.admin.DevicePolicyManager;
+import android.os.Bundle;
+import android.security.AppUriAuthenticationPolicy;
+import android.security.Credentials;
+import android.security.KeyChain;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settings.R;
+
+/**
+ * Displays a full screen to the user asking whether the calling app can manage the user's
+ * KeyChain credentials. This screen includes the authentication policy highlighting what apps and
+ * URLs the calling app can authenticate the user to.
+ * <p>
+ * Users can allow or deny the calling app. If denied, the calling app may re-request this
+ * capability. If allowed, the calling app will become the credential management app and will be
+ * able to manage the user's KeyChain credentials. The following APIs can be called to manage
+ * KeyChain credentials:
+ * {@link DevicePolicyManager#installKeyPair}
+ * {@link DevicePolicyManager#removeKeyPair}
+ * {@link DevicePolicyManager#generateKeyPair}
+ * {@link DevicePolicyManager#setKeyPairCertificate}
+ * <p>
+ *
+ * @see AppUriAuthenticationPolicy
+ */
+public class RequestManageCredentials extends Activity {
+
+    private static final String TAG = "ManageCredentials";
+
+    private String mCredentialManagerPackage;
+    private AppUriAuthenticationPolicy mAuthenticationPolicy;
+
+    private RecyclerView mRecyclerView;
+    private LinearLayoutManager mLayoutManager;
+    private LinearLayout mButtonPanel;
+
+    private boolean mDisplayingButtonPanel = false;
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        if (Credentials.ACTION_MANAGE_CREDENTIALS.equals(getIntent().getAction())) {
+            setContentView(R.layout.request_manage_credentials);
+            // This is not authenticated, as any app can ask to be the credential management app.
+            mCredentialManagerPackage = getReferrer().getHost();
+            mAuthenticationPolicy =
+                    getIntent().getParcelableExtra(KeyChain.EXTRA_AUTHENTICATION_POLICY);
+            enforceValidAuthenticationPolicy(mAuthenticationPolicy);
+
+            loadRecyclerView();
+            loadButtons();
+            addOnScrollListener();
+        } else {
+            Log.e(TAG, "Unable to start activity because intent action is not "
+                    + Credentials.ACTION_MANAGE_CREDENTIALS);
+            finish();
+        }
+    }
+
+    private void loadRecyclerView() {
+        mLayoutManager = new LinearLayoutManager(this);
+        mRecyclerView = findViewById(R.id.apps_list);
+        mRecyclerView.setLayoutManager(mLayoutManager);
+
+        CredentialManagementAppAdapter recyclerViewAdapter = new CredentialManagementAppAdapter(
+                this, mCredentialManagerPackage, mAuthenticationPolicy.getAppAndUriMappings());
+        mRecyclerView.setAdapter(recyclerViewAdapter);
+    }
+
+    private void loadButtons() {
+        mButtonPanel = findViewById(R.id.button_panel);
+        Button dontAllowButton = findViewById(R.id.dont_allow_button);
+        Button allowButton = findViewById(R.id.allow_button);
+
+        dontAllowButton.setOnClickListener(finishRequestManageCredentials());
+        allowButton.setOnClickListener(setCredentialManagementApp());
+    }
+
+    private View.OnClickListener finishRequestManageCredentials() {
+        return v -> {
+            Toast.makeText(this, R.string.request_manage_credentials_dont_allow,
+                    Toast.LENGTH_SHORT).show();
+            setResult(RESULT_CANCELED);
+            finish();
+        };
+    }
+
+    private View.OnClickListener setCredentialManagementApp() {
+        return v -> {
+            // TODO: Implement allow logic
+            Toast.makeText(this, R.string.request_manage_credentials_allow,
+                    Toast.LENGTH_SHORT).show();
+            finish();
+        };
+    }
+
+    private void addOnScrollListener() {
+        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+            @Override
+            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+                super.onScrolled(recyclerView, dx, dy);
+                if (!mDisplayingButtonPanel) {
+                    if (isRecyclerScrollable()) {
+                        hideButtonPanel();
+                    } else {
+                        showButtonPanel();
+                    }
+                }
+            }
+        });
+    }
+
+    private void showButtonPanel() {
+        // Add padding to remove overlap between recycler view and button panel.
+        int padding_in_px = (int) (60 * getResources().getDisplayMetrics().density + 0.5f);
+        mRecyclerView.setPadding(0, 0, 0, padding_in_px);
+        mButtonPanel.setVisibility(View.VISIBLE);
+        mDisplayingButtonPanel = true;
+    }
+
+    private void hideButtonPanel() {
+        mRecyclerView.setPadding(0, 0, 0, 0);
+        mButtonPanel.setVisibility(View.GONE);
+    }
+
+    private boolean isRecyclerScrollable() {
+        if (mLayoutManager == null || mRecyclerView.getAdapter() == null) {
+            return false;
+        }
+        return mLayoutManager.findLastCompletelyVisibleItemPosition()
+                < mRecyclerView.getAdapter().getItemCount() - 1;
+    }
+
+    private void enforceValidAuthenticationPolicy(AppUriAuthenticationPolicy policy) {
+        // TODO: Check whether any of the aliases in the policy already exist
+        if (policy == null || policy.getAppAndUriMappings().isEmpty()) {
+            Log.e(TAG, "Invalid authentication policy");
+            setResult(RESULT_CANCELED);
+            finish();
+        }
+    }
+}
diff --git a/src/com/android/settings/security/UriAuthenticationPolicyAdapter.java b/src/com/android/settings/security/UriAuthenticationPolicyAdapter.java
new file mode 100644
index 0000000..8aeb074
--- /dev/null
+++ b/src/com/android/settings/security/UriAuthenticationPolicyAdapter.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2020 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.security;
+
+import android.net.Uri;
+import android.security.AppUriAuthenticationPolicy;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settings.R;
+
+import java.util.List;
+
+/**
+ * Child adapter for the requesting credential management app. This adapter displays the list of
+ * URIs for each app in the requesting app's authentication policy, when
+ * {@link RequestManageCredentials} is started.
+ *
+ * @hide
+ * @see CredentialManagementAppAdapter
+ * @see RequestManageCredentials
+ * @see AppUriAuthenticationPolicy
+ */
+public class UriAuthenticationPolicyAdapter extends
+        RecyclerView.Adapter<UriAuthenticationPolicyAdapter.UriViewHolder> {
+
+    private final List<Uri> mUris;
+
+    /**
+     * View holder for each URI which is part of the authentication policy in the
+     * request manage credentials screen.
+     */
+    public class UriViewHolder extends RecyclerView.ViewHolder {
+        TextView mUriNameView;
+
+        public UriViewHolder(@NonNull View view) {
+            super(view);
+            mUriNameView = itemView.findViewById(R.id.uri_name);
+        }
+    }
+
+    UriAuthenticationPolicyAdapter(List<Uri> uris) {
+        this.mUris = uris;
+    }
+
+    @Override
+    public UriAuthenticationPolicyAdapter.UriViewHolder onCreateViewHolder(ViewGroup parent,
+            int viewType) {
+        View view = LayoutInflater.from(parent.getContext()).inflate(
+                R.layout.app_authentication_uri_item, parent, false);
+        return new UriViewHolder(view);
+    }
+
+    @Override
+    public void onBindViewHolder(UriAuthenticationPolicyAdapter.UriViewHolder holder,
+            int position) {
+        Uri uri = mUris.get(position);
+        holder.mUriNameView.setText(uri.toString());
+    }
+
+    @Override
+    public int getItemCount() {
+        return mUris.size();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/security/RequestManageCredentialsTest.java b/tests/robotests/src/com/android/settings/security/RequestManageCredentialsTest.java
new file mode 100644
index 0000000..ccc6a0b
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/security/RequestManageCredentialsTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2020 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.security;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.security.AppUriAuthenticationPolicy;
+import android.security.Credentials;
+import android.security.KeyChain;
+import android.widget.Button;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settings.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.shadows.ShadowActivity;
+
+@RunWith(RobolectricTestRunner.class)
+public class RequestManageCredentialsTest {
+
+    private static final AppUriAuthenticationPolicy AUTHENTICATION_POLICY =
+            new AppUriAuthenticationPolicy.Builder()
+                    .addAppAndUriMapping("com.android.test", Uri.parse("test.com"), "testAlias")
+                    .build();
+
+    private RequestManageCredentials mActivity;
+
+    private ShadowActivity mShadowActivity;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void onCreate_intentActionNotManageCredentials_finishActivity() {
+        final Intent intent = new Intent("android.security.ANOTHER_ACTION");
+
+        initActivity(intent);
+
+        assertThat(mActivity).isNotNull();
+        assertThat(mActivity.isFinishing()).isTrue();
+    }
+
+    @Test
+    public void onCreate_authenticationPolicyProvided_startActivity() {
+        final Intent intent = new Intent(Credentials.ACTION_MANAGE_CREDENTIALS);
+        intent.putExtra(KeyChain.EXTRA_AUTHENTICATION_POLICY, AUTHENTICATION_POLICY);
+
+        initActivity(intent);
+
+        assertThat(mActivity).isNotNull();
+        assertThat(mActivity.isFinishing()).isFalse();
+        assertThat((RecyclerView) mActivity.findViewById(R.id.apps_list)).isNotNull();
+        assertThat((LinearLayout) mActivity.findViewById(R.id.button_panel)).isNotNull();
+        assertThat((Button) mActivity.findViewById(R.id.allow_button)).isNotNull();
+        assertThat((Button) mActivity.findViewById(R.id.dont_allow_button)).isNotNull();
+    }
+
+    @Test
+    public void onCreate_dontAllowButtonClicked_finishActivity() {
+        final Intent intent = new Intent(Credentials.ACTION_MANAGE_CREDENTIALS);
+        intent.putExtra(KeyChain.EXTRA_AUTHENTICATION_POLICY, AUTHENTICATION_POLICY);
+
+        initActivity(intent);
+
+        Button dontAllowButton = mActivity.findViewById(R.id.dont_allow_button);
+        assertThat(dontAllowButton.hasOnClickListeners()).isTrue();
+        dontAllowButton.performClick();
+        assertThat(mActivity.isFinishing()).isTrue();
+        assertThat(mShadowActivity.getResultCode()).isEqualTo(Activity.RESULT_CANCELED);
+    }
+
+    private void initActivity(@NonNull Intent intent) {
+        mActivity = Robolectric.buildActivity(RequestManageCredentials.class, intent).setup().get();
+        mShadowActivity = shadowOf(mActivity);
+    }
+
+}