Android 更換系統主題app

項目需求:編寫主題app,一鍵切換桌面app圖標和系統壁紙。
Android版本:8.1


需求是寫一個系統主題app,實現類似於華爲手機內置系統主題app的功能,原生android是沒有主題app的,網上都是app換膚框架,是給自己單獨的app換主題,百般無奈只能自己動手寫了。還好我們是在源碼基礎上開發,可以任性的自定義功能。


步驟一

先找到Launcher加載各種app的地方

Launcher的初始化過程:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
		...
		LauncherAppState app = LauncherAppState.getInstance(this);
		mModel = app.setLauncher(this);
		...
        if (!mModel.startLoader(currentScreen)) {
            mDragLayer.setAlpha(0);
        } else {
            mWorkspace.setCurrentPage(currentScreen);
            setWorkspaceLoading(true);
        }
        ...
	}

進入LauncherModel,發現LauncherModel 居然是BroadcastReceiver

public class LauncherModel extends BroadcastReceiver

去看它的startLoader

public boolean startLoader(int synchronousBindPage) {
        // Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
        InstallShortcutReceiver.enableInstallQueue(InstallShortcutReceiver.FLAG_LOADER_RUNNING);
        synchronized (mLock) {
            if (mCallbacks != null && mCallbacks.get() != null) {
                final Callbacks oldCallbacks = mCallbacks.get();
                // Clear any pending bind-runnables from the synchronized load process.
                mUiExecutor.execute(new Runnable() {
                            public void run() {
                                oldCallbacks.clearPendingBinds();
                            }
                        });
                stopLoader();
                LoaderResults loaderResults = new LoaderResults(mApp, sBgDataModel,
                        mBgAllAppsList, synchronousBindPage, mCallbacks);
                if (mModelLoaded && !mIsLoaderTaskRunning) {
                    loaderResults.bindWorkspace();
                    loaderResults.bindAllApps();
                    loaderResults.bindDeepShortcuts();
                    loaderResults.bindWidgets();
                    return true;
                } else {
                    startLoaderForResults(loaderResults);
                }
            }
        }
        return false;
    }

第一次初始化mModelLoaded肯定是false無疑,所以進入startLoaderForResults(loaderResults)

    public void startLoaderForResults(LoaderResults results) {
        synchronized (mLock) {
            stopLoader();
            mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, sBgDataModel, results,packName,themeName);
            runOnWorkerThread(mLoaderTask);
        }
    }

LoaderTask是一個Runnable,

    private static void runOnWorkerThread(Runnable r) {
        if (sWorkerThread.getThreadId() == Process.myTid()) {
            r.run();
        } else {
            // If we are not on the worker thread, then post to the worker handler
            sWorker.post(r);
        }
    }

所以應該去看LoaderTask的run方法

public void run() {
        synchronized (this) {
            // Skip fast if we are already stopped.
            if (mStopped) {
                return;
            }
        }

        try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
            if (DEBUG_LOADERS) Log.d(TAG, "step 1.1: loading workspace");
            loadWorkspace();
            if (DEBUG_LOADERS) Log.d(TAG, "step 1.2: bind workspace workspace");
            mResults.bindWorkspace();
            
            if (DEBUG_LOADERS) Log.d(TAG, "step 2.1: loading all apps");
            loadAllApps();
            mResults.bindAllApps();
            
            if (DEBUG_LOADERS) Log.d(TAG, "step 2.3: Update icon cache");
            updateIconCache();
            
            // third step
            if (DEBUG_LOADERS) Log.d(TAG, "step 3.1: loading deep shortcuts");
            loadDeepShortcuts();
            if (DEBUG_LOADERS) Log.d(TAG, "step 3.2: bind deep shortcuts");
            mResults.bindDeepShortcuts();
            
            // fourth step
            if (DEBUG_LOADERS) Log.d(TAG, "step 4.1: loading widgets");
            mBgDataModel.widgetsModel.update(mApp, null);
            if (DEBUG_LOADERS) Log.d(TAG, "step 4.2: Binding widgets");
            mResults.bindWidgets();

            transaction.commit();
        } catch (CancellationException e) {
        }
    }

