blob: 08ed89b43cf45f6af00d182a93203b0921d877f6 [file] [log] [blame]
Sunny Goyal0fe505b2014-08-06 09:55:36 -07001/*
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
17package com.android.launcher3;
18
19import android.appwidget.AppWidgetHost;
20import android.appwidget.AppWidgetManager;
21import android.content.ComponentName;
22import android.content.ContentValues;
23import android.content.Context;
24import android.content.Intent;
25import android.content.pm.ActivityInfo;
26import android.content.pm.PackageManager;
27import android.content.res.Resources;
28import android.content.res.XmlResourceParser;
29import android.database.sqlite.SQLiteDatabase;
30import android.graphics.drawable.Drawable;
31import android.net.Uri;
32import android.os.Bundle;
33import android.text.TextUtils;
34import android.util.Log;
35import android.util.Pair;
36import android.util.Patterns;
37
38import com.android.launcher3.LauncherProvider.SqlArguments;
39import com.android.launcher3.LauncherProvider.WorkspaceLoader;
40import com.android.launcher3.LauncherSettings.Favorites;
41
42import org.xmlpull.v1.XmlPullParser;
43import org.xmlpull.v1.XmlPullParserException;
44
45import java.io.IOException;
46import java.util.ArrayList;
47import 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 */
54public 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 =
Sunny Goyal2233c882014-09-18 14:36:48 -070060 "android.autoinstalls.config.action.PLAY_AUTO_INSTALL";
Sunny Goyal0fe505b2014-08-06 09:55:36 -070061
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);
Sameer Padala8fd74832014-09-08 16:00:29 -0700145 } catch (Exception e) {
Sunny Goyal0fe505b2014-08-06 09:55:36 -0700146 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
Sunny Goyal34942622014-08-29 17:20:55 -0700301 mValues.put(Favorites.RESTORED, ShortcutInfo.FLAG_AUTOINTALL_ICON);
Sunny Goyal0fe505b2014-08-06 09:55:36 -0700302 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}