JobService 觸發deviceidle條件(源碼分析)

需求

很多時候會遇到一些類似雲控開關或下載升級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>

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章