這個方法很大,但是邏輯是順序的,分別先加載並綁定了 workspace ,然後再加載所有app的view,由於我們想找到的是加載app圖標的地方,所以肯定是 loadAllApps 了。

private void loadAllApps() {
        final long loadTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;
        final List<UserHandle> profiles = mUserManager.getUserProfiles();
        mBgAllAppsList.clear();

        for (UserHandle user : profiles) {
			...//省略
            // Create the ApplicationInfos
            for (int i = 0; i < apps.size(); i++) {
                LauncherActivityInfo app = apps.get(i);
                // This builds the icon bitmaps.
                Log.i(TAG, "loadAllApps--> app=" + app.getName());
                mBgAllAppsList.add(new AppInfo(app, user, quietMode), app);
            }
			...//省略
            ManagedProfileHeuristic.onAllAppsLoaded(mApp.getContext(), apps, user);
        }
		...//省略
    }

其中,for循環用LauncherActivityInfo封裝給AppInfo,再添加進list裏,

    public void add(AppInfo info, LauncherActivityInfo activityInfo) {
        if (!mAppFilter.shouldShowApp(info.componentName)) {
            return;
        }
        if (findAppInfo(info.componentName, info.user) != null) {
            return;
        }
        mIconCache.getTitleAndIcon(info, activityInfo, true /* useLowResIcon */);

        data.add(info);
        added.add(info);
    }

進入了AllAppsList,使用的是IconCache來管理appinfo的,

    public synchronized void getTitleAndIcon(ItemInfoWithIcon info,
            LauncherActivityInfo activityInfo, boolean useLowResIcon) {
        // If we already have activity info, no need to use package icon
        getTitleAndIcon(info, Provider.of(activityInfo), false, useLowResIcon);
    }
	---
    private synchronized void getTitleAndIcon(
            @NonNull ItemInfoWithIcon infoInOut,
            @NonNull Provider<LauncherActivityInfo> activityInfoProvider,
            boolean usePkgIcon, boolean useLowResIcon) {
        CacheEntry entry = cacheLocked(infoInOut.getTargetComponent(), activityInfoProvider,
                infoInOut.user, usePkgIcon, useLowResIcon);
        applyCacheEntry(entry, infoInOut);
    }

繼續封裝AppInfo

protected CacheEntry cacheLocked(
            @NonNull ComponentName componentName,
            @NonNull Provider<LauncherActivityInfo> infoProvider,
            UserHandle user, boolean usePackageIcon, boolean useLowResIcon) {
        Preconditions.assertWorkerThread();
        ComponentKey cacheKey = new ComponentKey(componentName, user);
        CacheEntry entry = mCache.get(cacheKey);
        if (entry == null || (entry.isLowResIcon && !useLowResIcon)) {
            entry = new CacheEntry();
            mCache.put(cacheKey, entry);

            // Check the DB first.
            LauncherActivityInfo info = null;
            boolean providerFetchedOnce = false;

            if (!getEntryFromDB(cacheKey, entry, useLowResIcon) || DEBUG_IGNORE_CACHE) {
                info = infoProvider.get();
                providerFetchedOnce = true;

                if (info != null) {
                    Log.i(TAG, "cacheLocked--> 1 create icon ="+info.getName());
                    entry.icon = LauncherIcons.createBadgedIconBitmap(
                            getFullResIcon(info), info.getUser(), mContext,
                            infoProvider.get().getApplicationInfo().targetSdkVersion);
                }
            }
			...//省略
        }
        return entry;
    }

