Add credential management app preference to Settings

Background
* This is part of the work to support
  a credential management app on
  unmanaged devices.
Changes
* Add a preference controller which will only
  be enabled if there is a credential management app.
* Add a fragment to display details on the credential
  management app and its policy. Add a remove button
  to remove the credential management app.
* Link CredentialManagementAppAdapter to new fragment
  and add expander.

Manual Testing
* Verify preference is disabled if there is no credential
  management app.
* Verify preference summary shows credential mangement
  app name if a credential management app exists.
* Verify fragment details page displays the
  authentication policy correctly.

Bug: 165641221
Test: Manual testing
      make RunSettingsRoboTests -j ROBOTEST_FILTER=com.android.settings.security.CredentialManagementAppFragmentTest
      make RunSettingsRoboTests -j ROBOTEST_FILTER=com.android.settings.security.CredentialManagementAppControllerTest
Change-Id: I06d6b88d0c89022f7a6cbf3031834defcb54bd51
This commit is contained in:
Alex Johnston
2020-11-27 21:35:50 +00:00
parent f7d2ec9e7a
commit 672c2aaaec
19 changed files with 940 additions and 12 deletions

View File

@@ -0,0 +1,25 @@
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M14.1,8H7.83l2.59,-2.59L9,4 4,9l5,5 1.41,-1.41L7.83,10h6.27c2.15,0 3.9,1.57 3.9,3.5S16.25,17 14.1,17H7v2h7.1c3.25,0 5.9,-2.47 5.9,-5.5S17.35,8 14.1,8z"/>
</vector>

View File

@@ -30,6 +30,7 @@
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_toRightOf="@id/app_icon" android:layout_toRightOf="@id/app_icon"
android:layout_toLeftOf="@id/expand"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:orientation="vertical"> android:orientation="vertical">
@@ -40,6 +41,13 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView
android:id="@+id/number_of_uris"
style="@style/AppAuthenticationPolicyNumberOfUrisText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"/>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/uris" android:id="@+id/uris"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -47,4 +55,11 @@
</LinearLayout> </LinearLayout>
<ImageView
android:id="@+id/expand"
style="@style/AppAuthenticationExpander"
android:layout_width="30dp"
android:layout_height="30dp"
android:visibility="gone"/>
</RelativeLayout> </RelativeLayout>

View File

@@ -0,0 +1,31 @@
<!--
~ 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="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="5dip"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp"
android:scrollbars="vertical"/>
</LinearLayout>

View File

@@ -6335,6 +6335,14 @@
<string name="request_manage_credentials_allow">Allow</string> <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] --> <!-- 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> <string name="request_manage_credentials_more">Show more</string>
<!-- Title of preference for the certificate management app [CHAR LIMIT=30] -->
<string name="certificate_management_app">Certificate management app</string>
<!-- Summary if there is no certificate management app [CHAR_LIMIT=NONE] -->
<string name="no_certificate_management_app">None</string>
<!-- Summary of preference if there is a certificate management app [CHAR LIMIT=NONE] -->
<string name="certificate_management_app_description">Certificates installed by this app identify you to the apps and URLs below</string>
<!-- Label for button to remove the credential management app [CHAR LIMIT=30] -->
<string name="remove_credential_management_app">Remove</string>
<!-- Sound settings screen, setting check box label --> <!-- Sound settings screen, setting check box label -->
<string name="emergency_tone_title">Emergency dialing signal</string> <string name="emergency_tone_title">Emergency dialing signal</string>

View File

