Constraints.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

import android.net.Uri
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.room.ColumnInfo
import androidx.work.impl.utils.toMillisCompat
import java.time.Duration
import java.util.concurrent.TimeUnit

/**
 * A specification of the requirements that need to be met before a [WorkRequest] can run.  By
 * default, WorkRequests do not have any requirements and can run immediately.  By adding
 * requirements, you can make sure that work only runs in certain situations - for example, when you
 * have an unmetered network and are charging.
 *
 * @property requiredNetworkType The type of network required for the work to run.
 * The default value is [NetworkType.NOT_REQUIRED].
 * @param requiresCharging whether device should be charging for the [WorkRequest] to run. The
 * default value is `false`.
 * @param requiresDeviceIdle whether device should be idle for the [WorkRequest] to run. The
 * default value is `false`.
 * @param requiresBatteryNotLow whether device battery should be at an acceptable level for the
 * [WorkRequest] to run. The default value is `false`.
 * @param requiresStorageNotLow whether the device's available storage should be at an acceptable
 * level for the [WorkRequest] to run. The default value is `false`.
 * @property contentTriggerUpdateDelayMillis the delay in milliseconds that is allowed from the
 * time a `content:` [Uri] change is detected to the time when the [WorkRequest] is scheduled.
 * If there are more changes during this time, the delay will be reset to the start of the most
 * recent change. This functionality is identical to the one found in `JobScheduler` and
 * is described in [android.app.job.JobInfo.Builder.setTriggerContentUpdateDelay]
 * @property contentTriggerMaxDelayMillis the maximum delay in milliseconds that is allowed
 * from the first time a `content:` [Uri] change is detected to the time when the [WorkRequest]
 * is scheduled. This functionality is identical to the one found in `JobScheduler` and is described
 * in [android.app.job.JobInfo.Builder.setTriggerContentMaxDelay].
 * @property contentUriTriggers set of [ContentUriTrigger]. [WorkRequest] will run when a local
 * `content:` [Uri] of one of the triggers in the set is updated.
 * This functionality is identical to the one found in `JobScheduler` and is described in
 * [android.app.job.JobInfo.Builder.addTriggerContentUri].
 */
