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