1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.server.job.controllers;
18
19import android.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.os.SystemClock;
24import android.os.UserHandle;
25import android.util.ArraySet;
26import android.util.Slog;
27
28import com.android.internal.annotations.VisibleForTesting;
29import com.android.server.job.JobSchedulerService;
30import com.android.server.job.StateChangedListener;
31import com.android.server.storage.DeviceStorageMonitorService;
32
33import java.io.PrintWriter;
34
35/**
36 * Simple controller that tracks the status of the device's storage.
37 */
38public final class StorageController extends StateController {
39    private static final String TAG = "JobScheduler.Stor";
40
41    private static final Object sCreationLock = new Object();
42    private static volatile StorageController sController;
43
44    private final ArraySet<JobStatus> mTrackedTasks = new ArraySet<JobStatus>();
45    private StorageTracker mStorageTracker;
46
47    public static StorageController get(JobSchedulerService taskManagerService) {
48        synchronized (sCreationLock) {
49            if (sController == null) {
50                sController = new StorageController(taskManagerService,
51                        taskManagerService.getContext(), taskManagerService.getLock());
52            }
53        }
54        return sController;
55    }
56
57    @VisibleForTesting
58    public StorageTracker getTracker() {
59        return mStorageTracker;
60    }
61
62    @VisibleForTesting
63    public static StorageController getForTesting(StateChangedListener stateChangedListener,
64            Context context) {
65        return new StorageController(stateChangedListener, context, new Object());
66    }
67
68    private StorageController(StateChangedListener stateChangedListener, Context context,
69            Object lock) {
70        super(stateChangedListener, context, lock);
71        mStorageTracker = new StorageTracker();
72        mStorageTracker.startTracking();
73    }
74
75    @Override
76    public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) {
77        if (taskStatus.hasStorageNotLowConstraint()) {
78            mTrackedTasks.add(taskStatus);
79            taskStatus.setTrackingController(JobStatus.TRACKING_STORAGE);
80            taskStatus.setStorageNotLowConstraintSatisfied(mStorageTracker.isStorageNotLow());
81        }
82    }
83
84    @Override
85    public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob,
86            boolean forUpdate) {
87        if (taskStatus.clearTrackingController(JobStatus.TRACKING_STORAGE)) {
88            mTrackedTasks.remove(taskStatus);
89        }
90    }
91
92    private void maybeReportNewStorageState() {
93        final boolean storageNotLow = mStorageTracker.isStorageNotLow();
94        boolean reportChange = false;
95        synchronized (mLock) {
96            for (int i = mTrackedTasks.size() - 1; i >= 0; i--) {
97                final JobStatus ts = mTrackedTasks.valueAt(i);
98                boolean previous = ts.setStorageNotLowConstraintSatisfied(storageNotLow);
99                if (previous != storageNotLow) {
100                    reportChange = true;
101                }
102            }
103        }
104        // Let the scheduler know that state has changed. This may or may not result in an
105        // execution.
106        if (reportChange) {
107            mStateChangedListener.onControllerStateChanged();
108        }
109        // Also tell the scheduler that any ready jobs should be flushed.
110        if (storageNotLow) {
111            mStateChangedListener.onRunJobNow(null);
112        }
113    }
114
115    public final class StorageTracker extends BroadcastReceiver {
116        /**
117         * Track whether storage is low.
118         */
119        private boolean mStorageLow;
120        /** Sequence number of last broadcast. */
121        private int mLastBatterySeq = -1;
122
123        public StorageTracker() {
124        }
125
126        public void startTracking() {
127            IntentFilter filter = new IntentFilter();
128
129            // Storage status.  Just need to register, since STORAGE_LOW is a sticky
130            // broadcast we will receive that if it is currently active.
131            filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
132            filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
133            mContext.registerReceiver(this, filter);
134        }
135
136        public boolean isStorageNotLow() {
137            return !mStorageLow;
138        }
139
140        public int getSeq() {
141            return mLastBatterySeq;
142        }
143
144        @Override
145        public void onReceive(Context context, Intent intent) {
146            onReceiveInternal(intent);
147        }
148
149        @VisibleForTesting
150        public void onReceiveInternal(Intent intent) {
151            final String action = intent.getAction();
152            mLastBatterySeq = intent.getIntExtra(DeviceStorageMonitorService.EXTRA_SEQUENCE,
153                    mLastBatterySeq);
154            if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) {
155                if (DEBUG) {
156                    Slog.d(TAG, "Available storage too low to do work. @ "
157                            + SystemClock.elapsedRealtime());
158                }
159                mStorageLow = true;
160            } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
161                if (DEBUG) {
162                    Slog.d(TAG, "Available stoage high enough to do work. @ "
163                            + SystemClock.elapsedRealtime());
164                }
165                mStorageLow = false;
166                maybeReportNewStorageState();
167            }
168        }
169    }
170
171    @Override
172    public void dumpControllerStateLocked(PrintWriter pw, int filterUid) {
173        pw.print("Storage: not low = ");
174        pw.print(mStorageTracker.isStorageNotLow());
175        pw.print(", seq=");
176        pw.println(mStorageTracker.getSeq());
177        pw.print("Tracking ");
178        pw.print(mTrackedTasks.size());
179        pw.println(":");
180        for (int i = 0; i < mTrackedTasks.size(); i++) {
181            final JobStatus js = mTrackedTasks.valueAt(i);
182            if (!js.shouldDump(filterUid)) {
183                continue;
184            }
185            pw.print("  #");
186            js.printUniqueId(pw);
187            pw.print(" from ");
188            UserHandle.formatUid(pw, js.getSourceUid());
189            pw.println();
190        }
191    }
192}
193