Merge "Keystore 2.0: Android Protected Confirmation" am: 30841f177c
Original change: https://android-review.googlesource.com/c/platform/frameworks/base/+/1508897 MUST ONLY BE SUBMITTED BY AUTOMERGER Change-Id: If0d51926f5045c83c12e04ba216f00e7cb287745
This commit is contained in:
@@ -481,6 +481,7 @@ java_library {
|
|||||||
"android.hardware.vibrator-V1.1-java",
|
"android.hardware.vibrator-V1.1-java",
|
||||||
"android.hardware.vibrator-V1.2-java",
|
"android.hardware.vibrator-V1.2-java",
|
||||||
"android.hardware.vibrator-V1.3-java",
|
"android.hardware.vibrator-V1.3-java",
|
||||||
|
"android.security.apc-java",
|
||||||
"android.system.keystore2-java",
|
"android.system.keystore2-java",
|
||||||
"android.system.suspend.control.internal-java",
|
"android.system.suspend.control.internal-java",
|
||||||
"devicepolicyprotosnano",
|
"devicepolicyprotosnano",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import android.content.ContentResolver;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.provider.Settings.SettingNotFoundException;
|
import android.provider.Settings.SettingNotFoundException;
|
||||||
|
import android.security.keystore.AndroidKeyStoreProvider;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
@@ -36,15 +37,15 @@ import java.util.concurrent.Executor;
|
|||||||
* compromised. Implementing confirmation prompts with these guarantees requires dedicated
|
* compromised. Implementing confirmation prompts with these guarantees requires dedicated
|
||||||
* hardware-support and may not always be available.
|
* hardware-support and may not always be available.
|
||||||
*
|
*
|
||||||
* <p>Confirmation prompts are typically used with an external entitity - the <i>Relying Party</i> -
|
* <p>Confirmation prompts are typically used with an external entity - the <i>Relying Party</i> -
|
||||||
* in the following way. The setup steps are as follows:
|
* in the following way. The setup steps are as follows:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li> Before first use, the application generates a key-pair with the
|
* <li> Before first use, the application generates a key-pair with the
|
||||||
* {@link android.security.keystore.KeyGenParameterSpec.Builder#setUserConfirmationRequired
|
* {@link android.security.keystore.KeyGenParameterSpec.Builder#setUserConfirmationRequired
|
||||||
* CONFIRMATION tag} set. Device attestation,
|
* CONFIRMATION tag} set. AndroidKeyStore key attestation, e.g.,
|
||||||
* e.g. {@link java.security.KeyStore#getCertificateChain getCertificateChain()}, is used to
|
* {@link android.security.keystore.KeyGenParameterSpec.Builder#setAttestationChallenge(byte[])}
|
||||||
* generate a certificate chain that includes the public key (<code>Kpub</code> in the following)
|
* is used to generate a certificate chain that includes the public key (<code>Kpub</code> in the
|
||||||
* of the newly generated key.
|
* following) of the newly generated key.
|
||||||
* <li> The application sends <code>Kpub</code> and the certificate chain resulting from device
|
* <li> The application sends <code>Kpub</code> and the certificate chain resulting from device
|
||||||
* attestation to the <i>Relying Party</i>.
|
* attestation to the <i>Relying Party</i>.
|
||||||
* <li> The <i>Relying Party</i> validates the certificate chain which involves checking the root
|
* <li> The <i>Relying Party</i> validates the certificate chain which involves checking the root
|
||||||
@@ -78,9 +79,10 @@ import java.util.concurrent.Executor;
|
|||||||
* previously created nonce. If all checks passes, the transaction is executed.
|
* previously created nonce. If all checks passes, the transaction is executed.
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* <p>A common way of implementing the "<code>promptText</code> is what is expected" check in the
|
* <p>Note: It is vital to check the <code>promptText</code> because this is the only part that
|
||||||
* last bullet, is to have the <i>Relying Party</i> generate <code>promptText</code> and store it
|
* the user has approved. To avoid writing parsers for all of the possible locales, it is
|
||||||
* along the nonce in the <code>extraData</code> blob.
|
* recommended that the <i>Relying Party</i> uses the same string generator as used on the device
|
||||||
|
* and performs a simple string comparison.
|
||||||
*/
|
*/
|
||||||
public class ConfirmationPrompt {
|
public class ConfirmationPrompt {
|
||||||
private static final String TAG = "ConfirmationPrompt";
|
private static final String TAG = "ConfirmationPrompt";
|
||||||
@@ -92,6 +94,14 @@ public class ConfirmationPrompt {
|
|||||||
private Context mContext;
|
private Context mContext;
|
||||||
|
|
||||||
private final KeyStore mKeyStore = KeyStore.getInstance();
|
private final KeyStore mKeyStore = KeyStore.getInstance();
|
||||||
|
private AndroidProtectedConfirmation mProtectedConfirmation;
|
||||||
|
|
||||||
|
private AndroidProtectedConfirmation getService() {
|
||||||
|
if (mProtectedConfirmation == null) {
|
||||||
|
mProtectedConfirmation = new AndroidProtectedConfirmation();
|
||||||
|
}
|
||||||
|
return mProtectedConfirmation;
|
||||||
|
}
|
||||||
|
|
||||||
private void doCallback(int responseCode, byte[] dataThatWasConfirmed,
|
private void doCallback(int responseCode, byte[] dataThatWasConfirmed,
|
||||||
ConfirmationCallback callback) {
|
ConfirmationCallback callback) {
|
||||||
@@ -119,6 +129,32 @@ public class ConfirmationPrompt {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void doCallback2(int responseCode, byte[] dataThatWasConfirmed,
|
||||||
|
ConfirmationCallback callback) {
|
||||||
|
switch (responseCode) {
|
||||||
|
case AndroidProtectedConfirmation.ERROR_OK:
|
||||||
|
callback.onConfirmed(dataThatWasConfirmed);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AndroidProtectedConfirmation.ERROR_CANCELED:
|
||||||
|
callback.onDismissed();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AndroidProtectedConfirmation.ERROR_ABORTED:
|
||||||
|
callback.onCanceled();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AndroidProtectedConfirmation.ERROR_SYSTEM_ERROR:
|
||||||
|
callback.onError(new Exception("System error returned by ConfirmationUI."));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
callback.onError(new Exception("Unexpected responseCode=" + responseCode
|
||||||
|
+ " from onConfirmtionPromptCompleted() callback."));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final android.os.IBinder mCallbackBinder =
|
private final android.os.IBinder mCallbackBinder =
|
||||||
new android.security.IConfirmationPromptCallback.Stub() {
|
new android.security.IConfirmationPromptCallback.Stub() {
|
||||||
@Override
|
@Override
|
||||||
@@ -144,6 +180,29 @@ public class ConfirmationPrompt {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private final android.security.apc.IConfirmationCallback mConfirmationCallback =
|
||||||
|
new android.security.apc.IConfirmationCallback.Stub() {
|
||||||
|
@Override
|
||||||
|
public void onCompleted(int result, byte[] dataThatWasConfirmed)
|
||||||
|
throws android.os.RemoteException {
|
||||||
|
if (mCallback != null) {
|
||||||
|
ConfirmationCallback callback = mCallback;
|
||||||
|
Executor executor = mExecutor;
|
||||||
|
mCallback = null;
|
||||||
|
mExecutor = null;
|
||||||
|
if (executor == null) {
|
||||||
|
doCallback2(result, dataThatWasConfirmed, callback);
|
||||||
|
} else {
|
||||||
|
executor.execute(new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
doCallback2(result, dataThatWasConfirmed, callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A builder that collects arguments, to be shown on the system-provided confirmation prompt.
|
* A builder that collects arguments, to be shown on the system-provided confirmation prompt.
|
||||||
*/
|
*/
|
||||||
@@ -211,6 +270,9 @@ public class ConfirmationPrompt {
|
|||||||
private static final int UI_OPTION_ACCESSIBILITY_MAGNIFIED_FLAG = 1 << 1;
|
private static final int UI_OPTION_ACCESSIBILITY_MAGNIFIED_FLAG = 1 << 1;
|
||||||
|
|
||||||
private int getUiOptionsAsFlags() {
|
private int getUiOptionsAsFlags() {
|
||||||
|
if (AndroidKeyStoreProvider.isKeystore2Enabled()) {
|
||||||
|
return getUiOptionsAsFlags2();
|
||||||
|
}
|
||||||
int uiOptionsAsFlags = 0;
|
int uiOptionsAsFlags = 0;
|
||||||
ContentResolver contentResolver = mContext.getContentResolver();
|
ContentResolver contentResolver = mContext.getContentResolver();
|
||||||
int inversionEnabled = Settings.Secure.getInt(contentResolver,
|
int inversionEnabled = Settings.Secure.getInt(contentResolver,
|
||||||
@@ -226,6 +288,22 @@ public class ConfirmationPrompt {
|
|||||||
return uiOptionsAsFlags;
|
return uiOptionsAsFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int getUiOptionsAsFlags2() {
|
||||||
|
int uiOptionsAsFlags = 0;
|
||||||
|
ContentResolver contentResolver = mContext.getContentResolver();
|
||||||
|
int inversionEnabled = Settings.Secure.getInt(contentResolver,
|
||||||
|
Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, 0);
|
||||||
|
if (inversionEnabled == 1) {
|
||||||
|
uiOptionsAsFlags |= AndroidProtectedConfirmation.FLAG_UI_OPTION_INVERTED;
|
||||||
|
}
|
||||||
|
float fontScale = Settings.System.getFloat(contentResolver,
|
||||||
|
Settings.System.FONT_SCALE, (float) 1.0);
|
||||||
|
if (fontScale > 1.0) {
|
||||||
|
uiOptionsAsFlags |= AndroidProtectedConfirmation.FLAG_UI_OPTION_MAGNIFIED;
|
||||||
|
}
|
||||||
|
return uiOptionsAsFlags;
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean isAccessibilityServiceRunning(Context context) {
|
private static boolean isAccessibilityServiceRunning(Context context) {
|
||||||
boolean serviceRunning = false;
|
boolean serviceRunning = false;
|
||||||
try {
|
try {
|
||||||
@@ -270,29 +348,53 @@ public class ConfirmationPrompt {
|
|||||||
mCallback = callback;
|
mCallback = callback;
|
||||||
mExecutor = executor;
|
mExecutor = executor;
|
||||||
|
|
||||||
int uiOptionsAsFlags = getUiOptionsAsFlags();
|
|
||||||
String locale = Locale.getDefault().toLanguageTag();
|
String locale = Locale.getDefault().toLanguageTag();
|
||||||
int responseCode = mKeyStore.presentConfirmationPrompt(
|
if (AndroidKeyStoreProvider.isKeystore2Enabled()) {
|
||||||
mCallbackBinder, mPromptText.toString(), mExtraData, locale, uiOptionsAsFlags);
|
int uiOptionsAsFlags = getUiOptionsAsFlags2();
|
||||||
switch (responseCode) {
|
int responseCode = getService().presentConfirmationPrompt(
|
||||||
case KeyStore.CONFIRMATIONUI_OK:
|
mConfirmationCallback, mPromptText.toString(), mExtraData, locale,
|
||||||
return;
|
uiOptionsAsFlags);
|
||||||
|
switch (responseCode) {
|
||||||
|
case AndroidProtectedConfirmation.ERROR_OK:
|
||||||
|
return;
|
||||||
|
|
||||||
case KeyStore.CONFIRMATIONUI_OPERATION_PENDING:
|
case AndroidProtectedConfirmation.ERROR_OPERATION_PENDING:
|
||||||
throw new ConfirmationAlreadyPresentingException();
|
throw new ConfirmationAlreadyPresentingException();
|
||||||
|
|
||||||
case KeyStore.CONFIRMATIONUI_UNIMPLEMENTED:
|
case AndroidProtectedConfirmation.ERROR_UNIMPLEMENTED:
|
||||||
throw new ConfirmationNotAvailableException();
|
throw new ConfirmationNotAvailableException();
|
||||||
|
|
||||||
case KeyStore.CONFIRMATIONUI_UIERROR:
|
default:
|
||||||
throw new IllegalArgumentException();
|
// Unexpected error code.
|
||||||
|
Log.w(TAG,
|
||||||
|
"Unexpected responseCode=" + responseCode
|
||||||
|
+ " from presentConfirmationPrompt() call.");
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int uiOptionsAsFlags = getUiOptionsAsFlags();
|
||||||
|
int responseCode = mKeyStore.presentConfirmationPrompt(
|
||||||
|
mCallbackBinder, mPromptText.toString(), mExtraData, locale, uiOptionsAsFlags);
|
||||||
|
switch (responseCode) {
|
||||||
|
case KeyStore.CONFIRMATIONUI_OK:
|
||||||
|
return;
|
||||||
|
|
||||||
default:
|
case KeyStore.CONFIRMATIONUI_OPERATION_PENDING:
|
||||||
// Unexpected error code.
|
throw new ConfirmationAlreadyPresentingException();
|
||||||
Log.w(TAG,
|
|
||||||
"Unexpected responseCode=" + responseCode
|
case KeyStore.CONFIRMATIONUI_UNIMPLEMENTED:
|
||||||
+ " from presentConfirmationPrompt() call.");
|
throw new ConfirmationNotAvailableException();
|
||||||
throw new IllegalArgumentException();
|
|
||||||
|
case KeyStore.CONFIRMATIONUI_UIERROR:
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Unexpected error code.
|
||||||
|
Log.w(TAG,
|
||||||
|
"Unexpected responseCode=" + responseCode
|
||||||
|
+ " from presentConfirmationPrompt() call.");
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,17 +408,33 @@ public class ConfirmationPrompt {
|
|||||||
* @throws IllegalStateException if no prompt is currently being presented.
|
* @throws IllegalStateException if no prompt is currently being presented.
|
||||||
*/
|
*/
|
||||||
public void cancelPrompt() {
|
public void cancelPrompt() {
|
||||||
int responseCode = mKeyStore.cancelConfirmationPrompt(mCallbackBinder);
|
if (AndroidKeyStoreProvider.isKeystore2Enabled()) {
|
||||||
if (responseCode == KeyStore.CONFIRMATIONUI_OK) {
|
int responseCode =
|
||||||
return;
|
getService().cancelConfirmationPrompt(mConfirmationCallback);
|
||||||
} else if (responseCode == KeyStore.CONFIRMATIONUI_OPERATION_PENDING) {
|
if (responseCode == AndroidProtectedConfirmation.ERROR_OK) {
|
||||||
throw new IllegalStateException();
|
return;
|
||||||
|
} else if (responseCode == AndroidProtectedConfirmation.ERROR_OPERATION_PENDING) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
} else {
|
||||||
|
// Unexpected error code.
|
||||||
|
Log.w(TAG,
|
||||||
|
"Unexpected responseCode=" + responseCode
|
||||||
|
+ " from cancelConfirmationPrompt() call.");
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Unexpected error code.
|
int responseCode = mKeyStore.cancelConfirmationPrompt(mCallbackBinder);
|
||||||
Log.w(TAG,
|
if (responseCode == KeyStore.CONFIRMATIONUI_OK) {
|
||||||
"Unexpected responseCode=" + responseCode
|
return;
|
||||||
+ " from cancelConfirmationPrompt() call.");
|
} else if (responseCode == KeyStore.CONFIRMATIONUI_OPERATION_PENDING) {
|
||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
|
} else {
|
||||||
|
// Unexpected error code.
|
||||||
|
Log.w(TAG,
|
||||||
|
"Unexpected responseCode=" + responseCode
|
||||||
|
+ " from cancelConfirmationPrompt() call.");
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,6 +448,9 @@ public class ConfirmationPrompt {
|
|||||||
if (isAccessibilityServiceRunning(context)) {
|
if (isAccessibilityServiceRunning(context)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (AndroidKeyStoreProvider.isKeystore2Enabled()) {
|
||||||
|
return new AndroidProtectedConfirmation().isConfirmationPromptSupported();
|
||||||
|
}
|
||||||
return KeyStore.getInstance().isConfirmationPromptSupported();
|
return KeyStore.getInstance().isConfirmationPromptSupported();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
118
keystore/java/android/security/AndroidProtectedConfirmation.java
Normal file
118
keystore/java/android/security/AndroidProtectedConfirmation.java
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.security;
|
||||||
|
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.os.ServiceManager;
|
||||||
|
import android.os.ServiceSpecificException;
|
||||||
|
import android.security.apc.IConfirmationCallback;
|
||||||
|
import android.security.apc.IProtectedConfirmation;
|
||||||
|
import android.security.apc.ResponseCode;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
public class AndroidProtectedConfirmation {
|
||||||
|
private static final String TAG = "AndroidProtectedConfirmation";
|
||||||
|
|
||||||
|
public static final int ERROR_OK = ResponseCode.OK;
|
||||||
|
public static final int ERROR_CANCELED = ResponseCode.CANCELLED;
|
||||||
|
public static final int ERROR_ABORTED = ResponseCode.ABORTED;
|
||||||
|
public static final int ERROR_OPERATION_PENDING = ResponseCode.OPERATION_PENDING;
|
||||||
|
public static final int ERROR_IGNORED = ResponseCode.IGNORED;
|
||||||
|
public static final int ERROR_SYSTEM_ERROR = ResponseCode.SYSTEM_ERROR;
|
||||||
|
public static final int ERROR_UNIMPLEMENTED = ResponseCode.UNIMPLEMENTED;
|
||||||
|
|
||||||
|
public static final int FLAG_UI_OPTION_INVERTED =
|
||||||
|
IProtectedConfirmation.FLAG_UI_OPTION_INVERTED;
|
||||||
|
public static final int FLAG_UI_OPTION_MAGNIFIED =
|
||||||
|
IProtectedConfirmation.FLAG_UI_OPTION_MAGNIFIED;
|
||||||
|
|
||||||
|
private IProtectedConfirmation mProtectedConfirmation;
|
||||||
|
|
||||||
|
public AndroidProtectedConfirmation() {
|
||||||
|
mProtectedConfirmation = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized IProtectedConfirmation getService() {
|
||||||
|
if (mProtectedConfirmation == null) {
|
||||||
|
mProtectedConfirmation = IProtectedConfirmation.Stub.asInterface(ServiceManager
|
||||||
|
.getService("android.security.apc"));
|
||||||
|
}
|
||||||
|
return mProtectedConfirmation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests keystore call into the confirmationui HAL to display a prompt.
|
||||||
|
*
|
||||||
|
* @param listener the binder to use for callbacks.
|
||||||
|
* @param promptText the prompt to display.
|
||||||
|
* @param extraData extra data / nonce from application.
|
||||||
|
* @param locale the locale as a BCP 47 language tag.
|
||||||
|
* @param uiOptionsAsFlags the UI options to use, as flags.
|
||||||
|
* @return one of the {@code CONFIRMATIONUI_*} constants, for
|
||||||
|
* example {@code KeyStore.CONFIRMATIONUI_OK}.
|
||||||
|
*/
|
||||||
|
public int presentConfirmationPrompt(IConfirmationCallback listener, String promptText,
|
||||||
|
byte[] extraData, String locale, int uiOptionsAsFlags) {
|
||||||
|
try {
|
||||||
|
getService().presentPrompt(listener, promptText, extraData, locale,
|
||||||
|
uiOptionsAsFlags);
|
||||||
|
return ERROR_OK;
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.w(TAG, "Cannot connect to keystore", e);
|
||||||
|
return ERROR_SYSTEM_ERROR;
|
||||||
|
} catch (ServiceSpecificException e) {
|
||||||
|
return e.errorCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests keystore call into the confirmationui HAL to cancel displaying a prompt.
|
||||||
|
*
|
||||||
|
* @param listener the binder passed to the {@link #presentConfirmationPrompt} method.
|
||||||
|
* @return one of the {@code CONFIRMATIONUI_*} constants, for
|
||||||
|
* example {@code KeyStore.CONFIRMATIONUI_OK}.
|
||||||
|
*/
|
||||||
|
public int cancelConfirmationPrompt(IConfirmationCallback listener) {
|
||||||
|
try {
|
||||||
|
getService().cancelPrompt(listener);
|
||||||
|
return ERROR_OK;
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.w(TAG, "Cannot connect to keystore", e);
|
||||||
|
return ERROR_SYSTEM_ERROR;
|
||||||
|
} catch (ServiceSpecificException e) {
|
||||||
|
return e.errorCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests keystore to check if the confirmationui HAL is available.
|
||||||
|
*
|
||||||
|
* @return whether the confirmationUI HAL is available.
|
||||||
|
*/
|
||||||
|
public boolean isConfirmationPromptSupported() {
|
||||||
|
try {
|
||||||
|
return getService().isSupported();
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.w(TAG, "Cannot connect to keystore", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user