1. 程式人生 > >android M Launcher之資料庫實現

android M Launcher之資料庫實現

前面一系列文章我們分析了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資料庫功能。這些比較零散的知識就需要大家在實際開發中去分析了。