1 /*
<lambda>null2  * Copyright (C) 2016 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.android.launcher3.provider
17 
18 import android.content.ContentValues
19 import android.content.Context
20 import android.content.pm.ShortcutInfo
21 import android.database.sqlite.SQLiteDatabase
22 import android.graphics.Bitmap
23 import android.graphics.BitmapFactory
24 import android.graphics.drawable.Icon
25 import android.os.PersistableBundle
26 import android.os.Process
27 import android.os.UserManager
28 import android.text.TextUtils
29 import com.android.launcher3.LauncherAppState
30 import com.android.launcher3.LauncherSettings
31 import com.android.launcher3.Utilities
32 import com.android.launcher3.icons.IconCache
33 import com.android.launcher3.model.LoaderCursor
34 import com.android.launcher3.model.UserManagerState
35 import com.android.launcher3.pm.PinRequestHelper
36 import com.android.launcher3.pm.UserCache
37 import com.android.launcher3.shortcuts.ShortcutKey
38 import com.android.launcher3.util.IntArray
39 import com.android.launcher3.util.IntSet
40 import com.android.launcher3.util.PackageManagerHelper
41 
42 /** A set of utility methods for Launcher DB used for DB updates and migration. */
43 object LauncherDbUtils {
44     /**
45      * Returns a string which can be used as a where clause for DB query to match the given itemId
46      */
47     @JvmStatic fun itemIdMatch(itemId: Int): String = "_id=$itemId"
48 
49     @JvmStatic
50     fun queryIntArray(
51         distinct: Boolean,
52         db: SQLiteDatabase,
53         tableName: String,
54         columnName: String,
55         selection: String?,
56         groupBy: String?,
57         orderBy: String?,
58     ): IntArray {
59         val out = IntArray()
60         db.query(
61                 distinct,
62                 tableName,
63                 arrayOf(columnName),
64                 selection,
65                 null,
66                 groupBy,
67                 null,
68                 orderBy,
69                 null,
70             )
71             .use { c ->
72                 while (c.moveToNext()) {
73                     out.add(c.getInt(0))
74                 }
75             }
76         return out
77     }
78 
79     @JvmStatic
80     fun tableExists(db: SQLiteDatabase, tableName: String): Boolean =
81         db.query(
82                 /* distinct = */ true,
83                 /* table = */ "sqlite_master",
84                 /* columns = */ arrayOf("tbl_name"),
85                 /* selection = */ "tbl_name = ?",
86                 /* selectionArgs = */ arrayOf(tableName),
87                 /* groupBy = */ null,
88                 /* having = */ null,
89                 /* orderBy = */ null,
90                 /* limit = */ null,
91                 /* cancellationSignal = */ null,
92             )
93             .use { c ->
94                 return c.count > 0
95             }
96 
97     @JvmStatic
98     fun dropTable(db: SQLiteDatabase, tableName: String) =
99         db.execSQL("DROP TABLE IF EXISTS $tableName")
100 
101     /** Copy fromTable in fromDb to toTable in toDb. */
102     @JvmStatic
103     fun copyTable(
104         fromDb: SQLiteDatabase,
105         fromTable: String,
106         toDb: SQLiteDatabase,
107         toTable: String,
108         context: Context,
109     ) {
110         val userSerial = UserCache.INSTANCE[context].getSerialNumberForUser(Process.myUserHandle())
111         dropTable(toDb, toTable)
112         LauncherSettings.Favorites.addTableToDb(toDb, userSerial, false, toTable)
113         if (fromDb != toDb) {
114             toDb.run {
115                 execSQL("ATTACH DATABASE '${fromDb.path}' AS from_db")
116                 execSQL(
117                     "INSERT INTO $toTable SELECT ${LauncherSettings.Favorites.getColumns(userSerial)} FROM from_db.$fromTable"
118                 )
119                 execSQL("DETACH DATABASE 'from_db'")
120             }
121         } else {
122             toDb.run {
123                 execSQL(
124                     "INSERT INTO $toTable SELECT ${
125                         LauncherSettings.Favorites.getColumns(
126                             userSerial
127                         )
128                     } FROM $fromTable"
129                 )
130             }
131         }
132     }
133 
134     @JvmStatic
135     fun shiftTableByXCells(db: SQLiteDatabase, x: Int, toTable: String) {
136         db.run { execSQL("UPDATE $toTable SET cellY = cellY + $x") }
137     }
138 
139     /**
140      * Migrates the legacy shortcuts to deep shortcuts pinned under Launcher. Removes any invalid
141      * shortcut or any shortcut which requires some permission to launch
142      */
143     @JvmStatic
144     fun migrateLegacyShortcuts(context: Context, db: SQLiteDatabase) {
145         val c =
146             db.query(
147                 LauncherSettings.Favorites.TABLE_NAME,
148                 null,
149                 "itemType = 1",
150                 null,
151                 null,
152                 null,
153                 null,
154             )
155         val pmHelper = PackageManagerHelper.INSTANCE[context]
156         val ums = UserManagerState()
157         ums.run {
158             init(UserCache.INSTANCE[context], context.getSystemService(UserManager::class.java))
159         }
160         val lc = LoaderCursor(c, LauncherAppState.getInstance(context), ums, pmHelper, null)
161         val deletedShortcuts = IntSet()
162 
163         while (lc.moveToNext()) {
164             if (lc.user !== Process.myUserHandle()) {
165                 deletedShortcuts.add(lc.id)
166                 continue
167             }
168             val intent = lc.parseIntent()
169             if (intent == null) {
170                 deletedShortcuts.add(lc.id)
171                 continue
172             }
173             if (TextUtils.isEmpty(lc.title)) {
174                 deletedShortcuts.add(lc.id)
175                 continue
176             }
177 
178             // Make sure the target intent can be launched without any permissions. Otherwise remove
179             // the shortcut
180             val ri = context.packageManager.resolveActivity(intent, 0)
181             if (ri == null || !TextUtils.isEmpty(ri.activityInfo.permission)) {
182                 deletedShortcuts.add(lc.id)
183                 continue
184             }
185             val extras =
186                 PersistableBundle().apply {
187                     putString(
188                         IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE,
189                         ri.activityInfo.packageName,
190                     )
191                 }
192             val infoBuilder =
193                 ShortcutInfo.Builder(context, "migrated_shortcut-${lc.id}")
194                     .setIntent(intent)
195                     .setExtras(extras)
196                     .setShortLabel(lc.title)
197 
198             var bitmap: Bitmap? = null
199             val iconData = lc.iconBlob
200             if (iconData != null) {
201                 bitmap = BitmapFactory.decodeByteArray(iconData, 0, iconData.size)
202             }
203             if (bitmap != null) {
204                 infoBuilder.setIcon(Icon.createWithBitmap(bitmap))
205             }
206 
207             val info = infoBuilder.build()
208             try {
209                 if (!PinRequestHelper.createRequestForShortcut(context, info).accept()) {
210                     deletedShortcuts.add(lc.id)
211                     continue
212                 }
213             } catch (e: Exception) {
214                 deletedShortcuts.add(lc.id)
215                 continue
216             }
217             val update =
218                 ContentValues().apply {
219                     put(
220                         LauncherSettings.Favorites.ITEM_TYPE,
221                         LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT,
222                     )
223                     put(
224                         LauncherSettings.Favorites.INTENT,
225                         ShortcutKey.makeIntent(info.id, context.packageName).toUri(0),
226                     )
227                 }
228             db.update(
229                 LauncherSettings.Favorites.TABLE_NAME,
230                 update,
231                 "_id = ?",
232                 arrayOf(lc.id.toString()),
233             )
234         }
235         lc.close()
236         if (deletedShortcuts.isEmpty.not()) {
237             db.delete(
238                 /* table = */ LauncherSettings.Favorites.TABLE_NAME,
239                 /* whereClause = */ Utilities.createDbSelectionQuery(
240                     LauncherSettings.Favorites._ID,
241                     deletedShortcuts.array,
242                 ),
243                 /* whereArgs = */ null,
244             )
245         }
246 
247         // Drop the unused columns
248         db.run {
249             execSQL("ALTER TABLE ${LauncherSettings.Favorites.TABLE_NAME} DROP COLUMN iconPackage;")
250             execSQL(
251                 "ALTER TABLE ${LauncherSettings.Favorites.TABLE_NAME} DROP COLUMN iconResource;"
252             )
253         }
254     }
255 
256     /** Utility class to simplify managing sqlite transactions */
257     class SQLiteTransaction(val db: SQLiteDatabase) : AutoCloseable {
258         init {
259             db.beginTransaction()
260         }
261 
262         fun commit() = db.setTransactionSuccessful()
263 
264         override fun close() = db.endTransaction()
265     }
266 }
267