1 /*
2  * 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 
17 package com.android.launcher3.provider;
18 
19 import static com.android.launcher3.LauncherSettings.Favorites.getColumns;
20 import static com.android.launcher3.icons.IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE;
21 
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.pm.ResolveInfo;
26 import android.content.pm.ShortcutInfo;
27 import android.database.Cursor;
28 import android.database.sqlite.SQLiteDatabase;
29 import android.graphics.Bitmap;
30 import android.graphics.BitmapFactory;
31 import android.graphics.drawable.Icon;
32 import android.os.PersistableBundle;
33 import android.os.Process;
34 import android.os.UserManager;
35 import android.text.TextUtils;
36 
37 import com.android.launcher3.LauncherAppState;
38 import com.android.launcher3.LauncherSettings.Favorites;
39 import com.android.launcher3.Utilities;
40 import com.android.launcher3.model.LoaderCursor;
41 import com.android.launcher3.model.UserManagerState;
42 import com.android.launcher3.pm.PinRequestHelper;
43 import com.android.launcher3.pm.UserCache;
44 import com.android.launcher3.shortcuts.ShortcutKey;
45 import com.android.launcher3.util.IntArray;
46 import com.android.launcher3.util.IntSet;
47 import com.android.launcher3.util.PackageManagerHelper;
48 
49 /**
50  * A set of utility methods for Launcher DB used for DB updates and migration.
51  */
52 public class LauncherDbUtils {
53     /**
54      * Returns a string which can be used as a where clause for DB query to match the given itemId
55      */
itemIdMatch(int itemId)56     public static String itemIdMatch(int itemId) {
57         return "_id=" + itemId;
58     }
59 
queryIntArray(boolean distinct, SQLiteDatabase db, String tableName, String columnName, String selection, String groupBy, String orderBy)60     public static IntArray queryIntArray(boolean distinct, SQLiteDatabase db, String tableName,
61             String columnName, String selection, String groupBy, String orderBy) {
62         IntArray out = new IntArray();
63         try (Cursor c = db.query(distinct, tableName, new String[] { columnName }, selection, null,
64                 groupBy, null, orderBy, null)) {
65             while (c.moveToNext()) {
66                 out.add(c.getInt(0));
67             }
68         }
69         return out;
70     }
71 
tableExists(SQLiteDatabase db, String tableName)72     public static boolean tableExists(SQLiteDatabase db, String tableName) {
73         try (Cursor c = db.query(true, "sqlite_master", new String[] {"tbl_name"},
74                 "tbl_name = ?", new String[] {tableName},
75                 null, null, null, null, null)) {
76             return c.getCount() > 0;
77         }
78     }
79 
dropTable(SQLiteDatabase db, String tableName)80     public static void dropTable(SQLiteDatabase db, String tableName) {
81         db.execSQL("DROP TABLE IF EXISTS " + tableName);
82     }
83 
84     /** Copy fromTable in fromDb to toTable in toDb. */
copyTable(SQLiteDatabase fromDb, String fromTable, SQLiteDatabase toDb, String toTable, Context context)85     public static void copyTable(SQLiteDatabase fromDb, String fromTable, SQLiteDatabase toDb,
86             String toTable, Context context) {
87         long userSerial = UserCache.INSTANCE.get(context).getSerialNumberForUser(
88                 Process.myUserHandle());
89         dropTable(toDb, toTable);
90         Favorites.addTableToDb(toDb, userSerial, false, toTable);
91         if (fromDb != toDb) {
92             toDb.execSQL("ATTACH DATABASE '" + fromDb.getPath() + "' AS from_db");
93             toDb.execSQL(
94                     "INSERT INTO " + toTable + " SELECT " + getColumns(userSerial)
95                         + " FROM from_db." + fromTable);
96             toDb.execSQL("DETACH DATABASE 'from_db'");
97         } else {
98             toDb.execSQL("INSERT INTO " + toTable + " SELECT " + getColumns(userSerial) + " FROM "
99                     + fromTable);
100         }
101     }
102 
103     /**
104      * Migrates the legacy shortcuts to deep shortcuts pinned under Launcher.
105      * Removes any invalid shortcut or any shortcut which requires some permission to launch
106      */
migrateLegacyShortcuts(Context context, SQLiteDatabase db)107     public static void migrateLegacyShortcuts(Context context, SQLiteDatabase db) {
108         Cursor c = db.query(
109                 Favorites.TABLE_NAME, null, "itemType = 1", null, null, null, null);
110         UserManagerState ums = new UserManagerState();
111         PackageManagerHelper pmHelper = PackageManagerHelper.INSTANCE.get(context);
112         ums.init(UserCache.INSTANCE.get(context),
113                 context.getSystemService(UserManager.class));
114         LoaderCursor lc = new LoaderCursor(c, LauncherAppState.getInstance(context), ums, pmHelper,
115                 null);
116         IntSet deletedShortcuts = new IntSet();
117 
118         while (lc.moveToNext()) {
119             if (lc.user != Process.myUserHandle()) {
120                 deletedShortcuts.add(lc.id);
121                 continue;
122             }
123             Intent intent = lc.parseIntent();
124             if (intent == null) {
125                 deletedShortcuts.add(lc.id);
126                 continue;
127             }
128             if (TextUtils.isEmpty(lc.getTitle())) {
129                 deletedShortcuts.add(lc.id);
130                 continue;
131             }
132 
133             // Make sure the target intent can be launched without any permissions. Otherwise remove
134             // the shortcut
135             ResolveInfo ri = context.getPackageManager().resolveActivity(intent, 0);
136             if (ri == null || !TextUtils.isEmpty(ri.activityInfo.permission)) {
137                 deletedShortcuts.add(lc.id);
138                 continue;
139             }
140             PersistableBundle extras = new PersistableBundle();
141             extras.putString(EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE, ri.activityInfo.packageName);
142             ShortcutInfo.Builder infoBuilder = new ShortcutInfo.Builder(
143                     context, "migrated_shortcut-" + lc.id)
144                     .setIntent(intent)
145                     .setExtras(extras)
146                     .setShortLabel(lc.getTitle());
147 
148             Bitmap bitmap = null;
149             byte[] iconData = lc.getIconBlob();
150             if (iconData != null) {
151                 bitmap = BitmapFactory.decodeByteArray(iconData, 0, iconData.length);
152             }
153             if (bitmap != null) {
154                 infoBuilder.setIcon(Icon.createWithBitmap(bitmap));
155             }
156 
157             ShortcutInfo info = infoBuilder.build();
158             try {
159                 if (!PinRequestHelper.createRequestForShortcut(context, info).accept()) {
160                     deletedShortcuts.add(lc.id);
161                     continue;
162                 }
163             } catch (Exception e) {
164                 deletedShortcuts.add(lc.id);
165                 continue;
166             }
167             ContentValues update = new ContentValues();
168             update.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_DEEP_SHORTCUT);
169             update.put(Favorites.INTENT,
170                     ShortcutKey.makeIntent(info.getId(), context.getPackageName()).toUri(0));
171             db.update(Favorites.TABLE_NAME, update, "_id = ?",
172                     new String[] {Integer.toString(lc.id)});
173         }
174         lc.close();
175         if (!deletedShortcuts.isEmpty()) {
176             db.delete(Favorites.TABLE_NAME,
177                     Utilities.createDbSelectionQuery(Favorites._ID, deletedShortcuts.getArray()),
178                     null);
179         }
180 
181         // Drop the unused columns
182         db.execSQL("ALTER TABLE " + Favorites.TABLE_NAME + " DROP COLUMN iconPackage;");
183         db.execSQL("ALTER TABLE " + Favorites.TABLE_NAME + " DROP COLUMN iconResource;");
184     }
185 
186     /**
187      * Utility class to simplify managing sqlite transactions
188      */
189     public static class SQLiteTransaction implements AutoCloseable {
190         private final SQLiteDatabase mDb;
191 
SQLiteTransaction(SQLiteDatabase db)192         public SQLiteTransaction(SQLiteDatabase db) {
193             mDb = db;
194             db.beginTransaction();
195         }
196 
commit()197         public void commit() {
198             mDb.setTransactionSuccessful();
199         }
200 
201         @Override
close()202         public void close() {
203             mDb.endTransaction();
204         }
205 
getDb()206         public SQLiteDatabase getDb() {
207             return mDb;
208         }
209     }
210 }
211