WorkTypeConverters.kt

/*
 * Copyright 2018 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.work.impl.model

import android.net.Uri
import android.os.Build
import androidx.room.TypeConverter
import androidx.work.BackoffPolicy
import androidx.work.ContentUriTriggers
import androidx.work.NetworkType
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkInfo
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.lang.IllegalArgumentException

/**
 * TypeConverters for WorkManager enums and classes.
 */
object WorkTypeConverters {
    /**
     * Integer identifiers that map to [WorkInfo.State].
     */
    object StateIds {
        const val ENQUEUED = 0
        const val RUNNING = 1
        const val SUCCEEDED = 2
        const val FAILED = 3
        const val BLOCKED = 4
        const val CANCELLED = 5
        const val COMPLETED_STATES = "($SUCCEEDED, $FAILED, $CANCELLED)"
    }

    /**
     * Integer identifiers that map to [BackoffPolicy].
     */
    private object BackoffPolicyIds {
        const val EXPONENTIAL = 0
        const val LINEAR = 1
    }

    /**
     * Integer identifiers that map to [NetworkType].
     */
    private object NetworkTypeIds {
        const val NOT_REQUIRED = 0
        const val CONNECTED = 1
        const val UNMETERED = 2
        const val NOT_ROAMING = 3
        const val METERED = 4
        const val TEMPORARILY_UNMETERED = 5
    }

    /**
     * Integer identifiers that map to [OutOfQuotaPolicy].
     */
    private object OutOfPolicyIds {
        const val RUN_AS_NON_EXPEDITED_WORK_REQUEST = 0
        const val DROP_WORK_REQUEST = 1
    }

    /**
     * TypeConverter for a State to an int.
     *
     * @param state The input State
     * @return The associated int constant
     */
    @JvmStatic
    @TypeConverter
    fun stateToInt(state: WorkInfo.State): Int {
        return when (state) {
            WorkInfo.State.ENQUEUED -> StateIds.ENQUEUED
            WorkInfo.State.RUNNING -> StateIds.RUNNING
            WorkInfo.State.SUCCEEDED -> StateIds.SUCCEEDED
            WorkInfo.State.FAILED -> StateIds.FAILED
            WorkInfo.State.BLOCKED -> StateIds.BLOCKED
            WorkInfo.State.CANCELLED -> StateIds.CANCELLED
        }
    }

    /**
     * TypeConverter for an int to a State.
     *
     * @param value The input integer
     * @return The associated State enum value
     */
    @JvmStatic
    @TypeConverter
    fun intToState(value: Int): WorkInfo.State {
        return when (value) {
            StateIds.ENQUEUED -> WorkInfo.State.ENQUEUED
            StateIds.RUNNING -> WorkInfo.State.RUNNING
            StateIds.SUCCEEDED -> WorkInfo.State.SUCCEEDED
            StateIds.FAILED -> WorkInfo.State.FAILED
            StateIds.BLOCKED -> WorkInfo.State.BLOCKED
            StateIds.CANCELLED -> WorkInfo.State.CANCELLED
            else -> throw IllegalArgumentException("Could not convert $value to State")
        }
    }

    /**
     * TypeConverter for a BackoffPolicy to an int.
     *
     * @param backoffPolicy The input BackoffPolicy
     * @return The associated int constant
     */
    @JvmStatic
    @TypeConverter
    fun backoffPolicyToInt(backoffPolicy: BackoffPolicy): Int {
        return when (backoffPolicy) {
            BackoffPolicy.EXPONENTIAL -> BackoffPolicyIds.EXPONENTIAL
            BackoffPolicy.LINEAR -> BackoffPolicyIds.LINEAR
        }
    }

    /**
     * TypeConverter for an int to a BackoffPolicy.
     *
     * @param value The input integer
     * @return The associated BackoffPolicy enum value
     */
    @JvmStatic
    @TypeConverter
    fun intToBackoffPolicy(value: Int): BackoffPolicy {
        return when (value) {
            BackoffPolicyIds.EXPONENTIAL -> BackoffPolicy.EXPONENTIAL
            BackoffPolicyIds.LINEAR -> BackoffPolicy.LINEAR
            else -> throw IllegalArgumentException("Could not convert $value to BackoffPolicy")
        }
    }

