Bootstrap

Android状态栏添加QS快捷开关

添加QS快捷开关

通过对xml文件视图的分析,我们确定快捷面板所在的类是QSFragment后,就可以跟踪代码去分析了从哪里加载了快捷开关了,然后按照它的代码逻辑,新增属于你自己的快捷开关。
快捷开发读取的是那个config.xml配置文件,可以打log看输出的tile顺序是和那个配置文件的对上的,一般是读取
frameworks/base/packages/systemui/res/values/config.xml,但是如果你的\vendor\xxxx\gms-overlay\frameworks\base\packages\SystemUI\res\values\目录下存在config.xml文件,则会形成覆盖,优先读取这一块的内容。当然你也可以定义一个自己的顺序,直接使用。
期间遇到的问题:
遇到的问题:
1、自己项目存在覆盖文件,导致自己写的tile一直没有加载出来,最后修改了配置文件后要全编,因为覆盖配置文件不属于SystemUI。(涉及项目保密问题,部分代码不可展示)在这里插入图片描述

2、自己创建的tile类要写好,一些代码错误会导致整个systemui崩溃或者不显示快捷开关
推荐从下面这个最简洁的tile类开始写起,先让其在手机上显示出来,然后再慢慢加事件上去

package com.ape.systemui.qs.tiles;

import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.qs.QSTile.BooleanState;
import com.android.systemui.statusbar.policy.KeyguardStateController;

import android.util.Log;
import android.os.UserManager;
import android.os.SystemClock;
import android.content.Intent;
import android.content.res.Resources;
import android.service.quicksettings.Tile;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import android.widget.Toast;

import com.android.systemui.R;

import javax.inject.Inject;

/** Quick settings tile: **/
public class ShutdownTile extends QSTileImpl<BooleanState> {

    //注解不要少加 Dagger2的知识
    @Inject
    public ShutdownTile(QSHost host) {
        super(host);
    }

    @Override
    public BooleanState newTileState() {
        return new BooleanState();
    }

    @Override
    protected void handleLongClick() {

    }

    @Override    //长按跳转的点击意图
    public Intent getLongClickIntent() {
        return null;
    }

    @Override
    public boolean isAvailable() { //控制快捷开关是否显示
        return mContext.getString(R.string.quick_settings_tiles_stock).contains("shutdown");
    }

    @Override
    protected void handleClick() {  //handleClick方法处理点击事件
        Toast.makeText(mContext,"正在关机",Toast.LENGTH_SHORT).show();
    }

    @Override
    public CharSequence getTileLabel() { //返回 快捷开关下面需要显示的字符串
         return mContext.getString(R.string.quick_settings_shutdown_label);
    }

    @Override   // handleUpdateState方法更新状态信息
    protected void handleUpdateState(BooleanState state, Object arg) {
        state.icon = ResourceIcon.get(R.drawable.ic_qs_dnd_on);//关机图标
        state.label = mContext.getString(R.string.quick_settings_shutdown_label);//获取前面定义的字符串
    }

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

    @Override
    public void handleSetListening(boolean listening) {

    }
}

MetricsEvent.QS_SHUTDOWN需要去frameworks\base\proto\src\metrics_constants\metric_constants.proto文件添加一个唯一值

 enum View {
...........
// OPEN: Quick Settings Shutdown
// CATEGORY: QUICK_SETTINGS
// OS: 6.0    jiaxian.zhang update 注释只是起说明作用,并无实际作用
    QS_SHUTDOWN = 1968;

}

3、QSFactoryImpl添加条件项目
方式1(如果显示不出来请用下面的方式2)
在这里插入图片描述
再添加2处
在这里插入图片描述
最后在switch (tileSpec) 中返回:
在这里插入图片描述
方式2:

//定义提供者,并用注解 
@Inject
Provider<ShutdownTile> mShutdownTileProvider;
  
  .....
  
//switch然后直接返回
case "shutdown":    
  return mShutdownTileProvider.get();

隐藏:
1、config.xml文件中删除字段
2、在case的条件判断中直接return null; 不return xxxProvider.get();

修改systemui背景圆角大小

在qs_panel.xml中

 <View
        android:id="@+id/quick_settings_background"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:elevation="4dp"
        android:background="@drawable/qs_background_primary" />

进入qs_background_primary.xml文件

<inset xmlns:android="http://schemas.android.com/apk/res/android">
    <shape>
    <!--  背景颜色:-->
        <solid android:color="@color/qs_background_dark"/>

        <!-- ?android:attr这个开头表示图角值的取值来源是和Theme有关·也就是在Resource资源中
        在主题Theme中取得。使用的时候该值会随着主题的不同而变化
        主题的路径是:/frantworks/base/core/res/res/values/目录下有多个theme.xml文件,不同的主题其
        item项,<item name="dialogCornerRadius">@dimen/config_dialogCornerRadius</item>所@dimen的值是不同的,最后发现
        当前主题引用的是config.xml全局配置文件的config_dialogCornerRadius值,该值写在/franeworks/base/core/res/res/values/config.xml
        我们选择修改它
        -->
        <corners android:radius="?android:attr/dialogCornerRadius" />
    </shape>
