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