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 addProfileId(XmlPullParser parser)264 private void addProfileId(XmlPullParser parser) { 265 Long profileId = mUserTypeToSerial.get(getAttributeValue(parser, ATTR_USER_TYPE)); 266 if (profileId != null) { 267 mValues.put(Favorites.PROFILE_ID, profileId); 268 } 269 } 270 271 /** 272 * Parses container and screenId attribute from the current tag, and puts it in the out. 273 * @param out array of size 2. 274 */ parseContainerAndScreen(XmlPullParser parser, int[] out)275 protected void parseContainerAndScreen(XmlPullParser parser, int[] out) { 276 if (HOTSEAT_CONTAINER_NAME.equals(getAttributeValue(parser, ATTR_CONTAINER))) { 277 out[0] = Favorites.CONTAINER_HOTSEAT; 278 // Hack: hotseat items are stored using screen ids 279 out[1] = Integer.parseInt(getAttributeValue(parser, ATTR_RANK)); 280 } else { 281 out[0] = Favorites.CONTAINER_DESKTOP; 282 out[1] = Integer.parseInt(getAttributeValue(parser, ATTR_SCREEN)); 283 } 284 } 285 286 /** 287 * Parses the current node and returns the number of elements added. 288 */ parseAndAddNode( XmlPullParser parser, ArrayMap<String, TagParser> tagParserMap)289 protected int parseAndAddNode( 290 XmlPullParser parser, ArrayMap<String, TagParser> tagParserMap) 291 throws XmlPullParserException, IOException { 292 293 if (TAG_INCLUDE.equals(parser.getName())) { 294 final int resId = getAttributeResourceValue(parser, ATTR_WORKSPACE, 0); 295 if (resId != 0) { 296 // recursively load some more favorites, why not? 297 return parseLayout(mSourceRes.getXml(resId)); 298 } else { 299 return 0; 300 } 301 } 302 303 mValues.clear(); 304 parseContainerAndScreen(parser, mTemp); 305 final int container = mTemp[0]; 306 final int screenId = mTemp[1]; 307 308 mValues.put(Favorites.CONTAINER, container); 309 mValues.put(Favorites.SCREEN, screenId); 310 311 mValues.put(Favorites.CELLX, 312 convertToDistanceFromEnd(getAttributeValue(parser, ATTR_X), mColumnCount)); 313 mValues.put(Favorites.CELLY, 314 convertToDistanceFromEnd(getAttributeValue(parser, ATTR_Y), mRowCount)); 315 316 TagParser tagParser = tagParserMap.get(parser.getName()); 317 if (tagParser == null) { 318 if (LOGD) Log.d(TAG, "Ignoring unknown element tag: " + parser.getName()); 319 return 0; 320 } 321 return tagParser.parseAndAdd(parser) >= 0 ? 1 : 0; 322 } 323 addShortcut(String title, Intent intent, int type)324 protected int addShortcut(String title, Intent intent, int type) { 325 int id = mCallback.generateNewItemId(); 326 mValues.put(Favorites.INTENT, intent.toUri(0)); 327 mValues.put(Favorites.TITLE, title); 328 mValues.put(Favorites.ITEM_TYPE, type); 329 mValues.put(Favorites.SPANX, 1); 330 mValues.put(Favorites.SPANY, 1); 331 mValues.put(Favorites._ID, id); 332 333 ComponentName cn = intent.getComponent(); 334 if (cn != null && type == ITEM_TYPE_APPLICATION 335 && !mValues.containsKey(Favorites.PROFILE_ID)) { 336 LauncherActivityInfo replacementInfo = mActivityOverride.get(cn.getPackageName()); 337 if (replacementInfo != null) { 338 mValues.put(Favorites.PROFILE_ID, UserCache.INSTANCE.get(mContext) 339 .getSerialNumberForUser(replacementInfo.getUser())); 340 mValues.put(Favorites.INTENT, AppInfo.makeLaunchIntent(replacementInfo).toUri(0)); 341 } 342 } 343 344 if (mCallback.insertAndCheck(mDb, mValues) < 0) { 345 return -1; 346 } else { 347 return id; 348 } 349 } 350 getFolderElementsMap()351 protected ArrayMap<String, TagParser> getFolderElementsMap() { 352 ArrayMap<String, TagParser> parsers = new ArrayMap<>(); 353 parsers.put(TAG_APP_ICON, new AppShortcutParser()); 354 parsers.put(TAG_AUTO_INSTALL, new AutoInstallParser()); 355 parsers.put(TAG_SHORTCUT, new ShortcutParser()); 356 return parsers; 357 } 358 getLayoutElementsMap()359 protected ArrayMap<String, TagParser> getLayoutElementsMap() { 360 ArrayMap<String, TagParser> parsers = new ArrayMap<>(); 361 parsers.put(TAG_APP_ICON, new AppShortcutParser()); 362 parsers.put(TAG_AUTO_INSTALL, new AutoInstallParser()); 363 parsers.put(TAG_FOLDER, new FolderParser()); 364 parsers.put(TAG_APPWIDGET, new PendingWidgetParser()); 365 parsers.put(TAG_SEARCH_WIDGET, new SearchWidgetParser()); 366 parsers.put(TAG_SHORTCUT, new ShortcutParser()); 367 return parsers; 368 } 369 370 protected interface TagParser { 371 /** 372 * Parses the tag and adds to the db 373 * @return the id of the row added or -1; 374 */ parseAndAdd(XmlPullParser parser)375 int parseAndAdd(XmlPullParser parser) 376 throws XmlPullParserException, IOException; 377 } 378 379 /** 380 * App shortcuts: required attributes packageName and className 381 */ 382 protected class AppShortcutParser implements TagParser { 383 384 @Override parseAndAdd(XmlPullParser parser)385 public int parseAndAdd(XmlPullParser parser) { 386 final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME); 387 final String className = getAttributeValue(parser, ATTR_CLASS_NAME); 388 addProfileId(parser); 389 if (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(className)) { 390 ActivityInfo info; 391 try { 392 ComponentName cn; 393 try { 394 cn = new ComponentName(packageName, className); 395 info = mPackageManager.getActivityInfo(cn, 0); 396 } catch (PackageManager.NameNotFoundException nnfe) { 397 String[] packages = mPackageManager.currentToCanonicalPackageNames( 398 new String[]{packageName}); 399 cn = new ComponentName(packages[0], className); 400 info = mPackageManager.getActivityInfo(cn, 0); 401 } 402 final Intent intent = new Intent(Intent.ACTION_MAIN, null) 403 .addCategory(Intent.CATEGORY_LAUNCHER) 404 .setComponent(cn) 405 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 406 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 407 408 return addShortcut(info.loadLabel(mPackageManager).toString(), 409 intent, ITEM_TYPE_APPLICATION); 410 } catch (PackageManager.NameNotFoundException e) { 411 Log.e(TAG, "Favorite not found: " + packageName + "/" + className); 412 } 413 return -1; 414 } else { 415 return invalidPackageOrClass(parser); 416 } 417 } 418 419 /** 420 * Helper method to allow extending the parser capabilities 421 */ invalidPackageOrClass(XmlPullParser parser)422 protected int invalidPackageOrClass(XmlPullParser parser) { 423 Log.w(TAG, "Skipping invalid <favorite> with no component"); 424 return -1; 425 } 426 } 427 428 /** 429 * AutoInstall: required attributes packageName and className 430 */ 431 protected class AutoInstallParser implements TagParser { 432 433 @Override parseAndAdd(XmlPullParser parser)434 public int parseAndAdd(XmlPullParser parser) { 435 final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME); 436 final String className = getAttributeValue(parser, ATTR_CLASS_NAME); 437 addProfileId(parser); 438 if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(className)) { 439 if (LOGD) Log.d(TAG, "Skipping invalid <favorite> with no component"); 440 return -1; 441 } 442 443 mValues.put(Favorites.RESTORED, WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON); 444 Intent intent = AppInfo.makeLaunchIntent(new ComponentName(packageName, className)); 445 return addShortcut(mContext.getString(R.string.package_state_unknown), intent, 446 ITEM_TYPE_APPLICATION); 447 } 448 } 449 450 /** 451 * Parses a deep shortcut. Required attributes packageName and shortcutId 452 */ 453 protected class ShortcutParser implements TagParser { 454 455 @Override parseAndAdd(XmlPullParser parser)456 public int parseAndAdd(XmlPullParser parser) { 457 final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME); 458 final String shortcutId = getAttributeValue(parser, ATTR_SHORTCUT_ID); 459 addProfileId(parser); 460 try { 461 LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class); 462 launcherApps.pinShortcuts(packageName, Collections.singletonList(shortcutId), 463 Process.myUserHandle()); 464 Intent intent = ShortcutKey.makeIntent(shortcutId, packageName); 465 mValues.put(Favorites.RESTORED, WorkspaceItemInfo.FLAG_RESTORED_ICON); 466 return addShortcut(null, intent, Favorites.ITEM_TYPE_DEEP_SHORTCUT); 467 } catch (Exception e) { 468 Log.e(TAG, "Unable to pin the shortcut for shortcut id = " + shortcutId 469 + " and package name = " + packageName, e); 470 } 471 return -1; 472 } 473 } 474 475 /** 476 * AppWidget parser: Required attributes packageName, className, spanX and spanY. 477 * Options child nodes: <extra key=... value=... /> 478 * It adds a pending widget which allows the widget to come later. If there are extras, those 479 * are passed to widget options during bind. 480 * The config activity for the widget (if present) is not shown, so any optional configurations 481 * should be passed as extras and the widget should support reading these widget options. 482 */ 483 protected class PendingWidgetParser implements TagParser { 484 485 @Nullable getComponentName(XmlPullParser parser)486 public ComponentName getComponentName(XmlPullParser parser) { 487 final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME); 488 final String className = getAttributeValue(parser, ATTR_CLASS_NAME); 489 addProfileId(parser); 490 if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(className)) { 491 return null; 492 } 493 return new ComponentName(packageName, className); 494 } 495 496 @Override parseAndAdd(XmlPullParser parser)497 public int parseAndAdd(XmlPullParser parser) 498 throws XmlPullParserException, IOException { 499 ComponentName cn = getComponentName(parser); 500 if (cn == null) { 501 if (LOGD) Log.d(TAG, "Skipping invalid <appwidget> with no component"); 502 return -1; 503 } 504 505 mValues.put(Favorites.SPANX, getAttributeValue(parser, ATTR_SPAN_X)); 506 mValues.put(Favorites.SPANY, getAttributeValue(parser, ATTR_SPAN_Y)); 507 mValues.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET); 508 509 // Read the extras 510 Bundle extras = new Bundle(); 511 int widgetDepth = parser.getDepth(); 512 int type; 513 while ((type = parser.next()) != XmlPullParser.END_TAG || 514 parser.getDepth() > widgetDepth) { 515 if (type != XmlPullParser.START_TAG) { 516 continue; 517 } 518 519 if (TAG_EXTRA.equals(parser.getName())) { 520 String key = getAttributeValue(parser, ATTR_KEY); 521 String value = getAttributeValue(parser, ATTR_VALUE); 522 if (key != null && value != null) { 523 extras.putString(key, value); 524 } else { 525 throw new RuntimeException("Widget extras must have a key and value"); 526 } 527 } else { 528 throw new RuntimeException("Widgets can contain only extras"); 529 } 530 } 531 return verifyAndInsert(cn, extras); 532 } 533 verifyAndInsert(ComponentName cn, Bundle extras)534 protected int verifyAndInsert(ComponentName cn, Bundle extras) { 535 mValues.put(Favorites.APPWIDGET_PROVIDER, cn.flattenToString()); 536 mValues.put(Favorites.RESTORED, 537 LauncherAppWidgetInfo.FLAG_ID_NOT_VALID 538 | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY 539 | LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG); 540 mValues.put(Favorites._ID, mCallback.generateNewItemId()); 541 if (!extras.isEmpty()) { 542 mValues.put(Favorites.INTENT, new Intent().putExtras(extras).toUri(0)); 543 } 544 545 int insertedId = mCallback.insertAndCheck(mDb, mValues); 546 if (insertedId < 0) { 547 return -1; 548 } else { 549 return insertedId; 550 } 551 } 552 } 553 554 protected class SearchWidgetParser extends PendingWidgetParser { 555 @Override 556 @Nullable 557 @WorkerThread getComponentName(XmlPullParser parser)558 public ComponentName getComponentName(XmlPullParser parser) { 559 return QsbContainerView.getSearchComponentName(mContext); 560 } 561 562 @Override verifyAndInsert(ComponentName cn, Bundle extras)563 protected int verifyAndInsert(ComponentName cn, Bundle extras) { 564 mValues.put(Favorites.OPTIONS, LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET); 565 int flags = mValues.getAsInteger(Favorites.RESTORED) 566 | WorkspaceItemInfo.FLAG_RESTORE_STARTED; 567 mValues.put(Favorites.RESTORED, flags); 568 return super.verifyAndInsert(cn, extras); 569 } 570 } 571 572 protected class FolderParser implements TagParser { 573 private final ArrayMap<String, TagParser> mFolderElements; 574 FolderParser()575 public FolderParser() { 576 this(getFolderElementsMap()); 577 } 578 FolderParser(ArrayMap<String, TagParser> elements)579 public FolderParser(ArrayMap<String, TagParser> elements) { 580 mFolderElements = elements; 581 } 582 583 @Override parseAndAdd(XmlPullParser parser)584 public int parseAndAdd(XmlPullParser parser) throws XmlPullParserException, IOException { 585 final String title; 586 final int titleResId = getAttributeResourceValue(parser, ATTR_TITLE, 0); 587 if (titleResId != 0) { 588 title = mSourceRes.getString(titleResId); 589 } else { 590 String titleText = getAttributeValue(parser, ATTR_TITLE_TEXT); 591 title = TextUtils.isEmpty(titleText) ? "" : titleText; 592 } 593 594 mValues.put(Favorites.TITLE, title); 595 mValues.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_FOLDER); 596 mValues.put(Favorites.SPANX, 1); 597 mValues.put(Favorites.SPANY, 1); 598 mValues.put(Favorites._ID, mCallback.generateNewItemId()); 599 int folderId = mCallback.insertAndCheck(mDb, mValues); 600 if (folderId < 0) { 601 if (LOGD) Log.e(TAG, "Unable to add folder"); 602 return -1; 603 } 604 605 final ContentValues myValues = new ContentValues(mValues); 606 IntArray folderItems = new IntArray(); 607 608 int type; 609 int folderDepth = parser.getDepth(); 610 int rank = 0; 611 while ((type = parser.next()) != XmlPullParser.END_TAG || 612 parser.getDepth() > folderDepth) { 613 if (type != XmlPullParser.START_TAG) { 614 continue; 615 } 616 mValues.clear(); 617 mValues.put(Favorites.CONTAINER, folderId); 618 mValues.put(Favorites.RANK, rank); 619 620 TagParser tagParser = mFolderElements.get(parser.getName()); 621 if (tagParser != null) { 622 final int id = tagParser.parseAndAdd(parser); 623 if (id >= 0) { 624 folderItems.add(id); 625 rank++; 626 } 627 } else { 628 throw new RuntimeException("Invalid folder item " + parser.getName()); 629 } 630 } 631 632 int addedId = folderId; 633 634 // We can only have folders with >= 2 items, so we need to remove the 635 // folder and clean up if less than 2 items were included, or some 636 // failed to add, and less than 2 were actually added 637 if (folderItems.size() < 2) { 638 // Delete the folder 639 mDb.delete(TABLE_NAME, itemIdMatch(folderId), null); 640 addedId = -1; 641 642 // If we have a single item, promote it to where the folder 643 // would have been. 644 if (folderItems.size() == 1) { 645 final ContentValues childValues = new ContentValues(); 646 copyInteger(myValues, childValues, Favorites.CONTAINER); 647 copyInteger(myValues, childValues, Favorites.SCREEN); 648 copyInteger(myValues, childValues, Favorites.CELLX); 649 copyInteger(myValues, childValues, Favorites.CELLY); 650 651 addedId = folderItems.get(0); 652 mDb.update(TABLE_NAME, childValues, 653 Favorites._ID + "=" + addedId, null); 654 } 655 } 656 return addedId; 657 } 658 } 659 beginDocument(XmlPullParser parser, String firstElementName)660 public static void beginDocument(XmlPullParser parser, String firstElementName) 661 throws XmlPullParserException, IOException { 662 int type; 663 while ((type = parser.next()) != XmlPullParser.START_TAG 664 && type != XmlPullParser.END_DOCUMENT); 665 666 if (type != XmlPullParser.START_TAG) { 667 throw new XmlPullParserException("No start tag found"); 668 } 669 670 if (!parser.getName().equals(firstElementName)) { 671 throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() + 672 ", expected " + firstElementName); 673 } 674 } 675 convertToDistanceFromEnd(String value, int endValue)676 private static String convertToDistanceFromEnd(String value, int endValue) { 677 if (!TextUtils.isEmpty(value)) { 678 int x = Integer.parseInt(value); 679 if (x < 0) { 680 return Integer.toString(endValue + x); 681 } 682 } 683 return value; 684 } 685 686 /** 687 * Return attribute value, attempting launcher-specific namespace first 688 * before falling back to anonymous attribute. 689 */ getAttributeValue(XmlPullParser parser, String attribute)690 protected static String getAttributeValue(XmlPullParser parser, String attribute) { 691 String value = parser.getAttributeValue( 692 "http://schemas.android.com/apk/res-auto/com.android.launcher3", attribute); 693 if (value == null) { 694 value = parser.getAttributeValue(null, attribute); 695 } 696 return value; 697 } 698 699 /** 700 * Return attribute resource value, attempting launcher-specific namespace 701 * first before falling back to anonymous attribute. 702 */ getAttributeResourceValue(XmlPullParser parser, String attribute, int defaultValue)703 protected static int getAttributeResourceValue(XmlPullParser parser, String attribute, 704 int defaultValue) { 705 AttributeSet attrs = Xml.asAttributeSet(parser); 706 int value = attrs.getAttributeResourceValue( 707 "http://schemas.android.com/apk/res-auto/com.android.launcher3", attribute, 708 defaultValue); 709 if (value == defaultValue) { 710 value = attrs.getAttributeResourceValue(null, attribute, defaultValue); 711 } 712 return value; 713 } 714 715 public interface LayoutParserCallback { generateNewItemId()716 int generateNewItemId(); 717 insertAndCheck(SQLiteDatabase db, ContentValues values)718 int insertAndCheck(SQLiteDatabase db, ContentValues values); 719 } 720 721 @Thunk copyInteger(ContentValues from, ContentValues to, String key)722 static void copyInteger(ContentValues from, ContentValues to, String key) { 723 to.put(key, from.getAsInteger(key)); 724 } 725 726 /** 727 * Wrapper over resources for easier abstraction 728 */ 729 public interface SourceResources { 730 731 /** 732 * Refer {@link Resources#getXml(int)} 733 */ getXml(@mlRes int id)734 default XmlResourceParser getXml(@XmlRes int id) throws NotFoundException { 735 throw new NotFoundException(); 736 } 737 738 /** 739 * Refer {@link Resources#getString(int)} 740 */ getString(@tringRes int id)741 default String getString(@StringRes int id) throws NotFoundException { 742 throw new NotFoundException(); 743 } 744 745 /** 746 * Returns a {@link SourceResources} corresponding to the provided resources 747 */ wrap(Resources res)748 static SourceResources wrap(Resources res) { 749 return new SourceResources() { 750 @Override 751 public XmlResourceParser getXml(int id) { 752 return res.getXml(id); 753 } 754 755 @Override 756 public String getString(int id) { 757 return res.getString(id); 758 } 759 }; 760 } 761 } 762 763 764 } 765