</inset>

/franeworks/base/core/res/res/values/config.xml

<!-- Corner radius of system dialogs-->
<dimen name="config_dialogCornerRadius">20dp</dimen>

由于修改的代码是frameworks/base/core目录下的资源文件,无法单编systemyui,我们选择单编frameworks-res.apk的方式:
进入项目目录 cd xxx工程目录/frameworks/base/core/res/ 执行mm 生成新的编译framework-res.apk位于out/target/product/xxx/system/frameworks/目录下
单编参考文章:https://editor.csdn.net/md/?articleId=125977277

控制状态栏icon图标的显示

参考博客:https://blog.csdn.net/weixin_33881140/article/details/92024232
通过SystemUi的加载流程,我们知道有这么一个方法makeStatusBarView(result)
是创建状态栏的,找到如下代码
在这里插入图片描述
这里的代码主要实现了:

  1. 使用 CollapsedStatusBarFragment 替换 status_bar_container(状态栏通知显示区域, status_bar_container在xml文件super_status_bar.xml文件中)
  2. statusBarFragment.initNotificationIconArea(mNotificationIconAreaController) 初始化通知栏区域
  3. mStatusBarView.setBar(this) 传递statusBar处理下拉事件和mStatusBarView.setPanel(mNotificationPanel) 传递 NotificationPanelView 显示下拉UI控制
    先看第一点CollapsedStatusBarFragment
    在视图创建后,需要创建图标,下面这里是status_icon的创建,也就是CollapsedStatusBarFragment 类
    CollapsedStatusBarFragment的onCreateView方法:
    在这里插入图片描述
    下面是视图的创建,将status_bar.xml显示创建视图到CollasedStatusBarFragment中
    我们先聚集在system_icon_area区域,就是显示蓝牙、wifi、VPN、网卡icon那块区域:
   @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        mStatusBar = (PhoneStatusBarView) view;
        if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_PANEL_STATE)) {
            mStatusBar.restoreHierarchyState(
                    savedInstanceState.getSparseParcelableArray(EXTRA_PANEL_STATE));
        }
        .......
        mDarkIconManager = new DarkIconManager(view.findViewById(R.id.statusIcons),
                Dependency.get(CommandQueue.class));
        mDarkIconManager.setShouldLog(true);
        Dependency.get(StatusBarIconController.class).addIconGroup(mDarkIconManager);
        mSystemIconArea = mStatusBar.findViewById(R.id.system_icon_area);
        mClockView = mStatusBar.findViewById(R.id.clock);
        showSystemIconArea(false);
        showClock(false);
        initEmergencyCryptkeeperText();
        initOperatorName();
    }

我们发现了这几行代码:
1、mDarkIconManager = new DarkIconManager(view.findViewById(R.id.statusIcons),
Dependency.get(CommandQueue.class)); //R.id.statusIcons即是system_icons.xml里面的控件。

system_icons.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:systemui="http://schemas.android.com/apk/res-auto"
    android:id="@+id/system_icons"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_vertical">

    <com.android.systemui.statusbar.phone.StatusIconContainer
        android:id="@+id/statusIcons"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="match_parent"
        android:paddingEnd="@dimen/signal_cluster_battery_padding"
        android:gravity="center_vertical"
        android:orientation="horizontal"/>

    <com.android.systemui.BatteryMeterView android:id="@+id/battery"
        android:layout_height="match_parent"
        android:layout_width="wrap_content"
        android:clipToPadding="false"
        android:clipChildren="false"
        systemui:textAppearance="@style/TextAppearance.StatusBar.Clock" />
</LinearLayout>

2、Dependency.get(StatusBarIconController.class).addIconGroup(mDarkIconManager); //进入其实现类StatusBarIconControllerImpl.java,在其构造函数中我们找到了实现