@@ -816,13 +816,13 @@
<style name="RequestManageCredentialsTitle"> <style name="RequestManageCredentialsTitle">
<item name="android:layout_marginTop">24dp</item> <item name="android:layout_marginTop">24dp</item>
<item name="android:textSize">36sp</item> <item name="android:textSize">36sp</item>
<item name="android:textColor">#202124</item> <item name="android:textColor">?android:attr/textColorPrimary</item>
</style> </style>
<style name="RequestManageCredentialsDescription"> <style name="RequestManageCredentialsDescription">
<item name="android:layout_marginTop">24dp</item> <item name="android:layout_marginTop">24dp</item>
<item name="android:textSize">18sp</item> <item name="android:textSize">18sp</item>
<item name="android:textColor">#202124</item> <item name="android:textColor">?android:attr/textColorPrimary</item>
</style> </style>
<style name="AppAuthenticationPolicyItem"> <style name="AppAuthenticationPolicyItem">
@@ -838,12 +838,25 @@
<style name="AppAuthenticationPolicyText"> <style name="AppAuthenticationPolicyText">
<item name="android:maxLines">1</item> <item name="android:maxLines">1</item>
<item name="android:textSize">20sp</item> <item name="android:textSize">20sp</item>
<item name="android:textColor">#202124</item> <item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
<style name="AppAuthenticationPolicyNumberOfUrisText">
<item name="android:layout_marginTop">4dp</item>
<item name="android:textSize">16sp</item>
<item name="android:textColor">?android:attr/textColorSecondary</item>
<item name="android:alpha">0.7</item>
</style>
<style name="AppAuthenticationExpander">
<item name="android:layout_marginTop">24dp</item>
<item name="android:layout_alignParentEnd">true</item>
<item name="android:scaleType">fitEnd</item>
</style> </style>
<style name="AppUriAuthenticationPolicyText"> <style name="AppUriAuthenticationPolicyText">
<item name="android:maxLines">1</item> <item name="android:maxLines">1</item>
<item name="android:textSize">16sp</item> <item name="android:textSize">16sp</item>
<item name="android:textColor">#5F6368</item> <item name="android:textColor">?android:attr/textColorSecondary</item>
</style> </style>
</resources> </resources>

View File

@@ -0,0 +1,47 @@
<?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.
-->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:title="@string/certificate_management_app">
<!-- Header -->
<com.android.settingslib.widget.LayoutPreference
android:key="header"
android:layout="@layout/settings_entity_header"
android:selectable="false"
android:order="-10000"
settings:allowDividerBelow="true"
settings:controller="com.android.settings.security.CredentialManagementAppHeaderController"/>
<!-- Buttons -->
<com.android.settingslib.widget.ActionButtonsPreference
android:key="buttons"
android:selectable="true"
android:order="-9999"
settings:allowDividerAbove="true"
settings:allowDividerBelow="true"
settings:controller="com.android.settings.security.CredentialManagementAppButtonsController"/>
<!-- Authentication Policy -->
<PreferenceCategory
android:key="authentication_policy"
android:layout="@layout/preference_category_no_label"
android:title="@string/summary_placeholder"
settings:controller="com.android.settings.security.CredentialManagementAppPolicyController"/>
</PreferenceScreen>

View File

@@ -69,6 +69,13 @@
</com.android.settingslib.RestrictedPreference> </com.android.settingslib.RestrictedPreference>
<Preference
android:key="certificate_management_app"
android:title="@string/certificate_management_app"
android:summary="@string/summary_placeholder"
android:fragment="com.android.settings.security.CredentialManagementAppFragment"
settings:controller="com.android.settings.security.CredentialManagementAppPreferenceController"/>
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

View File

