WatchFaceEditorContract.kt

/*
 * Copyright 2021 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.wear.watchface.editor

import android.annotation.SuppressLint
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.activity.result.contract.ActivityResultContract
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.wear.watchface.RenderParameters
import androidx.wear.watchface.client.DeviceConfig
import androidx.wear.watchface.client.EditorServiceClient
import androidx.wear.watchface.client.EditorState
import androidx.wear.watchface.client.WatchFaceControlClient
import androidx.wear.watchface.client.WatchFaceId
import androidx.wear.watchface.client.asApiDeviceConfig
import androidx.wear.watchface.data.DeviceConfig as WireDeviceConfig
import androidx.wear.watchface.data.RenderParametersWireFormat
import androidx.wear.watchface.style.UserStyle
import androidx.wear.watchface.style.UserStyleData
import java.time.Instant
import kotlinx.coroutines.TimeoutCancellationException

internal const val INSTANCE_ID_KEY: String = "INSTANCE_ID_KEY"
internal const val COMPONENT_NAME_KEY: String = "COMPONENT_NAME_KEY"
internal const val HEADLESS_DEVICE_CONFIG_KEY: String = "HEADLESS_DEVICE_CONFIG_KEY"
internal const val RENDER_PARAMETERS_KEY: String = "RENDER_PARAMETERS_KEY"
internal const val RENDER_TIME_MILLIS_KEY: String = "RENDER_TIME_MILLIS_KEY"
internal const val USER_STYLE_KEY: String = "USER_STYLE_KEY"
internal const val USER_STYLE_VALUES: String = "USER_STYLE_VALUES"

/**
 * Parameters for an optional final screenshot taken by [EditorSession] upon exit and reported via
 * [EditorState].
 *
 * @param renderParameters The [RenderParameters] to use when rendering the screen shot
 * @param instant The [Instant] to render with.
 */
public class PreviewScreenshotParams(
    public val renderParameters: RenderParameters,
    public val instant: Instant
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as PreviewScreenshotParams

        if (renderParameters != other.renderParameters) return false
        if (instant != other.instant) return false

        return true
    }

    override fun hashCode(): Int {
        var result = renderParameters.hashCode()
        result = 31 * result + instant.hashCode()
        return result
    }
}

/**
 * The request sent by [WatchFaceEditorContract.createIntent].
 *
 * @param watchFaceComponentName The [ComponentName] of the watch face being edited.
 * @param editorPackageName The package name of the watch face editor APK.
 * @param initialUserStyle The initial [UserStyle] stored as a [UserStyleData] or `null`. Only
 *   required for a headless [EditorSession].
 * @param watchFaceId Unique ID for the instance of the watch face being edited, only defined for
 *   Android R and beyond, it's `null` on Android P and earlier. Note each distinct [ComponentName]
 *   can have multiple instances.
 * @param headlessDeviceConfig If `non-null` then this is the [DeviceConfig] to use when creating a
 *   headless instance to back the [EditorSession]. If `null` then the current interactive instance
 *   will be used. If there isn't one then the [EditorSession] won't launch until it's been created.
 *   Note [supportsWatchFaceHeadlessEditing] can be used to determine if this feature is supported.
 *   If it's not supported this parameter will be ignored.
 * @param previewScreenshotParams If `non-null` then [EditorSession] upon closing will render a
 *   screenshot with [PreviewScreenshotParams] using the existing interactive or headless instance
 *   which will be sent in [EditorState] to any registered clients.
 */
