Launcher 记录自定义桌面

Launcher 记录自定义桌面

自定义桌面数据的创建、更新和删除。

前置文章

  1. Launcher的启动过程
  2. Launcher界面结构
  3. Launcher拖拽框架

前言

我们在使用手机的过程中,都会把我们手机的桌面布置成我们最喜欢,最习惯,最便捷的桌面。对手机而言,我们每一次对桌面的布置都会使数据发生变化,而数据在内存中是临时的,手机一旦关机,内存的数据就全部丢失了,因此,我们需要把自定义桌面的数据保存起来。在 Launcher,选择数据库来保存自定义桌面的数据。本文以 Android 的 Launcher3 为例,简述我们自定义桌面的时候,数据的产生、变化与删除。在赘述自定义桌面前,笔者对手上的手机的桌面做了一些自定义的设置布置。如下图:

device-2017-06-01-112006_00.PNG

上图为第一屏

device-2017-06-01-112106_00.png

上图为第二屏

device-2017-06-01-112124_00.png

上图为第三屏

如上面的三张图,都有一个底部常用的四个应用的图标,分别是拨号、短信、Chrome浏览器和照相机。另外,第一张图,放了图片管理应用 Gallery 的图标;第二张图,放了一个时钟的的窗口小部件(Widget)和 Calculator、Email 两个应用的图标;第三张图,则是放了一个音乐的窗口小部件。

数据保存方式

Launcher 采用 ContentProvider + database 的方式对自定义桌面的数据进行管理。读者可以阅读文章《Android System Server大纲之ContentService和ContentProvider原理剖析 》对 ContentProvider 更深入的了解。

Provider 信息

在 AndroidManifest.xml 中定义如下:

<provider
    android:name="com.android.launcher3.LauncherProvider"
    android:authorities="com.android.launcher3.settings"
    android:exported="true"
    android:writePermission="com.android.launcher3.permission.WRITE_SETTINGS"
    android:readPermission="com.android.launcher3.permission.READ_SETTINGS" />

(代码片段1)Provider 定义在文件 packages/apps/Launcher3/AndroidManifest.xml 中。

数据库信息

我们知道,在 Android 操作数据库时,都需要用到 SQLiteOpenHelper 来创建连接数据库,其中需要传入数据库文件名字。如下:

public class LauncherProvider extends ContentProvider {
    protected static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
        DatabaseHelper(Context context) {
            super(context, LauncherFiles.LAUNCHER_DB, null, DATABASE_VERSION);
            .....
        }
    }
}

(代码片段2)定义在文件 packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java 中。

上述第二个参数 LauncherFiles.LAUNCHER_DB 便是数据库文件名。定义如下:

public static final String LAUNCHER_DB = "launcher.db";

(代码片段3)变量定义在文件 packages/apps/Launcher3/src/com/android/launcher3/LauncherFiles.java 中。

因此,自定义桌面的数据则保存在手机的 /data/data/com.android.launcher3/databases/launcher.db 中。

数据库的初始化

我们第一次打开新的手机,或者恢复出厂设置后首次打开手机,Launcher 都有一个默认的桌面,这些默认的设置预设在 xml 文件中,当我们第一次启动 Launcher(读者可以阅读文章《Launcher的启动过程 》了解 Launcher 的启动)的时候,会创建数据库,创建数据库表,把默认的设置写入到数据库中。

当然,在 Android 的 SQLite 数据库框架中,会自动创建 launcher.db 数据库文件,我们直接创建需要的数据库表即可。代码如下:

protected static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
    private final Context mContext;
    static final String TABLE_FAVORITES = LauncherSettings.Favorites.TABLE_NAME;
    static final String TABLE_WORKSPACE_SCREENS = LauncherSettings.WorkspaceScreens.TABLE_NAME;
    .....

    DatabaseHelper(Context context) {
        if (!tableExists(TABLE_FAVORITES) || !tableExists(TABLE_WORKSPACE_SCREENS)) {}
    }

(代码片段4)定义在文件 packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java 中。

如上面的代码,实例化对象 DatabaseHelper 的时候,通过调用方法 tableExists() 判断数据库表 TABLE_FAVORITES=favorites、 TABLE_WORKSPACE_SCREENS=workspaceScreens 是否存在。如果不存在,则调用 addFavoritesTable() 和 addWorkspacesTable() 分别创建数据表 favorites 和 workspaceScreens。addFavoritesTable() 的实现如下:

private void addFavoritesTable(SQLiteDatabase db, boolean optional) {
    String ifNotExists = optional ? " IF NOT EXISTS " : "";
    db.execSQL("CREATE TABLE " + ifNotExists + 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 " + getDefaultUserSerial() + "," +
            "rank INTEGER NOT NULL DEFAULT 0," +
            "options INTEGER NOT NULL DEFAULT 0" +
            ");");
}

