很多时候会遇到一些类似云控开关或下载升级patch的需求。大概思路都是要从服务器下载一个配置文件来完成云控的策略。那么什么时候去下载对用户来说一种比较好的体验?
这里提供一种思路是通过JobService来实现特定场景下出发任务的方法。
JobService的使用和代码分析可以参考这两篇博客:
https://blog.csdn.net/allisonchen/article/details/79218713
https://blog.csdn.net/FightFightFight/article/details/86705847
基础使用方法上面的博客已经讲了,我不喜欢拷贝别人的写的文章,大家请自行学习。下面我只说一些为了上面提到的需求应该怎么做。
1.创建JobInfo
我们要用的是这样的策略:当用户在免费网络、充电、设备idle状态是进行文件下载,使用下面代码即可,比较简单不多做解释
public static void initJob(Context context) {
JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
JobInfo.Builder b = new JobInfo.Builder(JOB_ID, new ComponentName(context, MyJobService.class));
b.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED); // 非付费网络
b.setRequiresCharging(true); // 充电时
b.setRequiresDeviceIdle(true);
jobScheduler.schedule(b.build());
}
2.触发
上面代码中所设置的条件是免费网络、充电和设备idle状态。创造前面两个比较简单
免费网络连个wifi就好了,这里也可以使用adb来查看网络的状态
adb shell dumpsys connectivity | grep NetworkAgentInfo
可以看到类似的信息:
WIFI Capabilities: NOT_METERED&...
充电状态插上电源就好,也可以用adb来查看
adb shell dumpsys deviceidle | grep mCharging
输出:
mCharging=true
第三个条件deviceidle就比较复杂了,先告诉你答案:
1. 锁屏
2. 执行下面的命令
adb shell am broadcast -a com.android.server.ACTION_TRIGGER_IDLE
没错,第二个步骤其实就是发了一个广播,我们只针对deviceid的触发条件来分析下源码
JobInfo.java
先看JobInfo和JobInfo.Builder,这两个都在JobInfo.java里面
public Builder setRequiresDeviceIdle(boolean requiresDeviceIdle) {
mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_DEVICE_IDLE)
| (requiresDeviceIdle ? CONSTRAINT_FLAG_DEVICE_IDLE : 0);
return this;
}
private JobInfo(JobInfo.Builder b) {
...
constraintFlags = b.mConstraintFlags;
...
}
JobInfo.Builder设置了一个叫mConstraintFlags的位flag,并在build的时候赋给了JobInfo的constraintFlag
JobSchedulerService.java
再来看JobSchedulerService
public int scheduleAsPackage(JobInfo job, JobWorkItem work, int uId, String packageName,
int userId, String tag) {
...
synchronized (mLock) {
...
// If the job is immediately ready to run, then we can just immediately
// put it in the pending list and try to schedule it. This is especially
// important for jobs with a 0 deadline constraint, since they will happen a fair
// amount, we want to handle them as quickly as possible, and semantically we want to
// make sure we have started holding the wake lock for the job before returning to
// the caller.
// If the job is not yet ready to run, there is nothing more to do -- we are
// now just waiting for one of its controllers to change state and schedule
// the job appropriately.
if (isReadyToBeExecutedLocked(jobStatus)) { // 判断是否满足条件
// This is a new job, we can just immediately put it on the pending
// list and try to run it.
mJobPackageTracker.notePending(jobStatus);
addOrderedItem(mPendingJobs, jobStatus, mEnqueueTimeComparator); // 把job加入要执行的队列
maybeRunPendingJobsLocked(); // 执行job队列
}
}
return JobScheduler.RESULT_SUCCESS;
}
/**
* Criteria for moving a job into the pending queue:
* - It's ready.
* - It's not pending.
* - It's not already running on a JSC.
* - The user that requested the job is running.
* - The job's standby bucket has come due to be runnable.
* - The component is enabled and runnable.
*/
private boolean isReadyToBeExecutedLocked(JobStatus job) {
final boolean jobReady = job.isReady(); // 查看看是否ready
if (DEBUG) {
Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString()
+ " ready=" + jobReady);
}
// This is a condition that is very likely to be false (most jobs that are
// scheduled are sitting there, not ready yet) and very cheap to check (just
// a few conditions on data in JobStatus).
if (!jobReady) {
if (job.getSourcePackageName().equals("android.jobscheduler.cts.jobtestapp")) {
Slog.v(TAG, " NOT READY: " + job);
}
return false;
}
...
return componentPresent;
}
看上面代码大概能理解一些,判断一个任务是否满足执行条件,首先要检查job.isReady()
JobStatus.java
来看下JobStatus
/**
* @return Whether or not this job is ready to run, based on its requirements. This is true if
* the constraints are satisfied or the deadline on the job has expired.
* TODO: This function is called a *lot*. We should probably just have it check an
* already-computed boolean, which we updated whenever we see one of the states it depends
* on here change.
*/
public boolean isReady() {
// Deadline constraint trumps other constraints (except for periodic jobs where deadline
// is an implementation detail. A periodic job should only run if its constraints are
// satisfied).
// AppNotIdle implicit constraint must be satisfied
// DeviceNotDozing implicit constraint must be satisfied
// NotRestrictedInBackground implicit constraint must be satisfied
final boolean deadlineSatisfied = (!job.isPeriodic() && hasDeadlineConstraint()
&& (satisfiedConstraints & CONSTRAINT_DEADLINE) != 0);
final boolean notDozing = (satisfiedConstraints & CONSTRAINT_DEVICE_NOT_DOZING) != 0
|| (job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0;
final boolean notRestrictedInBg =
(satisfiedConstraints & CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0;
return (isConstraintsSatisfied() || deadlineSatisfied) && notDozing && notRestrictedInBg;
}
/**
* @return Whether the constraints set on this job are satisfied.
*/
public boolean isConstraintsSatisfied() {
if (overrideState == OVERRIDE_FULL) {
// force override: the job is always runnable
return true;
}
final int req = requiredConstraints & CONSTRAINTS_OF_INTEREST;
int sat = satisfiedConstraints & CONSTRAINTS_OF_INTEREST;
if (overrideState == OVERRIDE_SOFT) {
// override: pretend all 'soft' requirements are satisfied
sat |= (requiredConstraints & SOFT_OVERRIDE_CONSTRAINTS);
}
return (sat & req) == req;
}
satisfiedConstraints表示当前系统所满足的条件。这样上面的触发条件也已经很显然了。在非doz模式和非前台限制模式下,只要满足deadline或者isConstraintsSatisfied,就可以。(deadline也可以在JobInfo.Builder里面设置)
在isConstraintsSatisfied这个方法中,requiredConstraints表示这个Job需要满足的条件。所以isConstraintsSatisfied这个方法实际上就是判断satisfiedConstraints是否已经满足Job需求的所有条件。
requiredConstraints的值是怎么来的,可以在JobStatus的构造方法中看到。实际上就是最开始的JobInfo的constraintFlag
/**
* Core constructor for JobStatus instances. All other ctors funnel down to this one.
*
* @param job The actual requested parameters for the job
* @param callingUid Identity of the app that is scheduling the job. This may not be the
* app in which the job is implemented; such as with sync jobs.
* @param targetSdkVersion The targetSdkVersion of the app in which the job will run.
* @param sourcePackageName The package name of the app in which the job will run.
* @param sourceUserId The user in which the job will run
* @param standbyBucket The standby bucket that the source package is currently assigned to,
* cached here for speed of handling during runnability evaluations (and updated when bucket
* assignments are changed)
* @param heartbeat Timestamp of when the job was created, in the standby-related
* timebase.
* @param tag A string associated with the job for debugging/logging purposes.
* @param numFailures Count of how many times this job has requested a reschedule because
* its work was not yet finished.
* @param earliestRunTimeElapsedMillis Milestone: earliest point in time at which the job
* is to be considered runnable
* @param latestRunTimeElapsedMillis Milestone: point in time at which the job will be
* considered overdue
* @param lastSuccessfulRunTime When did we last run this job to completion?
* @param lastFailedRunTime When did we last run this job only to have it stop incomplete?
* @param internalFlags Non-API property flags about this job
*/
private JobStatus(JobInfo job, int callingUid, int targetSdkVersion, String sourcePackageName,
int sourceUserId, int standbyBucket, long heartbeat, String tag, int numFailures,
long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis,
long lastSuccessfulRunTime, long lastFailedRunTime, int internalFlags) {
...
int requiredConstraints = job.getConstraintFlags();
if (job.getRequiredNetwork() != null) {
requiredConstraints |= CONSTRAINT_CONNECTIVITY;
}
if (earliestRunTimeElapsedMillis != NO_EARLIEST_RUNTIME) {
requiredConstraints |= CONSTRAINT_TIMING_DELAY;
}
if (latestRunTimeElapsedMillis != NO_LATEST_RUNTIME) {
requiredConstraints |= CONSTRAINT_DEADLINE;
}
if (job.getTriggerContentUris() != null) {
requiredConstraints |= CONSTRAINT_CONTENT_TRIGGER;
}
this.requiredConstraints = requiredConstraints;
...
}
然后我们需要找我们最关心的deviceidle的条件,他被赋值到satisfiedConstraints上是在setIdleConstraintSatisfied方法里面。
boolean setIdleConstraintSatisfied(boolean state) {
return setConstraintSatisfied(CONSTRAINT_IDLE, state);
}
boolean setConstraintSatisfied(int constraint, boolean state) {
boolean old = (satisfiedConstraints&constraint) != 0;
if (old == state) {
return false;
}
satisfiedConstraints = (satisfiedConstraints&~constraint) | (state ? constraint : 0);
return true;
}
IdleController.java
现在我们需要知道是哪里调用了JobStatus的setIdleContraintSatisfied方法。来看IdleController.java的reportNewIdleState
/**
* Interaction with the task manager service
*/
void reportNewIdleState(boolean isIdle) {
synchronized (mLock) {
for (int i = mTrackedTasks.size()-1; i >= 0; i--) {
mTrackedTasks.valueAt(i).setIdleConstraintSatisfied(isIdle);
}
}
mStateChangedListener.onControllerStateChanged();
}
再来看谁调用这reportNewIdleState,这里最后一行mStateChangedListener.onControllerStateChanged()是通知JosSchedulerService检查满足条件的Job队列
final class IdlenessTracker extends BroadcastReceiver {
private AlarmManager mAlarm;
...
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (action.equals(Intent.ACTION_SCREEN_ON)
|| action.equals(Intent.ACTION_DREAMING_STOPPED)
|| action.equals(Intent.ACTION_DOCK_ACTIVE)) {
...
if (mIdle) {
// possible transition to not-idle
mIdle = false;
reportNewIdleState(mIdle);
}
} else if (action.equals(Intent.ACTION_SCREEN_OFF)
|| action.equals(Intent.ACTION_DREAMING_STARTED)
|| action.equals(Intent.ACTION_DOCK_IDLE)) {
// when the screen goes off or dreaming starts or wireless charging dock in idle,
// we schedule the alarm that will tell us when we have decided the device is
// truly idle.
if (action.equals(Intent.ACTION_DOCK_IDLE)) {
if (!mScreenOn) {
// Ignore this intent during screen off
return;
} else {
mDockIdle = true;
}
} else {
mScreenOn = false;
mDockIdle = false;
}
final long nowElapsed = sElapsedRealtimeClock.millis();
final long when = nowElapsed + mInactivityIdleThreshold;
if (DEBUG) {
Slog.v(TAG, "Scheduling idle : " + action + " now:" + nowElapsed + " when="
+ when);
}
mAlarm.setWindow(AlarmManager.ELAPSED_REALTIME_WAKEUP,
when, mIdleWindowSlop, "JS idleness", mIdleAlarmListener, null);
} else if (action.equals(ActivityManagerService.ACTION_TRIGGER_IDLE)) {
handleIdleTrigger();
}
}
private void handleIdleTrigger() {
// idle time starts now. Do not set mIdle if screen is on.
if (!mIdle && (!mScreenOn || mDockIdle)) {
if (DEBUG) {
Slog.v(TAG, "Idle trigger fired @ " + sElapsedRealtimeClock.millis());
}
mIdle = true;
reportNewIdleState(mIdle);
} else {
if (DEBUG) {
Slog.v(TAG, "TRIGGER_IDLE received but not changing state; idle="
+ mIdle + " screen=" + mScreenOn);
}
}
}
}
private AlarmManager.OnAlarmListener mIdleAlarmListener = () -> {
handleIdleTrigger();
};
很显然,IdleController里面创建了一个receiver,来修改jobstatus的deviceidle状态,一共有三个分支:
第一个分支:当屏幕亮起时,设置deviceidle为false
第二个分支:当屏幕灭掉时,设置一个alarm(定时器),时间为mInactivityIdleThreshold,到时后去调用下handleIdleTrigger方法
第三个分支:收到ActivityManagerService.ACTION_TRIGGER_IDLE这个广播后,直接调用handleIdleTrigger方法。这个广播就是在手动触发job时提到的广播。
仔细看一下handleIdleTrigger方法。如果之前不是idle状态,切现在锁屏了,那么就会把JobStatus的deviceidle置为true,这样就触发了我们所设置的条件。
那么真实的触发场景是怎样的:
1. 就像上面讲的,收到ActivityManagerService.ACTION_TRIGGER_IDLE系统发出的广播。具体发这个广播的逻辑我就不分析了。
2. 上面第二个分支做了一个定时,时间为mInactivityIdleThreshold,他的值是4260000(71分钟)。大概就是71分钟内没有用户进行手机操作,就会触发一次idle状态的检查把JobStatus的deviceidle设为true。我没等,有兴趣的可以试试顺便告诉我下答案。
/**
* Idle state tracking, and messaging with the task manager when
* significant state changes occur
*/
private void initIdleStateTracking() {
mInactivityIdleThreshold = mContext.getResources().getInteger(
com.android.internal.R.integer.config_jobSchedulerInactivityIdleThreshold);
mIdleWindowSlop = mContext.getResources().getInteger(
com.android.internal.R.integer.config_jobSchedulerIdleWindowSlop);
mIdleTracker = new IdlenessTracker();
mIdleTracker.startTracking();
}
4260000
300000