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