添加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)
是创建状态栏的,找到如下代码
这里的代码主要实现了:
- 使用 CollapsedStatusBarFragment 替换 status_bar_container(状态栏通知显示区域, status_bar_container在xml文件super_status_bar.xml文件中)
- statusBarFragment.initNotificationIconArea(mNotificationIconAreaController) 初始化通知栏区域
- 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));
}