Android 10.0 StatusBar—下拉菜单快捷方式

学习笔记:


QSPanel 创建是从 StatusBar#makeStatusBarView 开始的。

protected void makeStatusBarView() {
    //省略其他代码
   FragmentHostManager fragmentHostManager = FragmentHostManager.get(container);
            ExtensionFragmentListener.attachExtensonToFragment(container, QS.TAG, R.id.qs_frame,
                    mExtensionController
                            .newExtension(QS.class)
                            .withPlugin(QS.class)
                            .withDefault(this::createDefaultQSFragment)
                            .build());
            mBrightnessMirrorController = new BrightnessMirrorController(
                    mNotificationShadeWindowView,
                    mNotificationPanelViewController,
                    mNotificationShadeDepthControllerLazy.get(),
                    (visible) -> {
                        mBrightnessMirrorVisible = visible;
                        updateScrimController();
                    });
            fragmentHostManager.addTagListener(QS.TAG, (tag, f) -> {
                QS qs = (QS) f;
                if (qs instanceof QSFragment) {
                    mQSPanel = ((QSFragment) qs).getQsPanel();
                    mQSPanel.setBrightnessMirror(mBrightnessMirrorController);
                    mFooter = ((QSFragment) qs).getFooter();
                }
            });
    //省略其他代码
}

先看 QSFragment 的构造函数:

    @Inject
    public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
            InjectionInflationController injectionInflater, QSTileHost qsTileHost,
            StatusBarStateController statusBarStateController, CommandQueue commandQueue,
            QSContainerImplController.Builder qsContainerImplControllerBuilder) {
        mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler;
        mInjectionInflater = injectionInflater;
        mQSContainerImplControllerBuilder = qsContainerImplControllerBuilder;
        commandQueue.observe(getLifecycle(), this);
        mHost = qsTileHost;
        mStatusBarStateController = statusBarStateController;
    }

这里注意 @Inject 注解,这个是 Android dagger里的一种解决。
在这里,与Android 9.0及其以下版本实例化 QSTileHost类的方式不一样,这里是通dagger来实例化的。

QSTileHost的构造函数
      mainHandler.post(() -> {
            // This is technically a hack to avoid circular dependency of
            // QSTileHost -> XXXTile -> QSTileHost. Posting ensures creation
            // finishes before creating any tiles.
            tunerService.addTunable(this, TILES_SETTING);
            // AutoTileManager can modify mTiles so make sure mTiles has already been initialized.
            mAutoTiles = autoTiles.get();
        });

在QSTileHost的构造函数里,我们主要看tunerService.addTunable(this, TILES_SETTING);很明显,调用tunerService里的 addTunabe() 方法,跟进去会发现,最终的是调用的 TunerServiceImpl 里面的addTunabe()

TunerServiceImpl #addTunable

我们只关注下面两句话:

        // 读取config.xml里的字符串(例如:nfc,wifi)
        String value = DejankUtils.whitelistIpcs(() -> Settings.Secure
                .getStringForUser(mContentResolver, key, mCurrentUser));
        tunable.onTuningChanged(key, value);

tunable.onTuningChanged 回调 QSTileHost#onTuningChanged。

QSTileHost#onTuningChanged
@Override
public void onTuningChanged(String key, String newValue) {
    if (!TILES_SETTING.equals(key)) {
        return;
    }
    if (DEBUG) Log.d(TAG, "Recreating tiles");
    if (newValue == null && UserManager.isDeviceInDemoMode(mContext)) {
        newValue = mContext.getResources().getString(R.string.quick_settings_tiles_retail_mode);
    }
    //调用 QSTileHost#loadTileSpecs,获得 config 里字符串信息
    final List<String> tileSpecs = loadTileSpecs(mContext, newValue);
    int currentUser = ActivityManager.getCurrentUser();
    if (tileSpecs.equals(mTileSpecs) && currentUser == mCurrentUser) return;
    //进行了过滤
    mTiles.entrySet().stream().filter(tile -> !tileSpecs.contains(tile.getKey())).forEach(
            tile -> {
                if (DEBUG) Log.d(TAG, "Destroying tile: " + tile.getKey());
                tile.getValue().destroy();
            });
    final LinkedHashMap<String, QSTile> newTiles = new LinkedHashMap<>();
    for (String tileSpec : tileSpecs) {
        QSTile tile = mTiles.get(tileSpec);
        if (tile != null && (!(tile instanceof CustomTile)
                || ((CustomTile) tile).getUser() == currentUser)) {
            if (tile.isAvailable()) {
                if (DEBUG) Log.d(TAG, "Adding " + tile);
                tile.removeCallbacks();
                if (!(tile instanceof CustomTile) && mCurrentUser != currentUser) {
                    tile.userSwitch(currentUser);
                }
                newTiles.put(tileSpec, tile);
            } else {
                tile.destroy();
            }
        } else {
            if (DEBUG) Log.d(TAG, "Creating tile: " + tileSpec);
            try {
                //这里通过 字符串 一个个实例化 Tile
                tile = createTile(tileSpec);
                if (tile != null) {
                    if (tile.isAvailable()) {
                        tile.setTileSpec(tileSpec);
                        newTiles.put(tileSpec, tile);
                    } else {
                        tile.destroy();
                    }
                }
            } catch (Throwable t) {
                Log.w(TAG, "Error creating tile for spec: " + tileSpec, t);
            }
        }
    }
    mCurrentUser = currentUser;
    mTileSpecs.clear();
    mTileSpecs.addAll(tileSpecs);
    mTiles.clear();
    mTiles.putAll(newTiles);
    for (int i = 0; i < mCallbacks.size(); i++) {
        //注册,当开发状态改变时回调
        mCallbacks.get(i).onTilesChanged();
    }
}