(代码片段5)这个方法定义在文件 packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java 中。

如上创建数据表 favorites 的 SQL 语句,favorites 数据库表保存了应用图标的位置等信息,在后文中我们在赘述每个字段的作用。addWorkspacesTable() 的实现如下:

private void addWorkspacesTable(SQLiteDatabase db, boolean optional) {
    String ifNotExists = optional ? " IF NOT EXISTS " : "";
    db.execSQL("CREATE TABLE " + ifNotExists + TABLE_WORKSPACE_SCREENS + " (" +
            LauncherSettings.WorkspaceScreens._ID + " INTEGER PRIMARY KEY," +
            LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," +
            LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" +
            ");");
}

(代码片段6)这个方法定义在文件 packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java 中。

相对数据表 favorites,workspaceScreens 的结构显得简单不少,只有几个字段,workspaceScreens 保存的是 workspace 的个数等信息。对 workspace 不熟悉的读者,可以先阅读文章《 Launcher界面结构 》。

在 packages/apps/Launcher3/src/com/android/launcher3/LauncherModel.java 的 loadWorkspace() 方法中会加载默认的预设置项,本文就不再展述这儿过程。

数据库表字段定义

favorites 表

database_table_favorites.jpg

workspacescreens 表

Screenshot from 2017-06-02 09:57:56.png

产生自定义桌面数据

当在 workspace 新增一个应用图标或者小部件的时候,或者在文件夹中新增一个应用图标,就会产生新的自定义桌面数据。下文我们将会论述:从菜单中拖拽一个应用图标到 workspace,添加一个窗口小部件到 workspace,在文件夹中新增一个应用图标,三种方式的过程。

Workspace中新增应用图标

在文章《Launcher拖拽框架》中,我们知道,拖拽一个应用图标到 workspace 时,会调用 onDrop() 方法结束本次的拖拽,那么就需要在 onDrop() 方法中去完成拖拽后的一系列事务,包括我们自定义桌面数据的产生和保存。

public void onDrop(final DragObject d) {
    ......
    if (d.dragSource != this) {
        final int[] touchXY = new int[] { (int) mDragViewVisualCenter[0],
                (int) mDragViewVisualCenter[1] };
        onDropExternal(touchXY, d.dragInfo, dropTargetLayout, false, d);
    }

(代码片段7)这个方法定义在文件 packages/apps/Launcher3/src/com/android/launcher3/Workspace.java 中。

由于是从菜单里面拖拽出来的应用图标,因此会走 onDropExternal() 分支。

private void onDropExternal(final int[] touchXY, final Object dragInfo,
    final CellLayout cellLayout, boolean insertAtFirst, DragObject d) {
    .....
    // info: ItemInfo
    // container: -100
    // screenId: workspace 的id,workspace 的标识
    // mTargetCell[0]:应用图标的 X 轴方向矩阵位置
    // mTargetCell[1]:应用坐标的 Y 轴方向矩阵位置
    LauncherModel.addOrMoveItemInDatabase(mLauncher, info, container, screenId,
                    mTargetCell[0], mTargetCell[1]);

    addInScreen(view, container, screenId, mTargetCell[0], mTargetCell[1], info.spanX,
            info.spanY, insertAtFirst);
    cellLayout.onDropChild(view);
    .....
}

(代码片段8)这个方法定义在文件 packages/apps/Launcher3/src/com/android/launcher3/Workspace.java 中。

在上面的方法中,调用 addOrMoveItemInDatabase(Context context, ItemInfo item, long container, long screenId, int cellX, int cellY) 方法保存数据到数据库,container 确定是否是放在某个容器中的应用图标,如底部的 hotseat。-100 表示是 workspace 本身。screenId 确定应用的图标放置到那个 workspace,cellX 和 cellY 确定应用图标在 workspace 矩阵中的位置,workspace 矩阵一般是 33、44、55 等,分别表示 X 轴和 Y 轴方向应用图标的数量。如下图 33 的矩阵:

workspace矩阵.jpg

继续往下分析 addOrMoveItemInDatabase() 方法:

static void addOrMoveItemInDatabase(Context context, ItemInfo item, long container,
        long screenId, int cellX, int cellY) {
    if (item.container == ItemInfo.NO_ID) {
        // From all apps
        addItemToDatabase(context, item, container, screenId, cellX, cellY);
    } else {
        // From somewhere else
        moveItemInDatabase(context, item, container, screenId, cellX, cellY);
    }
}

(代码片段9)这个方法定义在文件 packages/apps/Launcher3/src/com/android/launcher3/LauncherModel.java 中。

我们是从菜单中拖拽应用,所以走 addItemToDatabase() 分支:

public static void addItemToDatabase(Context context, final ItemInfo item, final long container,
        final long screenId, final int cellX, final int cellY) {
    // 相关位置信息赋值到 ItemInfo 中
    item.container = container;
    item.cellX = cellX;
    item.cellY = cellY;
    .....
    final ContentValues values = new ContentValues();
    final ContentResolver cr = context.getContentResolver();
    // 设置键值对 values
    item.onAddToDatabase(context, values);
    // 最大 id + 1
    item.id = LauncherAppState.getLauncherProvider().generateNewItemId();
    // 设置 id 键值对到 values
    values.put(LauncherSettings.Favorites._ID, item.id);
    .....
    Runnable r = new Runnable() {
        public void run() {
            // 把自定义桌面数据插入数据库
            cr.insert(LauncherSettings.Favorites.CONTENT_URI, values);
            .....
        }
    };
    runOnWorkerThread(r);
}

(代码片段10)这个方法定义在文件 packages/apps/Launcher3/src/com/android/launcher3/LauncherModel.java 中。

在上面的方法中,调用 onAddToDatabase() 方法设置需要保存到数据库的键值对,分别有:

void onAddToDatabase(Context context, ContentValues values) {
    super.onAddToDatabase(context, values);
    // 应用图标名字
    String titleStr = title != null ? title.toString() : null;
    values.put(LauncherSettings.BaseLauncherColumns.TITLE, titleStr);
    // Intent,即点击应用图标的意图,需要转换成 String 类型的 Uri 保存
    String uri = promisedIntent != null ? promisedIntent.toUri(0)
            : (intent != null ? intent.toUri(0) : null);
    values.put(LauncherSettings.BaseLauncherColumns.INTENT, uri);
    values.put(LauncherSettings.Favorites.RESTORED, status);
    // 图标类型,应用图标可更换
    if (customIcon) {
        values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE,
                LauncherSettings.BaseLauncherColumns.ICON_TYPE_BITMAP);
        writeBitmap(values, mIcon);
    } else {
        if (!usingFallbackIcon) {
            writeBitmap(values, mIcon);
        }
        if (iconResource != null) {
            values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE,
                    LauncherSettings.BaseLauncherColumns.ICON_TYPE_RESOURCE);
            values.put(LauncherSettings.BaseLauncherColumns.ICON_PACKAGE,
                    iconResource.packageName);
            values.put(LauncherSettings.BaseLauncherColumns.ICON_RESOURCE,
                    iconResource.resourceName);
        }
    }
}

