SystemUI之第三方应用创建Tile到Qs

前言

上一篇中最后我们讲到,Qs加载分成两部分,一是加载SystemUI自带Tile,二是加载第三方应用通过Service注册的Tile。这里回顾一下如何加载第三方Tile:

private void addPackageTiles(final QSTileHost host) {
        mBgHandler.post(() -> {
            Collection<QSTile> params = host.getTiles();
            PackageManager pm = mContext.getPackageManager();
            List<ResolveInfo> services = pm.queryIntentServicesAsUser(
                    new Intent(TileService.ACTION_QS_TILE), 0, ActivityManager.getCurrentUser());
            String stockTiles = mContext.getString(R.string.quick_settings_tiles_stock);

            for (ResolveInfo info : services) {
                String packageName = info.serviceInfo.packageName;
                ComponentName componentName = new ComponentName(packageName, info.serviceInfo.name);

                // Don't include apps that are a part of the default tile set.
                if (stockTiles.contains(componentName.flattenToString())) {
                    continue;
                }

                final CharSequence appLabel = info.serviceInfo.applicationInfo.loadLabel(pm);
                String spec = CustomTile.toSpec(componentName);
                State state = getState(params, spec);
                if (state != null) {
                    addTile(spec, appLabel, state, false);
                    continue;
                }
                if (info.serviceInfo.icon == 0 && info.serviceInfo.applicationInfo.icon == 0) {
                    continue;
                }
                Drawable icon = info.serviceInfo.loadIcon(pm);
                if (!permission.BIND_QUICK_SETTINGS_TILE.equals(info.serviceInfo.permission)) {
                    continue;
                }
                if (icon == null) {
                    continue;
                }
                icon.mutate();
                icon.setTint(mContext.getColor(android.R.color.white));
                CharSequence label = info.serviceInfo.loadLabel(pm);
                addTile(spec, icon, label != null ? label.toString() : "null", appLabel);
            }

            notifyTilesChanged(true);
        });
    }

SystemUI在query Tile的时候,通过query 安装应用中Service 中带有"android.service.quicksettings.action.QS_TILE"的,进行注册。今天就来讲讲第三方应用如何注册Tile到Qs,以及不得不说的TileService。

正文

如何注册实现

这里我们通过自定义的一个测试应用来讲述一下如何实现第三方应用如何注册。注册其实就分两步:

创建一个Service

在工程中创建一个Service,使其继承TileService,重写onTileAdded()、onTileRemoved()、onStartListening()、onStopListening()和onClick()方法。

public class CustomTileService extends TileService {
    @Override
    public void onTileAdded() {
        super.onTileAdded();
    }

    @Override
    public void onTileRemoved() {
        super.onTileRemoved();
    }

    @Override
    public void onStartListening() {
        super.onStartListening();
    }

    @Override
    public void onStopListening() {
        super.onStopListening();
    }

    @Override
    public void onClick() {
        super.onClick();
    }
}

在Manifest中注册

<service
            android:name=".services.CustomTileService"
            android:icon="@drawable/ic_launcher_foreground"
            android:label="@string/tile_label"
            android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
            <intent-filter>
                <action android:name="android.service.quicksettings.action.QS_TILE" />
            </intent-filter>
</service>

这里需要关注4个地方:
1.icon: 这里的icon就是Qs中显示的icon;
2.label: Qs中Tile的显示名称;
3.permission: 绑定Tile需要申请对应的权限;
4.action: systemUI通过query此action进行注册
此时设备的Qs编辑中已经有注册的Tile了:


customtile.png

如图可以看出,注册的Tile已经在编辑区域了,用户只需将该Tile拖入常用区域即可使用。但是这里会发现,Tile每次点击后都是常亮状态,那么如何实现像飞行模式一样,关闭时灰显,打开时亮显呢?前面第一步中有重写onClick()方法,这里进一步实现一下:

@Override
    public void onClick() {
        Tile tile = getQsTile();
        if (tile == null) {
            return;
        }
        switch (tile.getState()) {
            case Tile.STATE_ACTIVE:
                tile.setState(Tile.STATE_INACTIVE);
                tile.updateTile();
                break;
            case Tile.STATE_INACTIVE:
                tile.setState(Tile.STATE_ACTIVE);
                tile.updateTile();
                break;
            default:
                break;
        }
    }

通过以上修改,即可实现tile的开关:


customtile_on.png

customtile_off.png

TileService

上面总结了第三方应用如何注册Tile到Qs,这里不得不提到集成的父类Service——TileService,这个service如何启动的呢?SystemUI又是如何知道应用注册了该Service呢?下面将对此类进行一个深入了解。

TileService的启动

在SystemUI的com/android/systemui/qs/external/TileLifecycleManager.java中,注册了一个"android.intent.action.PACKAGE_CHANGED"的广播接收者:

