/* * 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 com.google.android.bluetooth import android.content.ContentProvider import android.content.ContentValues import android.content.Context import android.content.UriMatcher import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.net.Uri import android.os.Bundle import android.util.Log /** * Define an implementation of ContentProvider for the Bluetooth migration */ class BluetoothLegacyMigration: ContentProvider() { companion object { private const val TAG = "BluetoothLegacyMigration" private const val AUTHORITY = "bluetooth_legacy.provider" private const val START_LEGACY_MIGRATION_CALL = "start_legacy_migration" private const val FINISH_LEGACY_MIGRATION_CALL = "finish_legacy_migration" private const val PHONEBOOK_ACCESS_PERMISSION = "phonebook_access_permission" private const val MESSAGE_ACCESS_PERMISSION = "message_access_permission" private const val SIM_ACCESS_PERMISSION = "sim_access_permission" private const val VOLUME_MAP = "bluetooth_volume_map" private const val OPP = "OPPMGR" private const val BLUETOOTH_OPP_CHANNEL = "btopp_channels" private const val BLUETOOTH_OPP_NAME = "btopp_names" private const val BLUETOOTH_SIGNED_DEFAULT = "com.google.android.bluetooth_preferences" private const val KEY_LIST = "key_list" private enum class UriId( val fileName: String, val handler: (ctx: Context) -> DatabaseHandler ) { BLUETOOTH(BluetoothDatabase.DATABASE_NAME, ::BluetoothDatabase), OPP(OppDatabase.DATABASE_NAME, ::OppDatabase), } private val URI_MATCHER = UriMatcher(UriMatcher.NO_MATCH).apply { UriId.values().map { addURI(AUTHORITY, it.fileName, it.ordinal) } } private fun putObjectInBundle(bundle: Bundle, key: String, obj: Any?) { when (obj) { is Boolean -> bundle.putBoolean(key, obj) is Int -> bundle.putInt(key, obj) is Long -> bundle.putLong(key, obj) is String -> bundle.putString(key, obj) null -> throw UnsupportedOperationException("null type is not handled") else -> throw UnsupportedOperationException("${obj.javaClass.simpleName}: type is not handled") } } } private lateinit var mContext: Context /** * Always return true, indicating that the * provider loaded correctly. */ override fun onCreate(): Boolean { mContext = context!!.createDeviceProtectedStorageContext() return true } /** * Use a content URI to get database name associated * * @param uri Content uri * @return A {@link Cursor} containing the results of the query. */ override fun getType(uri: Uri): String { val database = UriId.values().firstOrNull { it.ordinal == URI_MATCHER.match(uri) } ?: throw UnsupportedOperationException("This Uri is not supported: $uri") return database.fileName } /** * Use a content URI to get information about a database * * @param uri Content uri * @param projection unused * @param selection unused * @param selectionArgs unused * @param sortOrder unused * @return A {@link Cursor} containing the results of the query. * */ @Override override fun query( uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String? ): Cursor? { val database = UriId.values().firstOrNull { it.ordinal == URI_MATCHER.match(uri) } ?: throw UnsupportedOperationException("This Uri is not supported: $uri") return database.handler(mContext).toCursor() } /** * insert() is not supported */ override fun insert(uri: Uri, values: ContentValues?): Uri? { throw UnsupportedOperationException() } /** * delete() is not supported */ override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { throw UnsupportedOperationException() } /** * update() is not supported */ override fun update( uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array? ): Int { throw UnsupportedOperationException() } abstract class MigrationHandler { abstract fun toBundle(): Bundle? abstract fun delete() } private class SharedPreferencesHandler(private val ctx: Context, private val key: String) : MigrationHandler() { override fun toBundle(): Bundle? { val pref = ctx.getSharedPreferences(key, Context.MODE_PRIVATE) if (pref.all.isEmpty()) { Log.d(TAG, "No migration needed for shared preference: $key") return null } val bundle = Bundle() val keys = arrayListOf() for (e in pref.all) { keys += e.key putObjectInBundle(bundle, e.key, e.value) } bundle.putStringArrayList(KEY_LIST, keys) Log.d(TAG, "SharedPreferences migrating ${keys.size} key(s) from $key") return bundle } override fun delete() { ctx.deleteSharedPreferences(key) Log.d(TAG, "$key: SharedPreferences deleted") } } abstract class DatabaseHandler(private val ctx: Context, private val dbName: String) : MigrationHandler() { abstract val sql: String fun toCursor(): Cursor? { val databasePath = ctx.getDatabasePath(dbName) if (!databasePath.exists()) { Log.d(TAG, "No migration needed for database: $dbName") return null } val db = SQLiteDatabase.openDatabase( databasePath, SQLiteDatabase.OpenParams.Builder().addOpenFlags(SQLiteDatabase.OPEN_READONLY) .build() ) return db.rawQuery(sql, null) } override fun toBundle(): Bundle? { throw UnsupportedOperationException() } override fun delete() { val databasePath = ctx.getDatabasePath(dbName) databasePath.delete() Log.d(TAG, "$dbName: database deleted") } } private class BluetoothDatabase(ctx: Context) : DatabaseHandler(ctx, DATABASE_NAME) { companion object { const val DATABASE_NAME = "bluetooth_db" } private val dbTable = "metadata" override val sql = "select * from $dbTable" } private class OppDatabase(ctx: Context) : DatabaseHandler(ctx, DATABASE_NAME) { companion object { const val DATABASE_NAME = "btopp.db" } private val dbTable = "btopp" override val sql = "select * from $dbTable" } /** * Fetch legacy data describe by {@code arg} and perform {@code method} action on it * * @param method Action to perform. One of START_LEGACY_MIGRATION_CALL|FINISH_LEGACY_MIGRATION_CALL * @param arg item on witch to perform the action specified by {@code method} * @param extras unused * @return A {@link Bundle} containing the results of the query. */ override fun call(method: String, arg: String?, extras: Bundle?): Bundle? { val migrationHandler = when (arg) { OPP, VOLUME_MAP, BLUETOOTH_OPP_NAME, BLUETOOTH_OPP_CHANNEL, SIM_ACCESS_PERMISSION, MESSAGE_ACCESS_PERMISSION, PHONEBOOK_ACCESS_PERMISSION -> SharedPreferencesHandler(mContext, arg) BLUETOOTH_SIGNED_DEFAULT -> { val key = mContext.packageName + "_preferences" SharedPreferencesHandler(mContext, key) } BluetoothDatabase.DATABASE_NAME -> BluetoothDatabase(mContext) OppDatabase.DATABASE_NAME -> OppDatabase(mContext) else -> throw UnsupportedOperationException() } return when (method) { START_LEGACY_MIGRATION_CALL -> migrationHandler.toBundle() FINISH_LEGACY_MIGRATION_CALL -> { migrationHandler.delete() return null } else -> throw UnsupportedOperationException() } } }