• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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;
18 
19 import android.annotation.TargetApi;
20 import android.appwidget.AppWidgetHost;
21 import android.appwidget.AppWidgetManager;
22 import android.content.ComponentName;
23 import android.content.ContentProvider;
24 import android.content.ContentProviderOperation;
25 import android.content.ContentProviderResult;
26 import android.content.ContentUris;
27 import android.content.ContentValues;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.OperationApplicationException;
31 import android.content.SharedPreferences;
32 import android.content.pm.PackageManager.NameNotFoundException;
33 import android.content.res.Resources;
34 import android.database.Cursor;
35 import android.database.SQLException;
36 import android.database.sqlite.SQLiteDatabase;
37 import android.database.sqlite.SQLiteOpenHelper;
38 import android.database.sqlite.SQLiteQueryBuilder;
39 import android.database.sqlite.SQLiteStatement;
40 import android.net.Uri;
41 import android.os.Binder;
42 import android.os.Build;
43 import android.os.Bundle;
44 import android.os.Handler;
45 import android.os.Message;
46 import android.os.Process;
47 import android.os.Trace;
48 import android.os.UserManager;
49 import android.text.TextUtils;
50 import android.util.Log;
51 
52 import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
53 import com.android.launcher3.LauncherSettings.Favorites;
54 import com.android.launcher3.LauncherSettings.WorkspaceScreens;
55 import com.android.launcher3.compat.UserHandleCompat;
56 import com.android.launcher3.compat.UserManagerCompat;
57 import com.android.launcher3.config.FeatureFlags;
58 import com.android.launcher3.config.ProviderConfig;
59 import com.android.launcher3.dynamicui.ExtractionUtils;
60 import com.android.launcher3.provider.LauncherDbUtils;
61 import com.android.launcher3.provider.RestoreDbTask;
62 import com.android.launcher3.util.ManagedProfileHeuristic;
63 import com.android.launcher3.util.NoLocaleSqliteContext;
64 import com.android.launcher3.util.Preconditions;
65 import com.android.launcher3.util.Thunk;
66 
67 import java.net.URISyntaxException;
68 import java.util.ArrayList;
69 import java.util.Collections;
70 import java.util.Locale;
71 
72 public class LauncherProvider extends ContentProvider {
73     private static final String TAG = "LauncherProvider";
74     private static final boolean LOGD = false;
75 
76     private static final int DATABASE_VERSION = 27;
77 
78     public static final String AUTHORITY = ProviderConfig.AUTHORITY;
79 
80     static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
81 
82     private static final String RESTRICTION_PACKAGE_NAME = "workspace.configuration.package.name";
83 
84     private final ChangeListenerWrapper mListenerWrapper = new ChangeListenerWrapper();
85     private Handler mListenerHandler;
86 
87     protected DatabaseHelper mOpenHelper;
88 
89     @Override
onCreate()90     public boolean onCreate() {
91         if (ProviderConfig.IS_DOGFOOD_BUILD) {
92             Log.d(TAG, "Launcher process started");
93         }
94         mListenerHandler = new Handler(mListenerWrapper);
95 
96         LauncherAppState.setLauncherProvider(this);
97         return true;
98     }
99 
100     /**
101      * Sets a provider listener.
102      */
setLauncherProviderChangeListener(LauncherProviderChangeListener listener)103     public void setLauncherProviderChangeListener(LauncherProviderChangeListener listener) {
104         Preconditions.assertUIThread();
105         mListenerWrapper.mListener = listener;
106     }
107 
108     @Override
getType(Uri uri)109     public String getType(Uri uri) {
110         SqlArguments args = new SqlArguments(uri, null, null);
111         if (TextUtils.isEmpty(args.where)) {
112             return "vnd.android.cursor.dir/" + args.table;
113         } else {
114             return "vnd.android.cursor.item/" + args.table;
115         }
116     }
117 
118     /**
119      * Overridden in tests
120      */
createDbIfNotExists()121     protected synchronized void createDbIfNotExists() {
122         if (mOpenHelper == null) {
123             if (LauncherAppState.PROFILE_STARTUP) {
124                 Trace.beginSection("Opening workspace DB");
125             }
126             mOpenHelper = new DatabaseHelper(getContext(), mListenerHandler);
127 
128             if (RestoreDbTask.isPending(getContext())) {
129                 if (!RestoreDbTask.performRestore(mOpenHelper)) {
130                     mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
131                 }
132                 // Set is pending to false irrespective of the result, so that it doesn't get
133                 // executed again.
134                 RestoreDbTask.setPending(getContext(), false);
135             }
136 
137             if (LauncherAppState.PROFILE_STARTUP) {
138                 Trace.endSection();
139             }
140         }
141     }
142 
143     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)144     public Cursor query(Uri uri, String[] projection, String selection,
145             String[] selectionArgs, String sortOrder) {
146         createDbIfNotExists();
147 
148         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
149         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
150         qb.setTables(args.table);
151 
152         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
153         Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder);
154         result.setNotificationUri(getContext().getContentResolver(), uri);
155 
156         return result;
157     }
158 
dbInsertAndCheck(DatabaseHelper helper, SQLiteDatabase db, String table, String nullColumnHack, ContentValues values)159     @Thunk static long dbInsertAndCheck(DatabaseHelper helper,
160             SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) {
161         if (values == null) {
162             throw new RuntimeException("Error: attempting to insert null values");
163         }
164         if (!values.containsKey(LauncherSettings.ChangeLogColumns._ID)) {
165             throw new RuntimeException("Error: attempting to add item without specifying an id");
166         }
167         helper.checkId(table, values);
168         return db.insert(table, nullColumnHack, values);
169     }
170 
reloadLauncherIfExternal()171     private void reloadLauncherIfExternal() {
172         if (Utilities.ATLEAST_MARSHMALLOW && Binder.getCallingPid() != Process.myPid()) {
173             LauncherAppState app = LauncherAppState.getInstanceNoCreate();
174             if (app != null) {
175                 app.reloadWorkspace();
176             }
177         }
178     }
179 
180     @Override
insert(Uri uri, ContentValues initialValues)181     public Uri insert(Uri uri, ContentValues initialValues) {
182         createDbIfNotExists();
183         SqlArguments args = new SqlArguments(uri);
184 
185         // In very limited cases, we support system|signature permission apps to modify the db.
186         if (Binder.getCallingPid() != Process.myPid()) {
187             if (!initializeExternalAdd(initialValues)) {
188                 return null;
189             }
190         }
191 
192         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
193         addModifiedTime(initialValues);
194         final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
195         if (rowId < 0) return null;
196 
197         uri = ContentUris.withAppendedId(uri, rowId);
198         notifyListeners();
199 
200         if (Utilities.ATLEAST_MARSHMALLOW) {
201             reloadLauncherIfExternal();
202         } else {
203             // Deprecated behavior to support legacy devices which rely on provider callbacks.
204             LauncherAppState app = LauncherAppState.getInstanceNoCreate();
205             if (app != null && "true".equals(uri.getQueryParameter("isExternalAdd"))) {
206                 app.reloadWorkspace();
207             }
208 
209             String notify = uri.getQueryParameter("notify");
210             if (notify == null || "true".equals(notify)) {
211                 getContext().getContentResolver().notifyChange(uri, null);
212             }
213         }
214         return uri;
215     }
216 
initializeExternalAdd(ContentValues values)217     private boolean initializeExternalAdd(ContentValues values) {
218         // 1. Ensure that externally added items have a valid item id
219         long id = mOpenHelper.generateNewItemId();
220         values.put(LauncherSettings.Favorites._ID, id);
221 
222         // 2. In the case of an app widget, and if no app widget id is specified, we
223         // attempt allocate and bind the widget.
224         Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE);
225         if (itemType != null &&
226                 itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET &&
227                 !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) {
228 
229             final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getContext());
230             ComponentName cn = ComponentName.unflattenFromString(
231                     values.getAsString(Favorites.APPWIDGET_PROVIDER));
232 
233             if (cn != null) {
234                 try {
235                     int appWidgetId = new AppWidgetHost(getContext(), Launcher.APPWIDGET_HOST_ID)
236                             .allocateAppWidgetId();
237                     values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
238                     if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) {
239                         return false;
240                     }
241                 } catch (RuntimeException e) {
242                     Log.e(TAG, "Failed to initialize external widget", e);
243                     return false;
244                 }
245             } else {
246                 return false;
247             }
248         }
249 
250         // Add screen id if not present
251         long screenId = values.getAsLong(LauncherSettings.Favorites.SCREEN);
252         SQLiteStatement stmp = null;
253         try {
254             stmp = mOpenHelper.getWritableDatabase().compileStatement(
255                     "INSERT OR IGNORE INTO workspaceScreens (_id, screenRank) " +
256                             "select ?, (ifnull(MAX(screenRank), -1)+1) from workspaceScreens");
257             stmp.bindLong(1, screenId);
258 
259             ContentValues valuesInserted = new ContentValues();
260             valuesInserted.put(LauncherSettings.BaseLauncherColumns._ID, stmp.executeInsert());
261             mOpenHelper.checkId(WorkspaceScreens.TABLE_NAME, valuesInserted);
262             return true;
263         } catch (Exception e) {
264             return false;
265         } finally {
266             Utilities.closeSilently(stmp);
267         }
268     }
269 
270     @Override
bulkInsert(Uri uri, ContentValues[] values)271     public int bulkInsert(Uri uri, ContentValues[] values) {
272         createDbIfNotExists();
273         SqlArguments args = new SqlArguments(uri);
274 
275         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
276         db.beginTransaction();
277         try {
278             int numValues = values.length;
279             for (int i = 0; i < numValues; i++) {
280                 addModifiedTime(values[i]);
281                 if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) {
282                     return 0;
283                 }
284             }
285             db.setTransactionSuccessful();
286         } finally {
287             db.endTransaction();
288         }
289 
290         notifyListeners();
291         reloadLauncherIfExternal();
292         return values.length;
293     }
294 
295     @Override
applyBatch(ArrayList<ContentProviderOperation> operations)296     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
297             throws OperationApplicationException {
298         createDbIfNotExists();
299         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
300         db.beginTransaction();
301         try {
302             ContentProviderResult[] result =  super.applyBatch(operations);
303             db.setTransactionSuccessful();
304             reloadLauncherIfExternal();
305             return result;
306         } finally {
307             db.endTransaction();
308         }
309     }
310 
311     @Override
delete(Uri uri, String selection, String[] selectionArgs)312     public int delete(Uri uri, String selection, String[] selectionArgs) {
313         createDbIfNotExists();
314         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
315 
316         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
317 
318         if (Binder.getCallingPid() != Process.myPid()
319                 && Favorites.TABLE_NAME.equalsIgnoreCase(args.table)) {
320             String widgetSelection = TextUtils.isEmpty(args.where) ? "1=1" : args.where;
321             widgetSelection = String.format(Locale.ENGLISH, "%1$s = %2$d AND ( %3$s )",
322                     Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET, widgetSelection);
323             try (Cursor c = db.query(Favorites.TABLE_NAME, new String[] { Favorites.APPWIDGET_ID },
324                     widgetSelection, args.args, null, null, null)) {
325                 AppWidgetHost host = new AppWidgetHost(getContext(), Launcher.APPWIDGET_HOST_ID);
326                 while (c.moveToNext()) {
327                     int widgetId = c.getInt(0);
328                     if (widgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
329                         try {
330                             host.deleteAppWidgetId(widgetId);
331                         } catch (RuntimeException e) {
332                             Log.e(TAG, "Error deleting widget id " + widgetId, e);
333                         }
334                     }
335                 }
336             }
337         }
338         int count = db.delete(args.table, args.where, args.args);
339         if (count > 0) {
340             notifyListeners();
341             reloadLauncherIfExternal();
342         }
343         return count;
344     }
345 
346     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)347     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
348         createDbIfNotExists();
349         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
350 
351         addModifiedTime(values);
352         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
353         int count = db.update(args.table, values, args.where, args.args);
354         if (count > 0) notifyListeners();
355 
356         reloadLauncherIfExternal();
357         return count;
358     }
359 
360     @Override
call(String method, final String arg, final Bundle extras)361     public Bundle call(String method, final String arg, final Bundle extras) {
362         if (Binder.getCallingUid() != Process.myUid()) {
363             return null;
364         }
365         createDbIfNotExists();
366 
367         switch (method) {
368             case LauncherSettings.Settings.METHOD_SET_EXTRACTED_COLORS_AND_WALLPAPER_ID: {
369                 String extractedColors = extras.getString(
370                         LauncherSettings.Settings.EXTRA_EXTRACTED_COLORS);
371                 int wallpaperId = extras.getInt(LauncherSettings.Settings.EXTRA_WALLPAPER_ID);
372                 Utilities.getPrefs(getContext()).edit()
373                         .putString(ExtractionUtils.EXTRACTED_COLORS_PREFERENCE_KEY, extractedColors)
374                         .putInt(ExtractionUtils.WALLPAPER_ID_PREFERENCE_KEY, wallpaperId)
375                         .apply();
376                 mListenerHandler.sendEmptyMessage(ChangeListenerWrapper.MSG_EXTRACTED_COLORS_CHANGED);
377                 Bundle result = new Bundle();
378                 result.putString(LauncherSettings.Settings.EXTRA_VALUE, extractedColors);
379                 return result;
380             }
381             case LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG: {
382                 clearFlagEmptyDbCreated();
383                 return null;
384             }
385             case LauncherSettings.Settings.METHOD_WAS_EMPTY_DB_CREATED : {
386                 Bundle result = new Bundle();
387                 result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE,
388                         Utilities.getPrefs(getContext()).getBoolean(EMPTY_DATABASE_CREATED, false));
389                 return result;
390             }
391             case LauncherSettings.Settings.METHOD_DELETE_EMPTY_FOLDERS: {
392                 Bundle result = new Bundle();
393                 result.putSerializable(LauncherSettings.Settings.EXTRA_VALUE, deleteEmptyFolders());
394                 return result;
395             }
396             case LauncherSettings.Settings.METHOD_NEW_ITEM_ID: {
397                 Bundle result = new Bundle();
398                 result.putLong(LauncherSettings.Settings.EXTRA_VALUE, mOpenHelper.generateNewItemId());
399                 return result;
400             }
401             case LauncherSettings.Settings.METHOD_NEW_SCREEN_ID: {
402                 Bundle result = new Bundle();
403                 result.putLong(LauncherSettings.Settings.EXTRA_VALUE, mOpenHelper.generateNewScreenId());
404                 return result;
405             }
406             case LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB: {
407                 createEmptyDB();
408                 return null;
409             }
410             case LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES: {
411                 loadDefaultFavoritesIfNecessary();
412                 return null;
413             }
414             case LauncherSettings.Settings.METHOD_DELETE_DB: {
415                 // Are you sure? (y/n)
416                 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
417                 return null;
418             }
419         }
420         return null;
421     }
422 
423     /**
424      * Deletes any empty folder from the DB.
425      * @return Ids of deleted folders.
426      */
deleteEmptyFolders()427     private ArrayList<Long> deleteEmptyFolders() {
428         ArrayList<Long> folderIds = new ArrayList<>();
429         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
430         db.beginTransaction();
431         try {
432             // Select folders whose id do not match any container value.
433             String selection = LauncherSettings.Favorites.ITEM_TYPE + " = "
434                     + LauncherSettings.Favorites.ITEM_TYPE_FOLDER + " AND "
435                     + LauncherSettings.Favorites._ID +  " NOT IN (SELECT " +
436                             LauncherSettings.Favorites.CONTAINER + " FROM "
437                                 + Favorites.TABLE_NAME + ")";
438             Cursor c = db.query(Favorites.TABLE_NAME,
439                     new String[] {LauncherSettings.Favorites._ID},
440                     selection, null, null, null, null);
441             while (c.moveToNext()) {
442                 folderIds.add(c.getLong(0));
443             }
444             c.close();
445             if (!folderIds.isEmpty()) {
446                 db.delete(Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
447                         LauncherSettings.Favorites._ID, folderIds), null);
448             }
449             db.setTransactionSuccessful();
450         } catch (SQLException ex) {
451             Log.e(TAG, ex.getMessage(), ex);
452             folderIds.clear();
453         } finally {
454             db.endTransaction();
455         }
456         return folderIds;
457     }
458 
459     /**
460      * Overridden in tests
461      */
notifyListeners()462     protected void notifyListeners() {
463         mListenerHandler.sendEmptyMessage(ChangeListenerWrapper.MSG_LAUNCHER_PROVIDER_CHANGED);
464     }
465 
addModifiedTime(ContentValues values)466     @Thunk static void addModifiedTime(ContentValues values) {
467         values.put(LauncherSettings.ChangeLogColumns.MODIFIED, System.currentTimeMillis());
468     }
469 
470     /**
471      * Clears all the data for a fresh start.
472      */
createEmptyDB()473     synchronized private void createEmptyDB() {
474         mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
475     }
476 
clearFlagEmptyDbCreated()477     private void clearFlagEmptyDbCreated() {
478         Utilities.getPrefs(getContext()).edit().remove(EMPTY_DATABASE_CREATED).commit();
479     }
480 
481     /**
482      * Loads the default workspace based on the following priority scheme:
483      *   1) From the app restrictions
484      *   2) From a package provided by play store
485      *   3) From a partner configuration APK, already in the system image
486      *   4) The default configuration for the particular device
487      */
loadDefaultFavoritesIfNecessary()488     synchronized private void loadDefaultFavoritesIfNecessary() {
489         SharedPreferences sp = Utilities.getPrefs(getContext());
490 
491         if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
492             Log.d(TAG, "loading default workspace");
493 
494             AppWidgetHost widgetHost = new AppWidgetHost(getContext(), Launcher.APPWIDGET_HOST_ID);
495             AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost);
496             if (loader == null) {
497                 loader = AutoInstallsLayout.get(getContext(),widgetHost, mOpenHelper);
498             }
499             if (loader == null) {
500                 final Partner partner = Partner.get(getContext().getPackageManager());
501                 if (partner != null && partner.hasDefaultLayout()) {
502                     final Resources partnerRes = partner.getResources();
503                     int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
504                             "xml", partner.getPackageName());
505                     if (workspaceResId != 0) {
506                         loader = new DefaultLayoutParser(getContext(), widgetHost,
507                                 mOpenHelper, partnerRes, workspaceResId);
508                     }
509                 }
510             }
511 
512             final boolean usingExternallyProvidedLayout = loader != null;
513             if (loader == null) {
514                 loader = getDefaultLayoutParser(widgetHost);
515             }
516 
517             // There might be some partially restored DB items, due to buggy restore logic in
518             // previous versions of launcher.
519             createEmptyDB();
520             // Populate favorites table with initial favorites
521             if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
522                     && usingExternallyProvidedLayout) {
523                 // Unable to load external layout. Cleanup and load the internal layout.
524                 createEmptyDB();
525                 mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
526                         getDefaultLayoutParser(widgetHost));
527             }
528             clearFlagEmptyDbCreated();
529         }
530     }
531 
532     /**
533      * Creates workspace loader from an XML resource listed in the app restrictions.
534      *
535      * @return the loader if the restrictions are set and the resource exists; null otherwise.
536      */
537     @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
createWorkspaceLoaderFromAppRestriction(AppWidgetHost widgetHost)538     private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(AppWidgetHost widgetHost) {
539         // UserManager.getApplicationRestrictions() requires minSdkVersion >= 18
540         if (!Utilities.ATLEAST_JB_MR2) {
541             return null;
542         }
543 
544         Context ctx = getContext();
545         UserManager um = (UserManager) ctx.getSystemService(Context.USER_SERVICE);
546         Bundle bundle = um.getApplicationRestrictions(ctx.getPackageName());
547         if (bundle == null) {
548             return null;
549         }
550 
551         String packageName = bundle.getString(RESTRICTION_PACKAGE_NAME);
552         if (packageName != null) {
553             try {
554                 Resources targetResources = ctx.getPackageManager()
555                         .getResourcesForApplication(packageName);
556                 return AutoInstallsLayout.get(ctx, packageName, targetResources,
557                         widgetHost, mOpenHelper);
558             } catch (NameNotFoundException e) {
559                 Log.e(TAG, "Target package for restricted profile not found", e);
560                 return null;
561             }
562         }
563         return null;
564     }
565 
getDefaultLayoutParser(AppWidgetHost widgetHost)566     private DefaultLayoutParser getDefaultLayoutParser(AppWidgetHost widgetHost) {
567         int defaultLayout = LauncherAppState.getInstance()
568                 .getInvariantDeviceProfile().defaultLayoutId;
569         return new DefaultLayoutParser(getContext(), widgetHost,
570                 mOpenHelper, getContext().getResources(), defaultLayout);
571     }
572 
573     /**
574      * The class is subclassed in tests to create an in-memory db.
575      */
576     public static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
577         private final Handler mWidgetHostResetHandler;
578         private final Context mContext;
579         private long mMaxItemId = -1;
580         private long mMaxScreenId = -1;
581 
DatabaseHelper(Context context, Handler widgetHostResetHandler)582         DatabaseHelper(Context context, Handler widgetHostResetHandler) {
583             this(context, widgetHostResetHandler, LauncherFiles.LAUNCHER_DB);
584             // Table creation sometimes fails silently, which leads to a crash loop.
585             // This way, we will try to create a table every time after crash, so the device
586             // would eventually be able to recover.
587             if (!tableExists(Favorites.TABLE_NAME) || !tableExists(WorkspaceScreens.TABLE_NAME)) {
588                 Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate");
589                 // This operation is a no-op if the table already exists.
590                 addFavoritesTable(getWritableDatabase(), true);
591                 addWorkspacesTable(getWritableDatabase(), true);
592             }
593 
594             initIds();
595         }
596 
597         /**
598          * Constructor used in tests and for restore.
599          */
DatabaseHelper( Context context, Handler widgetHostResetHandler, String tableName)600         public DatabaseHelper(
601                 Context context, Handler widgetHostResetHandler, String tableName) {
602             super(new NoLocaleSqliteContext(context), tableName, null, DATABASE_VERSION);
603             mContext = context;
604             mWidgetHostResetHandler = widgetHostResetHandler;
605         }
606 
initIds()607         protected void initIds() {
608             // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
609             // the DB here
610             if (mMaxItemId == -1) {
611                 mMaxItemId = initializeMaxItemId(getWritableDatabase());
612             }
613             if (mMaxScreenId == -1) {
614                 mMaxScreenId = initializeMaxScreenId(getWritableDatabase());
615             }
616         }
617 
tableExists(String tableName)618         private boolean tableExists(String tableName) {
619             Cursor c = getReadableDatabase().query(
620                     true, "sqlite_master", new String[] {"tbl_name"},
621                     "tbl_name = ?", new String[] {tableName},
622                     null, null, null, null, null);
623             try {
624                 return c.getCount() > 0;
625             } finally {
626                 c.close();
627             }
628         }
629 
630         @Override
onCreate(SQLiteDatabase db)631         public void onCreate(SQLiteDatabase db) {
632             if (LOGD) Log.d(TAG, "creating new launcher database");
633 
634             mMaxItemId = 1;
635             mMaxScreenId = 0;
636 
637             addFavoritesTable(db, false);
638             addWorkspacesTable(db, false);
639 
640             // Fresh and clean launcher DB.
641             mMaxItemId = initializeMaxItemId(db);
642             onEmptyDbCreated();
643         }
644 
645         /**
646          * Overriden in tests.
647          */
onEmptyDbCreated()648         protected void onEmptyDbCreated() {
649             // Database was just created, so wipe any previous widgets
650             if (mWidgetHostResetHandler != null) {
651                 new AppWidgetHost(mContext, Launcher.APPWIDGET_HOST_ID).deleteHost();
652                 mWidgetHostResetHandler.sendMessage(Message.obtain(
653                         mWidgetHostResetHandler,
654                         ChangeListenerWrapper.MSG_APP_WIDGET_HOST_RESET,
655                         mContext
656                 ));
657             }
658 
659             // Set the flag for empty DB
660             Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit();
661 
662             // When a new DB is created, remove all previously stored managed profile information.
663             ManagedProfileHeuristic.processAllUsers(Collections.<UserHandleCompat>emptyList(),
664                     mContext);
665         }
666 
getDefaultUserSerial()667         public long getDefaultUserSerial() {
668             return UserManagerCompat.getInstance(mContext).getSerialNumberForUser(
669                     UserHandleCompat.myUserHandle());
670         }
671 
addFavoritesTable(SQLiteDatabase db, boolean optional)672         private void addFavoritesTable(SQLiteDatabase db, boolean optional) {
673             Favorites.addTableToDb(db, getDefaultUserSerial(), optional);
674         }
675 
addWorkspacesTable(SQLiteDatabase db, boolean optional)676         private void addWorkspacesTable(SQLiteDatabase db, boolean optional) {
677             String ifNotExists = optional ? " IF NOT EXISTS " : "";
678             db.execSQL("CREATE TABLE " + ifNotExists + WorkspaceScreens.TABLE_NAME + " (" +
679                     LauncherSettings.WorkspaceScreens._ID + " INTEGER PRIMARY KEY," +
680                     LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," +
681                     LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" +
682                     ");");
683         }
684 
removeOrphanedItems(SQLiteDatabase db)685         private void removeOrphanedItems(SQLiteDatabase db) {
686             // Delete items directly on the workspace who's screen id doesn't exist
687             //  "DELETE FROM favorites WHERE screen NOT IN (SELECT _id FROM workspaceScreens)
688             //   AND container = -100"
689             String removeOrphanedDesktopItems = "DELETE FROM " + Favorites.TABLE_NAME +
690                     " WHERE " +
691                     LauncherSettings.Favorites.SCREEN + " NOT IN (SELECT " +
692                     LauncherSettings.WorkspaceScreens._ID + " FROM " + WorkspaceScreens.TABLE_NAME + ")" +
693                     " AND " +
694                     LauncherSettings.Favorites.CONTAINER + " = " +
695                     LauncherSettings.Favorites.CONTAINER_DESKTOP;
696             db.execSQL(removeOrphanedDesktopItems);
697 
698             // Delete items contained in folders which no longer exist (after above statement)
699             //  "DELETE FROM favorites  WHERE container <> -100 AND container <> -101 AND container
700             //   NOT IN (SELECT _id FROM favorites WHERE itemType = 2)"
701             String removeOrphanedFolderItems = "DELETE FROM " + Favorites.TABLE_NAME +
702                     " WHERE " +
703                     LauncherSettings.Favorites.CONTAINER + " <> " +
704                     LauncherSettings.Favorites.CONTAINER_DESKTOP +
705                     " AND "
706                     + LauncherSettings.Favorites.CONTAINER + " <> " +
707                     LauncherSettings.Favorites.CONTAINER_HOTSEAT +
708                     " AND "
709                     + LauncherSettings.Favorites.CONTAINER + " NOT IN (SELECT " +
710                     LauncherSettings.Favorites._ID + " FROM " + Favorites.TABLE_NAME +
711                     " WHERE " + LauncherSettings.Favorites.ITEM_TYPE + " = " +
712                     LauncherSettings.Favorites.ITEM_TYPE_FOLDER + ")";
713             db.execSQL(removeOrphanedFolderItems);
714         }
715 
716         @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)717         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
718             if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion);
719             switch (oldVersion) {
720                 // The version cannot be lower that 12, as Launcher3 never supported a lower
721                 // version of the DB.
722                 case 12: {
723                     // With the new shrink-wrapped and re-orderable workspaces, it makes sense
724                     // to persist workspace screens and their relative order.
725                     mMaxScreenId = 0;
726                     addWorkspacesTable(db, false);
727                 }
728                 case 13: {
729                     db.beginTransaction();
730                     try {
731                         // Insert new column for holding widget provider name
732                         db.execSQL("ALTER TABLE favorites " +
733                                 "ADD COLUMN appWidgetProvider TEXT;");
734                         db.setTransactionSuccessful();
735                     } catch (SQLException ex) {
736                         Log.e(TAG, ex.getMessage(), ex);
737                         // Old version remains, which means we wipe old data
738                         break;
739                     } finally {
740                         db.endTransaction();
741                     }
742                 }
743                 case 14: {
744                     db.beginTransaction();
745                     try {
746                         // Insert new column for holding update timestamp
747                         db.execSQL("ALTER TABLE favorites " +
748                                 "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
749                         db.execSQL("ALTER TABLE workspaceScreens " +
750                                 "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
751                         db.setTransactionSuccessful();
752                     } catch (SQLException ex) {
753                         Log.e(TAG, ex.getMessage(), ex);
754                         // Old version remains, which means we wipe old data
755                         break;
756                     } finally {
757                         db.endTransaction();
758                     }
759                 }
760                 case 15: {
761                     if (!addIntegerColumn(db, Favorites.RESTORED, 0)) {
762                         // Old version remains, which means we wipe old data
763                         break;
764                     }
765                 }
766                 case 16: {
767                     // We use the db version upgrade here to identify users who may not have seen
768                     // clings yet (because they weren't available), but for whom the clings are now
769                     // available (tablet users). Because one of the possible cling flows (migration)
770                     // is very destructive (wipes out workspaces), we want to prevent this from showing
771                     // until clear data. We do so by marking that the clings have been shown.
772                     LauncherClings.markFirstRunClingDismissed(mContext);
773                 }
774                 case 17: {
775                     // No-op
776                 }
777                 case 18: {
778                     // Due to a data loss bug, some users may have items associated with screen ids
779                     // which no longer exist. Since this can cause other problems, and since the user
780                     // will never see these items anyway, we use database upgrade as an opportunity to
781                     // clean things up.
782                     removeOrphanedItems(db);
783                 }
784                 case 19: {
785                     // Add userId column
786                     if (!addProfileColumn(db)) {
787                         // Old version remains, which means we wipe old data
788                         break;
789                     }
790                 }
791                 case 20:
792                     if (!updateFolderItemsRank(db, true)) {
793                         break;
794                     }
795                 case 21:
796                     // Recreate workspace table with screen id a primary key
797                     if (!recreateWorkspaceTable(db)) {
798                         break;
799                     }
800                 case 22: {
801                     if (!addIntegerColumn(db, Favorites.OPTIONS, 0)) {
802                         // Old version remains, which means we wipe old data
803                         break;
804                     }
805                 }
806                 case 23:
807                     // No-op
808                 case 24:
809                     ManagedProfileHeuristic.markExistingUsersForNoFolderCreation(mContext);
810                 case 25:
811                     convertShortcutsToLauncherActivities(db);
812                 case 26:
813                     // QSB was moved to the grid. Clear the first row on screen 0.
814                     if (FeatureFlags.QSB_ON_FIRST_SCREEN &&
815                             !LauncherDbUtils.prepareScreenZeroToHostQsb(db)) {
816                         break;
817                     }
818                 case 27: {
819                     // DB Upgraded successfully
820                     return;
821                 }
822             }
823 
824             // DB was not upgraded
825             Log.w(TAG, "Destroying all old data.");
826             createEmptyDB(db);
827         }
828 
829         @Override
onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)830         public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
831             // This shouldn't happen -- throw our hands up in the air and start over.
832             Log.w(TAG, "Database version downgrade from: " + oldVersion + " to " + newVersion +
833                     ". Wiping databse.");
834             createEmptyDB(db);
835         }
836 
837         /**
838          * Clears all the data for a fresh start.
839          */
createEmptyDB(SQLiteDatabase db)840         public void createEmptyDB(SQLiteDatabase db) {
841             db.execSQL("DROP TABLE IF EXISTS " + Favorites.TABLE_NAME);
842             db.execSQL("DROP TABLE IF EXISTS " + WorkspaceScreens.TABLE_NAME);
843             onCreate(db);
844         }
845 
846         /**
847          * Replaces all shortcuts of type {@link Favorites#ITEM_TYPE_SHORTCUT} which have a valid
848          * launcher activity target with {@link Favorites#ITEM_TYPE_APPLICATION}.
849          */
convertShortcutsToLauncherActivities(SQLiteDatabase db)850         @Thunk void convertShortcutsToLauncherActivities(SQLiteDatabase db) {
851             db.beginTransaction();
852             Cursor c = null;
853             SQLiteStatement updateStmt = null;
854 
855             try {
856                 // Only consider the primary user as other users can't have a shortcut.
857                 long userSerial = getDefaultUserSerial();
858                 c = db.query(Favorites.TABLE_NAME, new String[] {
859                         Favorites._ID,
860                         Favorites.INTENT,
861                     }, "itemType=" + Favorites.ITEM_TYPE_SHORTCUT + " AND profileId=" + userSerial,
862                     null, null, null, null);
863 
864                 updateStmt = db.compileStatement("UPDATE favorites SET itemType="
865                         + Favorites.ITEM_TYPE_APPLICATION + " WHERE _id=?");
866 
867                 final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
868                 final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT);
869 
870                 while (c.moveToNext()) {
871                     String intentDescription = c.getString(intentIndex);
872                     Intent intent;
873                     try {
874                         intent = Intent.parseUri(intentDescription, 0);
875                     } catch (URISyntaxException e) {
876                         Log.e(TAG, "Unable to parse intent", e);
877                         continue;
878                     }
879 
880                     if (!Utilities.isLauncherAppTarget(intent)) {
881                         continue;
882                     }
883 
884                     long id = c.getLong(idIndex);
885                     updateStmt.bindLong(1, id);
886                     updateStmt.executeUpdateDelete();
887                 }
888                 db.setTransactionSuccessful();
889             } catch (SQLException ex) {
890                 Log.w(TAG, "Error deduping shortcuts", ex);
891             } finally {
892                 db.endTransaction();
893                 if (c != null) {
894                     c.close();
895                 }
896                 if (updateStmt != null) {
897                     updateStmt.close();
898                 }
899             }
900         }
901 
902         /**
903          * Recreates workspace table and migrates data to the new table.
904          */
recreateWorkspaceTable(SQLiteDatabase db)905         public boolean recreateWorkspaceTable(SQLiteDatabase db) {
906             db.beginTransaction();
907             try {
908                 Cursor c = db.query(WorkspaceScreens.TABLE_NAME,
909                         new String[] {LauncherSettings.WorkspaceScreens._ID},
910                         null, null, null, null,
911                         LauncherSettings.WorkspaceScreens.SCREEN_RANK);
912                 ArrayList<Long> sortedIDs = new ArrayList<Long>();
913                 long maxId = 0;
914                 try {
915                     while (c.moveToNext()) {
916                         Long id = c.getLong(0);
917                         if (!sortedIDs.contains(id)) {
918                             sortedIDs.add(id);
919                             maxId = Math.max(maxId, id);
920                         }
921                     }
922                 } finally {
923                     c.close();
924                 }
925 
926                 db.execSQL("DROP TABLE IF EXISTS " + WorkspaceScreens.TABLE_NAME);
927                 addWorkspacesTable(db, false);
928 
929                 // Add all screen ids back
930                 int total = sortedIDs.size();
931                 for (int i = 0; i < total; i++) {
932                     ContentValues values = new ContentValues();
933                     values.put(LauncherSettings.WorkspaceScreens._ID, sortedIDs.get(i));
934                     values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
935                     addModifiedTime(values);
936                     db.insertOrThrow(WorkspaceScreens.TABLE_NAME, null, values);
937                 }
938                 db.setTransactionSuccessful();
939                 mMaxScreenId = maxId;
940             } catch (SQLException ex) {
941                 // Old version remains, which means we wipe old data
942                 Log.e(TAG, ex.getMessage(), ex);
943                 return false;
944             } finally {
945                 db.endTransaction();
946             }
947             return true;
948         }
949 
updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn)950         @Thunk boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) {
951             db.beginTransaction();
952             try {
953                 if (addRankColumn) {
954                     // Insert new column for holding rank
955                     db.execSQL("ALTER TABLE favorites ADD COLUMN rank INTEGER NOT NULL DEFAULT 0;");
956                 }
957 
958                 // Get a map for folder ID to folder width
959                 Cursor c = db.rawQuery("SELECT container, MAX(cellX) FROM favorites"
960                         + " WHERE container IN (SELECT _id FROM favorites WHERE itemType = ?)"
961                         + " GROUP BY container;",
962                         new String[] {Integer.toString(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)});
963 
964                 while (c.moveToNext()) {
965                     db.execSQL("UPDATE favorites SET rank=cellX+(cellY*?) WHERE "
966                             + "container=? AND cellX IS NOT NULL AND cellY IS NOT NULL;",
967                             new Object[] {c.getLong(1) + 1, c.getLong(0)});
968                 }
969 
970                 c.close();
971                 db.setTransactionSuccessful();
972             } catch (SQLException ex) {
973                 // Old version remains, which means we wipe old data
974                 Log.e(TAG, ex.getMessage(), ex);
975                 return false;
976             } finally {
977                 db.endTransaction();
978             }
979             return true;
980         }
981 
addProfileColumn(SQLiteDatabase db)982         private boolean addProfileColumn(SQLiteDatabase db) {
983             return addIntegerColumn(db, Favorites.PROFILE_ID, getDefaultUserSerial());
984         }
985 
addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue)986         private boolean addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue) {
987             db.beginTransaction();
988             try {
989                 db.execSQL("ALTER TABLE favorites ADD COLUMN "
990                         + columnName + " INTEGER NOT NULL DEFAULT " + defaultValue + ";");
991                 db.setTransactionSuccessful();
992             } catch (SQLException ex) {
993                 Log.e(TAG, ex.getMessage(), ex);
994                 return false;
995             } finally {
996                 db.endTransaction();
997             }
998             return true;
999         }
1000 
1001         // Generates a new ID to use for an object in your database. This method should be only
1002         // called from the main UI thread. As an exception, we do call it when we call the
1003         // constructor from the worker thread; however, this doesn't extend until after the
1004         // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
1005         // after that point
1006         @Override
generateNewItemId()1007         public long generateNewItemId() {
1008             if (mMaxItemId < 0) {
1009                 throw new RuntimeException("Error: max item id was not initialized");
1010             }
1011             mMaxItemId += 1;
1012             return mMaxItemId;
1013         }
1014 
1015         @Override
insertAndCheck(SQLiteDatabase db, ContentValues values)1016         public long insertAndCheck(SQLiteDatabase db, ContentValues values) {
1017             return dbInsertAndCheck(this, db, Favorites.TABLE_NAME, null, values);
1018         }
1019 
checkId(String table, ContentValues values)1020         public void checkId(String table, ContentValues values) {
1021             long id = values.getAsLong(LauncherSettings.BaseLauncherColumns._ID);
1022             if (table == WorkspaceScreens.TABLE_NAME) {
1023                 mMaxScreenId = Math.max(id, mMaxScreenId);
1024             }  else {
1025                 mMaxItemId = Math.max(id, mMaxItemId);
1026             }
1027         }
1028 
initializeMaxItemId(SQLiteDatabase db)1029         private long initializeMaxItemId(SQLiteDatabase db) {
1030             return getMaxId(db, Favorites.TABLE_NAME);
1031         }
1032 
1033         // Generates a new ID to use for an workspace screen in your database. This method
1034         // should be only called from the main UI thread. As an exception, we do call it when we
1035         // call the constructor from the worker thread; however, this doesn't extend until after the
1036         // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
1037         // after that point
generateNewScreenId()1038         public long generateNewScreenId() {
1039             if (mMaxScreenId < 0) {
1040                 throw new RuntimeException("Error: max screen id was not initialized");
1041             }
1042             mMaxScreenId += 1;
1043             return mMaxScreenId;
1044         }
1045 
initializeMaxScreenId(SQLiteDatabase db)1046         private long initializeMaxScreenId(SQLiteDatabase db) {
1047             return getMaxId(db, WorkspaceScreens.TABLE_NAME);
1048         }
1049 
loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader)1050         @Thunk int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
1051             ArrayList<Long> screenIds = new ArrayList<Long>();
1052             // TODO: Use multiple loaders with fall-back and transaction.
1053             int count = loader.loadLayout(db, screenIds);
1054 
1055             // Add the screens specified by the items above
1056             Collections.sort(screenIds);
1057             int rank = 0;
1058             ContentValues values = new ContentValues();
1059             for (Long id : screenIds) {
1060                 values.clear();
1061                 values.put(LauncherSettings.WorkspaceScreens._ID, id);
1062                 values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
1063                 if (dbInsertAndCheck(this, db, WorkspaceScreens.TABLE_NAME, null, values) < 0) {
1064                     throw new RuntimeException("Failed initialize screen table"
1065                             + "from default layout");
1066                 }
1067                 rank++;
1068             }
1069 
1070             // Ensure that the max ids are initialized
1071             mMaxItemId = initializeMaxItemId(db);
1072             mMaxScreenId = initializeMaxScreenId(db);
1073 
1074             return count;
1075         }
1076     }
1077 
1078     /**
1079      * @return the max _id in the provided table.
1080      */
getMaxId(SQLiteDatabase db, String table)1081     @Thunk static long getMaxId(SQLiteDatabase db, String table) {
1082         Cursor c = db.rawQuery("SELECT MAX(_id) FROM " + table, null);
1083         // get the result
1084         long id = -1;
1085         if (c != null && c.moveToNext()) {
1086             id = c.getLong(0);
1087         }
1088         if (c != null) {
1089             c.close();
1090         }
1091 
1092         if (id == -1) {
1093             throw new RuntimeException("Error: could not query max id in " + table);
1094         }
1095 
1096         return id;
1097     }
1098 
1099     static class SqlArguments {
1100         public final String table;
1101         public final String where;
1102         public final String[] args;
1103 
SqlArguments(Uri url, String where, String[] args)1104         SqlArguments(Uri url, String where, String[] args) {
1105             if (url.getPathSegments().size() == 1) {
1106                 this.table = url.getPathSegments().get(0);
1107                 this.where = where;
1108                 this.args = args;
1109             } else if (url.getPathSegments().size() != 2) {
1110                 throw new IllegalArgumentException("Invalid URI: " + url);
1111             } else if (!TextUtils.isEmpty(where)) {
1112                 throw new UnsupportedOperationException("WHERE clause not supported: " + url);
1113             } else {
1114                 this.table = url.getPathSegments().get(0);
1115                 this.where = "_id=" + ContentUris.parseId(url);
1116                 this.args = null;
1117             }
1118         }
1119 
SqlArguments(Uri url)1120         SqlArguments(Uri url) {
1121             if (url.getPathSegments().size() == 1) {
1122                 table = url.getPathSegments().get(0);
1123                 where = null;
1124                 args = null;
1125             } else {
1126                 throw new IllegalArgumentException("Invalid URI: " + url);
1127             }
1128         }
1129     }
1130 
1131     private static class ChangeListenerWrapper implements Handler.Callback {
1132 
1133         private static final int MSG_LAUNCHER_PROVIDER_CHANGED = 1;
1134         private static final int MSG_EXTRACTED_COLORS_CHANGED = 2;
1135         private static final int MSG_APP_WIDGET_HOST_RESET = 3;
1136 
1137         private LauncherProviderChangeListener mListener;
1138 
1139         @Override
handleMessage(Message msg)1140         public boolean handleMessage(Message msg) {
1141             if (mListener != null) {
1142                 switch (msg.what) {
1143                     case MSG_LAUNCHER_PROVIDER_CHANGED:
1144                         mListener.onLauncherProviderChange();
1145                         break;
1146                     case MSG_EXTRACTED_COLORS_CHANGED:
1147                         mListener.onExtractedColorsChanged();
1148                         break;
1149                     case MSG_APP_WIDGET_HOST_RESET:
1150                         Context context = (Context) msg.obj;
1151                         if (context != null) {
1152                             context.sendBroadcast(new Intent(Launcher.ACTION_APPWIDGET_HOST_RESET)
1153                                     .setPackage(context.getPackageName()));
1154                         }
1155                         break;
1156                 }
1157             }
1158             return true;
1159         }
1160     }
1161 }
1162