| /* |
| * 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<String>?, |
| selection: String?, |
| selectionArgs: Array<String>?, |
| 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<String>?): Int { |
| throw UnsupportedOperationException() |
| } |
| |
| /** |
| * update() is not supported |
| */ |
| override fun update( |
| uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>? |
| ): 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<String>() |
| 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() |
| } |
| } |
| } |