在LauncherIcons的createBadgedIconBitmap裏處理appIcon,其中第一個參數來自於getFullResIcon(info)

    public Drawable getFullResIcon(LauncherActivityInfo info) {
        return getFullResIcon(info, true);
    }
    ---
    public Drawable getFullResIcon(LauncherActivityInfo info, boolean flattenDrawable) {
        return mIconProvider.getIcon(info, mIconDpi, flattenDrawable);
    }
    ---
    public Drawable getIcon(LauncherActivityInfo info, int iconDpi, boolean flattenDrawable) {
        return info.getIcon(iconDpi);
    }

獲取的Icon資源來自於 LauncherActivityInfo

public Drawable getIcon(int density) {
        // TODO: Go through LauncherAppsService
        final int iconRes = mActivityInfo.getIconResource();
        Drawable icon = null;
        // Get the preferred density icon from the app's resources
        if (density != 0 && iconRes != 0) {
            try {
                final Resources resources
                        = mPm.getResourcesForApplication(mActivityInfo.applicationInfo);
                icon = resources.getDrawableForDensity(iconRes, density);
            } catch (NameNotFoundException | Resources.NotFoundException exc) {
            }
        }
        // Get the default density icon
        if (icon == null) {
            icon = mActivityInfo.loadIcon(mPm);
        }
        return icon;
    }

關鍵點就是

    final Resources resources
            = mPm.getResourcesForApplication(mActivityInfo.applicationInfo);
    icon = resources.getDrawableForDensity(iconRes, density);

通過各個app的啓動Activity(就是每個app都有的主Activity),獲取到對應的ApplicationInfo,然後獲取到對應包的資源Resources,然後根據那個Resources找到對應Id的drawable.

所以我們找到了Launcher加載app列表,獲取app圖標的地方了,接下來就是怎麼攔截這個操作,使用我們自己主題app的圖標了。


步驟二

使Launcher加載我們自己寫的包裏的資源。

在上面的獲取Resources的時候,是通過對應的ApplicationInfo來獲取的,也就是每個應用程序都有自己對應的Resources對象來管理當前app的資源,我們怎麼去獲取到我們自己寫的包的Resources呢,如果要先去獲取Activityinfo,那就很繞彎了,很巧的是,還有個可用的重載方法。

getResourcesForApplication(ApplicationInfo app)
getResourcesForApplication(String appPackageName)

直接根據包名就可以獲取到資源Resources對象,當然這裏的報名直接寫我們自己的app包名。


所以接下來就是在getFullResIcon那裏做攔截,先到我們寫的app裏找資源,更改如下

    public Drawable getFullResIcon(LauncherActivityInfo info, boolean flattenDrawable) {
        if (!TextUtils.isEmpty(mPackname)) {
            Drawable drawable = getFullResIconLanco(info.getActivityInfo());
            if (drawable != null) {
                return drawable;
            }
        }
        return mIconProvider.getIcon(info, mIconDpi, flattenDrawable);
    }
public Drawable getFullResIconLanco(ActivityInfo info) {
        Drawable drawable = null;
        Resources resourcesX;
        Resources resourcesY;
        try {
            resourcesX= mPackageManager.getResourcesForApplication(
                    info.applicationInfo);
            resourcesY = mPackageManager.getResourcesForApplication(
                    mPackname);
        } catch (PackageManager.NameNotFoundException e) {
            resourcesX = null;
            resourcesY = null;
        }
        if (resourcesX != null && resourcesY != null) {
            int iconId = info.getIconResource();
            if (iconId != 0) {
                drawable = getFullResIcon(resourcesX, resourcesY, iconId);
                if (drawable != null) {
                    return drawable;
                }
            }
        }
        return drawable;
    }

