* Update string and icon * Flip icon if languate is RTL e.g. Arabic * Add dialog when 'Remove app' is selected * Add Material Next theme to dialog * Update string terminology when removing a CA or user certificate to from 'remove' to 'uninstall' * Final UI tweaks to RequestManageCredentials screen Screenshots * Light mode: https://screenshot.googleplex.com/66QKFLhtn6ZueZQ * Dark mode: https://screenshot.googleplex.com/9kkTaaDV6CiHudF * RTL: https://screenshot.googleplex.com/AkwSibXakARYM8H * Request light mode: https://screenshot.googleplex.com/6n6raBQ8drXRHcU * Request dark mode: https://screenshot.googleplex.com/3bmWNoTwE6JBqAX Bug: 189416800 Test: atest CredentialManagementAppButtonsControllerTest manual testing with TestDPC Change-Id: I5450d33603418930e92b03c4447aaa1095afe9a3
351 lines
13 KiB
Java
351 lines
13 KiB
Java
/*
|
|
* 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.DevicePolicyEventLogger;
|
|
import android.app.admin.DevicePolicyManager;
|
|
import android.content.Context;
|
|
import android.content.pm.ApplicationInfo;
|
|
import android.content.pm.PackageManager;
|
|
import android.content.pm.UserInfo;
|
|
import android.content.res.Configuration;
|
|
import android.net.Uri;
|
|
import android.os.Bundle;
|
|
import android.os.Handler;
|
|
import android.os.HandlerThread;
|
|
import android.os.Process;
|
|
import android.os.RemoteException;
|
|
import android.os.UserManager;
|
|
import android.security.AppUriAuthenticationPolicy;
|
|
import android.security.Credentials;
|
|
import android.security.KeyChain;
|
|
import android.stats.devicepolicy.DevicePolicyEnums;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
import android.view.View;
|
|
import android.widget.Button;
|
|
import android.widget.ImageView;
|
|
import android.widget.LinearLayout;
|
|
import android.widget.TextView;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.VisibleForTesting;
|
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
import androidx.recyclerview.widget.RecyclerView;
|
|
|
|
import com.android.settings.R;
|
|
|
|
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
|
|
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* 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 ExtendedFloatingActionButton mExtendedFab;
|
|
|
|
private HandlerThread mKeyChainTread;
|
|
private KeyChain.KeyChainConnection mKeyChainConnection;
|
|
|
|
private boolean mDisplayingButtonPanel = false;
|
|
private boolean mIsLandscapeMode = false;
|
|
|
|
@Override
|
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
|
super.onCreate(savedInstanceState);
|
|
|
|
if (!Credentials.ACTION_MANAGE_CREDENTIALS.equals(getIntent().getAction())) {
|
|
Log.e(TAG, "Unable to start activity because intent action is not "
|
|
+ Credentials.ACTION_MANAGE_CREDENTIALS);
|
|
logRequestFailure();
|
|
finishWithResultCancelled();
|
|
return;
|
|
}
|
|
if (isManagedDevice()) {
|
|
Log.e(TAG, "Credential management on managed devices should be done by the Device "
|
|
+ "Policy Controller, not a credential management app");
|
|
logRequestFailure();
|
|
finishWithResultCancelled();
|
|
return;
|
|
}
|
|
mCredentialManagerPackage = getLaunchedFromPackage();
|
|
if (TextUtils.isEmpty(mCredentialManagerPackage)) {
|
|
Log.e(TAG, "Unknown credential manager app");
|
|
logRequestFailure();
|
|
finishWithResultCancelled();
|
|
return;
|
|
}
|
|
DevicePolicyEventLogger
|
|
.createEvent(DevicePolicyEnums.CREDENTIAL_MANAGEMENT_APP_REQUEST_NAME)
|
|
.setStrings(mCredentialManagerPackage)
|
|
.write();
|
|
setContentView(R.layout.request_manage_credentials);
|
|
mIsLandscapeMode = getResources().getConfiguration().orientation
|
|
== Configuration.ORIENTATION_LANDSCAPE;
|
|
|
|
mKeyChainTread = new HandlerThread("KeyChainConnection");
|
|
mKeyChainTread.start();
|
|
mKeyChainConnection = getKeyChainConnection(this, mKeyChainTread);
|
|
|
|
AppUriAuthenticationPolicy policy =
|
|
getIntent().getParcelableExtra(KeyChain.EXTRA_AUTHENTICATION_POLICY);
|
|
if (!isValidAuthenticationPolicy(policy)) {
|
|
Log.e(TAG, "Invalid authentication policy");
|
|
logRequestFailure();
|
|
finishWithResultCancelled();
|
|
return;
|
|
}
|
|
mAuthenticationPolicy = policy;
|
|
DevicePolicyEventLogger
|
|
.createEvent(DevicePolicyEnums.CREDENTIAL_MANAGEMENT_APP_REQUEST_POLICY)
|
|
.setStrings(getNumberOfAuthenticationPolicyApps(mAuthenticationPolicy),
|
|
getNumberOfAuthenticationPolicyUris(mAuthenticationPolicy))
|
|
.write();
|
|
|
|
if (mIsLandscapeMode) {
|
|
loadHeader();
|
|
}
|
|
loadRecyclerView();
|
|
loadButtons();
|
|
loadExtendedFloatingActionButton();
|
|
addOnScrollListener();
|
|
}
|
|
|
|
@Override
|
|
protected void onDestroy() {
|
|
super.onDestroy();
|
|
if (mKeyChainConnection != null) {
|
|
mKeyChainConnection.close();
|
|
mKeyChainConnection = null;
|
|
mKeyChainTread.quitSafely();
|
|
}
|
|
}
|
|
|
|
private boolean isValidAuthenticationPolicy(AppUriAuthenticationPolicy policy) {
|
|
if (policy == null || policy.getAppAndUriMappings().isEmpty()) {
|
|
return false;
|
|
}
|
|
try {
|
|
// Check whether any of the aliases in the policy already exist
|
|
for (String alias : policy.getAliases()) {
|
|
if (mKeyChainConnection.getService().requestPrivateKey(alias) != null) {
|
|
return false;
|
|
}
|
|
}
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Invalid authentication policy", e);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private boolean isManagedDevice() {
|
|
DevicePolicyManager dpm = getSystemService(DevicePolicyManager.class);
|
|
|
|
return dpm.getDeviceOwnerUser() != null
|
|
|| dpm.getProfileOwner() != null
|
|
|| hasManagedProfile();
|
|
}
|
|
|
|
private boolean hasManagedProfile() {
|
|
UserManager um = getSystemService(UserManager.class);
|
|
for (final UserInfo userInfo : um.getProfiles(getUserId())) {
|
|
if (userInfo.isManagedProfile()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void loadRecyclerView() {
|
|
mLayoutManager = new LinearLayoutManager(this);
|
|
mRecyclerView = findViewById(R.id.apps_list);
|
|
mRecyclerView.setLayoutManager(mLayoutManager);
|
|
|
|
CredentialManagementAppAdapter recyclerViewAdapter = new CredentialManagementAppAdapter(
|
|
this, mCredentialManagerPackage, mAuthenticationPolicy.getAppAndUriMappings(),
|
|
/* include header= */ !mIsLandscapeMode, /* include expander= */ false);
|
|
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(b -> {
|
|
DevicePolicyEventLogger
|
|
.createEvent(DevicePolicyEnums.CREDENTIAL_MANAGEMENT_APP_REQUEST_DENIED)
|
|
.write();
|
|
finishWithResultCancelled();
|
|
});
|
|
allowButton.setOnClickListener(b -> setOrUpdateCredentialManagementAppAndFinish());
|
|
}
|
|
|
|
private void loadExtendedFloatingActionButton() {
|
|
mExtendedFab = findViewById(R.id.extended_fab);
|
|
mExtendedFab.setOnClickListener(v -> {
|
|
final int position = mIsLandscapeMode
|
|
? mAuthenticationPolicy.getAppAndUriMappings().size() - 1
|
|
: mAuthenticationPolicy.getAppAndUriMappings().size();
|
|
mRecyclerView.scrollToPosition(position);
|
|
mExtendedFab.hide();
|
|
showButtonPanel();
|
|
});
|
|
}
|
|
|
|
private void loadHeader() {
|
|
final ImageView mAppIconView = findViewById(R.id.credential_management_app_icon);
|
|
final TextView mTitleView = findViewById(R.id.credential_management_app_title);
|
|
try {
|
|
ApplicationInfo applicationInfo =
|
|
getPackageManager().getApplicationInfo(mCredentialManagerPackage, 0);
|
|
mAppIconView.setImageDrawable(getPackageManager().getApplicationIcon(applicationInfo));
|
|
mTitleView.setText(TextUtils.expandTemplate(
|
|
getText(R.string.request_manage_credentials_title),
|
|
applicationInfo.loadLabel(getPackageManager())));
|
|
} catch (PackageManager.NameNotFoundException e) {
|
|
mAppIconView.setImageDrawable(null);
|
|
mTitleView.setText(TextUtils.expandTemplate(
|
|
getText(R.string.request_manage_credentials_title),
|
|
mCredentialManagerPackage));
|
|
}
|
|
}
|
|
|
|
private void setOrUpdateCredentialManagementAppAndFinish() {
|
|
try {
|
|
mKeyChainConnection.getService().setCredentialManagementApp(
|
|
mCredentialManagerPackage, mAuthenticationPolicy);
|
|
DevicePolicyEventLogger
|
|
.createEvent(DevicePolicyEnums.CREDENTIAL_MANAGEMENT_APP_REQUEST_ACCEPTED)
|
|
.write();
|
|
setResult(RESULT_OK);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Unable to set credential manager app", e);
|
|
logRequestFailure();
|
|
}
|
|
finish();
|
|
}
|
|
|
|
@VisibleForTesting
|
|
KeyChain.KeyChainConnection getKeyChainConnection(Context context, HandlerThread thread) {
|
|
final Handler handler = new Handler(thread.getLooper());
|
|
try {
|
|
KeyChain.KeyChainConnection connection = KeyChain.bindAsUser(
|
|
context, handler, Process.myUserHandle());
|
|
return connection;
|
|
} catch (InterruptedException e) {
|
|
throw new RuntimeException("Faile to bind to KeyChain", e);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
// On down scroll, hide text in floating action button by setting
|
|
// extended to false.
|
|
if (dy > 0 && mExtendedFab.getVisibility() == View.VISIBLE) {
|
|
mExtendedFab.shrink();
|
|
}
|
|
if (isRecyclerScrollable()) {
|
|
mExtendedFab.show();
|
|
hideButtonPanel();
|
|
} else {
|
|
mExtendedFab.hide();
|
|
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 finishWithResultCancelled() {
|
|
setResult(RESULT_CANCELED);
|
|
finish();
|
|
}
|
|
|
|
private void logRequestFailure() {
|
|
DevicePolicyEventLogger
|
|
.createEvent(DevicePolicyEnums.CREDENTIAL_MANAGEMENT_APP_REQUEST_FAILED)
|
|
.write();
|
|
}
|
|
|
|
private String getNumberOfAuthenticationPolicyUris(AppUriAuthenticationPolicy policy) {
|
|
int numberOfUris = 0;
|
|
for (Map.Entry<String, Map<Uri, String>> appsToUris :
|
|
policy.getAppAndUriMappings().entrySet()) {
|
|
numberOfUris += appsToUris.getValue().size();
|
|
}
|
|
return String.valueOf(numberOfUris);
|
|
}
|
|
|
|
private String getNumberOfAuthenticationPolicyApps(AppUriAuthenticationPolicy policy) {
|
|
return String.valueOf(policy.getAppAndUriMappings().size());
|
|
}
|
|
|
|
}
|