看下 QSTileHost#loadTileSpecs,是获得 config 里字符串信息。

QSTileHost#loadTileSpecs
protected List<String> loadTileSpecs(Context context, String tileList) {
    final Resources res = context.getResources();
    final String defaultTileList = res.getString(R.string.quick_settings_tiles_default);
    if (tileList == null) {
        tileList = res.getString(R.string.quick_settings_tiles);
        if (DEBUG) Log.d(TAG, "Loaded tile specs from config: " + tileList);
    } else {
        if (DEBUG) Log.d(TAG, "Loaded tile specs from setting: " + tileList);
    }
    final ArrayList<String> tiles = new ArrayList<String>();
    boolean addedDefault = false;
    for (String tile : tileList.split(",")) {
        tile = tile.trim();
        if (tile.isEmpty()) continue;
        if (tile.equals("default")) {
            if (!addedDefault) {
                tiles.addAll(Arrays.asList(defaultTileList.split(",")));
                addedDefault = true;
            }
        } else {
            tiles.add(tile);
        }
    }
    return tiles;
}

其中 quick_settings_tiles_default 值在 AOSP/frameworks/base/packages/SystemUI/res/values/config.xml 里:

<string name="quick_settings_tiles_default" translatable="false">
    wifi,bt,dnd,flashlight,rotation,battery,cell,airplane,cast
</string>

这里就是我们所看到的快捷开关的文本描述。

再看 QSTileHost#onTuningChanged 中的调用 QSTileHost#createTile 方法。

QSTileHost#createTile
public QSTile createTile(String tileSpec) {
    for (int i = 0; i < mQsFactories.size(); i++) {
        QSTile t = mQsFactories.get(i).createTile(tileSpec);
        if (t != null) {
            return t;
        }
    }
    return null;
}

调用 QSFactory#createTile,由 QSFactoryImpl#createTile 实现了。

QSFactoryImpl#createTile
public QSTile createTile(String tileSpec) {
    QSTileImpl tile = createTileInternal(tileSpec);
    if (tile != null) {
        tile.handleStale(); // Tile was just created, must be stale.
    }
    return tile;
}
private QSTileImpl createTileInternal(String tileSpec) {
    // Stock tiles.
    switch (tileSpec) {
        case "wifi":
            return new WifiTile(mHost);
        case "bt":
            return new BluetoothTile(mHost);
        case "cell":
            return new CellularTile(mHost);
        case "dnd":
            return new DndTile(mHost);
        case "inversion":
            return new ColorInversionTile(mHost);
        case "airplane":
            return new AirplaneModeTile(mHost);
        case "work":
            return new WorkModeTile(mHost);
        case "rotation":
            return new RotationLockTile(mHost);
        case "flashlight":
            return new FlashlightTile(mHost);
        case "location":
            return new LocationTile(mHost);
        case "cast":
            return new CastTile(mHost);
        case "hotspot":
            return new HotspotTile(mHost);
        case "user":
            return new UserTile(mHost);
        case "battery":
            return new BatterySaverTile(mHost);
        case "saver":
            return new DataSaverTile(mHost);
        case "night":
            return new NightDisplayTile(mHost);
        case "nfc":
            return new NfcTile(mHost);
    }
    // Intent tiles.
    if (tileSpec.startsWith(IntentTile.PREFIX)) return IntentTile.create(mHost, tileSpec);
    if (tileSpec.startsWith(CustomTile.PREFIX)) return CustomTile.create(mHost, tileSpec);
    // Debug tiles.
    if (Build.IS_DEBUGGABLE) {
        if (tileSpec.equals(GarbageMonitor.MemoryTile.TILE_SPEC)) {
            return new GarbageMonitor.MemoryTile(mHost);
        }
    }
    // Broken tiles.
    Log.w(TAG, "Bad tile spec: " + tileSpec);
    return null;
}

