在Launcher项目中,WorkSpace及HotSeat的所有图标如ShortcutInfo、Folder、Widget等都需要用到持久化技术以根据用户喜好排列这些图标,并能在下次打开时很方便的找到目标应用或功能。Android持久化技术分为文件存储、SharedPreferences、数据库存储,这几种数据存储方式也各有优劣,文件储存一般用于存储图片、网络请求数据等文本数据或二进制数据,SharedPreferences则是用于存放简单的键值数据,而数据库则适于存放复杂的关系型数据。而对于Launcher应用则使用的是SQLite数据库存储方式,其运算速度快,占用内存小,方便增删改查。
与Launcher数据库有关的类如下:
我们先由LauncherProvider内部的数据库的构建开始看起:
/**
* 数据库
*/
private static class DatabaseHelper extends SQLiteOpenHelper {
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_EXTRA = "extra";
private final Context mContext;
private final AppWidgetHost mAppWidgetHost;
private long mMaxId = -1;
DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
mContext = context;
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 (mMaxId == -1) {
mMaxId = initializeMaxId(getWritableDatabase());
}
}
...
}
其中mMaxId是用于标识一个自增长字段_id的当前的最大值,并且在每次添加新数据时都会调用DatabaseHelper的generateNewId方法使mMaxId+1,在DatabaseHelper中initializeMaxId即是在使用数据库Helper之前保证获取到当前数据库中的最大值,并作为成员变量存在使添加新数据时保证能正确自增。
当我们使用getWritableDatabase或getReadableDatabase获取数据库实例时,这两方法其实都会调用getDatabaseLocked方法,在此方法中如果实例不存在,则会调用mContext的openOrCreateDatabase创建DataBase,然后根据version==0(即是否第一次创建)来执行onCreate、onDowndrade、onUpdate方法。
db.beginTransaction();
try {
if (version == 0) {
onCreate(db);
} else {
if (version > mNewVersion) {
onDowngrade(db, version, mNewVersion);
} else {
onUpgrade(db, version, mNewVersion);
}
}
b.setVersion(mNewVersion);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
由此可知道数据库第一次执行时即version==0时会调用onCreate方法。而其他version则根据版本升降分别调用onDowngrade或onUpgrade方法。
所以我们进入Launcher数据库DatabaseHelper的onCreate中:
@Override
public void onCreate(SQLiteDatabase db) {
if (LOGD) Log.d(TAG, "creating new launcher database");
mMaxId = 1;
final UserManager um =
(UserManager) mContext.getSystemService(Context.USER_SERVICE);
// Default profileId to the serial number of this user.
long userSerialNumber = um.getSerialNumberForUser(
android.os.Process.myUserHandle());
//表名favorites
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," +
"profileId INTEGER DEFAULT " + userSerialNumber +
");");
// Database was just created, so wipe any previous widgets
if (mAppWidgetHost != null) {
mAppWidgetHost.deleteHost();
sendAppWidgetResetNotify();
}
if (!convertDatabase(db)) {
// Set a shared pref so that we know we need to load the default workspace later
setFlagToLoadDefaultWorkspaceLater();
}
}
可以看到第一次获取数据库时会创建表favorites,此表正是用于持久化存储WorkSpace和HotSeat中各应用排列信息。下面了解下下各字段含义:
_id:用于标识区分各个应用图标,是表favorites的主键,当添加数据时通过generateNewId使_id值增加。
title:在WorkSpace(HotSeat中一般会隐藏掉)中展示的应用快捷图标的标题。
intent:当点击桌面图标时的负责启动应用的intent,它通过Intent.toUri()转换为String存储,在使用时通过Intent.parseUri()转换为intent。
container:指的是当前数据所在的容器类型,在Launcher中有两种container类型:1.CONTAINER_DESKTOP(-100),2.CONTAINER_HOTSEAT(-101)。
screen:用于标识当前数据所在的页。当container为-100时,则screen的值表现为我们桌面的页数的值,当container为-101即当前快捷图标处于HotSeat时,则为默认值-1。
cellX:当前快捷图标所在页(CellLayout)的X位置,即快捷图标在当前页横向的第X个位置。
cellY:当前快捷图标所在页(CellLayout)的Y位置,即快捷图标在当前页纵向的第Y个位置。
spanX:当前快捷图标的在所在页(CellLayout)的横向范围信息,如果当前图标为application、shortcut、folder则为1,表示图标横向上占据一个cell的位置范围。如果当前图标为Widget,则横向占据范围可能为多个cell。
spanY:当前快捷图标的在所在页(CellLayout)的纵向范围信息,如果当前图标为application、shortcut、folder则为1,表示图标纵向上占据一个cell的位置范围。如果当前图标为Widget,则纵向占据范围可能为多个cell。
itemType:当前快捷图标的类型,分为以下几种。
1.ITEM_TYPE_APPLICATION:用于标识应用程序,为默认入口。当我们创建应用时需指定
2.ITEM_TYPE_SHORTCUT:应用程序针对的单个页面发送的创建Shortcut快捷方式的intent,即为此类型。
3.ITEM_TYPE_LIVE_FOLDER:Launcher中没有用到,谷歌已抛弃此type。
4.ITEM_TYPE_APPWIDGET:用于标识此快捷图标为Widget组件。
5.ITEM_TYPE_WIDGET_CLOCK:用于标识此快捷图标为时钟组件。
6.ITEM_TYPE_WIDGET_SEARCH:用于标识此快捷图标为搜索组件。
7.ITEM_TYPE_WIDGET_PHOTO_FRAME:用于标识此快捷图标为相册组件。
appWidgetId:在表中定义为默认值为-1的非空整型字段,如果appWidgetId不为-1,说明此快捷图标是桌面小组件Widget。用于标识唯一的Widget控件,在AppWidgetHost(在Launcher中表现为LauncherAppWidgetHost)内部作为键(key)区分SpareArray中的各个AppWidgetHostView(在Launcher中表现为LauncherAppWidgetHostView),且在widget系统管理服务AppWidgetServiceImpl中appWidgetId同packageName一道作为区分各个Widget的标识。
isShortcut:用于区分是否应用程序通过intent创建的快捷图标。如果值为0,则表示当前数据非应用程序创建的快捷图标,值1则反之。
iconType:用于标识当前快捷图标的图标类型是资源类型ICON_TYPE_RESOURCE(值为0)还是bitmap类型ICON_TYPE_BITMAP(值为1)。如果是资源类型,则需要通过PackageManager的getResourcesForApplication方法获取Resources,再通过Resources获取bitmap。
iconPackage:如果iconType标识为资源类型,则此字段才有用,用于标识资源所在包的包名。
iconResource:如果iconType标识为资源类型,则此字段才有用,用于标识资源图片的id。
icon:如果iconType为bitmap类型,此字段才有用,用于存放二进制图片数据。
uri:当ITEM_TYPE为LIVE_FOLDER时才有用,当前此字段已不再使用。
displayMode:当ITEM_TYPE为LIVE_FOLDER时才有用,当前此字段已不再使用。
在Launcher中数据库DatabaseHelper并没有被直接使用到,而是以内容提供者即LauncherProvider的方式供外界访问,完成代码隔离。
可以看见要访问此Launcher快捷图标信息,需要使用固定字符串的authorities值,且需要读写Settings的权限。
LauncherProvider作为数据库操作的包装类对外提供了经过包装的增删改查功能。在其构造方法中创建了DatabaseHelper实例,并且把当前Provider设置给LauncherApplication,这样就可以通过LauncherApplication.getLauncherProvider()获取内容提供者。以下是LauncherProvider的增删改查实现。
@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;
}
@Override
public Uri insert(Uri uri, ContentValues initialValues) {
SqlArguments args = new SqlArguments(uri);
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
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 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);
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
int count = db.update(args.table, values, args.where, args.args);
if (count > 0) sendNotify(uri);
return count;
}
当然,在LauncherSetttings内部也为使用者提供了几种Uri。外界可通过这些Uri来访问LauncherProvider所管理的数据库。
/**
* favorites表的uri,带通知内容观察者功能
*/
static final Uri CONTENT_URI = Uri.parse("content://" +
LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES +
"?" + LauncherProvider.PARAMETER_NOTIFY + "=true");
/**
* favorites表的uri,不带通知内容观察者的功能
*/
static final Uri CONTENT_URI_NO_NOTIFICATION = Uri.parse("content://" +
LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES +
"?" + LauncherProvider.PARAMETER_NOTIFY + "=false");
/**
* 为CRUD某行数据而准备的uri可通过此方法获取
* @param id 具体的行id,即表中的_id值
* @param notify 是否带通知内容观察者功能
* @return 返回某一行的uri
*/
static Uri getContentUri(long id, boolean notify) {
return Uri.parse("content://" + LauncherProvider.AUTHORITY +
"/" + LauncherProvider.TABLE_FAVORITES + "/" + id + "?" +
LauncherProvider.PARAMETER_NOTIFY + "=" + notify);
}
我们知道,Launcher应用是通过LauncherModel的loadAndBindWorkspace方法来加载WorkSpace的应用图标信息,并绑定到桌面上的。而其中loadWorkspace即是通过内容提供者LauncherProvider从数据库中获取数据。
private void loadWorkspace() {
final long t = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;
final Context context = mContext;
final ContentResolver contentResolver = context.getContentResolver();
final PackageManager manager = context.getPackageManager();
final AppWidgetManager widgets = AppWidgetManager.getInstance(context);
final boolean isSafeMode = manager.isSafeMode();
// Make sure the default workspace is loaded, if needed
mApp.getLauncherProvider().loadDefaultFavoritesIfNecessary(0, false);
synchronized (sBgLock) {
sBgWorkspaceItems.clear();//管理workspace或者hotseat
sBgAppWidgets.clear();//管理widget
sBgFolders.clear();//管理文件夹
sBgItemsIdMap.clear();//所有info管理
sBgDbIconCache.clear();
final ArrayList itemsToRemove = new ArrayList();
/*查询本地数据库信息*/
final Cursor c = contentResolver.query(
LauncherSettings.Favorites.CONTENT_URI, null, null, null, null);
...省略
}
从上可以看到,
1.调用了LauncherProvider的loadDefaultFavoritesIfNecessary方法。其内部通过一个存放在SharedPreferences中的布尔值来判断当前是否是Launcher的第一次加载,如果是,则解析R.xml.default_workspace文件(此文件中包含默认的快捷图标)到Workspace中。从以下可以看见,默认会加载几个widget和快捷图标到WorkSpace中,且指定了HotSeat默认图标为电话、联系人、短信、浏览器。如果我们自己开发Launcher,可以通过修改Launcher的R.xml.default_workspace文件来达到显示何种默认桌面的问题。
2.隐式的使用了LauncherProvder进行本地数据库快捷图标信息的查询功能,查询出数据库中的数据显示到Workspace中。此隐式使用在LauncherModel中很常用,使用方式大同小异。
当然,LauncherModel不止在进入Launcher调用startLoader时才会通过LauncherProvider使用到数据库,当用户通过拖拽调整或删除图标的onDrop中也会使用到。LauncherModel中提供了很多详尽的方法来满足数据库的CRUD的操作。此处仅列举LauncherModel中使用频率较高的几个方法供读者参考。
/**
*
* 加到数据库同时同步到集合中
* Add an item to the database in a specified container. Sets the container, screen, cellX and
* cellY fields of the item. Also assigns an ID to the item.
*/
static void addItemToDatabase(Context context, final ItemInfo item, final long container,
final int screen, final int cellX, final int cellY, final boolean notify) {
item.container = container;
item.cellX = cellX;
item.cellY = cellY;
// We store hotseat items in canonical form which is this orientation invariant position
// in the hotseat
if (context instanceof Launcher && screen < 0 &&
container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
//如果在HotSeat中那么screen的值就是cellX。
item.screen = ((Launcher) context).getHotseat().getOrderInHotseat(cellX, cellY);
} else {
item.screen = screen;
}
final ContentValues values = new ContentValues();
final ContentResolver cr = context.getContentResolver();
//在container,cellX,cellY,screen赋值后,把itemInfo的各成员添加到values中
item.onAddToDatabase(context, values);
LauncherApplication app = (LauncherApplication) context.getApplicationContext();
//其内部其实就是maxId+1
item.id = app.getLauncherProvider().generateNewId();
values.put(LauncherSettings.Favorites._ID, item.id);
//更新cellX,cellY。
item.updateValuesWithCoordinates(values, item.cellX, item.cellY);
final StackTraceElement[] stackTrace = new Throwable().getStackTrace();
Runnable r = new Runnable() {
public void run() {
String transaction = "DbDebug Add item (" + item.title + ") to db, id: "
+ item.id + " (" + container + ", " + screen + ", " + cellX + ", "
+ cellY + ")";
Launcher.sDumpLogs.add(transaction);
Log.d(TAG, transaction);
//具体执行数据库的CURD操作
cr.insert(notify ? LauncherSettings.Favorites.CONTENT_URI :
LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, values);
// Lock on mBgLock *after* the db operation
synchronized (sBgLock) {
checkItemInfoLocked(item.id, item, stackTrace);
sBgItemsIdMap.put(item.id, item);//sBgItemsIdMap存放所有的info appwidget,folder,shortcut...
switch (item.itemType) {
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
sBgFolders.put(item.id, (FolderInfo) item);//sBgFolders存放所有FolderInfo
// Fall through
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
sBgWorkspaceItems.add(item);//存放所有ShortCutInfo
} else {
if (!sBgFolders.containsKey(item.container)) {
// Adding an item to a folder that doesn't exist.
String msg = "adding item: " + item + " to a folder that " +
" doesn't exist";
Log.e(TAG, msg);
Launcher.dumpDebugLogsToConsole();
}
}
break;
case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
sBgAppWidgets.add((LauncherAppWidgetInfo) item);//存放所有AppWidgetInfo
break;
}
}
}
};
runOnWorkerThread(r);
}
/**
* Move an item in the DB to a new
*/
static void moveItemInDatabase(Context context, final ItemInfo item, final long container,
final int screen, final int cellX, final int cellY) {
String transaction = "DbDebug Modify item (" + item.title + ") in db, id: " + item.id +
" (" + item.container + ", " + item.screen + ", " + item.cellX + ", " + item.cellY +
") --> " + "(" + container + ", " + screen + ", " + cellX + ", " + cellY + ")";
Launcher.sDumpLogs.add(transaction);
Log.d(TAG, transaction);
item.container = container;
item.cellX = cellX;
item.cellY = cellY;
// We store hotseat items in canonical form which is this orientation invariant position
// in the hotseat
if (context instanceof Launcher && screen < 0 &&
container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
item.screen = ((Launcher) context).getHotseat().getOrderInHotseat(cellX, cellY);
} else {
item.screen = screen;
}
final ContentValues values = new ContentValues();
values.put(LauncherSettings.Favorites.CONTAINER, item.container);
values.put(LauncherSettings.Favorites.CELLX, item.cellX);
values.put(LauncherSettings.Favorites.CELLY, item.cellY);
values.put(LauncherSettings.Favorites.SCREEN, item.screen);
updateItemInDatabaseHelper(context, values, item, "moveItemInDatabase");
}
/**
* 把itemInfo更新到db中,并且重新判断itemType来选择当前itemInfo该存放的List
* @param context
* @param values
* @param item
* @param callingFunction
*/
static void updateItemInDatabaseHelper(Context context, final ContentValues values,
final ItemInfo item, final String callingFunction) {
final long itemId = item.id;
final Uri uri = LauncherSettings.Favorites.getContentUri(itemId, false);
final ContentResolver cr = context.getContentResolver();
final StackTraceElement[] stackTrace = new Throwable().getStackTrace();
Runnable r = new Runnable() {
public void run() {
cr.update(uri, values, null, null);
// Lock on mBgLock *after* the db operation
synchronized (sBgLock) {
checkItemInfoLocked(itemId, item, stackTrace);
if (item.container != LauncherSettings.Favorites.CONTAINER_DESKTOP &&
item.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
// Item is in a folder, make sure this folder exists
if (!sBgFolders.containsKey(item.container)) {
// An items container is being set to a that of an item which is not in
// the list of Folders.
String msg = "item: " + item + " container being set to: " +
item.container + ", not in the list of folders";
Log.e(TAG, msg);
Launcher.dumpDebugLogsToConsole();
}
}
// Items are added/removed from the corresponding FolderInfo elsewhere, such
// as in Workspace.onDrop. Here, we just add/remove them from the list of items
// that are on the desktop, as appropriate
ItemInfo modelItem = sBgItemsIdMap.get(itemId);
if (modelItem.container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
modelItem.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
switch (modelItem.itemType) {
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
if (!sBgWorkspaceItems.contains(modelItem)) {
sBgWorkspaceItems.add(modelItem);
}
break;
default:
break;
}
} else {
sBgWorkspaceItems.remove(modelItem);
}
}
}
};
runOnWorkerThread(r);
}
/**
* Removes the specified item from the database 和list
* @param context
* @param item
*/
static void deleteItemFromDatabase(Context context, final ItemInfo item) {
final ContentResolver cr = context.getContentResolver();
final Uri uriToDelete = LauncherSettings.Favorites.getContentUri(item.id, false);
Runnable r = new Runnable() {
public void run() {
String transaction = "DbDebug Delete item (" + item.title + ") from db, id: "
+ item.id + " (" + item.container + ", " + item.screen + ", " + item.cellX +
", " + item.cellY + ")";
Launcher.sDumpLogs.add(transaction);
Log.d(TAG, transaction);
cr.delete(uriToDelete, null, null);
// Lock on mBgLock *after* the db operation
synchronized (sBgLock) {
switch (item.itemType) {
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
sBgFolders.remove(item.id);
for (ItemInfo info: sBgItemsIdMap.values()) {
if (info.container == item.id) {
// We are deleting a folder which still contains items that
// think they are contained by that folder.
String msg = "deleting a folder (" + item + ") which still " +
"contains items (" + info + ")";
Log.e(TAG, msg);
Launcher.dumpDebugLogsToConsole();
}
}
sBgWorkspaceItems.remove(item);
break;
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
sBgWorkspaceItems.remove(item);
break;
case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
sBgAppWidgets.remove((LauncherAppWidgetInfo) item);
break;
}
sBgItemsIdMap.remove(item.id);
sBgDbIconCache.remove(item);
}
}
};
runOnWorkerThread(r);
}
本文主要介绍了Launcher快捷图标的排列信息的数据库存储方式及字段含义,以及LauncherModel如何获取的这些排列信息,希望能对读者有帮助。