Separated flow when installing different certificate types
This is part of the changes to improve the UX and language for installing certificates.
Previously when installing a certificate, the same dialog appeared for all certificates.
This CL introduces a separate flow for the different types:
- CA certificate: confirm credential, select certificate, install
- User certificate: select certificate, name dialog, install to keystore
- WiFi certificate: select certificate, install to keystore
Bug: 139173976
Test: manual testing by installing different certificate types: CA, User and WiFi
Including testing certificate which needs to be extracted
go/enterprise-wifi-lab-access
Screenshot of the screen: https://hsv.googleplex.com/5666804900823040
Change-Id: I9b24c37b382eeecee6e5d0d74cbd4047c6d36182
diff --git a/res/layout/name_certificate_dialog.xml b/res/layout/name_certificate_dialog.xml
new file mode 100644
index 0000000..c838ba8
--- /dev/null
+++ b/res/layout/name_certificate_dialog.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="16dp">
+
+ <TextView android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/name_credential_dialog_title"
+ style="@style/dialog_title"/>
+
+ <TextView android:id="@+id/error"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textColor="@color/red"
+ android:textStyle="bold"
+ android:visibility="gone" />
+
+ <TextView android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/certificate_name"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/colorAccent"/>
+
+ <EditText android:id="@+id/certificate_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/dialog_edit_text"/>
+
+ </LinearLayout>
+</ScrollView>
diff --git a/res/layout/name_credential_dialog.xml b/res/layout/name_credential_dialog.xml
deleted file mode 100644
index 6dd46ff..0000000
--- a/res/layout/name_credential_dialog.xml
+++ /dev/null
@@ -1,85 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2009 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.
--->
-
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
-
- <LinearLayout
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:padding="15dip">
-
- <TextView android:id="@+id/error"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textColor="@color/red"
- android:textStyle="bold"
- android:visibility="gone" />
-
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/credential_name"
- android:textAppearance="?android:attr/textAppearanceMedium" />
-
- <EditText android:id="@+id/credential_name"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:singleLine="True"/>
-
- <LinearLayout
- android:id="@+id/credential_usage_group"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical" >
-
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingTop="12dp"
- android:text="@string/credential_usage_label"
- android:textAppearance="?android:attr/textAppearanceMedium" />
-
- <Spinner
- android:id="@+id/credential_usage"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:entries="@array/credential_usage" />
-
- <TextView
- android:id="@+id/credential_capabilities_warning"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingTop="12dp"
- android:text="@string/certificate_capabilities_warning"
- android:textColor="@color/red" />
-
- </LinearLayout>
-
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingTop="12dp"
- android:text="@string/credential_info" />
-
- <TextView android:id="@+id/credential_info"
- android:layout_width="match_parent"
- android:layout_height="wrap_content" />
-
- </LinearLayout>
-</ScrollView>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 020a014..881fc6c 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -26,10 +26,10 @@
<string name="extracting_pkcs12">Extracting\u2026</string>
<!-- Title of dialog to enter password for pkcs12 file -->
<string name="pkcs12_file_password_dialog_title">Extract from %s</string>
- <!-- Title of dialog to name a credential -->
- <string name="name_credential_dialog_title">Name the certificate</string>
- <!-- Description for the credential name input box -->
- <string name="credential_name">Certificate name:</string>
+ <!-- Title of a dialog. This title is asking the user to name the certificate they're installing on their device. -->
+ <string name="name_credential_dialog_title">Name this certificate</string>
+ <!-- Input field in a dialog. This dialog is asking the user to name the certificate they are installing on their device. -->
+ <string name="certificate_name">Certificate name</string>
<!-- Title of the credential info -->
<!-- Description for the credential password input box -->
<string name="credential_password">Type the password to extract the certificates.</string>
@@ -58,8 +58,12 @@
<string name="action_missing_private_key">Private key required to install a certificate</string>
<string name="action_missing_user_cert">Certificate required to install a private key</string>
- <!-- toast message -->
- <string name="cert_is_added"><xliff:g id="credential">%s</xliff:g> is installed.</string>
+ <!-- Confirmation toast. This toast lets the user know that they successfully installed a CA certificate on their device. -->
+ <string name="ca_cert_is_added">CA certificate installed</string>
+ <!-- Confirmation toast. This toast lets the user know that they successfully installed a user certificate on their device. -->
+ <string name="user_cert_is_added">User certificate installed</string>
+ <!-- Confirmation toast. This toast lets the user know that they successfully installed a Wi-Fi certificate on their device. -->
+ <string name="wifi_cert_is_added">Wi\u2011Fi certificate installed</string>
<!-- toast message -->
<string name="cert_too_large_error">Couldn\'t install because the certificate size is too large.</string>
<!-- toast message -->
diff --git a/res/values/styles.xml b/res/values/styles.xml
index b1d493e..877464b 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -28,4 +28,14 @@
<item name="android:textAppearance">@android:style/TextAppearance.Material.Subhead</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
+ <style name="dialog_title">
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:textSize">20sp</item>
+ <item name="android:layout_marginBottom">16dp</item>
+ </style>
+ <style name="dialog_edit_text">
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:backgroundTint">?android:attr/colorAccent</item>
+ <item name="android:singleLine">True</item>
+ </style>
</resources>
diff --git a/src/com/android/certinstaller/CertInstaller.java b/src/com/android/certinstaller/CertInstaller.java
index cd99c8a..a87cec9 100644
--- a/src/com/android/certinstaller/CertInstaller.java
+++ b/src/com/android/certinstaller/CertInstaller.java
@@ -19,7 +19,6 @@
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
-import android.app.KeyguardManager;
import android.app.ProgressDialog;
import android.content.ActivityNotFoundException;
import android.content.Context;
@@ -27,6 +26,7 @@
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Process;
+import android.security.Credentials;
import android.security.KeyChain;
import android.security.KeyChain.KeyChainConnection;
import android.security.KeyStore;
@@ -34,10 +34,7 @@
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.EditText;
-import android.widget.Spinner;
import android.widget.Toast;
import java.io.Serializable;
@@ -61,10 +58,6 @@
// key to states Bundle
private static final String NEXT_ACTION_KEY = "na";
- // Values for usage type spinner
- private static final int USAGE_TYPE_SYSTEM = 0;
- private static final int USAGE_TYPE_WIFI = 1;
-
private final ViewHelper mView = new ViewHelper();
private int mState;
@@ -153,7 +146,7 @@
return createPkcs12PasswordDialog();
case NAME_CREDENTIAL_DIALOG:
- return createNameCredentialDialog();
+ return createNameCertificateDialog();
case PROGRESS_BAR_DIALOG:
ProgressDialog dialog = new ProgressDialog(this);
@@ -178,9 +171,11 @@
}
Log.d(TAG, "credential is added: " + mCredentials.getName());
- Toast.makeText(this, getString(R.string.cert_is_added, mCredentials.getName()),
- Toast.LENGTH_LONG).show();
-
+ if (mCredentials.getCertTypeSelected().equals(Credentials.CERTIFICATE_USAGE_WIFI)) {
+ Toast.makeText(this, R.string.wifi_cert_is_added, Toast.LENGTH_LONG).show();
+ } else {
+ Toast.makeText(this, R.string.user_cert_is_added, Toast.LENGTH_LONG).show();
+ }
setResult(RESULT_OK);
finish();
break;
@@ -206,7 +201,8 @@
private class InstallVpnAndAppsTrustAnchorsTask extends AsyncTask<Void, Void, Boolean> {
- @Override protected Boolean doInBackground(Void... unused) {
+ @Override
+ protected Boolean doInBackground(Void... unused) {
try {
try (KeyChainConnection keyChainConnection = KeyChain.bind(CertInstaller.this)) {
return mCredentials.installVpnAndAppsTrustAnchors(CertInstaller.this,
@@ -218,8 +214,11 @@
}
}
- @Override protected void onPostExecute(Boolean success) {
+ @Override
+ protected void onPostExecute(Boolean success) {
if (success) {
+ Toast.makeText(getApplicationContext(), R.string.ca_cert_is_added,
+ Toast.LENGTH_LONG).show();
setResult(RESULT_OK);
}
finish();
@@ -242,13 +241,23 @@
return;
}
- nameCredential();
+ installCertificateOrShowNameDialog();
}
- private void nameCredential() {
+ private void installCertificateOrShowNameDialog() {
if (!mCredentials.hasAnyForSystemInstall()) {
toastErrorAndFinish(R.string.no_cert_to_saved);
+ } else if (mCredentials.getCertTypeSelected().equals(Credentials.CERTIFICATE_USAGE_WIFI)) {
+ mCredentials.setInstallAsUid(Process.WIFI_UID);
+ installCertificateToKeystore(this);
+ } else if (mCredentials.hasOnlyVpnAndAppsTrustAnchors()) {
+ // If there's only a CA certificate to install, then it's going to be used
+ // as a trust anchor. Install it and skip importing to Keystore.
+
+ // more work to do, don't finish just yet
+ new InstallVpnAndAppsTrustAnchorsTask().execute();
} else {
+ // Name is required if installing User certificate
showDialog(NAME_CREDENTIAL_DIALOG);
}
}
@@ -278,7 +287,7 @@
removeDialog(PROGRESS_BAR_DIALOG);
if (success) {
removeDialog(PKCS12_PASSWORD_DIALOG);
- nameCredential();
+ installCertificateOrShowNameDialog();
} else {
showDialog(PKCS12_PASSWORD_DIALOG);
mView.setText(R.id.credential_password, "");
@@ -313,53 +322,22 @@
return d;
}
- private Dialog createNameCredentialDialog() {
- ViewGroup view = (ViewGroup) View.inflate(this, R.layout.name_credential_dialog, null);
+ private Dialog createNameCertificateDialog() {
+ ViewGroup view = (ViewGroup) View.inflate(this, R.layout.name_certificate_dialog, null);
mView.setView(view);
if (mView.getHasEmptyError()) {
mView.showError(R.string.name_empty_error);
mView.setHasEmptyError(false);
}
- mView.setText(R.id.credential_info, mCredentials.getDescription(this).toString());
- final EditText nameInput = view.findViewById(R.id.credential_name);
- if (mCredentials.isInstallAsUidSet()) {
- view.findViewById(R.id.credential_usage_group).setVisibility(View.GONE);
- } else {
- final Spinner usageSpinner = view.findViewById(R.id.credential_usage);
- final View ca_capabilities_warning = view.findViewById(R.id.credential_capabilities_warning);
-
- usageSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
- @Override
- public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
- switch ((int) id) {
- case USAGE_TYPE_SYSTEM:
- ca_capabilities_warning.setVisibility(
- mCredentials.hasOnlyVpnAndAppsTrustAnchors() ?
- View.VISIBLE : View.GONE);
- mCredentials.setInstallAsUid(KeyStore.UID_SELF);
- break;
- case USAGE_TYPE_WIFI:
- ca_capabilities_warning.setVisibility(View.GONE);
- mCredentials.setInstallAsUid(Process.WIFI_UID);
- break;
- default:
- Log.w(TAG, "Unknown selection for scope: " + id);
- }
- }
-
- @Override
- public void onNothingSelected(AdapterView<?> parent) {
- }
- });
- }
+ final EditText nameInput = view.findViewById(R.id.certificate_name);
nameInput.setText(getDefaultName());
nameInput.selectAll();
final Context appContext = getApplicationContext();
+
Dialog d = new AlertDialog.Builder(this)
.setView(view)
- .setTitle(R.string.name_credential_dialog_title)
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
- String name = mView.getText(R.id.credential_name);
+ String name = mView.getText(R.id.certificate_name);
if (TextUtils.isEmpty(name)) {
mView.setHasEmptyError(true);
removeDialog(NAME_CREDENTIAL_DIALOG);
@@ -367,24 +345,8 @@
} else {
removeDialog(NAME_CREDENTIAL_DIALOG);
mCredentials.setName(name);
-
- // If there's only a CA certificate to install, then it's going to be used
- // as a trust anchor. Install it and skip importing to Keystore.
- if (mCredentials.hasOnlyVpnAndAppsTrustAnchors()) {
- // more work to do, don't finish just yet
- new InstallVpnAndAppsTrustAnchorsTask().execute();
- return;
- }
-
- // install everything to system keystore
- try {
- startActivityForResult(
- mCredentials.createSystemInstallIntent(appContext),
- REQUEST_SYSTEM_INSTALL_CODE);
- } catch (ActivityNotFoundException e) {
- Log.w(TAG, "systemInstall(): " + e);
- toastErrorAndFinish(R.string.cert_not_saved);
- }
+ mCredentials.setInstallAsUid(KeyStore.UID_SELF);
+ installCertificateToKeystore(appContext);
}
})
.setNegativeButton(android.R.string.cancel,
@@ -394,6 +356,17 @@
return d;
}
+ private void installCertificateToKeystore(Context context) {
+ try {
+ startActivityForResult(
+ mCredentials.createSystemInstallIntent(context),
+ REQUEST_SYSTEM_INSTALL_CODE);
+ } catch (ActivityNotFoundException e) {
+ Log.w(TAG, "installCertificateToKeystore(): ", e);
+ toastErrorAndFinish(R.string.cert_not_saved);
+ }
+ }
+
private String getDefaultName() {
String name = mCredentials.getName();
if (TextUtils.isEmpty(name)) {
diff --git a/src/com/android/certinstaller/CertInstallerMain.java b/src/com/android/certinstaller/CertInstallerMain.java
index 972afec..48608d3 100644
--- a/src/com/android/certinstaller/CertInstallerMain.java
+++ b/src/com/android/certinstaller/CertInstallerMain.java
@@ -29,6 +29,8 @@
import android.util.Log;
import android.widget.Toast;
+import libcore.io.IoUtils;
+
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -36,8 +38,6 @@
import java.util.HashMap;
import java.util.Map;
-import libcore.io.IoUtils;
-
/**
* The main class for installing certificates to the system keystore. It reacts
* to the public {@link Credentials#INSTALL_ACTION} intent.
@@ -136,10 +136,10 @@
}
private boolean installingCaCertificate(Bundle bundle) {
- return bundle.size() == 1 && bundle.containsKey(Credentials.EXTRA_CERTIFICATE_USAGE)
- && bundle.getString(Credentials.EXTRA_CERTIFICATE_USAGE).equals(
- Credentials.CERTIFICATE_USAGE_CA);
+ return bundle != null && bundle.size() == 1 && Credentials.CERTIFICATE_USAGE_CA.equals(
+ bundle.getString(Credentials.EXTRA_CERTIFICATE_USAGE));
}
+
private void confirmDeviceCredential() {
KeyguardManager keyguardManager = getSystemService(KeyguardManager.class);
Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(null,
@@ -203,10 +203,9 @@
final byte[] raw = readWithLimit(in);
- Intent intent = new Intent(this, CertInstaller.class);
+ Intent intent = getIntent();
intent.putExtra(target, raw);
startInstallActivity(intent);
-
} catch (IOException e) {
Log.e(TAG, "Failed to read certificate: " + e);
Toast.makeText(this, R.string.cert_read_error, Toast.LENGTH_LONG).show();
diff --git a/src/com/android/certinstaller/CredentialHelper.java b/src/com/android/certinstaller/CredentialHelper.java
index 7c8607d..4b4a428 100644
--- a/src/com/android/certinstaller/CredentialHelper.java
+++ b/src/com/android/certinstaller/CredentialHelper.java
@@ -71,6 +71,7 @@
private HashMap<String, byte[]> mBundle = new HashMap<String, byte[]>();
private String mName = "";
+ private String mCertTypeSelected = "";
private int mUid = -1;
private PrivateKey mUserKey;
private X509Certificate mUserCert;
@@ -91,6 +92,12 @@
mName = name;
}
+ String certTypeSelected = bundle.getString(Credentials.EXTRA_CERTIFICATE_USAGE);
+ bundle.remove(Credentials.EXTRA_CERTIFICATE_USAGE);
+ if (certTypeSelected != null) {
+ mCertTypeSelected = certTypeSelected;
+ }
+
mUid = bundle.getInt(Credentials.EXTRA_INSTALL_AS_UID, -1);
bundle.remove(Credentials.EXTRA_INSTALL_AS_UID);
@@ -274,6 +281,10 @@
return mUid;
}
+ String getCertTypeSelected() {
+ return mCertTypeSelected;
+ }
+
Intent createSystemInstallIntent(final Context context) {
Intent intent = new Intent("com.android.credentials.INSTALL");
// To prevent the private key from being sniffed, we explicitly spell