5aeef58131
Traditionally Launcher workspace customization is offered through overlays at build time, but we don't have access to partner-specific customization at build time. To solve this, this adds a new "partner-folder" tag which points at an XML resource provided by an external package. The external package XML can't depend on the binary XML attributes defined by Launcher3, so we switch to using manual string-based attribute lookups. Partners can also provide extra wallpapers. When a folder only results in a single item, promote that item into the folder location instead of deleting completely. Bug: 13340779 Change-Id: Ide558288bef4113565f288b700d8245055c0fee9
2066 lines
92 KiB
Java
2066 lines
92 KiB
Java
/*
|
|
* Copyright (C) 2008 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.launcher3;
|
|
|
|
import android.app.SearchManager;
|
|
import android.appwidget.AppWidgetHost;
|
|
import android.appwidget.AppWidgetManager;
|
|
import android.appwidget.AppWidgetProviderInfo;
|
|
import android.content.ComponentName;
|
|
import android.content.ContentProvider;
|
|
import android.content.ContentProviderOperation;
|
|
import android.content.ContentProviderResult;
|
|
import android.content.ContentResolver;
|
|
import android.content.ContentUris;
|
|
import android.content.ContentValues;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.OperationApplicationException;
|
|
import android.content.SharedPreferences;
|
|
import android.content.pm.ActivityInfo;
|
|
import android.content.pm.ApplicationInfo;
|
|
import android.content.pm.PackageInfo;
|
|
import android.content.pm.PackageManager;
|
|
import android.content.pm.ResolveInfo;
|
|
import android.content.res.Resources;
|
|
import android.content.res.TypedArray;
|
|
import android.content.res.XmlResourceParser;
|
|
import android.database.Cursor;
|
|
import android.database.SQLException;
|
|
import android.database.sqlite.SQLiteDatabase;
|
|
import android.database.sqlite.SQLiteOpenHelper;
|
|
import android.database.sqlite.SQLiteQueryBuilder;
|
|
import android.database.sqlite.SQLiteStatement;
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.BitmapFactory;
|
|
import android.net.Uri;
|
|
import android.os.Bundle;
|
|
import android.provider.Settings;
|
|
import android.text.TextUtils;
|
|
import android.util.AttributeSet;
|
|
import android.util.Log;
|
|
import android.util.SparseArray;
|
|
import android.util.Xml;
|
|
|
|
import com.android.launcher3.LauncherSettings.Favorites;
|
|
import com.android.launcher3.config.ProviderConfig;
|
|
|
|
import org.xmlpull.v1.XmlPullParser;
|
|
import org.xmlpull.v1.XmlPullParserException;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.net.URISyntaxException;
|
|
import java.util.ArrayList;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
|
|
public class LauncherProvider extends ContentProvider {
|
|
private static final String TAG = "Launcher.LauncherProvider";
|
|
private static final boolean LOGD = false;
|
|
|
|
private static final String DATABASE_NAME = "launcher.db";
|
|
|
|
private static final int DATABASE_VERSION = 18;
|
|
|
|
static final String OLD_AUTHORITY = "com.android.launcher2.settings";
|
|
static final String AUTHORITY = ProviderConfig.AUTHORITY;
|
|
|
|
// Should we attempt to load anything from the com.android.launcher2 provider?
|
|
static final boolean IMPORT_LAUNCHER2_DATABASE = false;
|
|
|
|
static final String TABLE_FAVORITES = "favorites";
|
|
static final String TABLE_WORKSPACE_SCREENS = "workspaceScreens";
|
|
static final String PARAMETER_NOTIFY = "notify";
|
|
static final String UPGRADED_FROM_OLD_DATABASE =
|
|
"UPGRADED_FROM_OLD_DATABASE";
|
|
static final String EMPTY_DATABASE_CREATED =
|
|
"EMPTY_DATABASE_CREATED";
|
|
static final String DEFAULT_WORKSPACE_RESOURCE_ID =
|
|
"DEFAULT_WORKSPACE_RESOURCE_ID";
|
|
|
|
private static final String ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE =
|
|
"com.android.launcher.action.APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE";
|
|
|
|
private LauncherProviderChangeListener mListener;
|
|
|
|
/**
|
|
* {@link Uri} triggered at any registered {@link android.database.ContentObserver} when
|
|
* {@link AppWidgetHost#deleteHost()} is called during database creation.
|
|
* Use this to recall {@link AppWidgetHost#startListening()} if needed.
|
|
*/
|
|
static final Uri CONTENT_APPWIDGET_RESET_URI =
|
|
Uri.parse("content://" + AUTHORITY + "/appWidgetReset");
|
|
|
|
private DatabaseHelper mOpenHelper;
|
|
private static boolean sJustLoadedFromOldDb;
|
|
|
|
@Override
|
|
public boolean onCreate() {
|
|
final Context context = getContext();
|
|
mOpenHelper = new DatabaseHelper(context);
|
|
LauncherAppState.setLauncherProvider(this);
|
|
return true;
|
|
}
|
|
|
|
public boolean wasNewDbCreated() {
|
|
return mOpenHelper.wasNewDbCreated();
|
|
}
|
|
|
|
public void setLauncherProviderChangeListener(LauncherProviderChangeListener listener) {
|
|
mListener = listener;
|
|
}
|
|
|
|
@Override
|
|
public String getType(Uri uri) {
|
|
SqlArguments args = new SqlArguments(uri, null, null);
|
|
if (TextUtils.isEmpty(args.where)) {
|
|
return "vnd.android.cursor.dir/" + args.table;
|
|
} else {
|
|
return "vnd.android.cursor.item/" + args.table;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Cursor query(Uri uri, String[] projection, String selection,
|
|
String[] selectionArgs, String sortOrder) {
|
|
|
|
SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
|
|
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
|
|
qb.setTables(args.table);
|
|
|
|
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
|
Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder);
|
|
result.setNotificationUri(getContext().getContentResolver(), uri);
|
|
|
|
return result;
|
|
}
|
|
|
|
private static long dbInsertAndCheck(DatabaseHelper helper,
|
|
SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) {
|
|
if (values == null) {
|
|
throw new RuntimeException("Error: attempting to insert null values");
|
|
}
|
|
if (!values.containsKey(LauncherSettings.BaseLauncherColumns._ID)) {
|
|
throw new RuntimeException("Error: attempting to add item without specifying an id");
|
|
}
|
|
helper.checkId(table, values);
|
|
return db.insert(table, nullColumnHack, values);
|
|
}
|
|
|
|
private static void deleteId(SQLiteDatabase db, long id) {
|
|
Uri uri = LauncherSettings.Favorites.getContentUri(id, false);
|
|
SqlArguments args = new SqlArguments(uri, null, null);
|
|
db.delete(args.table, args.where, args.args);
|
|
}
|
|
|
|
@Override
|
|
public Uri insert(Uri uri, ContentValues initialValues) {
|
|
SqlArguments args = new SqlArguments(uri);
|
|
|
|
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
|
addModifiedTime(initialValues);
|
|
final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
|
|
if (rowId <= 0) return null;
|
|
|
|
uri = ContentUris.withAppendedId(uri, rowId);
|
|
sendNotify(uri);
|
|
|
|
return uri;
|
|
}
|
|
|
|
@Override
|
|
public int bulkInsert(Uri uri, ContentValues[] values) {
|
|
SqlArguments args = new SqlArguments(uri);
|
|
|
|
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
|
db.beginTransaction();
|
|
try {
|
|
int numValues = values.length;
|
|
for (int i = 0; i < numValues; i++) {
|
|
addModifiedTime(values[i]);
|
|
if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) {
|
|
return 0;
|
|
}
|
|
}
|
|
db.setTransactionSuccessful();
|
|
} finally {
|
|
db.endTransaction();
|
|
}
|
|
|
|
sendNotify(uri);
|
|
return values.length;
|
|
}
|
|
|
|
@Override
|
|
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
|
|
throws OperationApplicationException {
|
|
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
|
db.beginTransaction();
|
|
try {
|
|
ContentProviderResult[] result = super.applyBatch(operations);
|
|
db.setTransactionSuccessful();
|
|
return result;
|
|
} finally {
|
|
db.endTransaction();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
|
SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
|
|
|
|
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
|
int count = db.delete(args.table, args.where, args.args);
|
|
if (count > 0) sendNotify(uri);
|
|
|
|
return count;
|
|
}
|
|
|
|
@Override
|
|
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
|
SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
|
|
|
|
addModifiedTime(values);
|
|
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
|
int count = db.update(args.table, values, args.where, args.args);
|
|
if (count > 0) sendNotify(uri);
|
|
|
|
return count;
|
|
}
|
|
|
|
private void sendNotify(Uri uri) {
|
|
String notify = uri.getQueryParameter(PARAMETER_NOTIFY);
|
|
if (notify == null || "true".equals(notify)) {
|
|
getContext().getContentResolver().notifyChange(uri, null);
|
|
}
|
|
|
|
// always notify the backup agent
|
|
LauncherBackupAgentHelper.dataChanged(getContext());
|
|
if (mListener != null) {
|
|
mListener.onLauncherProviderChange();
|
|
}
|
|
}
|
|
|
|
private void addModifiedTime(ContentValues values) {
|
|
values.put(LauncherSettings.ChangeLogColumns.MODIFIED, System.currentTimeMillis());
|
|
}
|
|
|
|
public long generateNewItemId() {
|
|
return mOpenHelper.generateNewItemId();
|
|
}
|
|
|
|
public void updateMaxItemId(long id) {
|
|
mOpenHelper.updateMaxItemId(id);
|
|
}
|
|
|
|
public long generateNewScreenId() {
|
|
return mOpenHelper.generateNewScreenId();
|
|
}
|
|
|
|
// This is only required one time while loading the workspace during the
|
|
// upgrade path, and should never be called from anywhere else.
|
|
public void updateMaxScreenId(long maxScreenId) {
|
|
mOpenHelper.updateMaxScreenId(maxScreenId);
|
|
}
|
|
|
|
/**
|
|
* @param Should we load the old db for upgrade? first run only.
|
|
*/
|
|
synchronized public boolean justLoadedOldDb() {
|
|
String spKey = LauncherAppState.getSharedPreferencesKey();
|
|
SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE);
|
|
|
|
boolean loadedOldDb = false || sJustLoadedFromOldDb;
|
|
|
|
sJustLoadedFromOldDb = false;
|
|
if (sp.getBoolean(UPGRADED_FROM_OLD_DATABASE, false)) {
|
|
|
|
SharedPreferences.Editor editor = sp.edit();
|
|
editor.remove(UPGRADED_FROM_OLD_DATABASE);
|
|
editor.commit();
|
|
loadedOldDb = true;
|
|
}
|
|
return loadedOldDb;
|
|
}
|
|
|
|
/**
|
|
* @param workspaceResId that can be 0 to use default or non-zero for specific resource
|
|
*/
|
|
synchronized public void loadDefaultFavoritesIfNecessary(int origWorkspaceResId) {
|
|
String spKey = LauncherAppState.getSharedPreferencesKey();
|
|
SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE);
|
|
|
|
if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
|
|
Log.d(TAG, "loading default workspace");
|
|
int workspaceResId = origWorkspaceResId;
|
|
|
|
// Use default workspace resource if none provided
|
|
if (workspaceResId == 0) {
|
|
workspaceResId =
|
|
sp.getInt(DEFAULT_WORKSPACE_RESOURCE_ID, getDefaultWorkspaceResourceId());
|
|
}
|
|
|
|
// Populate favorites table with initial favorites
|
|
SharedPreferences.Editor editor = sp.edit();
|
|
editor.remove(EMPTY_DATABASE_CREATED);
|
|
if (origWorkspaceResId != 0) {
|
|
editor.putInt(DEFAULT_WORKSPACE_RESOURCE_ID, origWorkspaceResId);
|
|
}
|
|
|
|
mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), workspaceResId);
|
|
mOpenHelper.setFlagJustLoadedOldDb();
|
|
editor.commit();
|
|
}
|
|
}
|
|
|
|
public void migrateLauncher2Shortcuts() {
|
|
mOpenHelper.migrateLauncher2Shortcuts(mOpenHelper.getWritableDatabase(),
|
|
Uri.parse(getContext().getString(R.string.old_launcher_provider_uri)));
|
|
}
|
|
|
|
private static int getDefaultWorkspaceResourceId() {
|
|
if (LauncherAppState.isDisableAllApps()) {
|
|
return R.xml.default_workspace_no_all_apps;
|
|
} else {
|
|
return R.xml.default_workspace;
|
|
}
|
|
}
|
|
|
|
private static interface ContentValuesCallback {
|
|
public void onRow(ContentValues values);
|
|
}
|
|
|
|
private static boolean shouldImportLauncher2Database(Context context) {
|
|
boolean isTablet = context.getResources().getBoolean(R.bool.is_tablet);
|
|
|
|
// We don't import the old databse for tablets, as the grid size has changed.
|
|
return !isTablet && IMPORT_LAUNCHER2_DATABASE;
|
|
}
|
|
|
|
public void deleteDatabase() {
|
|
// Are you sure? (y/n)
|
|
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
|
final File dbFile = new File(db.getPath());
|
|
mOpenHelper.close();
|
|
if (dbFile.exists()) {
|
|
SQLiteDatabase.deleteDatabase(dbFile);
|
|
}
|
|
mOpenHelper = new DatabaseHelper(getContext());
|
|
}
|
|
|
|
private static class DatabaseHelper extends SQLiteOpenHelper {
|
|
private static final String TAG_RESOLVE = "resolve";
|
|
private static final String TAG_FAVORITES = "favorites";
|
|
private static final String TAG_FAVORITE = "favorite";
|
|
private static final String TAG_CLOCK = "clock";
|
|
private static final String TAG_SEARCH = "search";
|
|
private static final String TAG_APPWIDGET = "appwidget";
|
|
private static final String TAG_SHORTCUT = "shortcut";
|
|
private static final String TAG_FOLDER = "folder";
|
|
private static final String TAG_PARTNER_FOLDER = "partner-folder";
|
|
private static final String TAG_EXTRA = "extra";
|
|
private static final String TAG_INCLUDE = "include";
|
|
|
|
private static final String ATTR_TITLE = "title";
|
|
private static final String ATTR_ICON = "icon";
|
|
private static final String ATTR_URI = "uri";
|
|
private static final String ATTR_PACKAGE_NAME = "packageName";
|
|
private static final String ATTR_CLASS_NAME = "className";
|
|
|
|
private final Context mContext;
|
|
private final PackageManager mPackageManager;
|
|
private final AppWidgetHost mAppWidgetHost;
|
|
private long mMaxItemId = -1;
|
|
private long mMaxScreenId = -1;
|
|
|
|
private boolean mNewDbCreated = false;
|
|
|
|
DatabaseHelper(Context context) {
|
|
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
|
mContext = context;
|
|
mPackageManager = context.getPackageManager();
|
|
mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID);
|
|
|
|
// In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
|
|
// the DB here
|
|
if (mMaxItemId == -1) {
|
|
mMaxItemId = initializeMaxItemId(getWritableDatabase());
|
|
}
|
|
if (mMaxScreenId == -1) {
|
|
mMaxScreenId = initializeMaxScreenId(getWritableDatabase());
|
|
}
|
|
}
|
|
|
|
public boolean wasNewDbCreated() {
|
|
return mNewDbCreated;
|
|
}
|
|
|
|
/**
|
|
* Send notification that we've deleted the {@link AppWidgetHost},
|
|
* probably as part of the initial database creation. The receiver may
|
|
* want to re-call {@link AppWidgetHost#startListening()} to ensure
|
|
* callbacks are correctly set.
|
|
*/
|
|
private void sendAppWidgetResetNotify() {
|
|
final ContentResolver resolver = mContext.getContentResolver();
|
|
resolver.notifyChange(CONTENT_APPWIDGET_RESET_URI, null);
|
|
}
|
|
|
|
@Override
|
|
public void onCreate(SQLiteDatabase db) {
|
|
if (LOGD) Log.d(TAG, "creating new launcher database");
|
|
|
|
mMaxItemId = 1;
|
|
mMaxScreenId = 0;
|
|
mNewDbCreated = true;
|
|
|
|
db.execSQL("CREATE TABLE favorites (" +
|
|
"_id INTEGER PRIMARY KEY," +
|
|
"title TEXT," +
|
|
"intent TEXT," +
|
|
"container INTEGER," +
|
|
"screen INTEGER," +
|
|
"cellX INTEGER," +
|
|
"cellY INTEGER," +
|
|
"spanX INTEGER," +
|
|
"spanY INTEGER," +
|
|
"itemType INTEGER," +
|
|
"appWidgetId INTEGER NOT NULL DEFAULT -1," +
|
|
"isShortcut INTEGER," +
|
|
"iconType INTEGER," +
|
|
"iconPackage TEXT," +
|
|
"iconResource TEXT," +
|
|
"icon BLOB," +
|
|
"uri TEXT," +
|
|
"displayMode INTEGER," +
|
|
"appWidgetProvider TEXT," +
|
|
"modified INTEGER NOT NULL DEFAULT 0," +
|
|
"restored INTEGER NOT NULL DEFAULT 0" +
|
|
");");
|
|
addWorkspacesTable(db);
|
|
|
|
// Database was just created, so wipe any previous widgets
|
|
if (mAppWidgetHost != null) {
|
|
mAppWidgetHost.deleteHost();
|
|
sendAppWidgetResetNotify();
|
|
}
|
|
|
|
if (shouldImportLauncher2Database(mContext)) {
|
|
// Try converting the old database
|
|
ContentValuesCallback permuteScreensCb = new ContentValuesCallback() {
|
|
public void onRow(ContentValues values) {
|
|
int container = values.getAsInteger(LauncherSettings.Favorites.CONTAINER);
|
|
if (container == Favorites.CONTAINER_DESKTOP) {
|
|
int screen = values.getAsInteger(LauncherSettings.Favorites.SCREEN);
|
|
screen = (int) upgradeLauncherDb_permuteScreens(screen);
|
|
values.put(LauncherSettings.Favorites.SCREEN, screen);
|
|
}
|
|
}
|
|
};
|
|
Uri uri = Uri.parse("content://" + Settings.AUTHORITY +
|
|
"/old_favorites?notify=true");
|
|
if (!convertDatabase(db, uri, permuteScreensCb, true)) {
|
|
// Try and upgrade from the Launcher2 db
|
|
uri = Uri.parse(mContext.getString(R.string.old_launcher_provider_uri));
|
|
if (!convertDatabase(db, uri, permuteScreensCb, false)) {
|
|
// If we fail, then set a flag to load the default workspace
|
|
setFlagEmptyDbCreated();
|
|
return;
|
|
}
|
|
}
|
|
// Right now, in non-default workspace cases, we want to run the final
|
|
// upgrade code (ie. to fix workspace screen indices -> ids, etc.), so
|
|
// set that flag too.
|
|
setFlagJustLoadedOldDb();
|
|
} else {
|
|
// Fresh and clean launcher DB.
|
|
mMaxItemId = initializeMaxItemId(db);
|
|
setFlagEmptyDbCreated();
|
|
}
|
|
}
|
|
|
|
private void addWorkspacesTable(SQLiteDatabase db) {
|
|
db.execSQL("CREATE TABLE " + TABLE_WORKSPACE_SCREENS + " (" +
|
|
LauncherSettings.WorkspaceScreens._ID + " INTEGER," +
|
|
LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," +
|
|
LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" +
|
|
");");
|
|
}
|
|
|
|
private void removeOrphanedItems(SQLiteDatabase db) {
|
|
db.execSQL("DELETE FROM " + TABLE_FAVORITES + " WHERE " +
|
|
LauncherSettings.Favorites.SCREEN + " NOT IN (SELECT " +
|
|
LauncherSettings.WorkspaceScreens._ID + " FROM " + TABLE_WORKSPACE_SCREENS +
|
|
")");
|
|
}
|
|
|
|
private void setFlagJustLoadedOldDb() {
|
|
String spKey = LauncherAppState.getSharedPreferencesKey();
|
|
SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE);
|
|
SharedPreferences.Editor editor = sp.edit();
|
|
editor.putBoolean(UPGRADED_FROM_OLD_DATABASE, true);
|
|
editor.putBoolean(EMPTY_DATABASE_CREATED, false);
|
|
editor.commit();
|
|
}
|
|
|
|
private void setFlagEmptyDbCreated() {
|
|
String spKey = LauncherAppState.getSharedPreferencesKey();
|
|
SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE);
|
|
SharedPreferences.Editor editor = sp.edit();
|
|
editor.putBoolean(EMPTY_DATABASE_CREATED, true);
|
|
editor.putBoolean(UPGRADED_FROM_OLD_DATABASE, false);
|
|
editor.commit();
|
|
}
|
|
|
|
// We rearrange the screens from the old launcher
|
|
// 12345 -> 34512
|
|
private long upgradeLauncherDb_permuteScreens(long screen) {
|
|
if (screen >= 2) {
|
|
return screen - 2;
|
|
} else {
|
|
return screen + 3;
|
|
}
|
|
}
|
|
|
|
private boolean convertDatabase(SQLiteDatabase db, Uri uri,
|
|
ContentValuesCallback cb, boolean deleteRows) {
|
|
if (LOGD) Log.d(TAG, "converting database from an older format, but not onUpgrade");
|
|
boolean converted = false;
|
|
|
|
final ContentResolver resolver = mContext.getContentResolver();
|
|
Cursor cursor = null;
|
|
|
|
try {
|
|
cursor = resolver.query(uri, null, null, null, null);
|
|
} catch (Exception e) {
|
|
// Ignore
|
|
}
|
|
|
|
// We already have a favorites database in the old provider
|
|
if (cursor != null) {
|
|
try {
|
|
if (cursor.getCount() > 0) {
|
|
converted = copyFromCursor(db, cursor, cb) > 0;
|
|
if (converted && deleteRows) {
|
|
resolver.delete(uri, null, null);
|
|
}
|
|
}
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
}
|
|
|
|
if (converted) {
|
|
// Convert widgets from this import into widgets
|
|
if (LOGD) Log.d(TAG, "converted and now triggering widget upgrade");
|
|
convertWidgets(db);
|
|
|
|
// Update max item id
|
|
mMaxItemId = initializeMaxItemId(db);
|
|
if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId);
|
|
}
|
|
|
|
return converted;
|
|
}
|
|
|
|
private int copyFromCursor(SQLiteDatabase db, Cursor c, ContentValuesCallback cb) {
|
|
final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
|
|
final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
|
|
final int titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
|
|
final int iconTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE);
|
|
final int iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
|
|
final int iconPackageIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
|
|
final int iconResourceIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
|
|
final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
|
|
final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
|
|
final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
|
|
final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
|
|
final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
|
|
final int uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI);
|
|
final int displayModeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE);
|
|
|
|
ContentValues[] rows = new ContentValues[c.getCount()];
|
|
int i = 0;
|
|
while (c.moveToNext()) {
|
|
ContentValues values = new ContentValues(c.getColumnCount());
|
|
values.put(LauncherSettings.Favorites._ID, c.getLong(idIndex));
|
|
values.put(LauncherSettings.Favorites.INTENT, c.getString(intentIndex));
|
|
values.put(LauncherSettings.Favorites.TITLE, c.getString(titleIndex));
|
|
values.put(LauncherSettings.Favorites.ICON_TYPE, c.getInt(iconTypeIndex));
|
|
values.put(LauncherSettings.Favorites.ICON, c.getBlob(iconIndex));
|
|
values.put(LauncherSettings.Favorites.ICON_PACKAGE, c.getString(iconPackageIndex));
|
|
values.put(LauncherSettings.Favorites.ICON_RESOURCE, c.getString(iconResourceIndex));
|
|
values.put(LauncherSettings.Favorites.CONTAINER, c.getInt(containerIndex));
|
|
values.put(LauncherSettings.Favorites.ITEM_TYPE, c.getInt(itemTypeIndex));
|
|
values.put(LauncherSettings.Favorites.APPWIDGET_ID, -1);
|
|
values.put(LauncherSettings.Favorites.SCREEN, c.getInt(screenIndex));
|
|
values.put(LauncherSettings.Favorites.CELLX, c.getInt(cellXIndex));
|
|
values.put(LauncherSettings.Favorites.CELLY, c.getInt(cellYIndex));
|
|
values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex));
|
|
values.put(LauncherSettings.Favorites.DISPLAY_MODE, c.getInt(displayModeIndex));
|
|
if (cb != null) {
|
|
cb.onRow(values);
|
|
}
|
|
rows[i++] = values;
|
|
}
|
|
|
|
int total = 0;
|
|
if (i > 0) {
|
|
db.beginTransaction();
|
|
try {
|
|
int numValues = rows.length;
|
|
for (i = 0; i < numValues; i++) {
|
|
if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, rows[i]) < 0) {
|
|
return 0;
|
|
} else {
|
|
total++;
|
|
}
|
|
}
|
|
db.setTransactionSuccessful();
|
|
} finally {
|
|
db.endTransaction();
|
|
}
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
@Override
|
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
|
if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion);
|
|
|
|
int version = oldVersion;
|
|
if (version < 3) {
|
|
// upgrade 1,2 -> 3 added appWidgetId column
|
|
db.beginTransaction();
|
|
try {
|
|
// Insert new column for holding appWidgetIds
|
|
db.execSQL("ALTER TABLE favorites " +
|
|
"ADD COLUMN appWidgetId INTEGER NOT NULL DEFAULT -1;");
|
|
db.setTransactionSuccessful();
|
|
version = 3;
|
|
} catch (SQLException ex) {
|
|
// Old version remains, which means we wipe old data
|
|
Log.e(TAG, ex.getMessage(), ex);
|
|
} finally {
|
|
db.endTransaction();
|
|
}
|
|
|
|
// Convert existing widgets only if table upgrade was successful
|
|
if (version == 3) {
|
|
convertWidgets(db);
|
|
}
|
|
}
|
|
|
|
if (version < 4) {
|
|
version = 4;
|
|
}
|
|
|
|
// Where's version 5?
|
|
// - Donut and sholes on 2.0 shipped with version 4 of launcher1.
|
|
// - Passion shipped on 2.1 with version 6 of launcher3
|
|
// - Sholes shipped on 2.1r1 (aka Mr. 3) with version 5 of launcher 1
|
|
// but version 5 on there was the updateContactsShortcuts change
|
|
// which was version 6 in launcher 2 (first shipped on passion 2.1r1).
|
|
// The updateContactsShortcuts change is idempotent, so running it twice
|
|
// is okay so we'll do that when upgrading the devices that shipped with it.
|
|
if (version < 6) {
|
|
// We went from 3 to 5 screens. Move everything 1 to the right
|
|
db.beginTransaction();
|
|
try {
|
|
db.execSQL("UPDATE favorites SET screen=(screen + 1);");
|
|
db.setTransactionSuccessful();
|
|
} catch (SQLException ex) {
|
|
// Old version remains, which means we wipe old data
|
|
Log.e(TAG, ex.getMessage(), ex);
|
|
} finally {
|
|
db.endTransaction();
|
|
}
|
|
|
|
// We added the fast track.
|
|
if (updateContactsShortcuts(db)) {
|
|
version = 6;
|
|
}
|
|
}
|
|
|
|
if (version < 7) {
|
|
// Version 7 gets rid of the special search widget.
|
|
convertWidgets(db);
|
|
version = 7;
|
|
}
|
|
|
|
if (version < 8) {
|
|
// Version 8 (froyo) has the icons all normalized. This should
|
|
// already be the case in practice, but we now rely on it and don't
|
|
// resample the images each time.
|
|
normalizeIcons(db);
|
|
version = 8;
|
|
}
|
|
|
|
if (version < 9) {
|
|
// The max id is not yet set at this point (onUpgrade is triggered in the ctor
|
|
// before it gets a change to get set, so we need to read it here when we use it)
|
|
if (mMaxItemId == -1) {
|
|
mMaxItemId = initializeMaxItemId(db);
|
|
}
|
|
|
|
// Add default hotseat icons
|
|
loadFavorites(db, R.xml.update_workspace);
|
|
version = 9;
|
|
}
|
|
|
|
// We bumped the version three time during JB, once to update the launch flags, once to
|
|
// update the override for the default launch animation and once to set the mimetype
|
|
// to improve startup performance
|
|
if (version < 12) {
|
|
// Contact shortcuts need a different set of flags to be launched now
|
|
// The updateContactsShortcuts change is idempotent, so we can keep using it like
|
|
// back in the Donut days
|
|
updateContactsShortcuts(db);
|
|
version = 12;
|
|
}
|
|
|
|
if (version < 13) {
|
|
// With the new shrink-wrapped and re-orderable workspaces, it makes sense
|
|
// to persist workspace screens and their relative order.
|
|
mMaxScreenId = 0;
|
|
|
|
// This will never happen in the wild, but when we switch to using workspace
|
|
// screen ids, redo the import from old launcher.
|
|
sJustLoadedFromOldDb = true;
|
|
|
|
addWorkspacesTable(db);
|
|
version = 13;
|
|
}
|
|
|
|
if (version < 14) {
|
|
db.beginTransaction();
|
|
try {
|
|
// Insert new column for holding widget provider name
|
|
db.execSQL("ALTER TABLE favorites " +
|
|
"ADD COLUMN appWidgetProvider TEXT;");
|
|
db.setTransactionSuccessful();
|
|
version = 14;
|
|
} catch (SQLException ex) {
|
|
// Old version remains, which means we wipe old data
|
|
Log.e(TAG, ex.getMessage(), ex);
|
|
} finally {
|
|
db.endTransaction();
|
|
}
|
|
}
|
|
|
|
if (version < 15) {
|
|
db.beginTransaction();
|
|
try {
|
|
// Insert new column for holding update timestamp
|
|
db.execSQL("ALTER TABLE favorites " +
|
|
"ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
|
|
db.execSQL("ALTER TABLE workspaceScreens " +
|
|
"ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
|
|
db.setTransactionSuccessful();
|
|
version = 15;
|
|
} catch (SQLException ex) {
|
|
// Old version remains, which means we wipe old data
|
|
Log.e(TAG, ex.getMessage(), ex);
|
|
} finally {
|
|
db.endTransaction();
|
|
}
|
|
}
|
|
|
|
|
|
if (version < 16) {
|
|
db.beginTransaction();
|
|
try {
|
|
// Insert new column for holding restore status
|
|
db.execSQL("ALTER TABLE favorites " +
|
|
"ADD COLUMN restored INTEGER NOT NULL DEFAULT 0;");
|
|
db.setTransactionSuccessful();
|
|
version = 16;
|
|
} catch (SQLException ex) {
|
|
// Old version remains, which means we wipe old data
|
|
Log.e(TAG, ex.getMessage(), ex);
|
|
} finally {
|
|
db.endTransaction();
|
|
}
|
|
}
|
|
|
|
if (version < 17) {
|
|
// We use the db version upgrade here to identify users who may not have seen
|
|
// clings yet (because they weren't available), but for whom the clings are now
|
|
// available (tablet users). Because one of the possible cling flows (migration)
|
|
// is very destructive (wipes out workspaces), we want to prevent this from showing
|
|
// until clear data. We do so by marking that the clings have been shown.
|
|
LauncherClings.synchonouslyMarkFirstRunClingDismissed(mContext);
|
|
version = 17;
|
|
}
|
|
|
|
if (version < 18) {
|
|
// Due to a data loss bug, some users may have items associated with screen ids
|
|
// which no longer exist. Since this can cause other problems, and since the user
|
|
// will never see these items anyway, we use database upgrade as an opportunity to
|
|
// clean things up.
|
|
|
|
// TODO: this needs to be fixed, currently causes data loss.
|
|
//removeOrphanedItems(db);
|
|
version = 18;
|
|
}
|
|
|
|
if (version != DATABASE_VERSION) {
|
|
Log.w(TAG, "Destroying all old data.");
|
|
db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES);
|
|
db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS);
|
|
|
|
onCreate(db);
|
|
}
|
|
}
|
|
|
|
private boolean updateContactsShortcuts(SQLiteDatabase db) {
|
|
final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE,
|
|
new int[] { Favorites.ITEM_TYPE_SHORTCUT });
|
|
|
|
Cursor c = null;
|
|
final String actionQuickContact = "com.android.contacts.action.QUICK_CONTACT";
|
|
db.beginTransaction();
|
|
try {
|
|
// Select and iterate through each matching widget
|
|
c = db.query(TABLE_FAVORITES,
|
|
new String[] { Favorites._ID, Favorites.INTENT },
|
|
selectWhere, null, null, null, null);
|
|
if (c == null) return false;
|
|
|
|
if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount());
|
|
|
|
final int idIndex = c.getColumnIndex(Favorites._ID);
|
|
final int intentIndex = c.getColumnIndex(Favorites.INTENT);
|
|
|
|
while (c.moveToNext()) {
|
|
long favoriteId = c.getLong(idIndex);
|
|
final String intentUri = c.getString(intentIndex);
|
|
if (intentUri != null) {
|
|
try {
|
|
final Intent intent = Intent.parseUri(intentUri, 0);
|
|
android.util.Log.d("Home", intent.toString());
|
|
final Uri uri = intent.getData();
|
|
if (uri != null) {
|
|
final String data = uri.toString();
|
|
if ((Intent.ACTION_VIEW.equals(intent.getAction()) ||
|
|
actionQuickContact.equals(intent.getAction())) &&
|
|
(data.startsWith("content://contacts/people/") ||
|
|
data.startsWith("content://com.android.contacts/" +
|
|
"contacts/lookup/"))) {
|
|
|
|
final Intent newIntent = new Intent(actionQuickContact);
|
|
// When starting from the launcher, start in a new, cleared task
|
|
// CLEAR_WHEN_TASK_RESET cannot reset the root of a task, so we
|
|
// clear the whole thing preemptively here since
|
|
// QuickContactActivity will finish itself when launching other
|
|
// detail activities.
|
|
newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
|
|
Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
|
newIntent.putExtra(
|
|
Launcher.INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION, true);
|
|
newIntent.setData(uri);
|
|
// Determine the type and also put that in the shortcut
|
|
// (that can speed up launch a bit)
|
|
newIntent.setDataAndType(uri, newIntent.resolveType(mContext));
|
|
|
|
final ContentValues values = new ContentValues();
|
|
values.put(LauncherSettings.Favorites.INTENT,
|
|
newIntent.toUri(0));
|
|
|
|
String updateWhere = Favorites._ID + "=" + favoriteId;
|
|
db.update(TABLE_FAVORITES, values, updateWhere, null);
|
|
}
|
|
}
|
|
} catch (RuntimeException ex) {
|
|
Log.e(TAG, "Problem upgrading shortcut", ex);
|
|
} catch (URISyntaxException e) {
|
|
Log.e(TAG, "Problem upgrading shortcut", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
db.setTransactionSuccessful();
|
|
} catch (SQLException ex) {
|
|
Log.w(TAG, "Problem while upgrading contacts", ex);
|
|
return false;
|
|
} finally {
|
|
db.endTransaction();
|
|
if (c != null) {
|
|
c.close();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private void normalizeIcons(SQLiteDatabase db) {
|
|
Log.d(TAG, "normalizing icons");
|
|
|
|
db.beginTransaction();
|
|
Cursor c = null;
|
|
SQLiteStatement update = null;
|
|
try {
|
|
boolean logged = false;
|
|
update = db.compileStatement("UPDATE favorites "
|
|
+ "SET icon=? WHERE _id=?");
|
|
|
|
c = db.rawQuery("SELECT _id, icon FROM favorites WHERE iconType=" +
|
|
Favorites.ICON_TYPE_BITMAP, null);
|
|
|
|
final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
|
|
final int iconIndex = c.getColumnIndexOrThrow(Favorites.ICON);
|
|
|
|
while (c.moveToNext()) {
|
|
long id = c.getLong(idIndex);
|
|
byte[] data = c.getBlob(iconIndex);
|
|
try {
|
|
Bitmap bitmap = Utilities.resampleIconBitmap(
|
|
BitmapFactory.decodeByteArray(data, 0, data.length),
|
|
mContext);
|
|
if (bitmap != null) {
|
|
update.bindLong(1, id);
|
|
data = ItemInfo.flattenBitmap(bitmap);
|
|
if (data != null) {
|
|
update.bindBlob(2, data);
|
|
update.execute();
|
|
}
|
|
bitmap.recycle();
|
|
}
|
|
} catch (Exception e) {
|
|
if (!logged) {
|
|
Log.e(TAG, "Failed normalizing icon " + id, e);
|
|
} else {
|
|
Log.e(TAG, "Also failed normalizing icon " + id);
|
|
}
|
|
logged = true;
|
|
}
|
|
}
|
|
db.setTransactionSuccessful();
|
|
} catch (SQLException ex) {
|
|
Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex);
|
|
} finally {
|
|
db.endTransaction();
|
|
if (update != null) {
|
|
update.close();
|
|
}
|
|
if (c != null) {
|
|
c.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generates a new ID to use for an object in your database. This method should be only
|
|
// called from the main UI thread. As an exception, we do call it when we call the
|
|
// constructor from the worker thread; however, this doesn't extend until after the
|
|
// constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
|
|
// after that point
|
|
public long generateNewItemId() {
|
|
if (mMaxItemId < 0) {
|
|
throw new RuntimeException("Error: max item id was not initialized");
|
|
}
|
|
mMaxItemId += 1;
|
|
return mMaxItemId;
|
|
}
|
|
|
|
public void updateMaxItemId(long id) {
|
|
mMaxItemId = id + 1;
|
|
}
|
|
|
|
public void checkId(String table, ContentValues values) {
|
|
long id = values.getAsLong(LauncherSettings.BaseLauncherColumns._ID);
|
|
if (table == LauncherProvider.TABLE_WORKSPACE_SCREENS) {
|
|
mMaxScreenId = Math.max(id, mMaxScreenId);
|
|
} else {
|
|
mMaxItemId = Math.max(id, mMaxItemId);
|
|
}
|
|
}
|
|
|
|
private long initializeMaxItemId(SQLiteDatabase db) {
|
|
Cursor c = db.rawQuery("SELECT MAX(_id) FROM favorites", null);
|
|
|
|
// get the result
|
|
final int maxIdIndex = 0;
|
|
long id = -1;
|
|
if (c != null && c.moveToNext()) {
|
|
id = c.getLong(maxIdIndex);
|
|
}
|
|
if (c != null) {
|
|
c.close();
|
|
}
|
|
|
|
if (id == -1) {
|
|
throw new RuntimeException("Error: could not query max item id");
|
|
}
|
|
|
|
return id;
|
|
}
|
|
|
|
// Generates a new ID to use for an workspace screen in your database. This method
|
|
// should be only called from the main UI thread. As an exception, we do call it when we
|
|
// call the constructor from the worker thread; however, this doesn't extend until after the
|
|
// constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
|
|
// after that point
|
|
public long generateNewScreenId() {
|
|
if (mMaxScreenId < 0) {
|
|
throw new RuntimeException("Error: max screen id was not initialized");
|
|
}
|
|
mMaxScreenId += 1;
|
|
// Log to disk
|
|
Launcher.addDumpLog(TAG, "11683562 - generateNewScreenId(): " + mMaxScreenId, true);
|
|
return mMaxScreenId;
|
|
}
|
|
|
|
public void updateMaxScreenId(long maxScreenId) {
|
|
// Log to disk
|
|
Launcher.addDumpLog(TAG, "11683562 - updateMaxScreenId(): " + maxScreenId, true);
|
|
mMaxScreenId = maxScreenId;
|
|
}
|
|
|
|
private long initializeMaxScreenId(SQLiteDatabase db) {
|
|
Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens._ID + ") FROM " + TABLE_WORKSPACE_SCREENS, null);
|
|
|
|
// get the result
|
|
final int maxIdIndex = 0;
|
|
long id = -1;
|
|
if (c != null && c.moveToNext()) {
|
|
id = c.getLong(maxIdIndex);
|
|
}
|
|
if (c != null) {
|
|
c.close();
|
|
}
|
|
|
|
if (id == -1) {
|
|
throw new RuntimeException("Error: could not query max screen id");
|
|
}
|
|
|
|
// Log to disk
|
|
Launcher.addDumpLog(TAG, "11683562 - initializeMaxScreenId(): " + id, true);
|
|
return id;
|
|
}
|
|
|
|
/**
|
|
* Upgrade existing clock and photo frame widgets into their new widget
|
|
* equivalents.
|
|
*/
|
|
private void convertWidgets(SQLiteDatabase db) {
|
|
final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
|
|
final int[] bindSources = new int[] {
|
|
Favorites.ITEM_TYPE_WIDGET_CLOCK,
|
|
Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME,
|
|
Favorites.ITEM_TYPE_WIDGET_SEARCH,
|
|
};
|
|
|
|
final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE, bindSources);
|
|
|
|
Cursor c = null;
|
|
|
|
db.beginTransaction();
|
|
try {
|
|
// Select and iterate through each matching widget
|
|
c = db.query(TABLE_FAVORITES, new String[] { Favorites._ID, Favorites.ITEM_TYPE },
|
|
selectWhere, null, null, null, null);
|
|
|
|
if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount());
|
|
|
|
final ContentValues values = new ContentValues();
|
|
while (c != null && c.moveToNext()) {
|
|
long favoriteId = c.getLong(0);
|
|
int favoriteType = c.getInt(1);
|
|
|
|
// Allocate and update database with new appWidgetId
|
|
try {
|
|
int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
|
|
|
|
if (LOGD) {
|
|
Log.d(TAG, "allocated appWidgetId=" + appWidgetId
|
|
+ " for favoriteId=" + favoriteId);
|
|
}
|
|
values.clear();
|
|
values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET);
|
|
values.put(Favorites.APPWIDGET_ID, appWidgetId);
|
|
|
|
// Original widgets might not have valid spans when upgrading
|
|
if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) {
|
|
values.put(LauncherSettings.Favorites.SPANX, 4);
|
|
values.put(LauncherSettings.Favorites.SPANY, 1);
|
|
} else {
|
|
values.put(LauncherSettings.Favorites.SPANX, 2);
|
|
values.put(LauncherSettings.Favorites.SPANY, 2);
|
|
}
|
|
|
|
String updateWhere = Favorites._ID + "=" + favoriteId;
|
|
db.update(TABLE_FAVORITES, values, updateWhere, null);
|
|
|
|
if (favoriteType == Favorites.ITEM_TYPE_WIDGET_CLOCK) {
|
|
// TODO: check return value
|
|
appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,
|
|
new ComponentName("com.android.alarmclock",
|
|
"com.android.alarmclock.AnalogAppWidgetProvider"));
|
|
} else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME) {
|
|
// TODO: check return value
|
|
appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,
|
|
new ComponentName("com.android.camera",
|
|
"com.android.camera.PhotoAppWidgetProvider"));
|
|
} else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) {
|
|
// TODO: check return value
|
|
appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,
|
|
getSearchWidgetProvider());
|
|
}
|
|
} catch (RuntimeException ex) {
|
|
Log.e(TAG, "Problem allocating appWidgetId", ex);
|
|
}
|
|
}
|
|
|
|
db.setTransactionSuccessful();
|
|
} catch (SQLException ex) {
|
|
Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex);
|
|
} finally {
|
|
db.endTransaction();
|
|
if (c != null) {
|
|
c.close();
|
|
}
|
|
}
|
|
|
|
// Update max item id
|
|
mMaxItemId = initializeMaxItemId(db);
|
|
if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId);
|
|
}
|
|
|
|
private static final void beginDocument(XmlPullParser parser, String firstElementName)
|
|
throws XmlPullParserException, IOException {
|
|
int type;
|
|
while ((type = parser.next()) != XmlPullParser.START_TAG
|
|
&& type != XmlPullParser.END_DOCUMENT) {
|
|
;
|
|
}
|
|
|
|
if (type != XmlPullParser.START_TAG) {
|
|
throw new XmlPullParserException("No start tag found");
|
|
}
|
|
|
|
if (!parser.getName().equals(firstElementName)) {
|
|
throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() +
|
|
", expected " + firstElementName);
|
|
}
|
|
}
|
|
|
|
private static Intent buildMainIntent() {
|
|
Intent intent = new Intent(Intent.ACTION_MAIN, null);
|
|
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
|
return intent;
|
|
}
|
|
|
|
/**
|
|
* Loads the default set of favorite packages from an xml file.
|
|
*
|
|
* @param db The database to write the values into
|
|
* @param filterContainerId The specific container id of items to load
|
|
*/
|
|
private int loadFavorites(SQLiteDatabase db, int workspaceResourceId) {
|
|
ContentValues values = new ContentValues();
|
|
|
|
if (LOGD) Log.v(TAG, String.format("Loading favorites from resid=0x%08x", workspaceResourceId));
|
|
|
|
int i = 0;
|
|
try {
|
|
XmlResourceParser parser = mContext.getResources().getXml(workspaceResourceId);
|
|
AttributeSet attrs = Xml.asAttributeSet(parser);
|
|
beginDocument(parser, TAG_FAVORITES);
|
|
|
|
final int depth = parser.getDepth();
|
|
|
|
int type;
|
|
while (((type = parser.next()) != XmlPullParser.END_TAG ||
|
|
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
|
|
|
|
if (type != XmlPullParser.START_TAG) {
|
|
continue;
|
|
}
|
|
|
|
boolean added = false;
|
|
final String name = parser.getName();
|
|
|
|
if (TAG_INCLUDE.equals(name)) {
|
|
final TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.Include);
|
|
|
|
final int resId = a.getResourceId(R.styleable.Include_workspace, 0);
|
|
|
|
if (LOGD) Log.v(TAG, String.format(("%" + (2*(depth+1)) + "s<include workspace=%08x>"),
|
|
"", resId));
|
|
|
|
if (resId != 0 && resId != workspaceResourceId) {
|
|
// recursively load some more favorites, why not?
|
|
i += loadFavorites(db, resId);
|
|
added = false;
|
|
} else {
|
|
Log.w(TAG, String.format("Skipping <include workspace=0x%08x>", resId));
|
|
}
|
|
|
|
a.recycle();
|
|
|
|
if (LOGD) Log.v(TAG, String.format(("%" + (2*(depth+1)) + "s</include>"), ""));
|
|
continue;
|
|
}
|
|
|
|
// Assuming it's a <favorite> at this point
|
|
TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.Favorite);
|
|
|
|
long container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
|
|
if (a.hasValue(R.styleable.Favorite_container)) {
|
|
container = Long.valueOf(a.getString(R.styleable.Favorite_container));
|
|
}
|
|
|
|
String screen = a.getString(R.styleable.Favorite_screen);
|
|
String x = a.getString(R.styleable.Favorite_x);
|
|
String y = a.getString(R.styleable.Favorite_y);
|
|
|
|
values.clear();
|
|
values.put(LauncherSettings.Favorites.CONTAINER, container);
|
|
values.put(LauncherSettings.Favorites.SCREEN, screen);
|
|
values.put(LauncherSettings.Favorites.CELLX, x);
|
|
values.put(LauncherSettings.Favorites.CELLY, y);
|
|
|
|
if (LOGD) {
|
|
final String title = a.getString(R.styleable.Favorite_title);
|
|
final String pkg = a.getString(R.styleable.Favorite_packageName);
|
|
final String something = title != null ? title : pkg;
|
|
Log.v(TAG, String.format(
|
|
("%" + (2*(depth+1)) + "s<%s%s c=%d s=%s x=%s y=%s>"),
|
|
"", name,
|
|
(something == null ? "" : (" \"" + something + "\"")),
|
|
container, screen, x, y));
|
|
}
|
|
|
|
if (TAG_FAVORITE.equals(name)) {
|
|
long id = addAppShortcut(db, values, parser);
|
|
added = id >= 0;
|
|
} else if (TAG_SEARCH.equals(name)) {
|
|
added = addSearchWidget(db, values);
|
|
} else if (TAG_CLOCK.equals(name)) {
|
|
added = addClockWidget(db, values);
|
|
} else if (TAG_APPWIDGET.equals(name)) {
|
|
added = addAppWidget(parser, attrs, type, db, values, a);
|
|
} else if (TAG_SHORTCUT.equals(name)) {
|
|
long id = addUriShortcut(db, values, mContext.getResources(), parser);
|
|
added = id >= 0;
|
|
} else if (TAG_RESOLVE.equals(name)) {
|
|
// This looks through the contained favorites (or meta-favorites) and
|
|
// attempts to add them as shortcuts in the fallback group's location
|
|
// until one is added successfully.
|
|
added = false;
|
|
final int groupDepth = parser.getDepth();
|
|
while ((type = parser.next()) != XmlPullParser.END_TAG ||
|
|
parser.getDepth() > groupDepth) {
|
|
if (type != XmlPullParser.START_TAG) {
|
|
continue;
|
|
}
|
|
final String fallback_item_name = parser.getName();
|
|
final TypedArray ar = mContext.obtainStyledAttributes(attrs,
|
|
R.styleable.Favorite);
|
|
if (!added) {
|
|
if (TAG_FAVORITE.equals(fallback_item_name)) {
|
|
final long id = addAppShortcut(db, values, parser);
|
|
added = id >= 0;
|
|
} else {
|
|
Log.e(TAG, "Fallback groups can contain only favorites "
|
|
+ ar.toString());
|
|
}
|
|
}
|
|
ar.recycle();
|
|
}
|
|
} else if (TAG_FOLDER.equals(name)) {
|
|
// Folder contents are nested in this XML file
|
|
added = loadFolder(db, values, mContext.getResources(), parser);
|
|
|
|
} else if (TAG_PARTNER_FOLDER.equals(name)) {
|
|
// Folder contents come from an external XML resource
|
|
final Partner partner = Partner.get(mPackageManager);
|
|
if (partner != null) {
|
|
final Resources partnerRes = partner.getResources();
|
|
final int resId = partnerRes.getIdentifier(Partner.RESOURCE_FOLDER,
|
|
"xml", partner.getPackageName());
|
|
if (resId != 0) {
|
|
final XmlResourceParser partnerParser = partnerRes.getXml(resId);
|
|
beginDocument(partnerParser, TAG_FOLDER);
|
|
added = loadFolder(db, values, partnerRes, partnerParser);
|
|
}
|
|
}
|
|
}
|
|
if (added) i++;
|
|
a.recycle();
|
|
}
|
|
} catch (XmlPullParserException e) {
|
|
Log.w(TAG, "Got exception parsing favorites.", e);
|
|
} catch (IOException e) {
|
|
Log.w(TAG, "Got exception parsing favorites.", e);
|
|
} catch (RuntimeException e) {
|
|
Log.w(TAG, "Got exception parsing favorites.", e);
|
|
}
|
|
|
|
// Update the max item id after we have loaded the database
|
|
if (mMaxItemId == -1) {
|
|
mMaxItemId = initializeMaxItemId(db);
|
|
}
|
|
|
|
return i;
|
|
}
|
|
|
|
/**
|
|
* Parse folder starting at current {@link XmlPullParser} location.
|
|
*/
|
|
private boolean loadFolder(SQLiteDatabase db, ContentValues values, Resources res,
|
|
XmlResourceParser parser) throws IOException, XmlPullParserException {
|
|
final String title;
|
|
final int titleResId = getAttributeResourceValue(parser, ATTR_TITLE, 0);
|
|
if (titleResId != 0) {
|
|
title = res.getString(titleResId);
|
|
} else {
|
|
title = mContext.getResources().getString(R.string.folder_name);
|
|
}
|
|
|
|
values.put(LauncherSettings.Favorites.TITLE, title);
|
|
long folderId = addFolder(db, values);
|
|
boolean added = folderId >= 0;
|
|
|
|
ArrayList<Long> folderItems = new ArrayList<Long>();
|
|
|
|
int type;
|
|
int folderDepth = parser.getDepth();
|
|
while ((type = parser.next()) != XmlPullParser.END_TAG ||
|
|
parser.getDepth() > folderDepth) {
|
|
if (type != XmlPullParser.START_TAG) {
|
|
continue;
|
|
}
|
|
final String tag = parser.getName();
|
|
|
|
final ContentValues childValues = new ContentValues();
|
|
childValues.put(LauncherSettings.Favorites.CONTAINER, folderId);
|
|
|
|
if (LOGD) {
|
|
final String pkg = getAttributeValue(parser, ATTR_PACKAGE_NAME);
|
|
final String uri = getAttributeValue(parser, ATTR_URI);
|
|
Log.v(TAG, String.format(("%" + (2*(folderDepth+1)) + "s<%s \"%s\">"), "",
|
|
tag, uri != null ? uri : pkg));
|
|
}
|
|
|
|
if (TAG_FAVORITE.equals(tag) && folderId >= 0) {
|
|
final long id = addAppShortcut(db, childValues, parser);
|
|
if (id >= 0) {
|
|
folderItems.add(id);
|
|
}
|
|
} else if (TAG_SHORTCUT.equals(tag) && folderId >= 0) {
|
|
final long id = addUriShortcut(db, childValues, res, parser);
|
|
if (id >= 0) {
|
|
folderItems.add(id);
|
|
}
|
|
} else {
|
|
throw new RuntimeException("Folders can contain only shortcuts");
|
|
}
|
|
}
|
|
|
|
// We can only have folders with >= 2 items, so we need to remove the
|
|
// folder and clean up if less than 2 items were included, or some
|
|
// failed to add, and less than 2 were actually added
|
|
if (folderItems.size() < 2 && folderId >= 0) {
|
|
// Delete the folder
|
|
deleteId(db, folderId);
|
|
|
|
// If we have a single item, promote it to where the folder
|
|
// would have been.
|
|
if (folderItems.size() == 1) {
|
|
final ContentValues childValues = new ContentValues();
|
|
copyInteger(values, childValues, LauncherSettings.Favorites.CONTAINER);
|
|
copyInteger(values, childValues, LauncherSettings.Favorites.SCREEN);
|
|
copyInteger(values, childValues, LauncherSettings.Favorites.CELLX);
|
|
copyInteger(values, childValues, LauncherSettings.Favorites.CELLY);
|
|
|
|
final long id = folderItems.get(0);
|
|
db.update(TABLE_FAVORITES, childValues,
|
|
LauncherSettings.Favorites._ID + "=" + id, null);
|
|
} else {
|
|
added = false;
|
|
}
|
|
}
|
|
return added;
|
|
}
|
|
|
|
// A meta shortcut attempts to resolve an intent specified as a URI in the XML, if a
|
|
// logical choice for what shortcut should be used for that intent exists, then it is
|
|
// added. Otherwise add nothing.
|
|
private long addAppShortcutByUri(SQLiteDatabase db, ContentValues values,
|
|
String intentUri) {
|
|
Intent metaIntent;
|
|
try {
|
|
metaIntent = Intent.parseUri(intentUri, 0);
|
|
} catch (URISyntaxException e) {
|
|
Log.e(TAG, "Unable to add meta-favorite: " + intentUri, e);
|
|
return -1;
|
|
}
|
|
|
|
ResolveInfo resolved = mPackageManager.resolveActivity(metaIntent,
|
|
PackageManager.MATCH_DEFAULT_ONLY);
|
|
final List<ResolveInfo> appList = mPackageManager.queryIntentActivities(
|
|
metaIntent, PackageManager.MATCH_DEFAULT_ONLY);
|
|
|
|
// Verify that the result is an app and not just the resolver dialog asking which
|
|
// app to use.
|
|
if (wouldLaunchResolverActivity(resolved, appList)) {
|
|
// If only one of the results is a system app then choose that as the default.
|
|
final ResolveInfo systemApp = getSingleSystemActivity(appList);
|
|
if (systemApp == null) {
|
|
// There is no logical choice for this meta-favorite, so rather than making
|
|
// a bad choice just add nothing.
|
|
Log.w(TAG, "No preference or single system activity found for "
|
|
+ metaIntent.toString());
|
|
return -1;
|
|
}
|
|
resolved = systemApp;
|
|
}
|
|
final ActivityInfo info = resolved.activityInfo;
|
|
final Intent intent = buildMainIntent();
|
|
intent.setComponent(new ComponentName(info.packageName, info.name));
|
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
|
|
Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
|
|
|
|
return addAppShortcut(db, values, info.loadLabel(mPackageManager).toString(), intent);
|
|
}
|
|
|
|
private ResolveInfo getSingleSystemActivity(List<ResolveInfo> appList) {
|
|
ResolveInfo systemResolve = null;
|
|
final int N = appList.size();
|
|
for (int i = 0; i < N; ++i) {
|
|
try {
|
|
ApplicationInfo info = mPackageManager.getApplicationInfo(
|
|
appList.get(i).activityInfo.packageName, 0);
|
|
if ((info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
|
|
if (systemResolve != null) {
|
|
return null;
|
|
} else {
|
|
systemResolve = appList.get(i);
|
|
}
|
|
}
|
|
} catch (PackageManager.NameNotFoundException e) {
|
|
Log.w(TAG, "Unable to get info about resolve results", e);
|
|
return null;
|
|
}
|
|
}
|
|
return systemResolve;
|
|
}
|
|
|
|
private boolean wouldLaunchResolverActivity(ResolveInfo resolved,
|
|
List<ResolveInfo> appList) {
|
|
// If the list contains the above resolved activity, then it can't be
|
|
// ResolverActivity itself.
|
|
for (int i = 0; i < appList.size(); ++i) {
|
|
ResolveInfo tmp = appList.get(i);
|
|
if (tmp.activityInfo.name.equals(resolved.activityInfo.name)
|
|
&& tmp.activityInfo.packageName.equals(resolved.activityInfo.packageName)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private long addAppShortcut(SQLiteDatabase db, ContentValues values,
|
|
XmlResourceParser parser) {
|
|
final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME);
|
|
final String className = getAttributeValue(parser, ATTR_CLASS_NAME);
|
|
final String uri = getAttributeValue(parser, ATTR_URI);
|
|
|
|
if (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(className)) {
|
|
ActivityInfo info;
|
|
try {
|
|
ComponentName cn;
|
|
try {
|
|
cn = new ComponentName(packageName, className);
|
|
info = mPackageManager.getActivityInfo(cn, 0);
|
|
} catch (PackageManager.NameNotFoundException nnfe) {
|
|
String[] packages = mPackageManager.currentToCanonicalPackageNames(
|
|
new String[] { packageName });
|
|
cn = new ComponentName(packages[0], className);
|
|
info = mPackageManager.getActivityInfo(cn, 0);
|
|
}
|
|
final Intent intent = buildMainIntent();
|
|
intent.setComponent(cn);
|
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
|
|
Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
|
|
|
|
return addAppShortcut(db, values, info.loadLabel(mPackageManager).toString(),
|
|
intent);
|
|
} catch (PackageManager.NameNotFoundException e) {
|
|
Log.w(TAG, "Unable to add favorite: " + packageName +
|
|
"/" + className, e);
|
|
}
|
|
return -1;
|
|
} else if (!TextUtils.isEmpty(uri)) {
|
|
// If no component specified try to find a shortcut to add from the URI.
|
|
return addAppShortcutByUri(db, values, uri);
|
|
} else {
|
|
Log.e(TAG, "Skipping invalid <favorite> with no component or uri");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
private long addAppShortcut(SQLiteDatabase db, ContentValues values, String title,
|
|
Intent intent) {
|
|
long id = generateNewItemId();
|
|
values.put(Favorites.INTENT, intent.toUri(0));
|
|
values.put(Favorites.TITLE, title);
|
|
values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPLICATION);
|
|
values.put(Favorites.SPANX, 1);
|
|
values.put(Favorites.SPANY, 1);
|
|
values.put(Favorites._ID, id);
|
|
if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) < 0) {
|
|
return -1;
|
|
} else {
|
|
return id;
|
|
}
|
|
}
|
|
|
|
private long addFolder(SQLiteDatabase db, ContentValues values) {
|
|
values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_FOLDER);
|
|
values.put(Favorites.SPANX, 1);
|
|
values.put(Favorites.SPANY, 1);
|
|
long id = generateNewItemId();
|
|
values.put(Favorites._ID, id);
|
|
if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) <= 0) {
|
|
return -1;
|
|
} else {
|
|
return id;
|
|
}
|
|
}
|
|
|
|
private ComponentName getSearchWidgetProvider() {
|
|
SearchManager searchManager =
|
|
(SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
|
|
ComponentName searchComponent = searchManager.getGlobalSearchActivity();
|
|
if (searchComponent == null) return null;
|
|
return getProviderInPackage(searchComponent.getPackageName());
|
|
}
|
|
|
|
/**
|
|
* Gets an appwidget provider from the given package. If the package contains more than
|
|
* one appwidget provider, an arbitrary one is returned.
|
|
*/
|
|
private ComponentName getProviderInPackage(String packageName) {
|
|
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
|
|
List<AppWidgetProviderInfo> providers = appWidgetManager.getInstalledProviders();
|
|
if (providers == null) return null;
|
|
final int providerCount = providers.size();
|
|
for (int i = 0; i < providerCount; i++) {
|
|
ComponentName provider = providers.get(i).provider;
|
|
if (provider != null && provider.getPackageName().equals(packageName)) {
|
|
return provider;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private boolean addSearchWidget(SQLiteDatabase db, ContentValues values) {
|
|
ComponentName cn = getSearchWidgetProvider();
|
|
return addAppWidget(db, values, cn, 4, 1, null);
|
|
}
|
|
|
|
private boolean addClockWidget(SQLiteDatabase db, ContentValues values) {
|
|
ComponentName cn = new ComponentName("com.android.alarmclock",
|
|
"com.android.alarmclock.AnalogAppWidgetProvider");
|
|
return addAppWidget(db, values, cn, 2, 2, null);
|
|
}
|
|
|
|
private boolean addAppWidget(XmlResourceParser parser, AttributeSet attrs, int type,
|
|
SQLiteDatabase db, ContentValues values, TypedArray a)
|
|
throws XmlPullParserException, IOException {
|
|
|
|
String packageName = a.getString(R.styleable.Favorite_packageName);
|
|
String className = a.getString(R.styleable.Favorite_className);
|
|
|
|
if (packageName == null || className == null) {
|
|
return false;
|
|
}
|
|
|
|
boolean hasPackage = true;
|
|
ComponentName cn = new ComponentName(packageName, className);
|
|
try {
|
|
mPackageManager.getReceiverInfo(cn, 0);
|
|
} catch (Exception e) {
|
|
String[] packages = mPackageManager.currentToCanonicalPackageNames(
|
|
new String[] { packageName });
|
|
cn = new ComponentName(packages[0], className);
|
|
try {
|
|
mPackageManager.getReceiverInfo(cn, 0);
|
|
} catch (Exception e1) {
|
|
hasPackage = false;
|
|
}
|
|
}
|
|
|
|
if (hasPackage) {
|
|
int spanX = a.getInt(R.styleable.Favorite_spanX, 0);
|
|
int spanY = a.getInt(R.styleable.Favorite_spanY, 0);
|
|
|
|
// Read the extras
|
|
Bundle extras = new Bundle();
|
|
int widgetDepth = parser.getDepth();
|
|
while ((type = parser.next()) != XmlPullParser.END_TAG ||
|
|
parser.getDepth() > widgetDepth) {
|
|
if (type != XmlPullParser.START_TAG) {
|
|
continue;
|
|
}
|
|
|
|
TypedArray ar = mContext.obtainStyledAttributes(attrs, R.styleable.Extra);
|
|
if (TAG_EXTRA.equals(parser.getName())) {
|
|
String key = ar.getString(R.styleable.Extra_key);
|
|
String value = ar.getString(R.styleable.Extra_value);
|
|
if (key != null && value != null) {
|
|
extras.putString(key, value);
|
|
} else {
|
|
throw new RuntimeException("Widget extras must have a key and value");
|
|
}
|
|
} else {
|
|
throw new RuntimeException("Widgets can contain only extras");
|
|
}
|
|
ar.recycle();
|
|
}
|
|
|
|
return addAppWidget(db, values, cn, spanX, spanY, extras);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
private boolean addAppWidget(SQLiteDatabase db, ContentValues values, ComponentName cn,
|
|
int spanX, int spanY, Bundle extras) {
|
|
boolean allocatedAppWidgets = false;
|
|
final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
|
|
|
|
try {
|
|
int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
|
|
|
|
values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET);
|
|
values.put(Favorites.SPANX, spanX);
|
|
values.put(Favorites.SPANY, spanY);
|
|
values.put(Favorites.APPWIDGET_ID, appWidgetId);
|
|
values.put(Favorites.APPWIDGET_PROVIDER, cn.flattenToString());
|
|
values.put(Favorites._ID, generateNewItemId());
|
|
dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values);
|
|
|
|
allocatedAppWidgets = true;
|
|
|
|
// TODO: need to check return value
|
|
appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, cn);
|
|
|
|
// Send a broadcast to configure the widget
|
|
if (extras != null && !extras.isEmpty()) {
|
|
Intent intent = new Intent(ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE);
|
|
intent.setComponent(cn);
|
|
intent.putExtras(extras);
|
|
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
|
|
mContext.sendBroadcast(intent);
|
|
}
|
|
} catch (RuntimeException ex) {
|
|
Log.e(TAG, "Problem allocating appWidgetId", ex);
|
|
}
|
|
|
|
return allocatedAppWidgets;
|
|
}
|
|
|
|
private long addUriShortcut(SQLiteDatabase db, ContentValues values, Resources res,
|
|
XmlResourceParser parser) {
|
|
final int iconResId = getAttributeResourceValue(parser, ATTR_ICON, 0);
|
|
final int titleResId = getAttributeResourceValue(parser, ATTR_TITLE, 0);
|
|
|
|
Intent intent;
|
|
String uri = null;
|
|
try {
|
|
uri = getAttributeValue(parser, ATTR_URI);
|
|
intent = Intent.parseUri(uri, 0);
|
|
} catch (URISyntaxException e) {
|
|
Log.w(TAG, "Shortcut has malformed uri: " + uri);
|
|
return -1; // Oh well
|
|
}
|
|
|
|
if (iconResId == 0 || titleResId == 0) {
|
|
Log.w(TAG, "Shortcut is missing title or icon resource ID");
|
|
return -1;
|
|
}
|
|
|
|
long id = generateNewItemId();
|
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
values.put(Favorites.INTENT, intent.toUri(0));
|
|
values.put(Favorites.TITLE, res.getString(titleResId));
|
|
values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_SHORTCUT);
|
|
values.put(Favorites.SPANX, 1);
|
|
values.put(Favorites.SPANY, 1);
|
|
values.put(Favorites.ICON_TYPE, Favorites.ICON_TYPE_RESOURCE);
|
|
values.put(Favorites.ICON_PACKAGE, res.getResourcePackageName(iconResId));
|
|
values.put(Favorites.ICON_RESOURCE, res.getResourceName(iconResId));
|
|
values.put(Favorites._ID, id);
|
|
|
|
if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) < 0) {
|
|
return -1;
|
|
}
|
|
return id;
|
|
}
|
|
|
|
public void migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri) {
|
|
final ContentResolver resolver = mContext.getContentResolver();
|
|
Cursor c = null;
|
|
int count = 0;
|
|
int curScreen = 0;
|
|
|
|
try {
|
|
c = resolver.query(uri, null, null, null, "title ASC");
|
|
} catch (Exception e) {
|
|
// Ignore
|
|
}
|
|
|
|
// We already have a favorites database in the old provider
|
|
if (c != null) {
|
|
try {
|
|
if (c.getCount() > 0) {
|
|
final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
|
|
final int intentIndex
|
|
= c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
|
|
final int titleIndex
|
|
= c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
|
|
final int iconTypeIndex
|
|
= c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE);
|
|
final int iconIndex
|
|
= c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
|
|
final int iconPackageIndex
|
|
= c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
|
|
final int iconResourceIndex
|
|
= c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
|
|
final int containerIndex
|
|
= c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
|
|
final int itemTypeIndex
|
|
= c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
|
|
final int screenIndex
|
|
= c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
|
|
final int cellXIndex
|
|
= c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
|
|
final int cellYIndex
|
|
= c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
|
|
final int uriIndex
|
|
= c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI);
|
|
final int displayModeIndex
|
|
= c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE);
|
|
|
|
int i = 0;
|
|
int curX = 0;
|
|
int curY = 0;
|
|
|
|
final LauncherAppState app = LauncherAppState.getInstance();
|
|
final DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
|
|
final int width = (int) grid.numColumns;
|
|
final int height = (int) grid.numRows;
|
|
final int hotseatWidth = (int) grid.numHotseatIcons;
|
|
PackageManager pm = mContext.getPackageManager();
|
|
|
|
final HashSet<String> seenIntents = new HashSet<String>(c.getCount());
|
|
|
|
final ArrayList<ContentValues> shortcuts = new ArrayList<ContentValues>();
|
|
final ArrayList<ContentValues> folders = new ArrayList<ContentValues>();
|
|
final SparseArray<ContentValues> hotseat = new SparseArray<ContentValues>();
|
|
|
|
while (c.moveToNext()) {
|
|
final int itemType = c.getInt(itemTypeIndex);
|
|
if (itemType != Favorites.ITEM_TYPE_APPLICATION
|
|
&& itemType != Favorites.ITEM_TYPE_SHORTCUT
|
|
&& itemType != Favorites.ITEM_TYPE_FOLDER) {
|
|
continue;
|
|
}
|
|
|
|
final int cellX = c.getInt(cellXIndex);
|
|
final int cellY = c.getInt(cellYIndex);
|
|
final int screen = c.getInt(screenIndex);
|
|
int container = c.getInt(containerIndex);
|
|
final String intentStr = c.getString(intentIndex);
|
|
Launcher.addDumpLog(TAG, "migrating \""
|
|
+ c.getString(titleIndex) + "\" ("
|
|
+ cellX + "," + cellY + "@"
|
|
+ LauncherSettings.Favorites.containerToString(container)
|
|
+ "/" + screen
|
|
+ "): " + intentStr, true);
|
|
|
|
if (itemType != Favorites.ITEM_TYPE_FOLDER) {
|
|
|
|
final Intent intent;
|
|
final ComponentName cn;
|
|
try {
|
|
intent = Intent.parseUri(intentStr, 0);
|
|
} catch (URISyntaxException e) {
|
|
// bogus intent?
|
|
Launcher.addDumpLog(TAG,
|
|
"skipping invalid intent uri", true);
|
|
continue;
|
|
}
|
|
|
|
cn = intent.getComponent();
|
|
if (TextUtils.isEmpty(intentStr)) {
|
|
// no intent? no icon
|
|
Launcher.addDumpLog(TAG, "skipping empty intent", true);
|
|
continue;
|
|
} else if (cn != null &&
|
|
!LauncherModel.isValidPackageComponent(pm, cn)) {
|
|
// component no longer exists.
|
|
Launcher.addDumpLog(TAG, "skipping item whose component " +
|
|
"no longer exists.", true);
|
|
continue;
|
|
} else if (container ==
|
|
LauncherSettings.Favorites.CONTAINER_DESKTOP) {
|
|
// Dedupe icons directly on the workspace
|
|
|
|
// Canonicalize
|
|
// the Play Store sets the package parameter, but Launcher
|
|
// does not, so we clear that out to keep them the same
|
|
intent.setPackage(null);
|
|
final String key = intent.toUri(0);
|
|
if (seenIntents.contains(key)) {
|
|
Launcher.addDumpLog(TAG, "skipping duplicate", true);
|
|
continue;
|
|
} else {
|
|
seenIntents.add(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
ContentValues values = new ContentValues(c.getColumnCount());
|
|
values.put(LauncherSettings.Favorites._ID, c.getInt(idIndex));
|
|
values.put(LauncherSettings.Favorites.INTENT, intentStr);
|
|
values.put(LauncherSettings.Favorites.TITLE, c.getString(titleIndex));
|
|
values.put(LauncherSettings.Favorites.ICON_TYPE,
|
|
c.getInt(iconTypeIndex));
|
|
values.put(LauncherSettings.Favorites.ICON, c.getBlob(iconIndex));
|
|
values.put(LauncherSettings.Favorites.ICON_PACKAGE,
|
|
c.getString(iconPackageIndex));
|
|
values.put(LauncherSettings.Favorites.ICON_RESOURCE,
|
|
c.getString(iconResourceIndex));
|
|
values.put(LauncherSettings.Favorites.ITEM_TYPE, itemType);
|
|
values.put(LauncherSettings.Favorites.APPWIDGET_ID, -1);
|
|
values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex));
|
|
values.put(LauncherSettings.Favorites.DISPLAY_MODE,
|
|
c.getInt(displayModeIndex));
|
|
|
|
if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
|
|
hotseat.put(screen, values);
|
|
}
|
|
|
|
if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP) {
|
|
// In a folder or in the hotseat, preserve position
|
|
values.put(LauncherSettings.Favorites.SCREEN, screen);
|
|
values.put(LauncherSettings.Favorites.CELLX, cellX);
|
|
values.put(LauncherSettings.Favorites.CELLY, cellY);
|
|
} else {
|
|
// For items contained directly on one of the workspace screen,
|
|
// we'll determine their location (screen, x, y) in a second pass.
|
|
}
|
|
|
|
values.put(LauncherSettings.Favorites.CONTAINER, container);
|
|
|
|
if (itemType != Favorites.ITEM_TYPE_FOLDER) {
|
|
shortcuts.add(values);
|
|
} else {
|
|
folders.add(values);
|
|
}
|
|
}
|
|
|
|
// Now that we have all the hotseat icons, let's go through them left-right
|
|
// and assign valid locations for them in the new hotseat
|
|
final int N = hotseat.size();
|
|
for (int idx=0; idx<N; idx++) {
|
|
int hotseatX = hotseat.keyAt(idx);
|
|
ContentValues values = hotseat.valueAt(idx);
|
|
|
|
if (hotseatX == grid.hotseatAllAppsRank) {
|
|
// let's drop this in the next available hole in the hotseat
|
|
while (++hotseatX < hotseatWidth) {
|
|
if (hotseat.get(hotseatX) == null) {
|
|
// found a spot! move it here
|
|
values.put(LauncherSettings.Favorites.SCREEN,
|
|
hotseatX);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (hotseatX >= hotseatWidth) {
|
|
// no room for you in the hotseat? it's off to the desktop with you
|
|
values.put(LauncherSettings.Favorites.CONTAINER,
|
|
Favorites.CONTAINER_DESKTOP);
|
|
}
|
|
}
|
|
|
|
final ArrayList<ContentValues> allItems = new ArrayList<ContentValues>();
|
|
// Folders first
|
|
allItems.addAll(folders);
|
|
// Then shortcuts
|
|
allItems.addAll(shortcuts);
|
|
|
|
// Layout all the folders
|
|
for (ContentValues values: allItems) {
|
|
if (values.getAsInteger(LauncherSettings.Favorites.CONTAINER) !=
|
|
LauncherSettings.Favorites.CONTAINER_DESKTOP) {
|
|
// Hotseat items and folder items have already had their
|
|
// location information set. Nothing to be done here.
|
|
continue;
|
|
}
|
|
values.put(LauncherSettings.Favorites.SCREEN, curScreen);
|
|
values.put(LauncherSettings.Favorites.CELLX, curX);
|
|
values.put(LauncherSettings.Favorites.CELLY, curY);
|
|
curX = (curX + 1) % width;
|
|
if (curX == 0) {
|
|
curY = (curY + 1);
|
|
}
|
|
// Leave the last row of icons blank on every screen
|
|
if (curY == height - 1) {
|
|
curScreen = (int) generateNewScreenId();
|
|
curY = 0;
|
|
}
|
|
}
|
|
|
|
if (allItems.size() > 0) {
|
|
db.beginTransaction();
|
|
try {
|
|
for (ContentValues row: allItems) {
|
|
if (row == null) continue;
|
|
if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, row)
|
|
< 0) {
|
|
return;
|
|
} else {
|
|
count++;
|
|
}
|
|
}
|
|
db.setTransactionSuccessful();
|
|
} finally {
|
|
db.endTransaction();
|
|
}
|
|
}
|
|
|
|
db.beginTransaction();
|
|
try {
|
|
for (i=0; i<=curScreen; i++) {
|
|
final ContentValues values = new ContentValues();
|
|
values.put(LauncherSettings.WorkspaceScreens._ID, i);
|
|
values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
|
|
if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values)
|
|
< 0) {
|
|
return;
|
|
}
|
|
}
|
|
db.setTransactionSuccessful();
|
|
} finally {
|
|
db.endTransaction();
|
|
}
|
|
}
|
|
} finally {
|
|
c.close();
|
|
}
|
|
}
|
|
|
|
Launcher.addDumpLog(TAG, "migrated " + count + " icons from Launcher2 into "
|
|
+ (curScreen+1) + " screens", true);
|
|
|
|
// ensure that new screens are created to hold these icons
|
|
setFlagJustLoadedOldDb();
|
|
|
|
// Update max IDs; very important since we just grabbed IDs from another database
|
|
mMaxItemId = initializeMaxItemId(db);
|
|
mMaxScreenId = initializeMaxScreenId(db);
|
|
if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId + " mMaxScreenId: " + mMaxScreenId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build a query string that will match any row where the column matches
|
|
* anything in the values list.
|
|
*/
|
|
static String buildOrWhereString(String column, int[] values) {
|
|
StringBuilder selectWhere = new StringBuilder();
|
|
for (int i = values.length - 1; i >= 0; i--) {
|
|
selectWhere.append(column).append("=").append(values[i]);
|
|
if (i > 0) {
|
|
selectWhere.append(" OR ");
|
|
}
|
|
}
|
|
return selectWhere.toString();
|
|
}
|
|
|
|
/**
|
|
* Return attribute value, attempting launcher-specific namespace first
|
|
* before falling back to anonymous attribute.
|
|
*/
|
|
static String getAttributeValue(XmlResourceParser parser, String attribute) {
|
|
String value = parser.getAttributeValue(
|
|
"http://schemas.android.com/apk/res-auto/com.android.launcher3", attribute);
|
|
if (value == null) {
|
|
value = parser.getAttributeValue(null, attribute);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Return attribute resource value, attempting launcher-specific namespace
|
|
* first before falling back to anonymous attribute.
|
|
*/
|
|
static int getAttributeResourceValue(XmlResourceParser parser, String attribute,
|
|
int defaultValue) {
|
|
int value = parser.getAttributeResourceValue(
|
|
"http://schemas.android.com/apk/res-auto/com.android.launcher3", attribute,
|
|
defaultValue);
|
|
if (value == defaultValue) {
|
|
value = parser.getAttributeResourceValue(null, attribute, defaultValue);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
private static void copyInteger(ContentValues from, ContentValues to, String key) {
|
|
to.put(key, from.getAsInteger(key));
|
|
}
|
|
|
|
static class SqlArguments {
|
|
public final String table;
|
|
public final String where;
|
|
public final String[] args;
|
|
|
|
SqlArguments(Uri url, String where, String[] args) {
|
|
if (url.getPathSegments().size() == 1) {
|
|
this.table = url.getPathSegments().get(0);
|
|
this.where = where;
|
|
this.args = args;
|
|
} else if (url.getPathSegments().size() != 2) {
|
|
throw new IllegalArgumentException("Invalid URI: " + url);
|
|
} else if (!TextUtils.isEmpty(where)) {
|
|
throw new UnsupportedOperationException("WHERE clause not supported: " + url);
|
|
} else {
|
|
this.table = url.getPathSegments().get(0);
|
|
this.where = "_id=" + ContentUris.parseId(url);
|
|
this.args = null;
|
|
}
|
|
}
|
|
|
|
SqlArguments(Uri url) {
|
|
if (url.getPathSegments().size() == 1) {
|
|
table = url.getPathSegments().get(0);
|
|
where = null;
|
|
args = null;
|
|
} else {
|
|
throw new IllegalArgumentException("Invalid URI: " + url);
|
|
}
|
|
}
|
|
}
|
|
}
|