• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 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 static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
20 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
21 import static com.android.launcher3.provider.LauncherDbUtils.itemIdMatch;
22 
23 import android.content.ComponentName;
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.pm.ActivityInfo;
28 import android.content.pm.LauncherActivityInfo;
29 import android.content.pm.LauncherApps;
30 import android.content.pm.PackageManager;
31 import android.content.res.Resources;
32 import android.content.res.Resources.NotFoundException;
33 import android.content.res.XmlResourceParser;
34 import android.database.sqlite.SQLiteDatabase;
35 import android.os.Bundle;
36 import android.os.Process;
37 import android.text.TextUtils;
38 import android.util.ArrayMap;
39 import android.util.AttributeSet;
40 import android.util.Log;
41 import android.util.Xml;
42 
43 import androidx.annotation.Nullable;
44 import androidx.annotation.StringRes;
45 import androidx.annotation.WorkerThread;
46 import androidx.annotation.XmlRes;
47 
48 import com.android.launcher3.LauncherSettings.Favorites;
49 import com.android.launcher3.model.data.AppInfo;
50 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
51 import com.android.launcher3.model.data.WorkspaceItemInfo;
52 import com.android.launcher3.pm.UserCache;
53 import com.android.launcher3.qsb.QsbContainerView;
54 import com.android.launcher3.shortcuts.ShortcutKey;
55 import com.android.launcher3.uioverrides.ApiWrapper;
56 import com.android.launcher3.util.IntArray;
57 import com.android.launcher3.util.Partner;
58 import com.android.launcher3.util.Thunk;
59 import com.android.launcher3.widget.LauncherWidgetHolder;
60 
61 import org.xmlpull.v1.XmlPullParser;
62 import org.xmlpull.v1.XmlPullParserException;
63 
64 import java.io.IOException;
65 import java.util.Collections;
66 import java.util.Locale;
67 import java.util.Map;
68 import java.util.function.Supplier;
69 
70 /**
71  * Layout parsing code for auto installs layout
72  */
73 public class AutoInstallsLayout {
74     private static final String TAG = "AutoInstalls";
75     private static final boolean LOGD = false;
76 
77     /** Marker action used to discover a package which defines launcher customization */
78     static final String ACTION_LAUNCHER_CUSTOMIZATION =
79             "android.autoinstalls.config.action.PLAY_AUTO_INSTALL";
80 
81     /**
82      * Layout resource which also includes grid size and hotseat count, e.g., default_layout_6x6_h5
83      */
84     private static final String FORMATTED_LAYOUT_RES_WITH_HOSTEAT = "default_layout_%dx%d_h%s";
85     private static final String FORMATTED_LAYOUT_RES = "default_layout_%dx%d";
86     private static final String LAYOUT_RES = "default_layout";
87 
get(Context context, LauncherWidgetHolder appWidgetHolder, LayoutParserCallback callback)88     public static AutoInstallsLayout get(Context context, LauncherWidgetHolder appWidgetHolder,
89             LayoutParserCallback callback) {
90         Partner partner = Partner.get(context.getPackageManager(), ACTION_LAUNCHER_CUSTOMIZATION);
91         if (partner == null) {
92             return null;
93         }
94         InvariantDeviceProfile grid = LauncherAppState.getIDP(context);
95 
96         // Try with grid size and hotseat count
97         String layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES_WITH_HOSTEAT,
98                 grid.numColumns, grid.numRows, grid.numDatabaseHotseatIcons);
99         int layoutId = partner.getXmlResId(layoutName);
100 
101         // Try with only grid size
102         if (layoutId == 0) {
103             Log.d(TAG, "Formatted layout: " + layoutName
104                     + " not found. Trying layout without hosteat");
105             layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES,
106                     grid.numColumns, grid.numRows);
107             layoutId = partner.getXmlResId(layoutName);
108         }
109 
110         // Try the default layout
111         if (layoutId == 0) {
112             Log.d(TAG, "Formatted layout: " + layoutName + " not found. Trying the default layout");
113             layoutId = partner.getXmlResId(LAYOUT_RES);
114         }
115 
116         if (layoutId == 0) {
117             Log.e(TAG, "Layout definition not found in package: " + partner.getPackageName());
118             return null;
119         }
120         return new AutoInstallsLayout(context, appWidgetHolder, callback, partner.getResources(),
121                 layoutId, TAG_WORKSPACE);
122     }
123 
124     // Object Tags
125     private static final String TAG_INCLUDE = "include";
126     public static final String TAG_WORKSPACE = "workspace";
127     private static final String TAG_APP_ICON = "appicon";
128     private static final String TAG_AUTO_INSTALL = "autoinstall";
129     private static final String TAG_FOLDER = "folder";
130     private static final String TAG_APPWIDGET = "appwidget";
131     protected static final String TAG_SEARCH_WIDGET = "searchwidget";
132     private static final String TAG_SHORTCUT = "shortcut";
133     private static final String TAG_EXTRA = "extra";
134 
135     private static final String ATTR_CONTAINER = "container";
136     private static final String ATTR_RANK = "rank";
137 
138     private static final String ATTR_PACKAGE_NAME = "packageName";
139     private static final String ATTR_CLASS_NAME = "className";
140     private static final String ATTR_TITLE = "title";
141     private static final String ATTR_TITLE_TEXT = "titleText";
142     private static final String ATTR_SCREEN = "screen";
143     private static final String ATTR_SHORTCUT_ID = "shortcutId";
144 
145     // x and y can be specified as negative integers, in which case -1 represents the
146     // last row / column, -2 represents the second last, and so on.
147     private static final String ATTR_X = "x";
148     private static final String ATTR_Y = "y";
149 
150     private static final String ATTR_SPAN_X = "spanX";
151     private static final String ATTR_SPAN_Y = "spanY";
152 
153     // Attrs for "Include"
154     private static final String ATTR_WORKSPACE = "workspace";
155 
156     // Style attrs -- "Extra"
157     private static final String ATTR_KEY = "key";
158     private static final String ATTR_VALUE = "value";
159 
160     private static final String HOTSEAT_CONTAINER_NAME =
161             Favorites.containerToString(Favorites.CONTAINER_HOTSEAT);
162 
163     protected final Context mContext;
164     protected final LauncherWidgetHolder mAppWidgetHolder;
165     protected final LayoutParserCallback mCallback;
166 
167     protected final PackageManager mPackageManager;
168     protected final SourceResources mSourceRes;
169     protected final Supplier<XmlPullParser> mInitialLayoutSupplier;
170 
171     private final InvariantDeviceProfile mIdp;
172     private final int mRowCount;
173     private final int mColumnCount;
174     private final Map<String, LauncherActivityInfo> mActivityOverride;
175     private final int[] mTemp = new int[2];
176     @Thunk
177     final ContentValues mValues;
178     protected final String mRootTag;
179 
180     protected SQLiteDatabase mDb;
181 
AutoInstallsLayout(Context context, LauncherWidgetHolder appWidgetHolder, LayoutParserCallback callback, Resources res, int layoutId, String rootTag)182     public AutoInstallsLayout(Context context, LauncherWidgetHolder appWidgetHolder,
183             LayoutParserCallback callback, Resources res,
184             int layoutId, String rootTag) {
185         this(context, appWidgetHolder, callback, SourceResources.wrap(res),
186                 () -> res.getXml(layoutId), rootTag);
187     }
188 
AutoInstallsLayout(Context context, LauncherWidgetHolder appWidgetHolder, LayoutParserCallback callback, SourceResources res, Supplier<XmlPullParser> initialLayoutSupplier, String rootTag)189     public AutoInstallsLayout(Context context, LauncherWidgetHolder appWidgetHolder,
190             LayoutParserCallback callback, SourceResources res,
191             Supplier<XmlPullParser> initialLayoutSupplier, String rootTag) {
192         mContext = context;
193         mAppWidgetHolder = appWidgetHolder;
194         mCallback = callback;
195 
196         mPackageManager = context.getPackageManager();
197         mValues = new ContentValues();
198         mRootTag = rootTag;
199 
200         mSourceRes = res;
201         mInitialLayoutSupplier = initialLayoutSupplier;
202 
203         mIdp = LauncherAppState.getIDP(context);
204         mRowCount = mIdp.numRows;
205         mColumnCount = mIdp.numColumns;
206         mActivityOverride = ApiWrapper.getActivityOverrides(context);
207     }
208 
209     /**
210      * Loads the layout in the db and returns the number of entries added on the desktop.
211      */
loadLayout(SQLiteDatabase db, IntArray screenIds)212     public int loadLayout(SQLiteDatabase db, IntArray screenIds) {
213         mDb = db;
214         try {
215             return parseLayout(mInitialLayoutSupplier.get(), screenIds);
216         } catch (Exception e) {
217             Log.e(TAG, "Error parsing layout: ", e);
218             return -1;
219         }
220     }
221 
222     /**
223      * Parses the layout and returns the number of elements added on the homescreen.
224      */
parseLayout(XmlPullParser parser, IntArray screenIds)225     protected int parseLayout(XmlPullParser parser, IntArray screenIds)
226             throws XmlPullParserException, IOException {
227         beginDocument(parser, mRootTag);
228         final int depth = parser.getDepth();
229         int type;
230         ArrayMap<String, TagParser> tagParserMap = getLayoutElementsMap();
231         int count = 0;
232 
233         while (((type = parser.next()) != XmlPullParser.END_TAG ||
234                 parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
235             if (type != XmlPullParser.START_TAG) {
236                 continue;
237             }
238             count += parseAndAddNode(parser, tagParserMap, screenIds);
239         }
240         return count;
241     }
242 
243     /**
244      * Parses container and screenId attribute from the current tag, and puts it in the out.
245      * @param out array of size 2.
246      */
parseContainerAndScreen(XmlPullParser parser, int[] out)247     protected void parseContainerAndScreen(XmlPullParser parser, int[] out) {
248         if (HOTSEAT_CONTAINER_NAME.equals(getAttributeValue(parser, ATTR_CONTAINER))) {
249             out[0] = Favorites.CONTAINER_HOTSEAT;
250             // Hack: hotseat items are stored using screen ids
251             out[1] = Integer.parseInt(getAttributeValue(parser, ATTR_RANK));
252         } else {
253             out[0] = Favorites.CONTAINER_DESKTOP;
254             out[1] = Integer.parseInt(getAttributeValue(parser, ATTR_SCREEN));
255         }
256     }
257 
258     /**
259      * Parses the current node and returns the number of elements added.
260      */
parseAndAddNode( XmlPullParser parser, ArrayMap<String, TagParser> tagParserMap, IntArray screenIds)261     protected int parseAndAddNode(
262             XmlPullParser parser, ArrayMap<String, TagParser> tagParserMap, IntArray screenIds)
263             throws XmlPullParserException, IOException {
264 
265         if (TAG_INCLUDE.equals(parser.getName())) {
266             final int resId = getAttributeResourceValue(parser, ATTR_WORKSPACE, 0);
267             if (resId != 0) {
268                 // recursively load some more favorites, why not?
269                 return parseLayout(mSourceRes.getXml(resId), screenIds);
270             } else {
271                 return 0;
272             }
273         }
274 
275         mValues.clear();
276         parseContainerAndScreen(parser, mTemp);
277         final int container = mTemp[0];
278         final int screenId = mTemp[1];
279 
280         mValues.put(Favorites.CONTAINER, container);
281         mValues.put(Favorites.SCREEN, screenId);
282 
283         mValues.put(Favorites.CELLX,
284                 convertToDistanceFromEnd(getAttributeValue(parser, ATTR_X), mColumnCount));
285         mValues.put(Favorites.CELLY,
286                 convertToDistanceFromEnd(getAttributeValue(parser, ATTR_Y), mRowCount));
287 
288         TagParser tagParser = tagParserMap.get(parser.getName());
289         if (tagParser == null) {
290             if (LOGD) Log.d(TAG, "Ignoring unknown element tag: " + parser.getName());
291             return 0;
292         }
293         int newElementId = tagParser.parseAndAdd(parser);
294         if (newElementId >= 0) {
295             // Keep track of the set of screens which need to be added to the db.
296             if (!screenIds.contains(screenId) &&
297                     container == Favorites.CONTAINER_DESKTOP) {
298                 screenIds.add(screenId);
299             }
300             return 1;
301         }
302         return 0;
303     }
304 
addShortcut(String title, Intent intent, int type)305     protected int addShortcut(String title, Intent intent, int type) {
306         int id = mCallback.generateNewItemId();
307         mValues.put(Favorites.INTENT, intent.toUri(0));
308         mValues.put(Favorites.TITLE, title);
309         mValues.put(Favorites.ITEM_TYPE, type);
310         mValues.put(Favorites.SPANX, 1);
311         mValues.put(Favorites.SPANY, 1);
312         mValues.put(Favorites._ID, id);
313 
314         if (type == ITEM_TYPE_APPLICATION) {
315             ComponentName cn = intent.getComponent();
316             if (cn != null && mActivityOverride.containsKey(cn.getPackageName())) {
317                 LauncherActivityInfo replacementInfo = mActivityOverride.get(cn.getPackageName());
318                 mValues.put(Favorites.PROFILE_ID, UserCache.INSTANCE.get(mContext)
319                         .getSerialNumberForUser(replacementInfo.getUser()));
320                 mValues.put(Favorites.INTENT, AppInfo.makeLaunchIntent(replacementInfo).toUri(0));
321             }
322         }
323 
324         if (mCallback.insertAndCheck(mDb, mValues) < 0) {
325             return -1;
326         } else {
327             return id;
328         }
329     }
330 
getFolderElementsMap()331     protected ArrayMap<String, TagParser> getFolderElementsMap() {
332         ArrayMap<String, TagParser> parsers = new ArrayMap<>();
333         parsers.put(TAG_APP_ICON, new AppShortcutParser());
334         parsers.put(TAG_AUTO_INSTALL, new AutoInstallParser());
335         parsers.put(TAG_SHORTCUT, new ShortcutParser());
336         return parsers;
337     }
338 
getLayoutElementsMap()339     protected ArrayMap<String, TagParser> getLayoutElementsMap() {
340         ArrayMap<String, TagParser> parsers = new ArrayMap<>();
341         parsers.put(TAG_APP_ICON, new AppShortcutParser());
342         parsers.put(TAG_AUTO_INSTALL, new AutoInstallParser());
343         parsers.put(TAG_FOLDER, new FolderParser());
344         parsers.put(TAG_APPWIDGET, new PendingWidgetParser());
345         parsers.put(TAG_SEARCH_WIDGET, new SearchWidgetParser());
346         parsers.put(TAG_SHORTCUT, new ShortcutParser());
347         return parsers;
348     }
349 
350     protected interface TagParser {
351         /**
352          * Parses the tag and adds to the db
353          * @return the id of the row added or -1;
354          */
parseAndAdd(XmlPullParser parser)355         int parseAndAdd(XmlPullParser parser)
356                 throws XmlPullParserException, IOException;
357     }
358 
359     /**
360      * App shortcuts: required attributes packageName and className
361      */
362     protected class AppShortcutParser implements TagParser {
363 
364         @Override
parseAndAdd(XmlPullParser parser)365         public int parseAndAdd(XmlPullParser parser) {
366             final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME);
367             final String className = getAttributeValue(parser, ATTR_CLASS_NAME);
368 
369             if (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(className)) {
370                 ActivityInfo info;
371                 try {
372                     ComponentName cn;
373                     try {
374                         cn = new ComponentName(packageName, className);
375                         info = mPackageManager.getActivityInfo(cn, 0);
376                     } catch (PackageManager.NameNotFoundException nnfe) {
377                         String[] packages = mPackageManager.currentToCanonicalPackageNames(
378                                 new String[]{packageName});
379                         cn = new ComponentName(packages[0], className);
380                         info = mPackageManager.getActivityInfo(cn, 0);
381                     }
382                     final Intent intent = new Intent(Intent.ACTION_MAIN, null)
383                             .addCategory(Intent.CATEGORY_LAUNCHER)
384                             .setComponent(cn)
385                             .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
386                                     | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
387 
388                     return addShortcut(info.loadLabel(mPackageManager).toString(),
389                             intent, ITEM_TYPE_APPLICATION);
390                 } catch (PackageManager.NameNotFoundException e) {
391                     Log.e(TAG, "Favorite not found: " + packageName + "/" + className);
392                 }
393                 return -1;
394             } else {
395                 return invalidPackageOrClass(parser);
396             }
397         }
398 
399         /**
400          * Helper method to allow extending the parser capabilities
401          */
invalidPackageOrClass(XmlPullParser parser)402         protected int invalidPackageOrClass(XmlPullParser parser) {
403             Log.w(TAG, "Skipping invalid <favorite> with no component");
404             return -1;
405         }
406     }
407 
408     /**
409      * AutoInstall: required attributes packageName and className
410      */
411     protected class AutoInstallParser implements TagParser {
412 
413         @Override
parseAndAdd(XmlPullParser parser)414         public int parseAndAdd(XmlPullParser parser) {
415             final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME);
416             final String className = getAttributeValue(parser, ATTR_CLASS_NAME);
417             if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(className)) {
418                 if (LOGD) Log.d(TAG, "Skipping invalid <favorite> with no component");
419                 return -1;
420             }
421 
422             mValues.put(Favorites.RESTORED, WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON);
423             final Intent intent = new Intent(Intent.ACTION_MAIN, null)
424                     .addCategory(Intent.CATEGORY_LAUNCHER)
425                     .setComponent(new ComponentName(packageName, className))
426                     .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
427                             | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
428             return addShortcut(mContext.getString(R.string.package_state_unknown), intent,
429                     ITEM_TYPE_APPLICATION);
430         }
431     }
432 
433     /**
434      * Parses a deep shortcut. Required attributes packageName and shortcutId
435      */
436     protected class ShortcutParser implements TagParser {
437 
438         @Override
parseAndAdd(XmlPullParser parser)439         public int parseAndAdd(XmlPullParser parser) {
440             final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME);
441             final String shortcutId = getAttributeValue(parser, ATTR_SHORTCUT_ID);
442 
443             try {
444                 LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
445                 launcherApps.pinShortcuts(packageName, Collections.singletonList(shortcutId),
446                         Process.myUserHandle());
447                 Intent intent = ShortcutKey.makeIntent(shortcutId, packageName);
448                 mValues.put(Favorites.RESTORED, WorkspaceItemInfo.FLAG_RESTORED_ICON);
449                 return addShortcut(null, intent, Favorites.ITEM_TYPE_DEEP_SHORTCUT);
450             } catch (Exception e) {
451                 Log.e(TAG, "Unable to pin the shortcut for shortcut id = " + shortcutId
452                         + " and package name = " + packageName, e);
453             }
454             return -1;
455         }
456     }
457 
458     /**
459      * AppWidget parser: Required attributes packageName, className, spanX and spanY.
460      * Options child nodes: <extra key=... value=... />
461      * It adds a pending widget which allows the widget to come later. If there are extras, those
462      * are passed to widget options during bind.
463      * The config activity for the widget (if present) is not shown, so any optional configurations
464      * should be passed as extras and the widget should support reading these widget options.
465      */
466     protected class PendingWidgetParser implements TagParser {
467 
468         @Nullable
getComponentName(XmlPullParser parser)469         public ComponentName getComponentName(XmlPullParser parser) {
470             final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME);
471             final String className = getAttributeValue(parser, ATTR_CLASS_NAME);
472             if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(className)) {
473                 return null;
474             }
475             return new ComponentName(packageName, className);
476         }
477 
478 
479         @Override
parseAndAdd(XmlPullParser parser)480         public int parseAndAdd(XmlPullParser parser)
481                 throws XmlPullParserException, IOException {
482             ComponentName cn = getComponentName(parser);
483             if (cn == null) {
484                 if (LOGD) Log.d(TAG, "Skipping invalid <appwidget> with no component");
485                 return -1;
486             }
487 
488             mValues.put(Favorites.SPANX, getAttributeValue(parser, ATTR_SPAN_X));
489             mValues.put(Favorites.SPANY, getAttributeValue(parser, ATTR_SPAN_Y));
490             mValues.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET);
491 
492             // Read the extras
493             Bundle extras = new Bundle();
494             int widgetDepth = parser.getDepth();
495             int type;
496             while ((type = parser.next()) != XmlPullParser.END_TAG ||
497                     parser.getDepth() > widgetDepth) {
498                 if (type != XmlPullParser.START_TAG) {
499                     continue;
500                 }
501 
502                 if (TAG_EXTRA.equals(parser.getName())) {
503                     String key = getAttributeValue(parser, ATTR_KEY);
504                     String value = getAttributeValue(parser, ATTR_VALUE);
505                     if (key != null && value != null) {
506                         extras.putString(key, value);
507                     } else {
508                         throw new RuntimeException("Widget extras must have a key and value");
509                     }
510                 } else {
511                     throw new RuntimeException("Widgets can contain only extras");
512                 }
513             }
514             return verifyAndInsert(cn, extras);
515         }
516 
verifyAndInsert(ComponentName cn, Bundle extras)517         protected int verifyAndInsert(ComponentName cn, Bundle extras) {
518             mValues.put(Favorites.APPWIDGET_PROVIDER, cn.flattenToString());
519             mValues.put(Favorites.RESTORED,
520                     LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
521                             | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY
522                             | LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG);
523             mValues.put(Favorites._ID, mCallback.generateNewItemId());
524             if (!extras.isEmpty()) {
525                 mValues.put(Favorites.INTENT, new Intent().putExtras(extras).toUri(0));
526             }
527 
528             int insertedId = mCallback.insertAndCheck(mDb, mValues);
529             if (insertedId < 0) {
530                 return -1;
531             } else {
532                 return insertedId;
533             }
534         }
535     }
536 
537     protected class SearchWidgetParser extends PendingWidgetParser {
538         @Override
539         @Nullable
540         @WorkerThread
getComponentName(XmlPullParser parser)541         public ComponentName getComponentName(XmlPullParser parser) {
542             return QsbContainerView.getSearchComponentName(mContext);
543         }
544 
545         @Override
verifyAndInsert(ComponentName cn, Bundle extras)546         protected int verifyAndInsert(ComponentName cn, Bundle extras) {
547             mValues.put(Favorites.OPTIONS, LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET);
548             int flags = mValues.getAsInteger(Favorites.RESTORED)
549                     | WorkspaceItemInfo.FLAG_RESTORE_STARTED;
550             mValues.put(Favorites.RESTORED, flags);
551             return super.verifyAndInsert(cn, extras);
552         }
553     }
554 
555     protected class FolderParser implements TagParser {
556         private final ArrayMap<String, TagParser> mFolderElements;
557 
FolderParser()558         public FolderParser() {
559             this(getFolderElementsMap());
560         }
561 
FolderParser(ArrayMap<String, TagParser> elements)562         public FolderParser(ArrayMap<String, TagParser> elements) {
563             mFolderElements = elements;
564         }
565 
566         @Override
parseAndAdd(XmlPullParser parser)567         public int parseAndAdd(XmlPullParser parser) throws XmlPullParserException, IOException {
568             final String title;
569             final int titleResId = getAttributeResourceValue(parser, ATTR_TITLE, 0);
570             if (titleResId != 0) {
571                 title = mSourceRes.getString(titleResId);
572             } else {
573                 String titleText = getAttributeValue(parser, ATTR_TITLE_TEXT);
574                 title = TextUtils.isEmpty(titleText) ? "" : titleText;
575             }
576 
577             mValues.put(Favorites.TITLE, title);
578             mValues.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_FOLDER);
579             mValues.put(Favorites.SPANX, 1);
580             mValues.put(Favorites.SPANY, 1);
581             mValues.put(Favorites._ID, mCallback.generateNewItemId());
582             int folderId = mCallback.insertAndCheck(mDb, mValues);
583             if (folderId < 0) {
584                 if (LOGD) Log.e(TAG, "Unable to add folder");
585                 return -1;
586             }
587 
588             final ContentValues myValues = new ContentValues(mValues);
589             IntArray folderItems = new IntArray();
590 
591             int type;
592             int folderDepth = parser.getDepth();
593             int rank = 0;
594             while ((type = parser.next()) != XmlPullParser.END_TAG ||
595                     parser.getDepth() > folderDepth) {
596                 if (type != XmlPullParser.START_TAG) {
597                     continue;
598                 }
599                 mValues.clear();
600                 mValues.put(Favorites.CONTAINER, folderId);
601                 mValues.put(Favorites.RANK, rank);
602 
603                 TagParser tagParser = mFolderElements.get(parser.getName());
604                 if (tagParser != null) {
605                     final int id = tagParser.parseAndAdd(parser);
606                     if (id >= 0) {
607                         folderItems.add(id);
608                         rank++;
609                     }
610                 } else {
611                     throw new RuntimeException("Invalid folder item " + parser.getName());
612                 }
613             }
614 
615             int addedId = folderId;
616 
617             // We can only have folders with >= 2 items, so we need to remove the
618             // folder and clean up if less than 2 items were included, or some
619             // failed to add, and less than 2 were actually added
620             if (folderItems.size() < 2) {
621                 // Delete the folder
622                 mDb.delete(TABLE_NAME, itemIdMatch(folderId), null);
623                 addedId = -1;
624 
625                 // If we have a single item, promote it to where the folder
626                 // would have been.
627                 if (folderItems.size() == 1) {
628                     final ContentValues childValues = new ContentValues();
629                     copyInteger(myValues, childValues, Favorites.CONTAINER);
630                     copyInteger(myValues, childValues, Favorites.SCREEN);
631                     copyInteger(myValues, childValues, Favorites.CELLX);
632                     copyInteger(myValues, childValues, Favorites.CELLY);
633 
634                     addedId = folderItems.get(0);
635                     mDb.update(TABLE_NAME, childValues,
636                             Favorites._ID + "=" + addedId, null);
637                 }
638             }
639             return addedId;
640         }
641     }
642 
beginDocument(XmlPullParser parser, String firstElementName)643     public static void beginDocument(XmlPullParser parser, String firstElementName)
644             throws XmlPullParserException, IOException {
645         int type;
646         while ((type = parser.next()) != XmlPullParser.START_TAG
647                 && type != XmlPullParser.END_DOCUMENT);
648 
649         if (type != XmlPullParser.START_TAG) {
650             throw new XmlPullParserException("No start tag found");
651         }
652 
653         if (!parser.getName().equals(firstElementName)) {
654             throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() +
655                     ", expected " + firstElementName);
656         }
657     }
658 
convertToDistanceFromEnd(String value, int endValue)659     private static String convertToDistanceFromEnd(String value, int endValue) {
660         if (!TextUtils.isEmpty(value)) {
661             int x = Integer.parseInt(value);
662             if (x < 0) {
663                 return Integer.toString(endValue + x);
664             }
665         }
666         return value;
667     }
668 
669     /**
670      * Return attribute value, attempting launcher-specific namespace first
671      * before falling back to anonymous attribute.
672      */
getAttributeValue(XmlPullParser parser, String attribute)673     protected static String getAttributeValue(XmlPullParser parser, String attribute) {
674         String value = parser.getAttributeValue(
675                 "http://schemas.android.com/apk/res-auto/com.android.launcher3", attribute);
676         if (value == null) {
677             value = parser.getAttributeValue(null, attribute);
678         }
679         return value;
680     }
681 
682     /**
683      * Return attribute resource value, attempting launcher-specific namespace
684      * first before falling back to anonymous attribute.
685      */
getAttributeResourceValue(XmlPullParser parser, String attribute, int defaultValue)686     protected static int getAttributeResourceValue(XmlPullParser parser, String attribute,
687             int defaultValue) {
688         AttributeSet attrs = Xml.asAttributeSet(parser);
689         int value = attrs.getAttributeResourceValue(
690                 "http://schemas.android.com/apk/res-auto/com.android.launcher3", attribute,
691                 defaultValue);
692         if (value == defaultValue) {
693             value = attrs.getAttributeResourceValue(null, attribute, defaultValue);
694         }
695         return value;
696     }
697 
698     public interface LayoutParserCallback {
generateNewItemId()699         int generateNewItemId();
700 
insertAndCheck(SQLiteDatabase db, ContentValues values)701         int insertAndCheck(SQLiteDatabase db, ContentValues values);
702     }
703 
704     @Thunk
copyInteger(ContentValues from, ContentValues to, String key)705     static void copyInteger(ContentValues from, ContentValues to, String key) {
706         to.put(key, from.getAsInteger(key));
707     }
708 
709     /**
710      * Wrapper over resources for easier abstraction
711      */
712     public interface SourceResources {
713 
714         /**
715          * Refer {@link Resources#getXml(int)}
716          */
getXml(@mlRes int id)717         default XmlResourceParser getXml(@XmlRes int id) throws NotFoundException {
718             throw new NotFoundException();
719         }
720 
721         /**
722          * Refer {@link Resources#getString(int)}
723          */
getString(@tringRes int id)724         default String getString(@StringRes int id) throws NotFoundException {
725             throw new NotFoundException();
726         }
727 
728         /**
729          * Returns a {@link SourceResources} corresponding to the provided resources
730          */
wrap(Resources res)731         static SourceResources wrap(Resources res) {
732             return new SourceResources() {
733                 @Override
734                 public XmlResourceParser getXml(int id) {
735                     return res.getXml(id);
736                 }
737 
738                 @Override
739                 public String getString(int id) {
740                     return res.getString(id);
741                 }
742             };
743         }
744     }
745 
746 
747 }
748