@@ -59,6 +59,9 @@ public class CredentialManagementAppAdapter extends RecyclerView.Adapter<Recycle
private final PackageManager mPackageManager; private final PackageManager mPackageManager;
private final RecyclerView.RecycledViewPool mViewPool; private final RecyclerView.RecycledViewPool mViewPool;
private final boolean mIncludeHeader;
private final boolean mIncludeExpander;
/** /**
* View holder for the header in the request manage credentials screen. * View holder for the header in the request manage credentials screen.
*/ */
@@ -96,13 +99,29 @@ public class CredentialManagementAppAdapter extends RecyclerView.Adapter<Recycle
public class AppAuthenticationViewHolder extends RecyclerView.ViewHolder { public class AppAuthenticationViewHolder extends RecyclerView.ViewHolder {
private final ImageView mAppIconView; private final ImageView mAppIconView;
private final TextView mAppNameView; private final TextView mAppNameView;
RecyclerView mChildRecyclerView; private final TextView mNumberOfUrisView;
private final ImageView mExpanderIconView;
private final RecyclerView mChildRecyclerView;
private final List<String> mExpandedApps;
public AppAuthenticationViewHolder(View view) { public AppAuthenticationViewHolder(View view) {
super(view); super(view);
mAppIconView = view.findViewById(R.id.app_icon); mAppIconView = view.findViewById(R.id.app_icon);
mAppNameView = view.findViewById(R.id.app_name); mAppNameView = view.findViewById(R.id.app_name);
mNumberOfUrisView = view.findViewById(R.id.number_of_uris);
mExpanderIconView = view.findViewById(R.id.expand);
mChildRecyclerView = view.findViewById(R.id.uris); mChildRecyclerView = view.findViewById(R.id.uris);
mExpandedApps = new ArrayList<>();
mExpanderIconView.setOnClickListener(view1 -> {
final String appName = mSortedAppNames.get(getBindingAdapterPosition());
if (mExpandedApps.contains(appName)) {
mExpandedApps.remove(appName);
} else {
mExpandedApps.add(appName);
}
bindPolicyView(appName);
});
} }
/** /**
@@ -119,32 +138,63 @@ public class CredentialManagementAppAdapter extends RecyclerView.Adapter<Recycle
mAppIconView.setImageDrawable(null); mAppIconView.setImageDrawable(null);
mAppNameView.setText(appName); mAppNameView.setText(appName);
} }
bindPolicyView(appName);
}
private void bindPolicyView(String appName) {
if (mIncludeExpander) {
mExpanderIconView.setVisibility(View.VISIBLE);
if (mExpandedApps.contains(appName)) {
mNumberOfUrisView.setVisibility(View.GONE);
mExpanderIconView.setImageResource(R.drawable.ic_expand_less);
bindChildView(mAppUriAuthentication.get(appName)); bindChildView(mAppUriAuthentication.get(appName));
} else {
mChildRecyclerView.setVisibility(View.GONE);
mNumberOfUrisView.setVisibility(View.VISIBLE);
mNumberOfUrisView.setText(
getNumberOfUrlsText(mAppUriAuthentication.get(appName)));
mExpanderIconView.setImageResource(
com.android.internal.R.drawable.ic_expand_more);
}
} else {
mNumberOfUrisView.setVisibility(View.GONE);
mExpanderIconView.setVisibility(View.GONE);
bindChildView(mAppUriAuthentication.get(appName));
}
} }
/** /**
* Bind the list of URIs for an app. * Bind the list of URIs for an app.
*/ */
public void bindChildView(Map<Uri, String> urisToAliases) { private void bindChildView(Map<Uri, String> urisToAliases) {
LinearLayoutManager layoutManager = new LinearLayoutManager( LinearLayoutManager layoutManager = new LinearLayoutManager(
mChildRecyclerView.getContext(), RecyclerView.VERTICAL, false); mChildRecyclerView.getContext(), RecyclerView.VERTICAL, false);
layoutManager.setInitialPrefetchItemCount(urisToAliases.size()); layoutManager.setInitialPrefetchItemCount(urisToAliases.size());
UriAuthenticationPolicyAdapter childItemAdapter = UriAuthenticationPolicyAdapter childItemAdapter =
new UriAuthenticationPolicyAdapter(new ArrayList<>(urisToAliases.keySet())); new UriAuthenticationPolicyAdapter(new ArrayList<>(urisToAliases.keySet()));
mChildRecyclerView.setLayoutManager(layoutManager); mChildRecyclerView.setLayoutManager(layoutManager);
mChildRecyclerView.setVisibility(View.VISIBLE);
mChildRecyclerView.setAdapter(childItemAdapter); mChildRecyclerView.setAdapter(childItemAdapter);
mChildRecyclerView.setRecycledViewPool(mViewPool); mChildRecyclerView.setRecycledViewPool(mViewPool);
} }
private String getNumberOfUrlsText(Map<Uri, String> urisToAliases) {
String url = urisToAliases.size() > 1 ? " URLs" : " URL";
return urisToAliases.size() + url;
}
} }
public CredentialManagementAppAdapter(Context context, String credentialManagerPackage, public CredentialManagementAppAdapter(Context context, String credentialManagerPackage,
Map<String, Map<Uri, String>> appUriAuthentication) { Map<String, Map<Uri, String>> appUriAuthentication,
boolean includeHeader, boolean includeExpander) {
mContext = context; mContext = context;
mCredentialManagerPackage = credentialManagerPackage; mCredentialManagerPackage = credentialManagerPackage;
mPackageManager = context.getPackageManager(); mPackageManager = context.getPackageManager();
mAppUriAuthentication = appUriAuthentication; mAppUriAuthentication = appUriAuthentication;
mSortedAppNames = sortPackageNames(mAppUriAuthentication); mSortedAppNames = sortPackageNames(mAppUriAuthentication);
mViewPool = new RecyclerView.RecycledViewPool(); mViewPool = new RecyclerView.RecycledViewPool();
mIncludeHeader = includeHeader;
mIncludeExpander = includeExpander;
} }
/** /**
@@ -198,19 +248,20 @@ public class CredentialManagementAppAdapter extends RecyclerView.Adapter<Recycle
if (viewHolder instanceof HeaderViewHolder) { if (viewHolder instanceof HeaderViewHolder) {
((HeaderViewHolder) viewHolder).bindView(); ((HeaderViewHolder) viewHolder).bindView();
} else if (viewHolder instanceof AppAuthenticationViewHolder) { } else if (viewHolder instanceof AppAuthenticationViewHolder) {
((AppAuthenticationViewHolder) viewHolder).bindView(i - 1); int position = mIncludeHeader ? i - 1 : i;
((AppAuthenticationViewHolder) viewHolder).bindView(position);
} }
} }
@Override @Override
public int getItemCount() { public int getItemCount() {
// Add an extra view to show the header view // Add an extra view to show the header view
return mAppUriAuthentication.size() + 1; return mIncludeHeader ? mAppUriAuthentication.size() + 1 : mAppUriAuthentication.size();
} }
@Override @Override
public int getItemViewType(int position) { public int getItemViewType(int position) {
if (position == 0) { if (mIncludeHeader && position == 0) {
return HEADER_VIEW; return HEADER_VIEW;
} }
return super.getItemViewType(position); return super.getItemViewType(position);

View File

@@ -0,0 +1,109 @@
/*
* 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.app.AppOpsManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.security.IKeyChainService;
import android.security.KeyChain;
import android.util.Log;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.widget.ActionButtonsPreference;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Controller that shows the remove button of the credential management app, which allows the user
* to remove the credential management app and its certificates.
*/
public class CredentialManagementAppButtonsController extends BasePreferenceController {
private static final String TAG = "CredentialManagementApp";
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final PackageManager mPackageManager;
private final AppOpsManager mAppOpsManager;
private boolean mHasCredentialManagerPackage;
private String mCredentialManagerPackageName;
public CredentialManagementAppButtonsController(Context context, String preferenceKey) {
super(context, preferenceKey);
mPackageManager = context.getPackageManager();
mAppOpsManager = context.getSystemService(AppOpsManager.class);
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE_UNSEARCHABLE;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mExecutor.execute(() -> {
try {
IKeyChainService service = KeyChain.bind(mContext).getService();
mHasCredentialManagerPackage = service.hasCredentialManagementApp();
mCredentialManagerPackageName = service.getCredentialManagementAppPackageName();
} catch (InterruptedException | RemoteException e) {
Log.e(TAG, "Unable to display credential management app buttons");
}
mHandler.post(() -> displayButtons(screen));
});
}
private void displayButtons(PreferenceScreen screen) {
if (mHasCredentialManagerPackage) {
((ActionButtonsPreference) screen.findPreference(getPreferenceKey()))
.setButton1Text(R.string.remove_credential_management_app)
.setButton1Icon(R.drawable.ic_undo_24)
.setButton1OnClickListener(view -> removeCredentialManagementApp());
}
}
private void removeCredentialManagementApp() {
try {
ApplicationInfo appInfo = mPackageManager.getApplicationInfo(
mCredentialManagerPackageName, 0);
mAppOpsManager.setMode(AppOpsManager.OP_MANAGE_CREDENTIALS,
appInfo.uid, mCredentialManagerPackageName, AppOpsManager.MODE_DEFAULT);
mExecutor.execute(() -> {
try {
IKeyChainService service = KeyChain.bind(mContext).getService();
service.removeCredentialManagementApp();
} catch (InterruptedException | RemoteException e) {
Log.e(TAG, "Unable to remove the credential management app");
}
});
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Unable to remove the credential management app");
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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.app.settings.SettingsEnums;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settingslib.search.SearchIndexable;
/**
* Settings fragment containing the credential management app. The credential management app has
* the ability to manage the user's credentials on unmanaged devices.
*/
@SearchIndexable
public class CredentialManagementAppFragment extends DashboardFragment {
private static final String TAG = "CredentialManagementApp";
@Override
protected int getPreferenceScreenResId() {
return R.xml.credential_management_app_fragment;
}
@Override
protected String getLogTag() {
return TAG;
}
@Override
public int getMetricsCategory() {
return SettingsEnums.CREDENTIAL_MANAGEMENT_APP;
}
public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new BaseSearchIndexProvider(R.xml.credential_management_app_fragment);
}

View File

@@ -0,0 +1,102 @@
/*
* 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.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.security.IKeyChainService;
import android.security.KeyChain;
import android.util.Log;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.widget.LayoutPreference;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Controller that shows the header of the credential management app, which includes credential
* management app's name, icon and a description.
*/
public class CredentialManagementAppHeaderController extends BasePreferenceController {
private static final String TAG = "CredentialManagementApp";
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
private final Handler mHandler = new Handler(Looper.getMainLooper());
public CredentialManagementAppHeaderController(Context context, String preferenceKey) {
super(context, preferenceKey);
mPackageManager = context.getPackageManager();
}
private final PackageManager mPackageManager;
private boolean mHasCredentialManagerPackage;
private String mCredentialManagerPackageName;
@Override
public int getAvailabilityStatus() {
return AVAILABLE_UNSEARCHABLE;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mExecutor.execute(() -> {
try {
IKeyChainService service = KeyChain.bind(mContext).getService();
mHasCredentialManagerPackage = service.hasCredentialManagementApp();
mCredentialManagerPackageName = service.getCredentialManagementAppPackageName();
} catch (InterruptedException | RemoteException e) {
Log.e(TAG, "Unable to display credential management app header");
}
mHandler.post(() -> displayHeader(screen));
});
}
private void displayHeader(PreferenceScreen screen) {
LayoutPreference headerPref = screen.findPreference(getPreferenceKey());
ImageView mAppIconView = headerPref.findViewById(R.id.entity_header_icon);
TextView mTitleView = headerPref.findViewById(R.id.entity_header_title);
TextView mDescriptionView = headerPref.findViewById(R.id.entity_header_summary);
try {
ApplicationInfo applicationInfo =
mPackageManager.getApplicationInfo(mCredentialManagerPackageName, 0);
mAppIconView.setImageDrawable(mPackageManager.getApplicationIcon(applicationInfo));
mTitleView.setText(applicationInfo.loadLabel(mPackageManager));
} catch (PackageManager.NameNotFoundException e) {
mAppIconView.setImageDrawable(null);
mTitleView.setText(mCredentialManagerPackageName);
}
// TODO (b/165641221): The description should be multi-lined, which is currently a
// limitation of using Settings entity header. However, the Settings entity header
// should be used to be consistent with the rest of Settings.
mDescriptionView.setText(
mContext.getString(R.string.request_manage_credentials_description));
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE;
import android.content.Context;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceScreen;
import com.android.settings.core.BasePreferenceController;
/**
* Controller that shows the credential management app's authentication policy.
*/
public class CredentialManagementAppPolicyController extends BasePreferenceController {
public CredentialManagementAppPolicyController(Context context, String preferenceKey) {
super(context, preferenceKey);
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE_UNSEARCHABLE;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
PreferenceGroup group = screen.findPreference(getPreferenceKey());
group.addPreference(new CredentialManagementAppPolicyPreference(group.getContext()));
}
}

View File

@@ -0,0 +1,89 @@
/*
* 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.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.security.AppUriAuthenticationPolicy;
import android.security.IKeyChainService;
import android.security.KeyChain;
import android.util.Log;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.android.settings.R;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Preference that shows the credential management app's authentication policy.
*/
public class CredentialManagementAppPolicyPreference extends Preference {
private static final String TAG = "CredentialManagementApp";
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final Context mContext;
private boolean mHasCredentialManagerPackage;
private String mCredentialManagerPackageName;
private AppUriAuthenticationPolicy mCredentialManagerPolicy;
public CredentialManagementAppPolicyPreference(Context context) {
super(context);
setLayoutResource(R.layout.credential_management_app_policy);
mContext = context;
}
@Override
public void onBindViewHolder(PreferenceViewHolder view) {
super.onBindViewHolder(view);
mExecutor.execute(() -> {
try {
IKeyChainService service = KeyChain.bind(mContext).getService();
mHasCredentialManagerPackage = service.hasCredentialManagementApp();
mCredentialManagerPackageName = service.getCredentialManagementAppPackageName();
mCredentialManagerPolicy = service.getCredentialManagementAppPolicy();
} catch (InterruptedException | RemoteException e) {
Log.e(TAG, "Unable to display credential management app policy");
}
mHandler.post(() -> displayPolicy(view));
});
}
private void displayPolicy(PreferenceViewHolder view) {
if (mHasCredentialManagerPackage) {
RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(mContext));
CredentialManagementAppAdapter recyclerViewAdapter = new CredentialManagementAppAdapter(
mContext, mCredentialManagerPackageName,
mCredentialManagerPolicy.getAppAndUriMappings(),
/* include header= */ false, /* include expander= */ true);
recyclerView.setAdapter(recyclerViewAdapter);
}
}
}

View File

@@ -0,0 +1,90 @@
/*
* 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.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.security.IKeyChainService;
import android.security.KeyChain;
import android.util.Log;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Controller that shows and updates the credential management app summary.
*/
public class CredentialManagementAppPreferenceController extends BasePreferenceController {
private static final String TAG = "CredentialManagementApp";
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final PackageManager mPackageManager;
private boolean mHasCredentialManagerPackage;
private String mCredentialManagerPackageName;
public CredentialManagementAppPreferenceController(Context context, String key) {
super(context, key);
mPackageManager = context.getPackageManager();
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
@Override
public void updateState(Preference preference) {
mExecutor.execute(() -> {
try {
IKeyChainService service = KeyChain.bind(mContext).getService();
mHasCredentialManagerPackage = service.hasCredentialManagementApp();
mCredentialManagerPackageName = service.getCredentialManagementAppPackageName();
} catch (InterruptedException | RemoteException e) {
Log.e(TAG, "Unable to display credential management app preference");
}
mHandler.post(() -> displayPreference(preference));
});
}
private void displayPreference(Preference preference) {
if (mHasCredentialManagerPackage) {
preference.setEnabled(true);
try {
ApplicationInfo applicationInfo =
mPackageManager.getApplicationInfo(mCredentialManagerPackageName, 0);
preference.setSummary(applicationInfo.loadLabel(mPackageManager));
} catch (PackageManager.NameNotFoundException e) {
preference.setSummary(mCredentialManagerPackageName);
}
} else {
preference.setEnabled(false);
preference.setSummary(R.string.no_certificate_management_app);
}
}
}

View File

@@ -93,7 +93,8 @@ public class RequestManageCredentials extends Activity {
mRecyclerView.setLayoutManager(mLayoutManager); mRecyclerView.setLayoutManager(mLayoutManager);
CredentialManagementAppAdapter recyclerViewAdapter = new CredentialManagementAppAdapter( CredentialManagementAppAdapter recyclerViewAdapter = new CredentialManagementAppAdapter(
this, mCredentialManagerPackage, mAuthenticationPolicy.getAppAndUriMappings()); this, mCredentialManagerPackage, mAuthenticationPolicy.getAppAndUriMappings(),
/* include header= */ true, /* include expander= */ false);
mRecyclerView.setAdapter(recyclerViewAdapter); mRecyclerView.setAdapter(recyclerViewAdapter);
} }

View File

@@ -0,0 +1,51 @@
/*
* 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 android.content.Context;
import com.android.settings.core.BasePreferenceController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
@RunWith(RobolectricTestRunner.class)
public class CredentialManagementAppButtonsControllerTest {
private Context mContext;
private CredentialManagementAppButtonsController mController;
private static final String PREF_KEY_CREDENTIAL_MANAGEMENT_APP = "certificate_management_app";
@Before
public void setUp() {
mContext = RuntimeEnvironment.application;
mController = new CredentialManagementAppButtonsController(
mContext, PREF_KEY_CREDENTIAL_MANAGEMENT_APP);
}
@Test
public void getAvailabilityStatus_shouldAlwaysReturnAvailableUnsearchable() {
assertThat(mController.getAvailabilityStatus())
.isEqualTo(BasePreferenceController.AVAILABLE_UNSEARCHABLE);
}
}

View File

@@ -0,0 +1,65 @@
/*
* 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 android.content.Context;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
@RunWith(RobolectricTestRunner.class)
public class CredentialManagementAppControllerTest {
private Context mContext;
private CredentialManagementAppPreferenceController mController;
private Preference mPreference;
private static final String PREF_KEY_CREDENTIAL_MANAGEMENT_APP = "certificate_management_app";
@Before
public void setUp() {
mContext = RuntimeEnvironment.application;
mController = new CredentialManagementAppPreferenceController(
mContext, PREF_KEY_CREDENTIAL_MANAGEMENT_APP);
mPreference = new Preference(mContext);
}
@Test
public void getAvailabilityStatus_shouldAlwaysReturnAvailable() {
assertThat(mController.getAvailabilityStatus())
.isEqualTo(BasePreferenceController.AVAILABLE);
}
@Test
public void updateState_noCredentialManagementApp_shouldDisablePreference() {
mController.updateState(mPreference);
assertThat(mPreference.isEnabled()).isEqualTo(false);
assertThat(mPreference.getSummary()).isEqualTo(
mContext.getText(R.string.no_certificate_management_app));
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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 android.app.settings.SettingsEnums.CREDENTIAL_MANAGEMENT_APP;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import android.content.Context;
import android.provider.SearchIndexableResource;
import com.android.settings.R;
import com.android.settings.testutils.shadow.ShadowDashboardFragment;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = ShadowDashboardFragment.class)
public class CredentialManagementAppFragmentTest {
private CredentialManagementAppFragment mFragment;
private Context mContext;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application);
mFragment = spy(new CredentialManagementAppFragment());
doReturn(mContext).when(mFragment).getContext();
}
@Test
public void searchIndexProvider_shouldIndexResource() {
final List<SearchIndexableResource> indexRes =
CredentialManagementAppFragment.SEARCH_INDEX_DATA_PROVIDER
.getXmlResourcesToIndex(mContext, true /* enabled */);
assertThat(indexRes).isNotNull();
assertThat(indexRes.get(0).xmlResId).isEqualTo(R.xml.credential_management_app_fragment);
}
@Test
public void getMetricsCategory_shouldReturnInstallCertificateFromStorage() {
assertThat(mFragment.getMetricsCategory()).isEqualTo(CREDENTIAL_MANAGEMENT_APP);
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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 android.content.Context;
import com.android.settings.core.BasePreferenceController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
@RunWith(RobolectricTestRunner.class)
public class CredentialManagementAppHeaderControllerTest {
private Context mContext;
private CredentialManagementAppHeaderController mController;
private static final String PREF_KEY_CREDENTIAL_MANAGEMENT_APP = "certificate_management_app";
@Before
public void setUp() {
mContext = RuntimeEnvironment.application;
mController = new CredentialManagementAppHeaderController(
mContext, PREF_KEY_CREDENTIAL_MANAGEMENT_APP);
}
@Test
public void getAvailabilityStatus_shouldAlwaysReturnAvailableUnsearchable() {
assertThat(mController.getAvailabilityStatus())
.isEqualTo(BasePreferenceController.AVAILABLE_UNSEARCHABLE);
}
}