在本文中,我将介绍如何获取Android系统的最后操作时间,即用户最后一次触摸屏幕的时间。这个功能在一些场景下是非常有用的,比如需要在一段时间内无操作时自动执行某些任务 , 或者在人体感应场景 休眠唤醒屏时的优先级处理 。我将以Android 11.0+为例,先介绍Android系统中最后操作时间的原理, 然后增加回调 可以给系统其他服务调用。
用户最后操作时间原理
Android系统中有一个服务叫做PowerManagerService,它负责管理设备的电源状态,包括唤醒、休眠、亮屏、暗屏等。这个服务中有一个变量叫做mLastUserActivityTime,它记录了用户最后一次触摸屏幕的时间。每当用户触摸屏幕时,这个变量就会被更新为当前的系统时间。
因此,我们可以通过读取这个变量的值,来判断用户是否在一段时间内没有操作屏幕。如果当前的系统时间减去这个变量的值大于我们设定的阈值(比如5秒),那么就说明用户已经超过5秒没有操作屏幕了。这时候,我们就可以执行相应的逻辑。
业务逻辑实现
如果你的业务逻辑比较简单 比如发个广播 启动个应用要实现这个功能,我们需要修改PowerManagerService.java这个文件,它位于frameworks/base/services/core/java/com/android/server/power/目录下。我们需要在updateUserActivitySummaryLocked()这个方法中添加我们的判断逻辑,以及发送一个消息给我们自己定义的Handler来处理播放视频的操作。
步骤1,在PowerManagerService类中定义一个常量和一个Handler:
// The message to indicate that the user has not touched the screen for a while private static final int MSG_USER_NOT_ACTION = 100; // The handler to handle the message private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_USER_NOT_ACTION: // 比如执行命令 启动服务 或者 播放视频 break; default: super.handleMessage(msg); } } };
步骤2,在updateUserActivitySummaryLocked()方法中添加我们的判断逻辑:
private void updateUserActivitySummaryLocked(long now, int dirty) { // Update the status of the user activity timeout timer. if ((dirty & (DIRTY_WAKE_LOCKS | DIRTY_USER_ACTIVITY | DIRTY_WAKEFULNESS | DIRTY_SETTINGS)) != 0) { mHandler.removeMessages(MSG_USER_ACTIVITY_TIMEOUT); long nextTimeout = 0; if (getWakefulnessLocked() == WAKEFULNESS_AWAKE || getWakefulnessLocked() == WAKEFULNESS_DREAMING || getWakefulnessLocked() == WAKEFULNESS_DOZING) { final long attentiveTimeout = getAttentiveTimeoutLocked(); final long sleepTimeout = getSleepTimeoutLocked(attentiveTimeout); final long screenOffTimeout = getScreenOffTimeoutLocked(sleepTimeout, attentiveTimeout); final long screenDimDuration = getScreenDimDurationLocked(screenOffTimeout); final boolean userInactiveOverride = mUserInactiveOverrideFromWindowManager; final long nextProfileTimeout = getNextProfileTimeoutLocked(now); mUserActivitySummary = 0; if (mLastUserActivityTime >= mLastWakeTime) { nextTimeout = mLastUserActivityTime + screenOffTimeout - screenDimDuration; if (now < nextTimeout) { mUserActivitySummary = USER_ACTIVITY_SCREEN_BRIGHT; } else { nextTimeout = mLastUserActivityTime + screenOffTimeout; if (now < nextTimeout) { mUserActivitySummary = USER_ACTIVITY_SCREEN_DIM; } } } if (mUserActivitySummary == 0 && mLastUserActivityTimeNoChangeLights >= mLastWakeTime) { nextTimeout = mLastUserActivityTimeNoChangeLights + screenOffTimeout; if (now < nextTimeout) { if (mDisplayPowerRequest.policy == DisplayPowerRequest.POLICY_BRIGHT || mDisplayPowerRequest.policy == DisplayPowerRequest.POLICY_VR) { mUserActivitySummary = USER_ACTIVITY_SCREEN_BRIGHT; } else if (mDisplayPowerRequest.policy == DisplayPowerRequest.POLICY_DIM) { mUserActivitySummary = USER_ACTIVITY_SCREEN_DIM; } } } if (mUserActivitySummary == 0) { if (sleepTimeout >= 0) { final long anyUserActivity = Math.max(mLastUserActivityTime, mLastUserActivityTimeNoChangeLights); if (anyUserActivity >= mLastWakeTime) { nextTimeout = anyUserActivity + sleepTimeout; if (now < nextTimeout) { mUserActivitySummary = USER_ACTIVITY_SCREEN_DREAM; } } } else { mUserActivitySummary = USER_ACTIVITY_SCREEN_DREAM; nextTimeout = -1; } } if (mUserActivitySummary != USER_ACTIVITY_SCREEN_DREAM && userInactiveOverride) { if ((mUserActivitySummary & (USER_ACTIVITY_SCREEN_BRIGHT | USER_ACTIVITY_SCREEN_DIM)) != 0) { // Device is being kept awake by recent user activity if (nextTimeout >= now && mOverriddenTimeout == -1) { // Save when the next timeout would have occurred mOverriddenTimeout = nextTimeout; } } mUserActivitySummary = USER_ACTIVITY_SCREEN_DREAM; nextTimeout = -1; } if ((mUserActivitySummary & USER_ACTIVITY_SCREEN_BRIGHT) != 0 && (mWakeLockSummary & WAKE_LOCK_STAY_AWAKE) == 0) { nextTimeout = mAttentionDetector.updateUserActivity(nextTimeout, screenDimDuration); } if (nextProfileTimeout > 0) { nextTimeout = Math.min(nextTimeout, nextProfileTimeout); } if (Integer.MAX_VALUE == screenOffTimeout) { mUserActivitySummary = USER_ACTIVITY_SCREEN_BRIGHT; //Slog.d(TAG, "set mUserActivitySummary USER_ACTIVITY_SCREEN_BRIGHT never sleep " + nextTimeout); } // Add our logic here to check if the user has not touched the screen for 10 seconds // ------------------------------- if(now-mLastUserActivityTime>=5000 && mBootCompleted && mSystemReady){ Message msg = mHandler.obtainMessage(MSG_USER_NOT_ACTION); msg.setAsynchronous(true); mHandler.sendMessage(msg); } // ------------------------------- if (mUserActivitySummary != 0 && nextTimeout >= 0) { scheduleUserInactivityTimeout(nextTimeout); } } else { mUserActivitySummary = 0; } if (DEBUG_SPEW) { Slog.d(TAG, "updateUserActivitySummaryLocked: mWakefulness=" + PowerManagerInternal.wakefulnessToString(getWakefulnessLocked()) + ", mUserActivitySummary=0x" + Integer.toHexString(mUserActivitySummary) + ", nextTimeout=" + TimeUtils.formatUptime(nextTimeout)); } } }
步骤3,在我们自己定义的Handler中添加我们的逻辑来播放视频,这里可以根据自己的需求来实现,比如调用一个广播或者一个服务来启动服务。
添加用户最后操作时间回调
什么? 你告诉我 你的业务逻辑在PowerManagerService.java 不方便搞? 什么 你告诉我有N个地方要获取最后时间 , 🆗 我们添加一个回调接口。
如果有多个服务需要获取回调,你可以将单个回调接口改为回调接口的列表。以下是具体的步骤:
- 修改回调的定义:将
UserActivityCallback
的单个实例改为List<UserActivityCallback>
的实例。例如:
private List<UserActivityCallback> mUserActivityCallbacks = new ArrayList<>();
- 修改设置回调的方法:将
setUserActivityCallback()
方法改为addUserActivityCallback()
方法,用于添加回调,而不是设置回调。例如:
public void addUserActivityCallback(UserActivityCallback callback) { mUserActivityCallbacks.add(callback); }
你也可以添加一个removeUserActivityCallback()
方法,用于移除回调:
public void removeUserActivityCallback(UserActivityCallback callback) { mUserActivityCallbacks.remove(callback); }
- 修改调用回调的代码:在
updateUserActivitySummaryLocked()
方法中,你需要遍历回调列表,对每个回调都调用onUserActivity()
方法。例如:
if (mLastUserActivityTime >= mLastWakeTime) { for (UserActivityCallback callback : mUserActivityCallbacks) { callback.onUserActivity(mLastUserActivityTime); } // ... }
- 在你的服务中添加回调:在你的每个服务中,你都需要调用
addUserActivityCallback()
方法来添加回调 (SystemBootAppServer 是我自定义的)。例如:
public class SystemBootAppServer extends SystemService { // ... @Override public void onStart() { PowerManagerService pms = (PowerManagerService) getSystemService(Context.POWER_SERVICE); pms.addUserActivityCallback(new PowerManagerService.UserActivityCallback() { @Override public void onUserActivity(long lastActivityTime) { // 在这里处理用户最后操作的时间 } }); } }
以上就是在有多个服务需要获取回调时的具体步骤。这种方法可以使得每个服务都能获取到用户最后操作的时间,而不仅仅是一个服务。
人体感应问题补充
在我开发过程中 , 我遇到过一个问题 , 就是添加人体感应之后 , 会休眠和唤醒屏幕。
假设人体感应被(误)触发需要熄屏 , 但是你用户正在操作呢 , 这个时候应该熄屏幕吗?
很显然 不应该熄灭 , 这是个优先级的问题 , 所以我补充一点。
在PowerManagerService.java这个文件的
@Override // Binder call public void wakeUp(long eventTime, @WakeReason int reason, String details, String opPackageName) { if (eventTime > mClock.uptimeMillis()) { throw new IllegalArgumentException("event time must not be in the future"); } mContext.enforceCallingOrSelfPermission( android.Manifest.permission.DEVICE_POWER, null); final int uid = Binder.getCallingUid(); final long ident = Binder.clearCallingIdentity(); try { wakeUpInternal(eventTime, reason, details, uid, opPackageName, uid); } finally { Binder.restoreCallingIdentity(ident); } } @Override // Binder call public void goToSleep(long eventTime, int reason, int flags) { // 假设人体感应被触发休眠指令, 但是人还在操作呢 , 会走这个流程 , // 我们在这里添加一个5秒的判断 , 如果最后操作时间小于5秒 // 则不执行休眠动作 , //-------------------------- long now = SystemClock.uptimeMillis(); Log.d("goToSleepTest","mLastUserActivityTime:"+mLastUserActivityTime+",now!!:"+now); if(now-mLastUserActivityTime>=5000 ){ //-------------------------- if (eventTime > mClock.uptimeMillis()) { throw new IllegalArgumentException("event time must not be in the future"); } mContext.enforceCallingOrSelfPermission( android.Manifest.permission.DEVICE_POWER, null); final int uid = Binder.getCallingUid(); final long ident = Binder.clearCallingIdentity(); try { goToSleepInternal(eventTime, reason, flags, uid); } finally { Binder.restoreCallingIdentity(ident); } } }
总结
通过以上的步骤,我们就可以实现在Android系统层获取最后操作时间的功能,并在一段时间内无操作时自动播放视频。这个功能可以用于一些需要不间断播放视频的应用场景,比如广告机、展示机等。我还介绍了如何添加回调 给系统其他自定义服务使用还考虑了多个服务同时获取的情况 , 最后补充了人体感应休眠优先级的问题。希望这篇博客能对你有所帮助,如果你有任何问题或建议,欢迎留言讨论。谢谢