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>

 

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值