public StatusBarIconControllerImpl(Context context) {
        super(context.getResources().getStringArray(
                com.android.internal.R.array.config_statusBarIcons));

我们可以发现状态栏icon加载的图标来源于framework/base/core/res/res/values/config.xml文件
在这里我们就找到了index和slot的出处,原来在初始化的时候就已经定义好了所有的slots,然后从framework中加载出来,index就是string-array中的顺序。

   <string-array name="config_statusBarIcons">
        <item><xliff:g id="id">@string/status_bar_alarm_clock</xliff:g></item>
        <item><xliff:g id="id">@string/status_bar_rotate</xliff:g></item>
        <item><xliff:g id="id">@string/status_bar_headset</xliff:g></item>
        <item><xliff:g id="id">@string/status_bar_data_saver</xliff:g></item>
        <item><xliff:g id="id">@string/status_bar_ime</xliff:g></item>
        <item><xliff:g id="id">@string/status_bar_sync_failing</xliff:g></item>
        <item><xliff:g id="id">@string/status_bar_sync_active</xliff:g></item>
        ................
    </string-array>
    
    <string translatable="false" name="status_bar_rotate">rotate</string>
    <string translatable="false" name="status_bar_headset">headset</string>
    <string translatable="false" name="status_bar_data_saver">data_saver</string>
    <string translatable="false" name="status_bar_managed_profile">managed_profile</string>
    <string translatable="false" name="status_bar_ime">ime</string>
    <string translatable="false" name="status_bar_sync_failing">sync_failing</string>
    <string translatable="false" name="status_bar_sync_active">sync_active</string>
  .......

然后是StatusBarIconControllerImpl.java这个控制器来控制icon的加载显示和移除。
由PhoneStatusBarPolicy.java类来负责调用执行:
有2种方式实现对ICON的控制显示和隐藏
1、首先类本身实现了大量的回调接口,通过重写这些接口类的方法,会在触发的时候被回调,然后更新ICON显示和隐藏

public class PhoneStatusBarPolicy
        implements BluetoothController.Callback,
                CommandQueue.Callbacks,
                RotationLockControllerCallback,
                Listener,
                ZenModeController.Callback,
                DeviceProvisionedListener,
                KeyguardStateController.Callback,
                LocationController.LocationChangeCallback,
                RecordingController.RecordingStateChangeCallback,
                TrainController.Callback{
                ...

2、在初始化的时候又注册了大量的监听,能够监听广播进行调节改变显示

public PhoneStatusBarPolicy(Context context, StatusBarIconController iconController) {
        mContext = context;
        //  初始化headset的slot
        mSlotHeadset = context.getString(com.android.internal.R.string.status_bar_headset);
 
        // listen for broadcasts
        IntentFilter filter = new IntentFilter();
        filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
        filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION);
        //  注册headset状态变化的action
        filter.addAction(AudioManager.ACTION_HEADSET_PLUG);
        filter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED);
        filter.addAction(TelecomManager.ACTION_CURRENT_TTY_MODE_CHANGED);
        filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE);
        filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
        filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED);
        mContext.registerReceiver(mIntentReceiver, filter, null, mHandler);
 

依靠监听和回调机制,可以用来控制状态栏icon图标的显示、隐藏。在PhoneStatusBarPolicy.java中来实现,下面我们以闹钟为例,我们不希望设置闹钟之后状态栏显示闹钟icon。每个icon对应一个updatexxx(),我们找到关于闹钟的updateAlarm()

   private void updateAlarm() {
        final AlarmClockInfo alarm = mAlarmManager.getNextAlarmClock(UserHandle.USER_CURRENT);
        final boolean hasAlarm = alarm != null && alarm.getTriggerTime() > 0;
        int zen = mZenController.getZen();
        final boolean zenNone = zen == Global.ZEN_MODE_NO_INTERRUPTIONS;
        mIconController.setIcon(mSlotAlarmClock, zenNone ? R.drawable.stat_sys_alarm_dim
                : R.drawable.stat_sys_alarm, buildAlarmContentDescription());
        //mIconController.setIconVisibility(mSlotAlarmClock, mCurrentUserSetup && hasAlarm);
        //jiaxian.zhang 直接修改为false
        mIconController.setIconVisibility(mSlotAlarmClock, false);
    }

修改后push到手机发现成功实现!

锁屏密码验证

锁屏加载流程参考:https://www.jianshu.com/p/d62d5872820f
代码集中在SystemUI模块。 frameworks/base/packages/SystemUI/src/com/android/systemui/keyguard
frameworks/base/packages/SystemUI/src/com/android/systemui
1)登陆密码验证界面
这里涉及到2个重要的类 KeyguardAbsKeyInputView.java和 KeyguardPasswordView.java ,密码的授权验证在KeyguardAbsKeyInputView.java类中处理,界面的显示是在KeyguardPasswordView.java 中进行处理。 我们看下下图贴的代码 KeyguardPasswordView.java 里面的onEditorAction()里面的处理,有个verifyPasswordAndUnlock()的方法,就是跳转到KeyguardAbsKeyInputView.java中进行密码的验证了。
如果我们希望修改密码验证的逻辑的话,就可以修改KeyguardAbsKeyInputView.java中的verifyPasswordAndUnlock()里面的内容。
密码验证对应的布局文件keyguard_password_view.xml,如果我们希望在布局上添加内容,就可以在这个布局文件上添加了。

