文章目录
问题背景
Android官方提供了一个DreamService类以实现屏保的功能,内部提供了两个回调:
- onAttachToWindow()
- onDetachToWindow()
昨天碰到了一个系统性的问题:
在启动DreamService屏保之后,系统的其他App直接startActivity()并不会退出屏保,也就是DreamService并不会收到onDetachToWindow()回调?
分析问题
1. DreamService 怎么实现屏保
论如何实现屏保可以参考《基于DreamService的屏保》
DreamService实现屏保功能很简单,只需要两个步骤
- 实现DreamService的onAttachedToWindow()和onDetachToWindow()
- 声明权限
android:permission="android.permission.BIND_DREAM_SERVICE
如果你熟悉NotificationManagerService和NotificationListenerService,你会发现DreamService跟他们的实现是一致的。
DreamService 就是利用窗口Window,来显示屏保功能,如果你是做系统应用的,那你应该很熟悉了。
// DreamService.java
private final void attach(IBinder windowToken, boolean canDoze, IRemoteCallback started) {
...
mWindow = new PhoneWindow(this);
mWindow.setCallback(this);
getWindowManager().addView(mWindow.getDecorView(), mWindow.getAttributes());
...
mHandler.post(new Runnable() {
@Override
public void run() {
...
onDreamingStarted();
...
}
});
}
// 设定布局
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
}
DreamService在attach的私有方法内部创建了PhoneWindow,跟Activity创建很像。
在WIndow创建成功以后会回调到onAttachToWindow(),我们就可以直接在onAttachToWindow()添加我们的自定义布局setContentView()
2. DreamManagerService 怎么启动 DreamService
上面问题1,大概简述了DreamService怎么利用Window创建自己的屏保,但是attach()方式是哪里调用的呢?
DreamService的onBind()创建了DreamServiceWrapper,DreamServiceWrapper实现了IDreamService的binder接口,内部对外提供了attach(),detach()和wakeUp()接口。DreamService作为了binder的服务端。
其中,wakeUp()就是唤醒,attach()屏保启动,detach()结束屏保服务
其实是DreamManagerService绑定服务DreamService的binder。
private final class DreamServiceWrapper extends IDreamService.Stub {
@Override
public void attach(final IBinder windowToken, final boolean canDoze,
IRemoteCallback started) {
mHandler.post(new Runnable() {
@Override
public void run() {
DreamService.this.attach(windowToken, canDoze, started);
}
});
}
@Override
public void detach() {
mHandler.post(new Runnable() {
@Override
public void run() {
DreamService.this.detach();
}
});
}
@Override
public void wakeUp() {
mHandler.post(new Runnable() {
@Override
public void run() {
DreamService.this.wakeUp(true /*fromSystem*/);
}
});
}
}
DreamManagerService是系统实现的一个SystemService,他怎么会去bind一个用户实现的DreamService呢?
如上面所说,这部分跟NotificationManagerService如何发现并绑定NotificationListenerService原理是一样的。
DreamManagerService 利用权限发现自定义 DreamService
DreamManagerService 继承了SystemService,类似AMS或者NMS。他们的启动和注册方式也是一样的
// DreamManagerService
@Override
public void onStart() {
publishBinderService(DreamService.DREAM_SERVICE, new BinderService());
publishLocalService(DreamManagerInternal.class, new LocalService());
}
来看如何发现和绑定 DreamService?
// DreamManagerService
private final class LocalService extends DreamManagerInternal {
@Override
public void startDream(boolean doze) {
startDreamInternal(doze);
}
...
}
//DreamManagerService
private void startDreamInternal(boolean doze) {
final int userId = ActivityManager.getCurrentUser();
final ComponentName dream = chooseDreamForUser(doze, userId);
if (dream != null) {
synchronized (mLock) {
startDreamLocked(dream, false /*isTest*/, doze, userId);
}
}
}
重点来了,chooseDreamForUser()就是发现DreamService,最后调用到getDreamComponentsForUser()
private ComponentName[] getDreamComponentsForUser(int userId) {
String names = Settings.Secure.getStringForUser(mContext.getContentResolver(),
Settings.Secure.SCREENSAVER_COMPONENTS,
userId);
ComponentName[] components = componentsFromString(names);
// 验证
List<ComponentName> validComponents = new ArrayList<ComponentName>();
if (components != null) {
for (ComponentName component : components) {
if (validateDream(component)) {
validComponents.add(component);
}
}
}
return validComponents.toArray(new ComponentName[validComponents.size()]);
}
可见,DreamManagerService利用Settings.Secure权限发现DreamService,当有多个DreamService时候,会选择第一个启动。
DreamManagerService 绑定 DreamService
上面利用了权限选择自定义的DreamService,那如何绑定就很简单了
private void startDreamLocked(final ComponentName name,
final boolean isTest, final boolean canDoze, final int userId) {
// 结束屏保
stopDreamLocked(true /*immediate*/);
....
// 启动不熄屏
PowerManager.WakeLock wakeLock = mPowerManager
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "startDream");
mHandler.post(wakeLock.wrap(
// 启动屏保
() -> mController.startDream(newToken, name, isTest, canDoze, userId, wakeLock)));
}
如何你跟下去会走到DreamController
// DreamController
public void startDream(Binder token, ComponentName name,
boolean isTest, boolean canDoze, int userId, PowerManager.WakeLock wakeLock) {
Intent intent = new Intent(DreamService.SERVICE_INTERFACE);
intent.setComponent(name);
...
if (!mContext.bindServiceAsUser(intent, mCurrentDream,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
new UserHandle(userId))) {
stopDream(true /*immediate*/);
return;
}
}
可见,最后DreamManagerService借助DreamController去bind对应的DreamService
3. 有什么方法停止屏保
(1) DreamService Input事件
在DreamService的keyEvent接收所有输入事件,当收到key事件时候,直接退出屏保
public boolean dispatchKeyEvent(KeyEvent event) {
if (!mInteractive) {
wakeUp();
return true;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
wakeUp();
return true;
}
return mWindow.superDispatchKeyEvent(event);
}
public boolean dispatchKeyShortcutEvent(KeyEvent event) {
if (!mInteractive) {
wakeUp();
return true;
}
return mWindow.superDispatchKeyShortcutEvent(event);
}
public boolean dispatchTouchEvent(MotionEvent event) {
if (!mInteractive) {
wakeUp();
return true;
}
return mWindow.superDispatchTouchEvent(event);
}
很直接,在激活状态下,只要收到点击、key事件就退出了屏保。
(2) PowerManagerService的 ACTION_DREAMING_STOPPED广播
利用DreamManagerService的binder接口,去wakeUp()屏保。但是呢!
哎!有个很可惜的地方,DreamManagerService并不在SystemServiceRegistry,意味着我们不能直接通过 Context.getSystemService来拿到DreamManagerService来停止屏保动作。
最后我发现DreamManagerService跟PowerManagerService是有关联性的
// PowerManagerService
filter = new IntentFilter();
filter.addAction(Intent.ACTION_DREAMING_STARTED);
filter.addAction(Intent.ACTION_DREAMING_STOPPED);
mContext.registerReceiver(new DreamReceiver(), filter, null, mHandler);
PowerManagerService接收了两个广播:开始屏保和结束屏保。
个人找了好久才找到
private final class DreamReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
synchronized (mLock) {
scheduleSandmanLocked();
}
}
}
private void scheduleSandmanLocked() {
if (!mSandmanScheduled) {
mSandmanScheduled = true;
Message msg = mHandler.obtainMessage(MSG_SANDMAN);
msg.setAsynchronous(true);
mHandler.sendMessage(msg);
}
}
最后,看下如何处理MSG_SANDMAN,就是handleSandman()方法
private void handleSandman() {
// runs on handler thread
···
// Stop dream.
if (isDreaming) {
mDreamManager.stopDream(false /*immediate*/);
}
}
但是,这里的结束屏保是异步的动作。
总结
自做系统应用以来,养成了一个很好的习惯就是习惯性阅读源码,很多时候都是可以从源码寻找解决之道。
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/tech/app/6264.html