當獲取對應Icon的時候,一併得到我們自己app的Resources

    private Drawable getFullResIcon(Resources resourcesReal,Resources resourcesFake, int iconId) {
        Drawable d;
        try {
            String name = resourcesReal.getResourceEntryName(iconId);
            String type = resourcesReal.getResourceTypeName(iconId);
            if (!mThemename.equals("")) {
                name = name + "_" + mThemename;
            }
            int fakeid = resourcesFake.getIdentifier(name,"drawable", mPackname);
            Log.i(TAG, "getFullResIcon--> name = " + name + "--type =" + type+"--iconId="+iconId+"--fakeid="+fakeid);
            d = resourcesFake.getDrawableForDensity(fakeid, mIconDpi);
        } catch (Resources.NotFoundException e) {
            d = null;
        }
        return d ;
    }

根據真實的Resources獲取到對應icon的名稱,然後根據名稱,在我們寫的app的Resources裏找到同名的id,再找到對應的Drawable。
其中,根據mThemename主題名稱的不同,我們加載不同的icon名稱,
比如:如果主題名字叫theme,正常的appIcon名稱 ic_launcher_main, 對應我們包裏的icon就命名成ic_launcher_main_theme, 不同主題加不同的後綴,用於區分不同的id,
關於mThemename和mPackname,主題名和包名的傳遞,是在我們自己app裏直接發送Intent過來的,然後把對應名稱設置到IconCache這個類裏來就行了,別忘了,前面特別提到LauncherModel是個BroadcastReceiver,發廣播的事情不提了。


步驟三

在我們的app裏定義不同的主題包

首先要內置第三方app,然後在makefile裏區別資源包,我的方法是給出三個目錄

LOCAL_RESOURCE_DIR := \
    $(LOCAL_PATH)/res \
    $(LOCAL_PATH)/res_fake\
    $(LOCAL_PATH)/res_xposed \

創建不同的主題資源,其餘的就是對應圖片命名,然後別忘了引用他們,不然編譯不能生成對應R資源。

<resources>
    <drawable name="ic_launcher_browser_fake">@drawable/ic_launcher_browser_fake</drawable>
    <drawable name="ic_launcher_phone_fake">@drawable/ic_launcher_phone_fake</drawable>
    <drawable name="ic_launcher_mms_fake">@drawable/ic_launcher_mms_fake</drawable>
</resources>

然後R文件裏就會有我們的ID了,

    public static final int ic_launcher_browser_fake=0x7f08006e;
    public static final int ic_launcher_phone_fake=0x7f080080;
    public static final int ic_launcher_mms_fake=0x7f08007d;

這樣,在Launcher找我們包裏的圖片的時候,找的就是這些我們加進來的圖片id.


Tips

Launcher 初始化之後,就不會再加載一次圖片,因爲那些app列表信息會存儲到數據庫裏,要想每次我們切換主題都有效,就必須強制清空數據,

mIconCache.clear();

這是必須的,以及

    if (clearDb) {
        Log.d(TAG, "loadWorkspace: resetting launcher database");
        LauncherSettings.Settings.call(contentResolver,
                LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
    }

這是清除數據庫,具體的都在源碼裏。只有這樣纔會更新,不然就從緩存裏和數據庫裏找資源了。


步驟四

更改對應壁紙

這裏只需要在切換主題的時候更換壁紙,

    try {
        if (mWallPaperid == 0) {
            clearWallpaper();
        }else {
            WallpaperManager.getInstance(this).
            	setBitmap(BitmapFactory.decodeResource(getResources(), mWallPaperid));
        }
    } catch (IOException e) {
        e.printStackTrace();
    }

效果圖:


總結:

以上過程思路都是我個人摸索的,我不知道像華爲和小米那些公司是怎麼在處理主題切換的,不過我能想到的是,他們也是改ResID來主題app裏找resource,只不過會封裝的很多,這也許只有等以後技術厲害了纔會知道吧~
關於這個app,我這裏只是簡單實現了一鍵切換桌面app圖標和壁紙,其他的比如系統其他圖標和動態壁紙這些還不清楚怎麼實現,還有關於網絡下載主題加載資源包的問題,這裏也沒有給出辦法,不過目前需求只是內置幾套主題可以,以後升級加載的事再說。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章