AuthenticationAction.kt

/*
 * 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.annotation.SuppressLint
import android.app.PendingIntent
import android.app.slice.Slice
import android.app.slice.SliceSpec
import android.net.Uri
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import java.util.Collections

/**
 * An entry on the selector, denoting that the provider service is locked and authentication
 * is needed to proceed.
 *
 * Providers should set this entry when the provider app is locked, and no credentials can
 * be returned.
 * Providers must set the [PendingIntent] that leads to their unlock activity. When the user
 * selects this entry, the corresponding [PendingIntent] is fired and the unlock activity is
 * invoked.
 *
 * When the user is done with the authentication flow and the provider has  credential entries
 * to return, provider must call [android.app.Activity.setResult] with the result code as
 * [android.app.Activity.RESULT_OK], and the [android.content.Intent] data that has been prepared
 * by setting [BeginGetCredentialResponse] using
 * [PendingIntentHandler.setBeginGetCredentialResponse], or by setting
 * [androidx.credentials.exceptions.GetCredentialException] using
 * [PendingIntentHandler.setGetCredentialException] before ending the activity.
 * If the provider does not have a credential, or an exception to return, provider must call
 * [android.app.Activity.setResult] with the result code as [android.app.Activity.RESULT_CANCELED].
 * Setting the result code to [android.app.Activity.RESULT_CANCELED] will re-surface the selector,
 * with this authentication action labeled as having no valid credentials.
 *
 * @constructor constructs an instance of [AuthenticationAction]
 *
 * @param title the title to be shown with this entry on the account selector UI
 * @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
 * entry, must be created with a unique request code per entry,
 * with flag [PendingIntent.FLAG_MUTABLE] to allow the Android system to attach the
 * final request, and NOT with flag [PendingIntent.FLAG_ONE_SHOT] as it can be invoked multiple
 * times
 *
 * @see android.service.credentials.BeginGetCredentialResponse
 * for more usage details.
 *
 * @throws NullPointerException If the [pendingIntent] or [title] is null
 * @throws IllegalArgumentException If the [title] is empty
 */
class AuthenticationAction constructor(
    val title: CharSequence,
    val pendingIntent: PendingIntent,
) {
    init {
        require(title.isNotEmpty()) { "title must not be empty" }
    }

    /**
     * A builder for [AuthenticationAction]
     *
     * @param title the title to be displayed with this authentication action entry
     * @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
     * entry, must be created with a unique request code per entry,
     * with flag [PendingIntent.FLAG_MUTABLE] to allow the Android system to attach the
     * final request, and NOT with flag [PendingIntent.FLAG_ONE_SHOT] as it can be invoked multiple
     * times
     */
    class Builder constructor(
        private val title: CharSequence,
        private val pendingIntent: PendingIntent
    ) {
        /**
         * Builds an instance of [AuthenticationAction]
         */
        fun build(): AuthenticationAction {
            return AuthenticationAction(title, pendingIntent)
        }
    }

    internal companion object {
        private const val TAG = "AuthenticationAction"
        private const val SLICE_SPEC_REVISION = 0
        private const val SLICE_SPEC_TYPE = "AuthenticationAction"

        private const val SLICE_HINT_TITLE =
            "androidx.credentials.provider.authenticationAction.SLICE_HINT_TITLE"

        private const val SLICE_HINT_PENDING_INTENT =
            "androidx.credentials.provider.authenticationAction.SLICE_HINT_PENDING_INTENT"

        @RestrictTo(RestrictTo.Scope.LIBRARY)
        @RequiresApi(28)
        @JvmStatic
        fun toSlice(authenticationAction: AuthenticationAction): Slice {
            val title = authenticationAction.title
            val pendingIntent = authenticationAction.pendingIntent
            val sliceBuilder = Slice.Builder(
                Uri.EMPTY, SliceSpec(
                    SLICE_SPEC_TYPE,
                    SLICE_SPEC_REVISION
                )
            )
            sliceBuilder
                .addAction(
                    pendingIntent,
                    Slice.Builder(sliceBuilder)
                        .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
                        .build(),
                    /*subType=*/null
                )
                .addText(title, /*subType=*/null, listOf(SLICE_HINT_TITLE))
            return sliceBuilder.build()
        }

        /**
         * Returns an instance of [AuthenticationAction] derived from a [Slice] object.
         *
         * @param slice the [Slice] object that contains the information required for
         * constructing an instance of this class.
         *
         */
        @RestrictTo(RestrictTo.Scope.LIBRARY)
        @RequiresApi(28)
        @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
        @JvmStatic
        fun fromSlice(slice: Slice): AuthenticationAction? {
            var title: CharSequence? = null
            var pendingIntent: PendingIntent? = null

            slice.items.forEach {
                if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
                    pendingIntent = it.action
                } else if (it.hasHint(SLICE_HINT_TITLE)) {
                    title = it.text
                }
            }
            return try {
                AuthenticationAction(title!!, pendingIntent!!)
            } catch (e: Exception) {
                Log.i(TAG, "fromSlice failed with: " + e.message)
                null
            }
        }
    }
}