/*
* Copyright 2022 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 androidx.credentials.provider
import android.app.Activity
import android.app.PendingIntent
import android.content.Intent
import android.service.credentials.BeginCreateCredentialResponse
import android.service.credentials.CreateCredentialRequest
import android.service.credentials.CredentialEntry
import android.service.credentials.CredentialProviderService
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.credentials.CreateCredentialResponse
import androidx.credentials.CredentialOption
import androidx.credentials.GetCredentialResponse
import androidx.credentials.exceptions.CreateCredentialException
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.provider.utils.BeginGetCredentialUtil
import java.util.stream.Collectors
/**
* PendingIntentHandler to be used by credential providers to extract requests from a given intent,
* or to set back a response or an exception to a given intent while dealing with activities
* invoked by pending intents set on a [CreateEntry] for the create flow, or on a
* [CredentialEntry], [AuthenticationAction], [Action], or a [RemoteEntry] set for a get flow.
*
* When user selects one of the entries mentioned above, the credential provider's corresponding
* activity is invoked. The intent associated with this activity must be extracted and passed
* into the utils in this class to extract the required requests.
*
* When user interaction is complete, credential providers must set the activity result by calling
* [android.app.Activity.setResult] by setting an appropriate result code and data of type
* [Intent]. This data should also be prepared by using the utils in this class to populate
* the required response/exception.
*
* See extension functions for [Intent] in IntentHandlerConverters.kt to help test intents that are
* set on pending intents in different entry classes.
*/
@RequiresApi(34)
class PendingIntentHandler {
companion object {
private const val TAG = "PendingIntentHandler"
/**
* Extracts the [ProviderCreateCredentialRequest] from the provider's
* [PendingIntent] invoked by the Android system.
*
* @param intent the intent associated with the [Activity] invoked through the
* [PendingIntent]
*
* @throws NullPointerException If [intent] is null
*/
@JvmStatic
fun retrieveProviderCreateCredentialRequest(intent: Intent):
ProviderCreateCredentialRequest? {
val frameworkReq: CreateCredentialRequest? =
intent.getParcelableExtra(
CredentialProviderService
.EXTRA_CREATE_CREDENTIAL_REQUEST, CreateCredentialRequest::class.java
)
if (frameworkReq == null) {
Log.i(TAG, "Request not found in pendingIntent")
return frameworkReq
}
return ProviderCreateCredentialRequest(
androidx.credentials.CreateCredentialRequest
.createFrom(
frameworkReq.type,
frameworkReq.data,
frameworkReq.data,
requireSystemProvider = false,
frameworkReq.callingAppInfo.origin
) ?: return null,
CallingAppInfo(
frameworkReq.callingAppInfo.packageName,
frameworkReq.callingAppInfo.signingInfo,
frameworkReq.callingAppInfo.origin
)
)
}
/**
* Extracts the [BeginGetCredentialRequest] from the provider's
* [PendingIntent] invoked by the Android system when the user
* selects an [AuthenticationAction].
*
* @param intent the intent associated with the [Activity] invoked through the
* [PendingIntent]
*
* @throws NullPointerException If [intent] is null
*/
@JvmStatic
fun retrieveBeginGetCredentialRequest(intent: Intent): BeginGetCredentialRequest? {
val request = intent.getParcelableExtra(
"android.service.credentials.extra.BEGIN_GET_CREDENTIAL_REQUEST",
android.service.credentials.BeginGetCredentialRequest::class.java
)
return request?.let { BeginGetCredentialUtil.convertToJetpackRequest(it) }
}
/**
* Sets the [CreateCredentialResponse] on the intent passed in. This intent is then
* set as the data associated with the result of the activity invoked by the
* [PendingIntent] set on a [CreateEntry]. The intent is set using the
* [Activity.setResult] method that takes in the intent, as well as a result code.
*
* A credential provider must set the result code to [Activity.RESULT_OK] if a valid
* response, or a valid exception is being set as the data to the result. However,
* if the credential provider is unable to resolve to a valid response or exception,
* the result code must be set to [Activity.RESULT_CANCELED]. Note that setting the
* result code to [Activity.RESULT_CANCELED] will re-surface the account selection
* bottom sheet that displayed the original [CredentialEntry], hence allowing the user
* to re-select.
*
* @param intent the intent to be set on the result of the [Activity] invoked through the
* [PendingIntent]
* @param response the response to be set as an extra on the [intent]
*
* @throws NullPointerException If [intent], or [response] is null
*/
@JvmStatic
fun setCreateCredentialResponse(
intent: Intent,
response: CreateCredentialResponse
) {
intent.putExtra(
CredentialProviderService.EXTRA_CREATE_CREDENTIAL_RESPONSE,
android.credentials.CreateCredentialResponse(response.data)
)
}
/**
* Extracts the [ProviderGetCredentialRequest] from the provider's
* [PendingIntent] invoked by the Android system, when the user selects a
* [CredentialEntry].
*
* @param intent the intent associated with the [Activity] invoked through the
* [PendingIntent]
*
* @throws NullPointerException If [intent] is null
*/
@JvmStatic
fun retrieveProviderGetCredentialRequest(intent: Intent):
ProviderGetCredentialRequest? {
val frameworkReq = intent.getParcelableExtra(
CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST,
android.service.credentials.GetCredentialRequest::class.java
)
if (frameworkReq == null) {
Log.i(TAG, "Get request from framework is null")
return null
}
return ProviderGetCredentialRequest.createFrom(
frameworkReq.credentialOptions.stream()
.map { option ->
CredentialOption.createFrom(
option.type,
option.credentialRetrievalData,
option.candidateQueryData,
option.isSystemProviderRequired,
option.allowedProviders,
)
}
.collect(Collectors.toList()),
CallingAppInfo(
frameworkReq.callingAppInfo.packageName,
frameworkReq.callingAppInfo.signingInfo,
frameworkReq.callingAppInfo.origin
)
)
}
/**
* Sets the [android.credentials.GetCredentialResponse] on the intent passed in. This
* intent is then set as the data associated with the result of the activity invoked by
* the [PendingIntent], set on a [CredentialEntry]. The intent is set using the
* [Activity.setResult] method that takes in the intent, as well as a result code.
*
* A credential provider must set the result code to [Activity.RESULT_OK] if a valid
* credential, or a valid exception is being set as the data to the result. However,
* if the credential provider is unable to resolve to a valid response or exception,
* the result code must be set to [Activity.RESULT_CANCELED]. Note that setting the
* result code to [Activity.RESULT_CANCELED] will re-surface the account selection
* bottom sheet that displayed the original [CredentialEntry], hence allowing the user
* to re-select.
*
* @param intent the intent to be set on the result of the [Activity] invoked through the
* [PendingIntent]
* @param response the response to be set as an extra on the [intent]
*
* @throws NullPointerException If [intent], or [response] is null
*/
@JvmStatic
fun setGetCredentialResponse(
intent: Intent,
response: GetCredentialResponse
) {
intent.putExtra(
CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE,
android.credentials.GetCredentialResponse(
android.credentials.Credential(
response.credential.type,
response.credential.data
)
)
)
}
/**
* Sets the [android.service.credentials.BeginGetCredentialResponse] on the intent passed
* in. This intent is then set as the data associated with the result of the activity
* invoked by the [PendingIntent], set on an [AuthenticationAction]. The intent is set
* using the [Activity.setResult] method that takes in the intent, as well as a result code.
*
* A credential provider must set the result code to [Activity.RESULT_OK] if a valid
* response, or a valid exception is being set as part of the data to the result. However,
* if the credential provider is unable to resolve to a valid response or exception,
* the result code must be set to [Activity.RESULT_CANCELED]. Note that setting the
* result code to [Activity.RESULT_CANCELED] will re-surface the account selection
* bottom sheet that displayed the original [CredentialEntry], hence allowing the user to
* re-select.
*
* @param intent the intent to be set on the result of the [Activity] invoked through the
* [PendingIntent]
* @param response the response to be set as an extra on the [intent]
*
* @throws NullPointerException If [intent], or [response] is null
*/
@JvmStatic
fun setBeginGetCredentialResponse(
intent: Intent,
response: BeginGetCredentialResponse
) {
intent.putExtra(
CredentialProviderService.EXTRA_BEGIN_GET_CREDENTIAL_RESPONSE,
BeginGetCredentialUtil.convertToFrameworkResponse(response)
)
}
/**
* Sets the [androidx.credentials.exceptions.GetCredentialException] if an error is
* encountered during the final phase of the get credential flow.
*
* A credential provider service returns a list of [CredentialEntry] as part of
* the [BeginGetCredentialResponse] to the query phase of the get-credential flow.
* If the user selects one of these entries, the corresponding [PendingIntent]
* is fired and the provider's activity is invoked.
* If there is an error encountered during the lifetime of that activity, the provider
* must use this API to set an exception on the given intent before finishing the
* activity in question.
*
* The intent is set using the [Activity.setResult] method that takes in the intent,
* as well as a result code. A credential provider must set the result code to
* [Activity.RESULT_OK] if a valid credential, or a valid exception is being set as
* the data to the result. However, if the credential provider is unable to resolve to a
* valid response or exception, the result code must be set to [Activity.RESULT_CANCELED].
*
* Note that setting the result code to [Activity.RESULT_CANCELED] will re-surface the
* account selection bottom sheet that displayed the original [CredentialEntry], hence
* allowing the user to re-select.
*
* @param intent the intent to be set on the result of the [Activity] invoked through the
* [PendingIntent]
* @param exception the exception to be set as an extra to the [intent]
*
* @throws NullPointerException If [intent], or [exception] is null
*/
@JvmStatic
fun setGetCredentialException(
intent: Intent,
exception: GetCredentialException
) {
intent.putExtra(
CredentialProviderService.EXTRA_GET_CREDENTIAL_EXCEPTION,
android.credentials.GetCredentialException(exception.type, exception.message)
)
}
/**
* Sets the [androidx.credentials.exceptions.CreateCredentialException] if an error is
* encountered during the final phase of the create credential flow.
*
* A credential provider service returns a list of [CreateEntry] as part of
* the [BeginCreateCredentialResponse] to the query phase of the get-credential flow.
*
* If the user selects one of these entries, the corresponding [PendingIntent]
* is fired and the provider's activity is invoked. If there is an error encountered
* during the lifetime of that activity, the provider must use this API to set
* an exception before finishing the activity.
*
* The intent is set using the [Activity.setResult] method that takes in the intent,
* as well as a result code. A credential provider must set the result code to
* [Activity.RESULT_OK] if a valid credential, or a valid exception is being set as
* the data to the result. However, if the credential provider is unable to resolve to a
* valid response or exception, the result code must be set to [Activity.RESULT_CANCELED].
*
* Note that setting the result code to [Activity.RESULT_CANCELED] will re-surface the
* account selection bottom sheet that displayed the original [CreateEntry], hence allowing
* the user to re-select.
*
* @param intent the intent to be set on the result of the [Activity] invoked through the
* [PendingIntent]
* @param exception the exception to be set as an extra to the [intent]
*
* @throws NullPointerException If [intent], or [exception] is null
*/
@JvmStatic
fun setCreateCredentialException(
intent: Intent,
exception: CreateCredentialException
) {
intent.putExtra(
CredentialProviderService.EXTRA_CREATE_CREDENTIAL_EXCEPTION,
android.credentials.CreateCredentialException(exception.type, exception.message)
)
}
}
}