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