1 /*
2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.bluetooth
17 
18 import android.content.ContentProvider
19 import android.content.ContentValues
20 import android.content.Context
21 import android.content.UriMatcher
22 import android.database.Cursor
23 import android.database.sqlite.SQLiteDatabase
24 import android.net.Uri
25 import android.os.Bundle
26 import android.util.Log
27 
28 /**
29  * Define an implementation of ContentProvider for the Bluetooth migration
30  */
31 class BluetoothLegacyMigration: ContentProvider() {
32     companion object {
33         private const val TAG = "BluetoothLegacyMigration"
34 
35         private const val AUTHORITY = "bluetooth_legacy.provider"
36 
37         private const val START_LEGACY_MIGRATION_CALL = "start_legacy_migration"
38         private const val FINISH_LEGACY_MIGRATION_CALL = "finish_legacy_migration"
39 
40         private const val PHONEBOOK_ACCESS_PERMISSION = "phonebook_access_permission"
41         private const val MESSAGE_ACCESS_PERMISSION = "message_access_permission"
42         private const val SIM_ACCESS_PERMISSION = "sim_access_permission"
43 
44         private const val VOLUME_MAP = "bluetooth_volume_map"
45 
46         private const val OPP = "OPPMGR"
47         private const val BLUETOOTH_OPP_CHANNEL = "btopp_channels"
48         private const val BLUETOOTH_OPP_NAME = "btopp_names"
49 
50         private const val BLUETOOTH_SIGNED_DEFAULT = "com.google.android.bluetooth_preferences"
51 
52         private const val KEY_LIST = "key_list"
53 
54         private enum class UriId(
55             val fileName: String,
56             val handler: (ctx: Context) -> DatabaseHandler
57         ) {
58             BLUETOOTH(BluetoothDatabase.DATABASE_NAME, ::BluetoothDatabase),
59             OPP(OppDatabase.DATABASE_NAME, ::OppDatabase),
60         }
61 
<lambda>null62         private val URI_MATCHER = UriMatcher(UriMatcher.NO_MATCH).apply {
63             UriId.values().map { addURI(AUTHORITY, it.fileName, it.ordinal) }
64         }
65 
putObjectInBundlenull66         private fun putObjectInBundle(bundle: Bundle, key: String, obj: Any?) {
67             when (obj) {
68                 is Boolean -> bundle.putBoolean(key, obj)
69                 is Int -> bundle.putInt(key, obj)
70                 is Long -> bundle.putLong(key, obj)
71                 is String -> bundle.putString(key, obj)
72                 null -> throw UnsupportedOperationException("null type is not handled")
73                 else -> throw UnsupportedOperationException("${obj.javaClass.simpleName}: type is not handled")
74             }
75         }
76     }
77 
78     private lateinit var mContext: Context
79 
80     /**
81      * Always return true, indicating that the
82      * provider loaded correctly.
83      */
onCreatenull84     override fun onCreate(): Boolean {
85         mContext = context!!.createDeviceProtectedStorageContext()
86         return true
87     }
88 
89     /**
90      * Use a content URI to get database name associated
91      *
92      * @param uri Content uri
93      * @return A {@link Cursor} containing the results of the query.
94      */
getTypenull95     override fun getType(uri: Uri): String {
96         val database = UriId.values().firstOrNull { it.ordinal == URI_MATCHER.match(uri) }
97             ?: throw UnsupportedOperationException("This Uri is not supported: $uri")
98         return database.fileName
99     }
100 
101     /**
102      * Use a content URI to get information about a database
103      *
104      * @param uri Content uri
105      * @param projection unused
106      * @param selection unused
107      * @param selectionArgs unused
108      * @param sortOrder unused
109      * @return A {@link Cursor} containing the results of the query.
110      *
111      */
112     @Override
querynull113     override fun query(
114         uri: Uri,
115         projection: Array<String>?,
116         selection: String?,
117         selectionArgs: Array<String>?,
118         sortOrder: String?
119     ): Cursor? {
120         val database = UriId.values().firstOrNull { it.ordinal == URI_MATCHER.match(uri) }
121             ?: throw UnsupportedOperationException("This Uri is not supported: $uri")
122         return database.handler(mContext).toCursor()
123     }
124 
125     /**
126      * insert() is not supported
127      */
insertnull128     override fun insert(uri: Uri, values: ContentValues?): Uri? {
129         throw UnsupportedOperationException()
130     }
131 
132     /**
133      * delete() is not supported
134      */
deletenull135     override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
136         throw UnsupportedOperationException()
137     }
138 
139     /**
140      * update() is not supported
141      */
updatenull142     override fun update(
143         uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?
144     ): Int {
145         throw UnsupportedOperationException()
146     }
147 
148     abstract class MigrationHandler {
toBundlenull149         abstract fun toBundle(): Bundle?
150         abstract fun delete()
151     }
152 
153     private class SharedPreferencesHandler(private val ctx: Context, private val key: String) :
154         MigrationHandler() {
155 
156         override fun toBundle(): Bundle? {
157             val pref = ctx.getSharedPreferences(key, Context.MODE_PRIVATE)
158             if (pref.all.isEmpty()) {
159                 Log.d(TAG, "No migration needed for shared preference: $key")
160                 return null
161             }
162             val bundle = Bundle()
163             val keys = arrayListOf<String>()
164             for (e in pref.all) {
165                 keys += e.key
166                 putObjectInBundle(bundle, e.key, e.value)
167             }
168             bundle.putStringArrayList(KEY_LIST, keys)
169             Log.d(TAG, "SharedPreferences migrating ${keys.size} key(s) from $key")
170             return bundle
171         }
172 
173         override fun delete() {
174             ctx.deleteSharedPreferences(key)
175             Log.d(TAG, "$key: SharedPreferences deleted")
176         }
177     }
178 
179     abstract class DatabaseHandler(private val ctx: Context, private val dbName: String) :
180         MigrationHandler() {
181 
182         abstract val sql: String
183 
toCursornull184         fun toCursor(): Cursor? {
185             val databasePath = ctx.getDatabasePath(dbName)
186             if (!databasePath.exists()) {
187                 Log.d(TAG, "No migration needed for database: $dbName")
188                 return null
189             }
190             val db = SQLiteDatabase.openDatabase(
191                 databasePath,
192                 SQLiteDatabase.OpenParams.Builder().addOpenFlags(SQLiteDatabase.OPEN_READONLY)
193                     .build()
194             )
195             return db.rawQuery(sql, null)
196         }
197 
toBundlenull198         override fun toBundle(): Bundle? {
199             throw UnsupportedOperationException()
200         }
201 
deletenull202         override fun delete() {
203             val databasePath = ctx.getDatabasePath(dbName)
204             databasePath.delete()
205             Log.d(TAG, "$dbName: database deleted")
206         }
207     }
208 
209     private class BluetoothDatabase(ctx: Context) : DatabaseHandler(ctx, DATABASE_NAME) {
210         companion object {
211             const val DATABASE_NAME = "bluetooth_db"
212         }
213         private val dbTable = "metadata"
214         override val sql = "select * from $dbTable"
215     }
216 
217     private class OppDatabase(ctx: Context) : DatabaseHandler(ctx, DATABASE_NAME) {
218         companion object {
219             const val DATABASE_NAME = "btopp.db"
220         }
221         private val dbTable = "btopp"
222         override val sql = "select * from $dbTable"
223     }
224 
225     /**
226      * Fetch legacy data describe by {@code arg} and perform {@code method} action on it
227      *
228      * @param method Action to perform. One of START_LEGACY_MIGRATION_CALL|FINISH_LEGACY_MIGRATION_CALL
229      * @param arg item on witch to perform the action specified by {@code method}
230      * @param extras unused
231      * @return A {@link Bundle} containing the results of the query.
232      */
callnull233     override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
234         val migrationHandler = when (arg) {
235             OPP,
236             VOLUME_MAP,
237             BLUETOOTH_OPP_NAME,
238             BLUETOOTH_OPP_CHANNEL,
239             SIM_ACCESS_PERMISSION,
240             MESSAGE_ACCESS_PERMISSION,
241             PHONEBOOK_ACCESS_PERMISSION -> SharedPreferencesHandler(mContext, arg)
242             BLUETOOTH_SIGNED_DEFAULT -> {
243                 val key = mContext.packageName + "_preferences"
244                 SharedPreferencesHandler(mContext, key)
245             }
246             BluetoothDatabase.DATABASE_NAME -> BluetoothDatabase(mContext)
247             OppDatabase.DATABASE_NAME -> OppDatabase(mContext)
248             else -> throw UnsupportedOperationException()
249         }
250         return when (method) {
251             START_LEGACY_MIGRATION_CALL -> migrationHandler.toBundle()
252             FINISH_LEGACY_MIGRATION_CALL -> {
253                 migrationHandler.delete()
254                 return null
255             }
256             else -> throw UnsupportedOperationException()
257         }
258     }
259 }
260