WorkDatabasePathHelper.kt

/*
 * Copyright 2019 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

import android.content.Context
import android.os.Build
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.work.Logger
import java.io.File

private val TAG = Logger.tagWithPrefix("WrkDbPathHelper")

/**
 * @return The name of the database.
 */
internal const val WORK_DATABASE_NAME = "androidx.work.workdb"

// Supporting files for a SQLite database
private val DATABASE_EXTRA_FILES = arrayOf("-journal", "-shm", "-wal")

/**
 * Keeps track of {@link WorkDatabase} paths.
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
object WorkDatabasePathHelper {
    /**
     * Migrates [WorkDatabase] to the no-backup directory.
     *
     * @param context The application context.
     */
    @JvmStatic
    fun migrateDatabase(context: Context) {
        val defaultDatabasePath = getDefaultDatabasePath(context)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && defaultDatabasePath.exists()) {
            Logger.get().debug(TAG, "Migrating WorkDatabase to the no-backup directory")
            migrationPaths(context).forEach { (source, destination) ->
                if (source.exists()) {
                    if (destination.exists()) {
                        Logger.get().warning(TAG, "Over-writing contents of $destination")
                    }
                    val renamed = source.renameTo(destination)
                    val message = if (renamed) {
                        "Migrated ${source}to $destination"
                    } else {
                        "Renaming $source to $destination failed"
                    }
                    Logger.get().debug(TAG, message)
                }
            }
        }
    }

    /**
     * Returns a [Map] of all paths which need to be migrated to the no-backup directory.
     *
     * @param context The application [Context]
     * @return a [Map] of paths to be migrated from source -> destination
     */
    fun migrationPaths(context: Context): Map<File, File> {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            val databasePath = getDefaultDatabasePath(context)
            val migratedPath = getDatabasePath(context)
            val map = DATABASE_EXTRA_FILES.associate { extra ->
                File(databasePath.path + extra) to File(migratedPath.path + extra)
            }
            map + (databasePath to migratedPath)
        } else emptyMap()
    }

    /**
     * @param context The application [Context]
     * @return The database path before migration to the no-backup directory.
     */
    fun getDefaultDatabasePath(context: Context): File {
        return context.getDatabasePath(WORK_DATABASE_NAME)
    }

    /**
     * @param context The application [Context]
     * @return The the migrated database path.
     */
    fun getDatabasePath(context: Context): File {
        return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            // No notion of a backup directory exists.
            getDefaultDatabasePath(context)
        } else {
            getNoBackupPath(context)
        }
    }

    /**
     * Return the path for a [File] path in the [Context.getNoBackupFilesDir]
     * identified by the [String] fragment.
     *
     * @param context  The application [Context]
     * @return the [File]
     */
    @RequiresApi(23)
    private fun getNoBackupPath(context: Context): File {
        return File(Api21Impl.getNoBackupFilesDir(context), WORK_DATABASE_NAME)
    }
}

@RequiresApi(21)
internal object Api21Impl {
    @DoNotInline
    fun getNoBackupFilesDir(context: Context): File {
        return context.noBackupFilesDir
    }
}