public class TileLifecycleManager extends BroadcastReceiver implements
        IQSTileService, ServiceConnection, IBinder.DeathRecipient {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (DEBUG) Log.d(TAG, "onReceive: " + intent);
        if (!Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
            Uri data = intent.getData();
            String pkgName = data.getEncodedSchemeSpecificPart();
            if (!Objects.equals(pkgName, mIntent.getComponent().getPackageName())) {
                return;
            }
        }
        if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction()) && mChangeListener != null) {
            mChangeListener.onTileChanged(mIntent.getComponent());
        }
        stopPackageListening();
        if (mBound) {
            // Trying to bind again will check the state of the package before bothering to bind.
            if (DEBUG) Log.d(TAG, "Trying to rebind");
            setBindService(true);
        }
    }

当apk安装时,会触发setBindService(true):

public void setBindService(boolean bind) {
        if (mBound && mUnbindImmediate) {
            // If we are already bound and expecting to unbind, this means we should stay bound
            // because something else wants to hold the connection open.
            mUnbindImmediate = false;
            return;
        }
        mBound = bind;
        if (bind) {
            if (mBindTryCount == MAX_BIND_RETRIES) {
                // Too many failures, give up on this tile until an update.
                startPackageListening();
                return;
            }
            if (!checkComponentState()) {
                return;
            }
            if (DEBUG) Log.d(TAG, "Binding service " + mIntent + " " + mUser);
            mBindTryCount++;
            try {
                mIsBound = mContext.bindServiceAsUser(mIntent, this,
                        Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
                        mUser);
            } catch (SecurityException e) {
                Log.e(TAG, "Failed to bind to service", e);
                mIsBound = false;
            }
        } else {
            if (DEBUG) Log.d(TAG, "Unbinding service " + mIntent + " " + mUser);
            // Give it another chance next time it needs to be bound, out of kindness.
            mBindTryCount = 0;
            mWrapper = null;
            if (mIsBound) {
                mContext.unbindService(this);
                mIsBound = false;
            }
        }
    }

这里通过bindServiceAsUser来启动TileService,Service启动后回调onServiceConnected:

@Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        if (DEBUG) Log.d(TAG, "onServiceConnected " + name);
        // Got a connection, set the binding count to 0.
        mBindTryCount = 0;
        final QSTileServiceWrapper wrapper = new QSTileServiceWrapper(Stub.asInterface(service));
        try {
            service.linkToDeath(this, 0);
        } catch (RemoteException e) {
        }
        mWrapper = wrapper;
        handlePendingMessages();
    }

这样子IBind就被保存到QSTileServiceWrapper中,这里的Stub.asInterface(service)其实就是IQSTileService.Stub,这样子就可以实现跨进程,最终第三方应用的操作都在QSTileServiceWrapper的实例wrapper调用:

public class QSTileServiceWrapper {
    private static final String TAG = "IQSTileServiceWrapper";

    private final IQSTileService mService;

    public QSTileServiceWrapper(IQSTileService service) {
        mService = service;
    }

    public IBinder asBinder() {
        return mService.asBinder();
    }

    public boolean onTileAdded() {
        try {
            mService.onTileAdded();
            return true;
        } catch (Exception e) {
            Log.d(TAG, "Caught exception from TileService", e);
            return false;
        }
    }

    public boolean onTileRemoved() {
        try {
            mService.onTileRemoved();
            return true;
        } catch (Exception e) {
            Log.d(TAG, "Caught exception from TileService", e);
            return false;
        }
    }

    public boolean onStartListening() {
        try {
            mService.onStartListening();
            return true;
        } catch (Exception e) {
            Log.d(TAG, "Caught exception from TileService", e);
            return false;
        }
    }

    public boolean onStopListening() {
        try {
            mService.onStopListening();
            return true;
        } catch (Exception e) {
            Log.d(TAG, "Caught exception from TileService", e);
            return false;
        }
    }

    public boolean onClick(IBinder token) {
        try {
            mService.onClick(token);
            return true;
        } catch (Exception e) {
            Log.d(TAG, "Caught exception from TileService", e);
            return false;
        }
    }

    public boolean onUnlockComplete() {
        try {
            mService.onUnlockComplete();
            return true;
        } catch (Exception e) {
            Log.d(TAG, "Caught exception from TileService", e);
            return false;
        }
    }

    public IQSTileService getService() {
        return mService;
    }
}

总结

到这里,第三方如何注册Tile到Qs已经很明了了,其中最关键的就是TileService,希望通过这篇文章能够让大家对SystemUI Qs Tile有一个比较深入的了解。

本文已独家授权公众号ApeClub,转载请注明出处。

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

推荐阅读更多精彩内容