(代码片段11)这个方法定义在文件 packages/apps/Launcher3/src/com/android/launcher3/ShortcutInfo.java 中。

void onAddToDatabase(Context context, ContentValues values) {
    // item 类型,shortcut, widget 等
    values.put(LauncherSettings.BaseLauncherColumns.ITEM_TYPE, itemType);
    // container, screenId, cellX, cellY,参考上文
    values.put(LauncherSettings.Favorites.CONTAINER, container);
    values.put(LauncherSettings.Favorites.SCREEN, screenId);
    values.put(LauncherSettings.Favorites.CELLX, cellX);
    values.put(LauncherSettings.Favorites.CELLY, cellY);
    // 跨度,X Y 轴方向占用几个 workspace 矩阵方格
    values.put(LauncherSettings.Favorites.SPANX, spanX);
    values.put(LauncherSettings.Favorites.SPANY, spanY);
    values.put(LauncherSettings.Favorites.RANK, rank);
}

(代码片段12)这个方法定义在文件 packages/apps/Launcher3/src/com/android/launcher3/ItemInfo.java 中。

回到 addItemToDatabase() 方法(代码片段10),设置完成键值对后,LauncherAppState.getLauncherProvider().generateNewItemId() 获取一个 id,这个 id 比数据库中最大的 id 大1,在 Launcher 中,每次添加一个新的 item 到 workspace,id 都是往上加 1。随后调用 ContentProvider 的 insert() 方法插入到数据库。

时序图

all_app_drag_to_workspace.jpg

Workspace 中新增小部件(Widget)

由于过程和“Workspace中新增应用图标”差不多,就不赘述和贴代码了。

时序图

widget_to_workspace.jpg

创建文件夹

两个图标拖拽时放到一起,创建一个文件夹图标,过程和上面的差不多,就不赘述和贴代码了。

时序图

folder_to_workspace.jpg

"")

更新自定义桌面数据

由于过程简单,本文不再论述和贴代码。

时序图

update.jpg

删除自定义桌面数据

由于过程简单,本文不再论述和贴代码。

时序图

delete.jpg

总结

本文讲述了 Launcher 在 workspace 中添加,移动,删除应用图标和窗口小部件时,所对应的图标或者小部件位置等信息的变化和保存。每次 Launcher 启动后从数据中加载这些数据,对用户自定义的桌面进行恢复。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容