前面一系列文章我们分析了LauncherModel的工作过程,它会把数据绑定到桌面上。从今天开始我们来分析下Launcher的数据来源即Launcher数据库的实现。
一个完整的数据库实现都应该包括两方面的内容,第一是数据库实体SQLiteOpenHelper的实现,第二是数据库ContentProvider的实现。数据库的实体包含了数据库实体以及相关的操作,ContentProvider负责数据库内容的访问接口实现。
1、Launcher数据库的实现
private static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
private final Context mContext;
@Thunk final AppWidgetHost mAppWidgetHost;
private long mMaxItemId = -1;
private long mMaxScreenId = -1;
private boolean mNewDbCreated = false;
@Thunk LauncherProviderChangeListener mListener;
DatabaseHelper(Context context) {
super(context, LauncherFiles.LAUNCHER_DB, 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 (mMaxItemId == -1) {
mMaxItemId = initializeMaxItemId(getWritableDatabase());
}
if (mMaxScreenId == -1) {
mMaxScreenId = initializeMaxScreenId(getWritableDatabase());
}
}
...
}
相信大家对SQLiteOpenHelper 的构造方法都比较了解我们主要看下Launcher相关的
if (mMaxItemId == -1) {
mMaxItemId = initializeMaxItemId(getWritableDatabase());
}
if (mMaxScreenId == -1) {
mMaxScreenId = initializeMaxScreenId(getWritableDatabase());
}
通常每一个数据库表都包含一个可以自增长的id字段,但Launcher比较特殊,id字段只作为数据库表的主键存在,因此,我们每一次在桌面上增加一个组件,都需要在当前最大的id号上加1,以做新的id号这就是mMaxItemId 。
在Launcher3之前。Launcher的桌面页数是固定的,随着Launcher3的到来,桌面的页数已经修改为可以动态增加了。如果当前桌面上已经没有额外的空间来加载新增的桌面组件,Launcher3将会根据当前最大的桌面页ID再增加一个桌面页。initializeMaxScreenId方法就是用来获取最大桌面页数的。
上面我们在构造函数中创建了Launcher的数据库,下面我们将在oncreate中创建表。代码如下:
@Override
public void onCreate(SQLiteDatabase db) {
if (LOGD) Log.d(TAG, "creating new launcher database");
mMaxItemId = 1;
mMaxScreenId = 0;
mNewDbCreated = true;
UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
long userSerialNumber = userManager.getSerialNumberForUser(
UserHandleCompat.myUserHandle());
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," +
"profileId INTEGER DEFAULT " + userSerialNumber + "," +
"rank INTEGER NOT NULL DEFAULT 0," +
"options INTEGER NOT NULL DEFAULT 0" +
");");
addWorkspacesTable(db);
// Database was just created, so wipe any previous widgets
if (mAppWidgetHost != null) {
mAppWidgetHost.deleteHost();
/**
* 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.
*/
new MainThreadExecutor().execute(new Runnable() {
@Override
public void run() {
if (mListener != null) {
mListener.onAppWidgetHostReset();
}
}
});
}
// Fresh and clean launcher DB.
mMaxItemId = initializeMaxItemId(db);
setFlagEmptyDbCreated();
// When a new DB is created, remove all previously stored managed profile information.
ManagedProfileHeuristic.processAllUsers(Collections.<UserHandleCompat>emptyList(), mContext);
}
在这里 首先创建了一张名为favorites的表,还是看上面的代码吧 ,就不在复制一份了,这个表主要记录桌面组件自身的信息以及在桌面上的属性信息。我们看下这张表的主要字段含义。
_id 这是每个桌面组件在favorites表中的唯一标示,也是favorites表的主键,这个特殊的地方就是它不是自增长类型,所有只能手动确保这个字段的唯一性。
title 表示应用程序快捷方式的标题,
intent 只有在桌面上摆放的是应用程序快捷方式的时候,该字段才会有值,别的情况下它是没有值的,因为应用程序的快捷方式涉及到应用程序的启动,而对于启动应用程序而言,Intent是至关重要的。
container 用于标示一个快捷方式处于什么样的容器中,目前Launcher提供了两种不同的容器,分别是热键区CONTAINER_HOTSEAT,取值为-101 和桌面容器CONTAINER_DESKTOP 取值为-100.
screen 用于标示快捷方式所在的屏幕ID。
cellX cellY 这两个整型字段用来表示桌面组件在桌面容器中的位置信息。举个栗子 如果Launcher将桌面区域分为5X6,那么cellX的取值范围就是0-4 cellY取值范围就是0-5.
spanX spanY 这两个字段用来表示桌面组件所占据的桌面范围信息,以cellX和cellY中的栗子说明 spanX取值范围是1-5 spanY的取值范围是1-6 如果X轴和Y轴取值大于或者小于这个范围,那么组件是无法加载到桌面上的。
itemType 用来表示快捷方式的类型。
- ITEM_TYPE_APPLICTION值为0 意味着这个快捷方式来自应用程序
- ITEM_TYPE_SHORTCUT 值为1 意味着这个快捷方式来自用用程序创建的快捷方式 比如联系人的快捷方式
- ITEM_TYPE_FOLDER 值为2 意味着这个组件是一个文件夹
- ITEM_TYPE_LIVE_FOLDER 值为3 意味着这个组件为一个实时文件夹
- ITEM_TYPE_APPWIDGET 值为4 意味着这个组件是一个桌面小部件
- ITEM_TYPE_WIDGET_CLOCK 值为1000 意味着这个组件是一个时钟桌面小部件
- ITEM_TYPE_WIDGET_SEARCH 值为1001 意味着这个组件是搜索桌面小部件
- ITEM_TYPE_WIDGET_PHOTO_FRAME 值为1002 意味着这个组件是相册桌面小部件
appWidgetId 该字段在favorites中被定义为不能为空并且默认值是-1的整型字段 桌面上除了可以加载不同的应用程序快捷方式以及文件夹等常见的桌面组件外,还可以加载应用程序提供的桌面小部件 这也是Android的特色之一,而桌面小部件主要依赖AppWidgetHost才能运行,每个应用程序都可以创建自己的AppWidgetHost来加载其他或者本应用提供的桌面小部件,每一个桌面小部件在其加载的AppWidgetHost中都被赋予了一个ID,来标示这个桌面小部件,appWidgetId 就是为了保存Launcher创建的AppWidgetHost中某一个桌面小部件的ID 如果桌面加载的并非小部件这个id将是-1 否则为大于或者等于0的值。
iconType 表示当此快捷方式需要图标的时候,可能需要保持的图标信息。
iconPackage iconResource : iconPackage 描述了图标来用的应用程序包名,iconResource 记录了该资源的ID号
icon 用于保持图片的实体
uri 当桌面的快捷方式为一个网页链接的时候,这个字段将会保持这个链接的地址否则为null
profileId 当前桌面组件所属的用户ID
创建好数据表后,Launcher需要在第一次启动或者数据库被清理的情况下创建页面配置信息表,这张表的目的是保存当前桌面中包含的桌面页的信息
private void addWorkspacesTable(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + TABLE_WORKSPACE_SCREENS + " (" +
LauncherSettings.WorkspaceScreens._ID + " INTEGER PRIMARY KEY," +
LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," +
LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" +
");");
}
以上就是Launcher数据库的创建过程。
2、接下来我们看下Launcher的ContentProvider
Launcher的数据库操作都封装在LauncherProvider中,我们在做应用程序开发的时候要对外提供数据,都是使用ContentProvider对数据进行一次包装,然后通过它对外提供数据,这是因为数据库文件往往被创建在应用程序的私有空间,通过ContentProvider可实现跨进程间的访问。Launcher也采用了这种方式。
温馨提示: 每一个ContentProvider的创建都需要比应用程序创建的更早,当在
应用程序清单文件中配置了ContentProvider节点的时候,当应用程序第一次启动
时,框架层就回先将此ContentProvider创建并发布出去。主要就是调用了
ContentProvider的OnCreate方法
Launcher也是遵循这个原则的
@Override
public boolean onCreate() {
final Context context = getContext();
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
mOpenHelper = new DatabaseHelper(context);
StrictMode.setThreadPolicy(oldPolicy);
LauncherAppState.setLauncherProvider(this);
return true;
}
这里应该都好理解。
通过ContentProvider进行数据库操作都需要通过适当的URI,并配以不同的条件,Launcher的数据库提供者提供了一个专门用于保存并确保这些信息合法的类SqlArguments。
它有三个成员变量
//需要查询的表名
public final String table;
//SQL的查询条件
public final String where;
//保存了where条件中所需的参数
public final String[] args;
知道了成员变量的含义这个函数就很好理解了 这里就不在分析了。
准备知识都完成后 我们继续看下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;
}
这里需要注意的是,在Launcher的Provider创建的时候创建了mOpenHelper实例,当需要对数据库进行操作的时候,需要从中获取数据库的实例,只有通过这个实例才能进行查询操作。
- 修改
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
//更新参数:表名 where条件以及条件参数的设置
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) notifyListeners();
reloadLauncherIfExternal();
return count;
}
- 增加
@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);
//插入成功rowId 是大于0的
if (rowId < 0) return null;
//如果插入成功则依据输入的URI为基础拼接上返回的id形成新的URI
uri = ContentUris.withAppendedId(uri, rowId);
notifyListeners();
if (Utilities.ATLEAST_MARSHMALLOW) {
reloadLauncherIfExternal();
} else {
// Deprecated behavior to support legacy devices which rely on provider callbacks.
LauncherAppState app = LauncherAppState.getInstanceNoCreate();
if (app != null && "true".equals(uri.getQueryParameter("isExternalAdd"))) {
app.reloadWorkspace();
}
String notify = uri.getQueryParameter("notify");
if (notify == null || "true".equals(notify)) {
getContext().getContentResolver().notifyChange(uri, null);
}
}
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) notifyListeners();
reloadLauncherIfExternal();
return count;
}
好了以上就是Launcher数据库的实现了,当然Launcher除了提供这些常用的访问方式外,还在内部提供了一些接口工具,以便Launcher的其他组件可以方便的使用Launcher数据库功能。这些比较零散的知识就需要大家在实际开发中去分析了。