1 /*
2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.credentials.playservices.controllers
18 
19 import android.app.Activity
20 import android.content.Context
21 import android.os.Bundle
22 import android.os.CancellationSignal
23 import androidx.credentials.CredentialManagerCallback
24 import androidx.credentials.exceptions.CreateCredentialCancellationException
25 import androidx.credentials.exceptions.CreateCredentialException
26 import androidx.credentials.exceptions.CreateCredentialUnknownException
27 import androidx.credentials.exceptions.GetCredentialCancellationException
28 import androidx.credentials.exceptions.GetCredentialException
29 import androidx.credentials.exceptions.GetCredentialUnknownException
30 import androidx.credentials.playservices.CredentialProviderPlayServicesImpl
31 import java.util.concurrent.Executor
32 
33 /**
34  * Extensible abstract class for credential controllers. Please implement this class per every
35  * request/response credential type. Unique logic is left to the use case of the implementation. If
36  * you are building your own version as an OEM, the below can be mimicked to your own credential
37  * provider equivalent and whatever internal service you invoke.
38  *
39  * @param T1 the credential request type from credential manager
40  * @param T2 the credential request type converted to play services
41  * @param R2 the credential response type from play services
42  * @param R1 the credential response type converted back to that used by credential manager
43  * @param E1 the credential error type to throw
44  */
45 @Suppress("deprecation")
46 internal abstract class CredentialProviderController<
47     T1 : Any,
48     T2 : Any,
49     R2 : Any,
50     R1 : Any,
51     E1 : Any
52 >(private val context: Context) : CredentialProviderBaseController(context) {
53 
54     companion object {
55 
56         internal const val ERROR_MESSAGE_START_ACTIVITY_FAILED =
57             "Failed to launch the selector UI. Hint: ensure the `context` parameter is an" +
58                 " Activity-based context."
59 
60         /**
61          * This handles result code exception reporting across all create flows.
62          *
63          * @return a boolean indicating if the create flow contains a result code exception
64          */
65         @JvmStatic
maybeReportErrorResultCodeCreatenull66         protected fun maybeReportErrorResultCodeCreate(
67             resultCode: Int,
68             cancelOnError: (CancellationSignal?, () -> Unit) -> Unit,
69             onError: (CreateCredentialException) -> Unit,
70             cancellationSignal: CancellationSignal?
71         ): Boolean {
72             if (resultCode != Activity.RESULT_OK) {
73                 var exception: CreateCredentialException =
74                     CreateCredentialUnknownException(generateErrorStringUnknown(resultCode))
75                 if (resultCode == Activity.RESULT_CANCELED) {
76                     exception = CreateCredentialCancellationException(generateErrorStringCanceled())
77                 }
78                 cancelOnError(cancellationSignal) { onError(exception) }
79                 return true
80             }
81             return false
82         }
83 
generateErrorStringUnknownnull84         internal fun generateErrorStringUnknown(resultCode: Int): String {
85             return "activity with result code: $resultCode indicating not RESULT_OK"
86         }
87 
generateErrorStringCancelednull88         internal fun generateErrorStringCanceled(): String {
89             return "activity is cancelled by the user."
90         }
91 
92         /**
93          * This allows catching result code errors from the get flow if they exist.
94          *
95          * @return a boolean indicating if the get flow had an error
96          */
97         @JvmStatic
maybeReportErrorResultCodeGetnull98         protected fun maybeReportErrorResultCodeGet(
99             resultCode: Int,
100             cancelOnError: (CancellationSignal?, () -> Unit) -> Unit,
101             onError: (GetCredentialException) -> Unit,
102             cancellationSignal: CancellationSignal?
103         ): Boolean {
104             if (resultCode != Activity.RESULT_OK) {
105                 var exception: GetCredentialException =
106                     GetCredentialUnknownException(generateErrorStringUnknown(resultCode))
107                 if (resultCode == Activity.RESULT_CANCELED) {
108                     exception = GetCredentialCancellationException(generateErrorStringCanceled())
109                 }
110                 cancelOnError(cancellationSignal) { onError(exception) }
111                 return true
112             }
113             return false
114         }
115 
116         /**
117          * This will check for cancellation, and will otherwise set a result to the callback, or an
118          * exception.
119          */
120         @JvmStatic
cancelOrCallbackExceptionOrResultnull121         protected fun cancelOrCallbackExceptionOrResult(
122             cancellationSignal: CancellationSignal?,
123             onResultOrException: () -> Unit
124         ) {
125             if (CredentialProviderPlayServicesImpl.cancellationReviewer(cancellationSignal)) {
126                 return
127             }
128             onResultOrException()
129         }
130     }
131 
132     /**
133      * To avoid redundant logic across all controllers for exceptions parceled back from the hidden
134      * activity, this can be generally implemented.
135      *
136      * @return a boolean indicating if an error was reported or not by the result receiver
137      */
maybeReportErrorFromResultReceivernull138     protected fun maybeReportErrorFromResultReceiver(
139         resultData: Bundle,
140         conversionFn: (String?, String?) -> E1,
141         executor: Executor,
142         callback: CredentialManagerCallback<R1, E1>,
143         cancellationSignal: CancellationSignal?
144     ): Boolean {
145         val isError = resultData.getBoolean(FAILURE_RESPONSE_TAG)
146         if (!isError) {
147             return false
148         }
149         val errType = resultData.getString(EXCEPTION_TYPE_TAG)
150         val errMsg = resultData.getString(EXCEPTION_MESSAGE_TAG)
151         val exception = conversionFn(errType, errMsg)
152         cancelOrCallbackExceptionOrResult(
153             cancellationSignal = cancellationSignal,
154             onResultOrException = { executor.execute { callback.onError(exception) } }
155         )
156         return true
157     }
158 
159     /**
160      * Invokes the flow that starts retrieving credential data. In this use case, we invoke play
161      * service modules.
162      *
163      * @param request a credential provider request
164      * @param callback a credential manager callback with a credential provider response
165      * @param executor to be used in any multi-threaded operation calls, such as listenable futures
166      */
invokePlayServicesnull167     abstract fun invokePlayServices(
168         request: T1,
169         callback: CredentialManagerCallback<R1, E1>,
170         executor: Executor,
171         cancellationSignal: CancellationSignal?
172     )
173 
174     /**
175      * Allows converting from a credential provider request to a play service request.
176      *
177      * @param request a credential provider request
178      * @return a play service request
179      */
180     protected abstract fun convertRequestToPlayServices(request: T1): T2
181 
182     /**
183      * Allows converting from a play service response to a credential provider response.
184      *
185      * @param response a play service response
186      * @return a credential provider response
187      */
188     protected abstract fun convertResponseToCredentialManager(response: R2): R1
189 }
190