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.appwidget.AppWidgetManager; 21 import android.content.ComponentName; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.ActivityInfo; 26 import android.content.pm.PackageManager; 27 import android.content.res.Resources; 28 import android.content.res.XmlResourceParser; 29 import android.database.sqlite.SQLiteDatabase; 30 import android.graphics.drawable.Drawable; 31 import android.net.Uri; 32 import android.os.Bundle; 33 import android.text.TextUtils; 34 import android.util.Log; 35 import android.util.Pair; 36 import android.util.Patterns; 37 38 import com.android.launcher3.LauncherProvider.SqlArguments; 39 import com.android.launcher3.LauncherProvider.WorkspaceLoader; 40 import com.android.launcher3.LauncherSettings.Favorites; 41 42 import org.xmlpull.v1.XmlPullParser; 43 import org.xmlpull.v1.XmlPullParserException; 44 45 import java.io.IOException; 46 import java.util.ArrayList; 47 import java.util.HashMap; 48 49 /** 50 * This class contains contains duplication of functionality as found in 51 * LauncherProvider#DatabaseHelper. It has been isolated and differentiated in order 52 * to cleanly and separately represent AutoInstall default layout format and policy. 53 */ 54 public class AutoInstallsLayout implements WorkspaceLoader { 55 private static final String TAG = "AutoInstalls"; 56 private static final boolean LOGD = true; 57 58 /** Marker action used to discover a package which defines launcher customization */ 59 static final String ACTION_LAUNCHER_CUSTOMIZATION = 60 "android.autoinstalls.config.action.PLAY_AUTO_INSTALL"; 61 62 private static final String LAYOUT_RES = "default_layout"; 63 64 static AutoInstallsLayout get(Context context, AppWidgetHost appWidgetHost, 65 LayoutParserCallback callback) { 66 Pair<String, Resources> customizationApkInfo = Utilities.findSystemApk( 67 ACTION_LAUNCHER_CUSTOMIZATION, context.getPackageManager()); 68 if (customizationApkInfo == null) { 69 return null; 70 } 71 72 String pkg = customizationApkInfo.first; 73 Resources res = customizationApkInfo.second; 74 int layoutId = res.getIdentifier(LAYOUT_RES, "xml", pkg); 75 if (layoutId == 0) { 76 Log.e(TAG, "Layout definition not found in package: " + pkg); 77 return null; 78 } 79 return new AutoInstallsLayout(context, appWidgetHost, callback, pkg, res, layoutId); 80 } 81 82 // Object Tags 83 private static final String TAG_WORKSPACE = "workspace"; 84 private static final String TAG_APP_ICON = "appicon"; 85 private static final String TAG_AUTO_INSTALL = "autoinstall"; 86 private static final String TAG_FOLDER = "folder"; 87 private static final String TAG_APPWIDGET = "appwidget"; 88 private static final String TAG_SHORTCUT = "shortcut"; 89 private static final String TAG_EXTRA = "extra"; 90 91 private static final String ATTR_CONTAINER = "container"; 92 private static final String ATTR_RANK = "rank"; 93 94 private static final String ATTR_PACKAGE_NAME = "packageName"; 95 private static final String ATTR_CLASS_NAME = "className"; 96 private static final String ATTR_TITLE = "title"; 97 private static final String ATTR_SCREEN = "screen"; 98 private static final String ATTR_X = "x"; 99 private static final String ATTR_Y = "y"; 100 private static final String ATTR_SPAN_X = "spanX"; 101 private static final String ATTR_SPAN_Y = "spanY"; 102 private static final String ATTR_ICON = "icon"; 103 private static final String ATTR_URL = "url"; 104 105 // Style attrs -- "Extra" 106 private static final String ATTR_KEY = "key"; 107 private static final String ATTR_VALUE = "value"; 108 109 private static final String HOTSEAT_CONTAINER_NAME = 110 Favorites.containerToString(Favorites.CONTAINER_HOTSEAT); 111 112 private static final String ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE = 113 "com.android.launcher.action.APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE"; 114 115 private final Context mContext; 116 private final AppWidgetHost mAppWidgetHost; 117 private final LayoutParserCallback mCallback; 118 119 private final PackageManager mPackageManager; 120 private final ContentValues mValues; 121 122 private final Resources mRes; 123 private final int mLayoutId; 124 125 private SQLiteDatabase mDb; 126 127 public AutoInstallsLayout(Context context, AppWidgetHost appWidgetHost, 128 LayoutParserCallback callback, String packageName, Resources res, int layoutId) { 129 mContext = context; 130 mAppWidgetHost = appWidgetHost; 131 mCallback = callback; 132 133 mPackageManager = context.getPackageManager(); 134 mValues = new ContentValues(); 135 136 mRes = res; 137 mLayoutId = layoutId; 138 } 139 140 @Override 141 public int loadLayout(SQLiteDatabase db, ArrayList<Long> screenIds) { 142 mDb = db; 143 try { 144 return parseLayout(mRes, mLayoutId, screenIds); 145 } catch (XmlPullParserException | IOException | RuntimeException e) { 146 Log.w(TAG, "Got exception parsing layout.", e); 147 return -1; 148 } 149 } 150 151 private int parseLayout(Resources res, int layoutId, ArrayList<Long> screenIds) 152 throws XmlPullParserException, IOException { 153 final int hotseatAllAppsRank = LauncherAppState.getInstance() 154 .getDynamicGrid().getDeviceProfile().hotseatAllAppsRank; 155 156 XmlResourceParser parser = res.getXml(layoutId); 157 beginDocument(parser, TAG_WORKSPACE); 158 final int depth = parser.getDepth(); 159 int type; 160 HashMap<String, TagParser> tagParserMap = getLayoutElementsMap(); 161 int count = 0; 162 163 while (((type = parser.next()) != XmlPullParser.END_TAG || 164 parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { 165 if (type != XmlPullParser.START_TAG) { 166 continue; 167 } 168 169 mValues.clear(); 170 final int container; 171 final long screenId; 172 173 if (HOTSEAT_CONTAINER_NAME.equals(getAttributeValue(parser, ATTR_CONTAINER))) { 174 container = Favorites.CONTAINER_HOTSEAT; 175 176 // Hack: hotseat items are stored using screen ids 177 long rank = Long.parseLong(getAttributeValue(parser, ATTR_RANK)); 178 screenId = (rank < hotseatAllAppsRank) ? rank : (rank + 1); 179 180 } else { 181 container = Favorites.CONTAINER_DESKTOP; 182 screenId = Long.parseLong(getAttributeValue(parser, ATTR_SCREEN)); 183 184 mValues.put(Favorites.CELLX, getAttributeValue(parser, ATTR_X)); 185 mValues.put(Favorites.CELLY, getAttributeValue(parser, ATTR_Y)); 186 } 187 188 mValues.put(Favorites.CONTAINER, container); 189 mValues.put(Favorites.SCREEN, screenId); 190 191 TagParser tagParser = tagParserMap.get(parser.getName()); 192 if (tagParser == null) { 193 if (LOGD) Log.d(TAG, "Ignoring unknown element tag: " + parser.getName()); 194 continue; 195 } 196 long newElementId = tagParser.parseAndAdd(parser, res); 197 if (newElementId >= 0) { 198 // Keep track of the set of screens which need to be added to the db. 199 if (!screenIds.contains(screenId) && 200 container == Favorites.CONTAINER_DESKTOP) { 201 screenIds.add(screenId); 202 } 203 count++; 204 } 205 } 206 return count; 207 } 208 209 protected long addShortcut(String title, Intent intent, int type) { 210 long id = mCallback.generateNewItemId(); 211 mValues.put(Favorites.INTENT, intent.toUri(0)); 212 mValues.put(Favorites.TITLE, title); 213 mValues.put(Favorites.ITEM_TYPE, type); 214 mValues.put(Favorites.SPANX, 1); 215 mValues.put(Favorites.SPANY, 1); 216 mValues.put(Favorites._ID, id); 217 if (mCallback.insertAndCheck(mDb, mValues) < 0) { 218 return -1; 219 } else { 220 return id; 221 } 222 } 223 224 protected HashMap<String, TagParser> getFolderElementsMap() { 225 HashMap<String, TagParser> parsers = new HashMap<String, TagParser>(); 226 parsers.put(TAG_APP_ICON, new AppShortcutParser()); 227 parsers.put(TAG_AUTO_INSTALL, new AutoInstallParser()); 228 parsers.put(TAG_SHORTCUT, new ShortcutParser()); 229 return parsers; 230 } 231 232 protected HashMap<String, TagParser> getLayoutElementsMap() { 233 HashMap<String, TagParser> parsers = new HashMap<String, TagParser>(); 234 parsers.put(TAG_APP_ICON, new AppShortcutParser()); 235 parsers.put(TAG_AUTO_INSTALL, new AutoInstallParser()); 236 parsers.put(TAG_FOLDER, new FolderParser()); 237 parsers.put(TAG_APPWIDGET, new AppWidgetParser()); 238 parsers.put(TAG_SHORTCUT, new ShortcutParser()); 239 return parsers; 240 } 241 242 private interface TagParser { 243 /** 244 * Parses the tag and adds to the db 245 * @return the id of the row added or -1; 246 */ 247 long parseAndAdd(XmlResourceParser parser, Resources res) 248 throws XmlPullParserException, IOException; 249 } 250 251 private class AppShortcutParser implements TagParser { 252 253 @Override 254 public long parseAndAdd(XmlResourceParser parser, Resources res) { 255 final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME); 256 final String className = getAttributeValue(parser, ATTR_CLASS_NAME); 257 258 if (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(className)) { 259 ActivityInfo info; 260 try { 261 ComponentName cn; 262 try { 263 cn = new ComponentName(packageName, className); 264 info = mPackageManager.getActivityInfo(cn, 0); 265 } catch (PackageManager.NameNotFoundException nnfe) { 266 String[] packages = mPackageManager.currentToCanonicalPackageNames( 267 new String[] { packageName }); 268 cn = new ComponentName(packages[0], className); 269 info = mPackageManager.getActivityInfo(cn, 0); 270 } 271 final Intent intent = new Intent(Intent.ACTION_MAIN, null) 272 .addCategory(Intent.CATEGORY_LAUNCHER) 273 .setComponent(cn) 274 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | 275 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 276 277 return addShortcut(info.loadLabel(mPackageManager).toString(), 278 intent, Favorites.ITEM_TYPE_APPLICATION); 279 } catch (PackageManager.NameNotFoundException e) { 280 Log.w(TAG, "Unable to add favorite: " + packageName + "/" + className, e); 281 } 282 return -1; 283 } else { 284 if (LOGD) Log.d(TAG, "Skipping invalid <favorite> with no component or uri"); 285 return -1; 286 } 287 } 288 } 289 290 private class AutoInstallParser implements TagParser { 291 292 @Override 293 public long parseAndAdd(XmlResourceParser parser, Resources res) { 294 final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME); 295 final String className = getAttributeValue(parser, ATTR_CLASS_NAME); 296 if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(className)) { 297 if (LOGD) Log.d(TAG, "Skipping invalid <favorite> with no component"); 298 return -1; 299 } 300 301 mValues.put(Favorites.RESTORED, ShortcutInfo.FLAG_AUTOINTALL_ICON); 302 final Intent intent = new Intent(Intent.ACTION_MAIN, null) 303 .addCategory(Intent.CATEGORY_LAUNCHER) 304 .setComponent(new ComponentName(packageName, className)) 305 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | 306 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 307 return addShortcut(mContext.getString(R.string.package_state_unknown), intent, 308 Favorites.ITEM_TYPE_APPLICATION); 309 } 310 } 311 312 private class ShortcutParser implements TagParser { 313 314 @Override 315 public long parseAndAdd(XmlResourceParser parser, Resources res) { 316 final String url = getAttributeValue(parser, ATTR_URL); 317 final int titleResId = getAttributeResourceValue(parser, ATTR_TITLE, 0); 318 final int iconId = getAttributeResourceValue(parser, ATTR_ICON, 0); 319 320 if (titleResId == 0 || iconId == 0) { 321 if (LOGD) Log.d(TAG, "Ignoring shortcut"); 322 return -1; 323 } 324 325 if (TextUtils.isEmpty(url) || !Patterns.WEB_URL.matcher(url).matches()) { 326 if (LOGD) Log.d(TAG, "Ignoring shortcut, invalid url: " + url); 327 return -1; 328 } 329 Drawable icon = res.getDrawable(iconId); 330 if (icon == null) { 331 if (LOGD) Log.d(TAG, "Ignoring shortcut, can't load icon"); 332 return -1; 333 } 334 335 ItemInfo.writeBitmap(mValues, Utilities.createIconBitmap(icon, mContext)); 336 final Intent intent = new Intent(Intent.ACTION_VIEW, null) 337 .setData(Uri.parse(url)) 338 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | 339 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 340 return addShortcut(res.getString(titleResId), intent, Favorites.ITEM_TYPE_SHORTCUT); 341 } 342 } 343 344 private class AppWidgetParser implements TagParser { 345 346 @Override 347 public long parseAndAdd(XmlResourceParser parser, Resources res) 348 throws XmlPullParserException, IOException { 349 final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME); 350 final String className = getAttributeValue(parser, ATTR_CLASS_NAME); 351 if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(className)) { 352 if (LOGD) Log.d(TAG, "Skipping invalid <favorite> with no component"); 353 return -1; 354 } 355 356 ComponentName cn = new ComponentName(packageName, className); 357 try { 358 mPackageManager.getReceiverInfo(cn, 0); 359 } catch (Exception e) { 360 String[] packages = mPackageManager.currentToCanonicalPackageNames( 361 new String[] { packageName }); 362 cn = new ComponentName(packages[0], className); 363 try { 364 mPackageManager.getReceiverInfo(cn, 0); 365 } catch (Exception e1) { 366 if (LOGD) Log.d(TAG, "Can't find widget provider: " + className); 367 return -1; 368 } 369 } 370 371 mValues.put(Favorites.SPANX, getAttributeValue(parser, ATTR_SPAN_X)); 372 mValues.put(Favorites.SPANY, getAttributeValue(parser, ATTR_SPAN_Y)); 373 374 // Read the extras 375 Bundle extras = new Bundle(); 376 int widgetDepth = parser.getDepth(); 377 int type; 378 while ((type = parser.next()) != XmlPullParser.END_TAG || 379 parser.getDepth() > widgetDepth) { 380 if (type != XmlPullParser.START_TAG) { 381 continue; 382 } 383 384 if (TAG_EXTRA.equals(parser.getName())) { 385 String key = getAttributeValue(parser, ATTR_KEY); 386 String value = getAttributeValue(parser, ATTR_VALUE); 387 if (key != null && value != null) { 388 extras.putString(key, value); 389 } else { 390 throw new RuntimeException("Widget extras must have a key and value"); 391 } 392 } else { 393 throw new RuntimeException("Widgets can contain only extras"); 394 } 395 } 396 397 final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext); 398 long insertedId = -1; 399 try { 400 int appWidgetId = mAppWidgetHost.allocateAppWidgetId(); 401 402 if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, cn)) { 403 if (LOGD) Log.e(TAG, "Unable to bind app widget id " + cn); 404 return -1; 405 } 406 407 mValues.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET); 408 mValues.put(Favorites.APPWIDGET_ID, appWidgetId); 409 mValues.put(Favorites.APPWIDGET_PROVIDER, cn.flattenToString()); 410 mValues.put(Favorites._ID, mCallback.generateNewItemId()); 411 insertedId = mCallback.insertAndCheck(mDb, mValues); 412 if (insertedId < 0) { 413 mAppWidgetHost.deleteAppWidgetId(appWidgetId); 414 return insertedId; 415 } 416 417 // Send a broadcast to configure the widget 418 if (!extras.isEmpty()) { 419 Intent intent = new Intent(ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE); 420 intent.setComponent(cn); 421 intent.putExtras(extras); 422 intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); 423 mContext.sendBroadcast(intent); 424 } 425 } catch (RuntimeException ex) { 426 if (LOGD) Log.e(TAG, "Problem allocating appWidgetId", ex); 427 } 428 return insertedId; 429 } 430 } 431 432 private class FolderParser implements TagParser { 433 private final HashMap<String, TagParser> mFolderElements = getFolderElementsMap(); 434 435 @Override 436 public long parseAndAdd(XmlResourceParser parser, Resources res) 437 throws XmlPullParserException, IOException { 438 final String title; 439 final int titleResId = getAttributeResourceValue(parser, ATTR_TITLE, 0); 440 if (titleResId != 0) { 441 title = res.getString(titleResId); 442 } else { 443 title = mContext.getResources().getString(R.string.folder_name); 444 } 445 446 mValues.put(Favorites.TITLE, title); 447 mValues.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_FOLDER); 448 mValues.put(Favorites.SPANX, 1); 449 mValues.put(Favorites.SPANY, 1); 450 mValues.put(Favorites._ID, mCallback.generateNewItemId()); 451 long folderId = mCallback.insertAndCheck(mDb, mValues); 452 if (folderId < 0) { 453 if (LOGD) Log.e(TAG, "Unable to add folder"); 454 return -1; 455 } 456 457 final ContentValues myValues = new ContentValues(mValues); 458 ArrayList<Long> folderItems = new ArrayList<Long>(); 459 460 int type; 461 int folderDepth = parser.getDepth(); 462 while ((type = parser.next()) != XmlPullParser.END_TAG || 463 parser.getDepth() > folderDepth) { 464 if (type != XmlPullParser.START_TAG) { 465 continue; 466 } 467 mValues.clear(); 468 mValues.put(Favorites.CONTAINER, folderId); 469 470 TagParser tagParser = mFolderElements.get(parser.getName()); 471 if (tagParser != null) { 472 final long id = tagParser.parseAndAdd(parser, res); 473 if (id >= 0) { 474 folderItems.add(id); 475 } 476 } else { 477 throw new RuntimeException("Invalid folder item " + parser.getName()); 478 } 479 } 480 481 long addedId = folderId; 482 483 // We can only have folders with >= 2 items, so we need to remove the 484 // folder and clean up if less than 2 items were included, or some 485 // failed to add, and less than 2 were actually added 486 if (folderItems.size() < 2) { 487 // Delete the folder 488 Uri uri = Favorites.getContentUri(folderId, false); 489 SqlArguments args = new SqlArguments(uri, null, null); 490 mDb.delete(args.table, args.where, args.args); 491 addedId = -1; 492 493 // If we have a single item, promote it to where the folder 494 // would have been. 495 if (folderItems.size() == 1) { 496 final ContentValues childValues = new ContentValues(); 497 copyInteger(myValues, childValues, Favorites.CONTAINER); 498 copyInteger(myValues, childValues, Favorites.SCREEN); 499 copyInteger(myValues, childValues, Favorites.CELLX); 500 copyInteger(myValues, childValues, Favorites.CELLY); 501 502 addedId = folderItems.get(0); 503 mDb.update(LauncherProvider.TABLE_FAVORITES, childValues, 504 Favorites._ID + "=" + addedId, null); 505 } 506 } 507 return addedId; 508 } 509 } 510 511 private static final void beginDocument(XmlPullParser parser, String firstElementName) 512 throws XmlPullParserException, IOException { 513 int type; 514 while ((type = parser.next()) != XmlPullParser.START_TAG 515 && type != XmlPullParser.END_DOCUMENT); 516 517 if (type != XmlPullParser.START_TAG) { 518 throw new XmlPullParserException("No start tag found"); 519 } 520 521 if (!parser.getName().equals(firstElementName)) { 522 throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() + 523 ", expected " + firstElementName); 524 } 525 } 526 527 /** 528 * Return attribute value, attempting launcher-specific namespace first 529 * before falling back to anonymous attribute. 530 */ 531 private static String getAttributeValue(XmlResourceParser parser, String attribute) { 532 String value = parser.getAttributeValue( 533 "http://schemas.android.com/apk/res-auto/com.android.launcher3", attribute); 534 if (value == null) { 535 value = parser.getAttributeValue(null, attribute); 536 } 537 return value; 538 } 539 540 /** 541 * Return attribute resource value, attempting launcher-specific namespace 542 * first before falling back to anonymous attribute. 543 */ 544 private static int getAttributeResourceValue(XmlResourceParser parser, String attribute, 545 int defaultValue) { 546 int value = parser.getAttributeResourceValue( 547 "http://schemas.android.com/apk/res-auto/com.android.launcher3", attribute, 548 defaultValue); 549 if (value == defaultValue) { 550 value = parser.getAttributeResourceValue(null, attribute, defaultValue); 551 } 552 return value; 553 } 554 555 public static interface LayoutParserCallback { 556 long generateNewItemId(); 557 558 long insertAndCheck(SQLiteDatabase db, ContentValues values); 559 } 560 561 private static void copyInteger(ContentValues from, ContentValues to, String key) { 562 to.put(key, from.getAsInteger(key)); 563 } 564 } 565