    /**
     * TypeConverter for a NetworkType to an int.
     *
     * @param networkType The input NetworkType
     * @return The associated int constant
     */
    @JvmStatic
    @TypeConverter
    fun networkTypeToInt(networkType: NetworkType): Int {
        return when (networkType) {
            NetworkType.NOT_REQUIRED -> NetworkTypeIds.NOT_REQUIRED
            NetworkType.CONNECTED -> NetworkTypeIds.CONNECTED
            NetworkType.UNMETERED -> NetworkTypeIds.UNMETERED
            NetworkType.NOT_ROAMING -> NetworkTypeIds.NOT_ROAMING
            NetworkType.METERED -> NetworkTypeIds.METERED
            else -> {
                if (Build.VERSION.SDK_INT >= 30 && networkType == NetworkType.TEMPORARILY_UNMETERED)
                    NetworkTypeIds.TEMPORARILY_UNMETERED
                else
                    throw IllegalArgumentException("Could not convert $networkType to int")
            }
        }
    }

    /**
     * TypeConverter for an int to a NetworkType.
     *
     * @param value The input integer
     * @return The associated NetworkType enum value
     */
    @JvmStatic
    @TypeConverter
    fun intToNetworkType(value: Int): NetworkType {
        return when (value) {
            NetworkTypeIds.NOT_REQUIRED -> NetworkType.NOT_REQUIRED
            NetworkTypeIds.CONNECTED -> NetworkType.CONNECTED
            NetworkTypeIds.UNMETERED -> NetworkType.UNMETERED
            NetworkTypeIds.NOT_ROAMING -> NetworkType.NOT_ROAMING
            NetworkTypeIds.METERED -> NetworkType.METERED
            else -> {
                if (Build.VERSION.SDK_INT >= 30 && value == NetworkTypeIds.TEMPORARILY_UNMETERED) {
                    return NetworkType.TEMPORARILY_UNMETERED
                } else throw IllegalArgumentException("Could not convert $value to NetworkType")
            }
        }
    }

    /**
     * Converts a [OutOfQuotaPolicy] to an int.
     *
     * @param policy The [OutOfQuotaPolicy] policy being used
     * @return the corresponding int representation.
     */
    @JvmStatic
    @TypeConverter
    fun outOfQuotaPolicyToInt(policy: OutOfQuotaPolicy): Int {
        return when (policy) {
            OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST ->
                OutOfPolicyIds.RUN_AS_NON_EXPEDITED_WORK_REQUEST
            OutOfQuotaPolicy.DROP_WORK_REQUEST -> OutOfPolicyIds.DROP_WORK_REQUEST
        }
    }

    /**
     * Converter from an int to a [OutOfQuotaPolicy].
     *
     * @param value The input integer
     * @return An [OutOfQuotaPolicy]
     */
    @JvmStatic
    @TypeConverter
    fun intToOutOfQuotaPolicy(value: Int): OutOfQuotaPolicy {
        return when (value) {
            OutOfPolicyIds.RUN_AS_NON_EXPEDITED_WORK_REQUEST ->
                OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST
            OutOfPolicyIds.DROP_WORK_REQUEST -> OutOfQuotaPolicy.DROP_WORK_REQUEST
            else -> throw IllegalArgumentException("Could not convert $value to OutOfQuotaPolicy")
        }
    }

    /**
     * Converts a list of [ContentUriTriggers.Trigger]s to byte array representation
     * @param triggers the list of [ContentUriTriggers.Trigger]s to convert
     * @return corresponding byte array representation
     */
    @JvmStatic
    @TypeConverter
    fun contentUriTriggersToByteArray(triggers: ContentUriTriggers): ByteArray? {
        if (triggers.size() == 0) {
            return null
        }
        val outputStream = ByteArrayOutputStream()
        outputStream.use {
            ObjectOutputStream(outputStream).use { objectOutputStream ->
                objectOutputStream.writeInt(triggers.size())
                for (trigger in triggers.triggers) {
                    objectOutputStream.writeUTF(trigger.uri.toString())
                    objectOutputStream.writeBoolean(trigger.shouldTriggerForDescendants())
                }
            }
        }

        return outputStream.toByteArray()
    }

    /**
     * Converts a byte array to list of [ContentUriTriggers.Trigger]s
     * @param bytes byte array representation to convert
     * @return list of [ContentUriTriggers.Trigger]s
     */
    @JvmStatic
    @TypeConverter
    fun byteArrayToContentUriTriggers(bytes: ByteArray?): ContentUriTriggers {
        val triggers = ContentUriTriggers()
        if (bytes == null) {
            // bytes will be null if there are no Content Uri Triggers
            return triggers
        }
        val inputStream = ByteArrayInputStream(bytes)
        inputStream.use {
            try {
                ObjectInputStream(inputStream).use { objectInputStream ->
                    repeat(objectInputStream.readInt()) {
                        val uri = Uri.parse(objectInputStream.readUTF())
                        val triggersForDescendants = objectInputStream.readBoolean()
                        triggers.add(uri, triggersForDescendants)
                    }
                }
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
        return triggers
    }
}