需求
很多時候會遇到一些類似雲控開關或下載升級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 <strong>or</strong> 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();
}
<!-- Inactivity threshold (in milliseconds) used in JobScheduler. JobScheduler will consider
the device to be "idle" after being inactive for this long. -->
<integer name="config_jobSchedulerInactivityIdleThreshold">4260000</integer>
<!-- The alarm window (in milliseconds) that JobScheduler uses to enter the idle state -->
<integer name="config_jobSchedulerIdleWindowSlop">300000</integer>