看到这里通过对应的字符串分别实例化 Tile。

以上涉及资源文件加载及对应实例化,接下来看看代码如何加载的,看 QSPanel#onAttachedToWindow 方法。

QSPanel#onAttachedToWindow
@Override
protected void onAttachedToWindow() {
    super.onAttachedToWindow();
    final TunerService tunerService = Dependency.get(TunerService.class);
    tunerService.addTunable(this, QS_SHOW_BRIGHTNESS);
    if (mHost != null) {
        setTiles(mHost.getTiles());
    }
    if (mBrightnessMirrorController != null) {
        mBrightnessMirrorController.addCallback(this);
    }
}
public void setTiles(Collection<QSTile> tiles) {
    setTiles(tiles, false);
}
public void setTiles(Collection<QSTile> tiles, boolean collapsedView) {
    if (!collapsedView) {
        mQsTileRevealController.updateRevealedTiles(tiles);
    }
    for (TileRecord record : mRecords) {
        mTileLayout.removeTile(record);
        record.tile.removeCallback(record.callback);
    }
    mRecords.clear();
    for (QSTile tile : tiles) {
        addTile(tile, collapsedView);
    }
}
protected TileRecord addTile(final QSTile tile, boolean collapsedView) {
    final TileRecord r = new TileRecord();
    r.tile = tile;
    r.tileView = createTileView(tile, collapsedView);
    //省略其他代码
    r.tileView.init(r.tile);
    r.tile.refreshState();
    mRecords.add(r);
    if (mTileLayout != null) {
        mTileLayout.addTile(r);
    }
    return r;
}

mTileLayout.addTile(r);由 PagedTileLayout#addTile 实现。

PagedTileLayout#addTile

PagedTileLayout 是 ViewPager,重点看 setAdapter,看数据源如何 add 的。

@Override
public void addTile(TileRecord tile) {
    mTiles.add(tile);
    postDistributeTiles();
}
private void postDistributeTiles() {
    removeCallbacks(mDistribute);
    post(mDistribute);
}
private final Runnable mDistribute = new Runnable() {
    @Override
    public void run() {
        distributeTiles();
    }
};
private void distributeTiles() {
    if (DEBUG) Log.d(TAG, "Distributing tiles");
    final int NP = mPages.size();
    for (int i = 0; i < NP; i++) {
        mPages.get(i).removeAllViews();
    }
    int index = 0;
    final int NT = mTiles.size();
    for (int i = 0; i < NT; i++) {
        TileRecord tile = mTiles.get(i);
        if (mPages.get(index).isFull()) {
            if (++index == mPages.size()) {
                if (DEBUG) Log.d(TAG, "Adding page for "
                        + tile.tile.getClass().getSimpleName());
                mPages.add((TilePage) LayoutInflater.from(getContext())
                        .inflate(R.layout.qs_paged_page, this, false));
            }
        }
        if (DEBUG) Log.d(TAG, "Adding " + tile.tile.getClass().getSimpleName() + " to "
                + index);
        mPages.get(index).addTile(tile);
    }
    if (mNumPages != index + 1) {
        mNumPages = index + 1;
        while (mPages.size() > mNumPages) {
            mPages.remove(mPages.size() - 1);
        }
        if (DEBUG) Log.d(TAG, "Size: " + mNumPages);
        mPageIndicator.setNumPages(mNumPages);
        setAdapter(mAdapter);
        mAdapter.notifyDataSetChanged();
        setCurrentItem(0, false);
    }
}

至此,SystemUI 下拉状态栏快捷开关模块代码流程分析完毕。

新增一个快捷开关

例如:wifi
1、找到对应的config.xml,添加对应的tile。
2、在 AOSP/frameworks/base/packages/SystemUI/src/com/android/systemui/qs/tiles/ 目录下创建 WifiTile.java,实现 QSTileImpl。

在快捷设置添加新项时,需要重写getMetricsCategory方法。

    @Override
    public int getMetricsCategory() {
        return MetricsEvent.QS_WIFI;
    }

这个LED_BRIGHTNESS_LEVEL的定义在frameworks\base\proto\src\metrics_constants.proto ,需要往后翻到预留的位置添加新的ID QS_WIFI= 126;这个ID需要避免重复。

其他与这个MetricsEvent相关的文件在:
frameworks\base\core\java\com\android\internal\logging\MetricsLogger.java
frameworks\base\packages\SettingsLib\src\com\android\settingslib\drawer\Tile.java
frameworks\base\packages\SettingsLib\src\com\android\settingslib\drawer\TileUtils.java

3、在 QSFactoryImpl.java 中的 createTileInternal() 方法中,增加对应的case:

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

推荐阅读更多精彩内容