--------KeyguardPasswordView.java --------
   
    @Override
    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
        // Check if this was the result of hitting the enter key
        final boolean isSoftImeEvent = event == null
                && (actionId == EditorInfo.IME_NULL
                || actionId == EditorInfo.IME_ACTION_DONE
                || actionId == EditorInfo.IME_ACTION_NEXT);
        final boolean isKeyboardEnterKey = event != null
                && KeyEvent.isConfirmKey(event.getKeyCode())
                && event.getAction() == KeyEvent.ACTION_DOWN;
        if (isSoftImeEvent || isKeyboardEnterKey) {
            verifyPasswordAndUnlock();
            return true;
        }
        return false;
    }

修改案例:在其父类KeyguardAbsKeyInputView的verifyPasswordAndUnlock()中我们发现以下代码:

 // To avoid accidental lockout due to events while the device in in the pocket, ignore
    // any passwords with length less than or equal to this length.
    protected static final int MINIMUM_PASSWORD_LENGTH_BEFORE_REPORT = 3;

//意思是当你输入的密码长度少于等于3,则无论输入多少次密码都不发触发30秒后重试,
//只有当你输入的密码大于3,输出错误等于5次后就会触发30秒后重试
//MINIMUM_PASSWORD_LENGTH_BEFORE_REPORT参数就是控制这个密码长度的
if (password.size() <= MINIMUM_PASSWORD_LENGTH_BEFORE_REPORT) {
    // to avoid accidental lockout, only count attempts that are long enough to be a
    // real password. This may require some tweaking.
    setPasswordEntryInputEnabled(true);
    onPasswordChecked(userId, false /* matched */, 0, false /* not valid - too short */);
    password.zeroize();
    return;
}

2)如何验证?
如果你输入的密码是大于3的,比如是4位,就不会进入那个if判断,会进入下面这里onCheck()方法(可以查看日志log的输出)

mKeyguardUpdateMonitor.setCredentialAttempted();
        mPendingLockCheck = LockPatternChecker.checkCredential(
                mLockPatternUtils,
                password,
                userId,
                new LockPatternChecker.OnCheckCallback() {

                    @Override
                    public void onEarlyMatched() {
                        if (LatencyTracker.isEnabled(mContext)) {
                            LatencyTracker.getInstance(mContext).onActionEnd(
                                    ACTION_CHECK_CREDENTIAL);
                        }
                        onPasswordChecked(userId, true /* matched */, 0 /* timeoutMs */,
                                true /* isValidPassword */);
                        password.zeroize();
                    }

                    //输入密码后进入的验证方法
                    @Override
                    public void onChecked(boolean matched, int timeoutMs) {
                        Log.d(TAG, "verifyPasswordAndUnlock onChecked");
                        if (LatencyTracker.isEnabled(mContext)) {
                            //这个onActionEnd进去则是修改数据库剩余尝试次数-1
                            LatencyTracker.getInstance(mContext).onActionEnd(
                                    ACTION_CHECK_CREDENTIAL_UNLOCKED);
                        }
                        setPasswordEntryInputEnabled(true);
                        mPendingLockCheck = null;
                        if (!matched) {
                            onPasswordChecked(userId, false /* matched */, timeoutMs,
                                    true /* isValidPassword */);
                        }
                        password.zeroize();
                    }

                    @Override
                    public void onCancelled() {
                        Log.d(TAG, "verifyPasswordAndUnlock onCancelled");
                        // We already got dismissed with the early matched callback, so we cancelled
                        // the check. However, we still need to note down the latency.
                        if (LatencyTracker.isEnabled(mContext)) {
                            LatencyTracker.getInstance(mContext).onActionEnd(
                                    ACTION_CHECK_CREDENTIAL_UNLOCKED);
                        }
                        password.zeroize();
                    }
                });

onChecked()即使输入密码后执行验证的方法,if里面执行了以下代码
LatencyTracker.getInstance(mContext).onActionEnd(
ACTION_CHECK_CREDENTIAL_UNLOCKED);
下面我们进入LatencyTracker.java中查看,大致的逻辑就是从4开始,每次进入都会-1更改到数据库中,直到等于-1后return ; 也就是说期间有5次机会,对应锁屏出错次数允许5次。只有当正确的时候才会重置这个数值

public static final int ACTION_CHECK_CREDENTIAL_UNLOCKED = 4;

public void onActionEnd(int action) {
        if (!mEnabled) {
            return;
        }
        long endRtc = SystemClock.elapsedRealtime();
        long startRtc = mStartRtc.get(action, -1);
        if (startRtc == -1) {
            return;
        }
        mStartRtc.delete(action);
        Trace.asyncTraceEnd(Trace.TRACE_TAG_APP, NAMES[action], 0);
        logAction(action, (int)(endRtc - startRtc));
    }
;