class Constraints(
    @ColumnInfo(name = "required_network_type")
    val requiredNetworkType: NetworkType = NetworkType.NOT_REQUIRED,
    @ColumnInfo(name = "requires_charging")
    private val requiresCharging: Boolean = false,
    @ColumnInfo(name = "requires_device_idle")
    private val requiresDeviceIdle: Boolean = false,
    @ColumnInfo(name = "requires_battery_not_low")
    private val requiresBatteryNotLow: Boolean = false,
    @ColumnInfo(name = "requires_storage_not_low")
    private val requiresStorageNotLow: Boolean = false,
    @ColumnInfo(name = "trigger_content_update_delay")
    val contentTriggerUpdateDelayMillis: Long = -1,
    @ColumnInfo(name = "trigger_max_content_delay")
    val contentTriggerMaxDelayMillis: Long = -1,
    @ColumnInfo(name = "content_uri_triggers")
    val contentUriTriggers: Set<ContentUriTrigger> = setOf(),
) {
    constructor(other: Constraints) : this(
        requiresCharging = other.requiresCharging,
        requiresDeviceIdle = other.requiresDeviceIdle,
        requiredNetworkType = other.requiredNetworkType,
        requiresBatteryNotLow = other.requiresBatteryNotLow,
        requiresStorageNotLow = other.requiresStorageNotLow,
        contentUriTriggers = other.contentUriTriggers,
        contentTriggerUpdateDelayMillis = other.contentTriggerUpdateDelayMillis,
        contentTriggerMaxDelayMillis = other.contentTriggerMaxDelayMillis,
    )

    /**
     * @return `true` if the work should only execute while the device is charging
     */
    fun requiresCharging(): Boolean {
        return requiresCharging
    }

    /**
     * @return `true` if the work should only execute while the device is idle
     */
    @RequiresApi(23)
    fun requiresDeviceIdle(): Boolean {
        return requiresDeviceIdle
    }

    /**
     * @return `true` if the work should only execute when the battery isn't low
     */
    fun requiresBatteryNotLow(): Boolean {
        return requiresBatteryNotLow
    }

    /**
     * @return `true` if the work should only execute when the storage isn't low
     */
    fun requiresStorageNotLow(): Boolean {
        return requiresStorageNotLow
    }

    /**
     * @return `true` if [ContentUriTrigger] is not empty
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    fun hasContentUriTriggers(): Boolean {
        return contentUriTriggers.isNotEmpty()
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other == null || javaClass != other.javaClass) return false
        val that = other as Constraints
        if (requiresCharging != that.requiresCharging) return false
        if (requiresDeviceIdle != that.requiresDeviceIdle) return false
        if (requiresBatteryNotLow != that.requiresBatteryNotLow) return false
        if (requiresStorageNotLow != that.requiresStorageNotLow) return false
        if (contentTriggerUpdateDelayMillis != that.contentTriggerUpdateDelayMillis) return false
        if (contentTriggerMaxDelayMillis != that.contentTriggerMaxDelayMillis) return false
        return if (requiredNetworkType != that.requiredNetworkType) false
        else contentUriTriggers == that.contentUriTriggers
    }

    override fun hashCode(): Int {
        var result = requiredNetworkType.hashCode()
        result = 31 * result + if (requiresCharging) 1 else 0
        result = 31 * result + if (requiresDeviceIdle) 1 else 0
        result = 31 * result + if (requiresBatteryNotLow) 1 else 0
        result = 31 * result + if (requiresStorageNotLow) 1 else 0
        result = 31 * result +
            (contentTriggerUpdateDelayMillis xor (contentTriggerUpdateDelayMillis ushr 32)).toInt()
        result = 31 * result +
            (contentTriggerMaxDelayMillis xor (contentTriggerMaxDelayMillis ushr 32)).toInt()
        result = 31 * result + contentUriTriggers.hashCode()
        return result
    }

    /**
     * A Builder for a [Constraints] object.
     */
    class Builder {
        private var requiresCharging = false
        private var requiresDeviceIdle = false
        private var requiredNetworkType = NetworkType.NOT_REQUIRED
        private var requiresBatteryNotLow = false
        private var requiresStorageNotLow = false
        // Same defaults as JobInfo
        private var triggerContentUpdateDelay: Long = -1
        private var triggerContentMaxDelay: Long = -1
        private var contentUriTriggers = mutableSetOf<ContentUriTrigger>()

        constructor() {
            // default public constructor
        }

        /**
         * @hide
         */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        constructor(constraints: Constraints) {
            requiresCharging = constraints.requiresCharging()
            requiresDeviceIdle = Build.VERSION.SDK_INT >= 23 && constraints.requiresDeviceIdle()
            requiredNetworkType = constraints.requiredNetworkType
            requiresBatteryNotLow = constraints.requiresBatteryNotLow()
            requiresStorageNotLow = constraints.requiresStorageNotLow()
            if (Build.VERSION.SDK_INT >= 24) {
                triggerContentUpdateDelay = constraints.contentTriggerUpdateDelayMillis
                triggerContentMaxDelay = constraints.contentTriggerMaxDelayMillis
                contentUriTriggers = constraints.contentUriTriggers.toMutableSet()
            }
        }

        /**
         * Sets whether device should be charging for the [WorkRequest] to run.  The
         * default value is `false`.
         *
         * @param requiresCharging `true` if device must be charging for the work to run
         * @return The current [Builder]
         */
        fun setRequiresCharging(requiresCharging: Boolean): Builder {
            this.requiresCharging = requiresCharging
            return this
        }

        /**
         * Sets whether device should be idle for the [WorkRequest] to run. The default
         * value is `false`.
         *
         * @param requiresDeviceIdle `true` if device must be idle for the work to run
         * @return The current [Builder]
         */
        @RequiresApi(23)
        fun setRequiresDeviceIdle(requiresDeviceIdle: Boolean): Builder {
            this.requiresDeviceIdle = requiresDeviceIdle
            return this
        }

        /**
         * Sets whether device should have a particular [NetworkType] for the
         * [WorkRequest] to run. The default value is [NetworkType.NOT_REQUIRED].
         *
         * @param networkType The type of network required for the work to run
         * @return The current [Builder]
         */
        fun setRequiredNetworkType(networkType: NetworkType): Builder {
            requiredNetworkType = networkType
            return this
        }

        /**
         * Sets whether device battery should be at an acceptable level for the
         * [WorkRequest] to run. The default value is `false`.
         *
         * @param requiresBatteryNotLow `true` if the battery should be at an acceptable level
         * for the work to run
         * @return The current [Builder]
         */
        fun setRequiresBatteryNotLow(requiresBatteryNotLow: Boolean): Builder {
            this.requiresBatteryNotLow = requiresBatteryNotLow
            return this
        }

        /**
         * Sets whether the device's available storage should be at an acceptable level for the
         * [WorkRequest] to run. The default value is `false`.
         *
         * @param requiresStorageNotLow `true` if the available storage should not be below a
         * a critical threshold for the work to run
         * @return The current [Builder]
         */
        fun setRequiresStorageNotLow(requiresStorageNotLow: Boolean): Builder {
            this.requiresStorageNotLow = requiresStorageNotLow
            return this
        }

        /**
         * Sets whether the [WorkRequest] should run when a local `content:` [Uri]
         * is updated.  This functionality is identical to the one found in `JobScheduler` and
         * is described in
         * `JobInfo.Builder#addTriggerContentUri(android.app.job.JobInfo.TriggerContentUri)`.
         *
         * @param uri The local `content:` Uri to observe
         * @param triggerForDescendants `true` if any changes in descendants cause this
         * [WorkRequest] to run
         * @return The current [Builder]
         */
        @RequiresApi(24)
        fun addContentUriTrigger(uri: Uri, triggerForDescendants: Boolean): Builder {
            contentUriTriggers.add(ContentUriTrigger(uri, triggerForDescendants))
            return this
        }

        /**
         * Sets the delay that is allowed from the time a `content:` [Uri]
         * change is detected to the time when the [WorkRequest] is scheduled.  If there are
         * more changes during this time, the delay will be reset to the start of the most recent
         * change. This functionality is identical to the one found in `JobScheduler` and
         * is described in `JobInfo.Builder#setTriggerContentUpdateDelay(long)`.
         *
         * @param duration The length of the delay in `timeUnit` units
         * @param timeUnit The units of time for `duration`
         * @return The current [Builder]
         */
        @RequiresApi(24)
        fun setTriggerContentUpdateDelay(duration: Long, timeUnit: TimeUnit): Builder {
            triggerContentUpdateDelay = timeUnit.toMillis(duration)
            return this
        }

        /**
         * Sets the delay that is allowed from the time a `content:` [Uri] change
         * is detected to the time when the [WorkRequest] is scheduled.  If there are more
         * changes during this time, the delay will be reset to the start of the most recent change.
         * This functionality is identical to the one found in `JobScheduler` and
         * is described in `JobInfo.Builder#setTriggerContentUpdateDelay(long)`.
         *
         * @param duration The length of the delay
         * @return The current [Builder]
         */
        @RequiresApi(26)
        fun setTriggerContentUpdateDelay(duration: Duration): Builder {
            triggerContentUpdateDelay = duration.toMillisCompat()
            return this
        }

        /**
         * Sets the maximum delay that is allowed from the first time a `content:`
         * [Uri] change is detected to the time when the [WorkRequest] is scheduled.
         * This functionality is identical to the one found in `JobScheduler` and
         * is described in `JobInfo.Builder#setTriggerContentMaxDelay(long)`.
         *
         * @param duration The length of the delay in `timeUnit` units
         * @param timeUnit The units of time for `duration`
         * @return The current [Builder]
         */
        @RequiresApi(24)
        fun setTriggerContentMaxDelay(duration: Long, timeUnit: TimeUnit): Builder {
            triggerContentMaxDelay = timeUnit.toMillis(duration)
            return this
        }

        /**
         * Sets the maximum delay that is allowed from the first time a `content:` [Uri]
         * change is detected to the time when the [WorkRequest] is scheduled. This
         * functionality is identical to the one found in `JobScheduler` and is described
         * in `JobInfo.Builder#setTriggerContentMaxDelay(long)`.
         *
         * @param duration The length of the delay
         * @return The current [Builder]
         */
        @RequiresApi(26)
        fun setTriggerContentMaxDelay(duration: Duration): Builder {
            triggerContentMaxDelay = duration.toMillisCompat()
            return this
        }

        /**
         * Generates the [Constraints] from this Builder.
         *
         * @return The [Constraints] specified by this Builder
         */
        fun build(): Constraints {
            val contentUriTriggers: Set<ContentUriTrigger>
            val triggerContentUpdateDelay: Long
            val triggerMaxContentDelay: Long
            if (Build.VERSION.SDK_INT >= 24) {
                contentUriTriggers = this.contentUriTriggers.toSet()
                triggerContentUpdateDelay = this.triggerContentUpdateDelay
                triggerMaxContentDelay = triggerContentMaxDelay
            } else {
                contentUriTriggers = emptySet()
                triggerContentUpdateDelay = -1
                triggerMaxContentDelay = -1
            }

            return Constraints(
                requiresCharging = requiresCharging,
                requiresDeviceIdle = Build.VERSION.SDK_INT >= 23 && requiresDeviceIdle,
                requiredNetworkType = requiredNetworkType,
                requiresBatteryNotLow = requiresBatteryNotLow,
                requiresStorageNotLow = requiresStorageNotLow,
                contentTriggerMaxDelayMillis = triggerMaxContentDelay,
                contentTriggerUpdateDelayMillis = triggerContentUpdateDelay,
                contentUriTriggers = contentUriTriggers,
            )
        }
    }

    /**
     * This class describes a content uri trigger on the [WorkRequest]: it should run when a local
     * `content:` [Uri] is updated.  This functionality is identical to the one found
     * in `JobScheduler` and is described in
     * `JobInfo.Builder#addTriggerContentUri(android.app.job.JobInfo.TriggerContentUri)`.
     *
     * @property uri The local `content:` Uri to observe
     * @property isTriggeredForDescendants `true` if trigger also applies to descendants of the [Uri]
     */
    class ContentUriTrigger(val uri: Uri, val isTriggeredForDescendants: Boolean) {
        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false

            other as ContentUriTrigger

            if (uri != other.uri) return false
            if (isTriggeredForDescendants != other.isTriggeredForDescendants) return false

            return true
        }

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

    companion object {
        /**
         * Represents a Constraints object with no requirements.
         */
        @JvmField
        val NONE = Constraints()
    }
}