public class EditorRequest
@RequiresApi(Build.VERSION_CODES.R)
constructor(
    public val watchFaceComponentName: ComponentName,
    public val editorPackageName: String,
    public val initialUserStyle: UserStyleData?,
    @get:RequiresApi(Build.VERSION_CODES.R)
    @RequiresApi(Build.VERSION_CODES.R)
    public val watchFaceId: WatchFaceId,
    public val headlessDeviceConfig: DeviceConfig?,
    public val previewScreenshotParams: PreviewScreenshotParams?
) {
    /**
     * Constructs an [EditorRequest] without a [WatchFaceId]. This is for use pre-android R.
     *
     * @param watchFaceComponentName The [ComponentName] of the watch face being edited.
     * @param editorPackageName The package name of the watch face editor APK.
     * @param initialUserStyle The initial [UserStyle] stored as a [UserStyleData] or `null`. Only
     *   required for a headless [EditorSession]. [EditorSession].
     */
    @SuppressLint("NewApi")
    public constructor(
        watchFaceComponentName: ComponentName,
        editorPackageName: String,
        initialUserStyle: UserStyleData?
    ) : this(
        watchFaceComponentName,
        editorPackageName,
        initialUserStyle,
        WatchFaceId(""),
        null,
        null
    )

    public companion object {
        /**
         * Returns an [EditorRequest] saved to a [Intent] by [WatchFaceEditorContract.createIntent]
         * if there is one or `null` otherwise. Intended for use by the watch face editor activity.
         *
         * @throws [TimeoutCancellationException] in case of en error.
         */
        @Suppress("DEPRECATION")
        @SuppressLint("NewApi")
        @JvmStatic
        @Throws(TimeoutCancellationException::class)
        public fun createFromIntent(intent: Intent): EditorRequest =
            EditorRequest(
                watchFaceComponentName =
                    intent.getParcelableExtra<ComponentName>(COMPONENT_NAME_KEY)!!,
                editorPackageName = intent.getPackage() ?: "",
                initialUserStyle =
                    intent.getStringArrayExtra(USER_STYLE_KEY)?.let {
                        UserStyleData(
                            HashMap<String, ByteArray>().apply {
                                for (i in it.indices) {
                                    val userStyleValue =
                                        intent.getByteArrayExtra(USER_STYLE_VALUES + i)!!
                                    put(it[i], userStyleValue)
                                }
                            }
                        )
                    },
                watchFaceId = WatchFaceId(intent.getStringExtra(INSTANCE_ID_KEY) ?: ""),
                headlessDeviceConfig =
                    intent
                        .getParcelableExtra<WireDeviceConfig>(HEADLESS_DEVICE_CONFIG_KEY)
                        ?.asApiDeviceConfig(),
                previewScreenshotParams =
                    intent
                        .getParcelableExtra<RenderParametersWireFormat>(RENDER_PARAMETERS_KEY)
                        ?.let {
                            PreviewScreenshotParams(
                                RenderParameters(it),
                                Instant.ofEpochMilli(intent.getLongExtra(RENDER_TIME_MILLIS_KEY, 0))
                            )
                        }
            )

        internal const val ANDROIDX_WATCHFACE_API_VERSION = "androidx.wear.watchface.api_version"
        internal const val WATCHFACE_CONTROL_SERVICE =
            "androidx.wear.watchface.control.WatchFaceControlService"

        /**
         * Intended to be used in conjunction with [EditorRequest], inspects the watchface's
         * manifest to determine whether or not it supports headless editing.
         *
         * @param packageManager The [PackageManager].
         * @param watchfacePackageName The package name of the watchface, see
         *   [ComponentName.getPackageName].
         * @throws [PackageManager.NameNotFoundException] if watchfacePackageName is not recognized.
         * @hide
         */
        @JvmStatic
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        @Suppress("DEPRECATION")
        @Throws(PackageManager.NameNotFoundException::class)
        public fun supportsWatchFaceHeadlessEditing(
            packageManager: PackageManager,
            watchfacePackageName: String
        ): Boolean {
            val metaData =
                packageManager
                    .getServiceInfo(
                        ComponentName(watchfacePackageName, WATCHFACE_CONTROL_SERVICE),
                        PackageManager.GET_META_DATA
                    )
                    .metaData
                    ?: return false
            return metaData.getInt(ANDROIDX_WATCHFACE_API_VERSION) >= 4
        }
    }
}

/**
 * An [ActivityResultContract] for invoking a watch face editor. Note watch face editors are invoked
 * by SysUI and the normal activity result isn't used for returning [EditorState] because
 * [Activity.onStop] isn't guaranteed to be called when SysUI UX needs it to. Instead [EditorState]
 * is broadcast by the editor using[EditorSession.close], to observe these broadcasts use
 * [WatchFaceControlClient.getEditorServiceClient] and [EditorServiceClient.addListener].
 */
public open class WatchFaceEditorContract : ActivityResultContract<EditorRequest, Unit>() {

    public companion object {
        public const val ACTION_WATCH_FACE_EDITOR: String =
            "androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR"
    }

    override fun createIntent(context: Context, input: EditorRequest): Intent {
        return Intent(ACTION_WATCH_FACE_EDITOR).apply {
            setPackage(input.editorPackageName)
            putExtra(COMPONENT_NAME_KEY, input.watchFaceComponentName)
            putExtra(INSTANCE_ID_KEY, input.watchFaceId.id)
            input.initialUserStyle?.let {
                putExtra(USER_STYLE_KEY, it.userStyleMap.keys.toTypedArray())
                for ((index, value) in it.userStyleMap.values.withIndex()) {
                    putExtra(USER_STYLE_VALUES + index, value)
                }
            }
            putExtra(HEADLESS_DEVICE_CONFIG_KEY, input.headlessDeviceConfig?.asWireDeviceConfig())
            input.previewScreenshotParams?.let {
                putExtra(RENDER_PARAMETERS_KEY, it.renderParameters.toWireFormat())
                putExtra(RENDER_TIME_MILLIS_KEY, it.instant.toEpochMilli())
            }
        }
    }

    override fun parseResult(resultCode: Int, intent: Intent?) {}
}