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 
17 package com.android.server.job.controllers;
18 
19 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
20 
21 import android.content.BroadcastReceiver;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.os.UserHandle;
26 import android.util.ArraySet;
27 import android.util.IndentingPrintWriter;
28 import android.util.Log;
29 import android.util.Slog;
30 import android.util.proto.ProtoOutputStream;
31 
32 import com.android.internal.annotations.VisibleForTesting;
33 import com.android.server.job.JobSchedulerService;
34 import com.android.server.job.StateControllerProto;
35 import com.android.server.storage.DeviceStorageMonitorService;
36 
37 import java.util.function.Predicate;
38 
39 /**
40  * Simple controller that tracks the status of the device's storage.
41  */
42 public final class StorageController extends StateController {
43     private static final String TAG = "JobScheduler.Storage";
44     private static final boolean DEBUG = JobSchedulerService.DEBUG
45             || Log.isLoggable(TAG, Log.DEBUG);
46 
47     private final ArraySet<JobStatus> mTrackedTasks = new ArraySet<JobStatus>();
48     private final StorageTracker mStorageTracker;
49 
50     @VisibleForTesting
getTracker()51     public StorageTracker getTracker() {
52         return mStorageTracker;
53     }
54 
StorageController(JobSchedulerService service)55     public StorageController(JobSchedulerService service) {
56         super(service);
57         mStorageTracker = new StorageTracker();
58         mStorageTracker.startTracking();
59     }
60 
61     @Override
maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob)62     public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) {
63         if (taskStatus.hasStorageNotLowConstraint()) {
64             final long nowElapsed = sElapsedRealtimeClock.millis();
65             mTrackedTasks.add(taskStatus);
66             taskStatus.setTrackingController(JobStatus.TRACKING_STORAGE);
67             taskStatus.setStorageNotLowConstraintSatisfied(
68                     nowElapsed, mStorageTracker.isStorageNotLow());
69         }
70     }
71 
72     @Override
maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate)73     public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob,
74             boolean forUpdate) {
75         if (taskStatus.clearTrackingController(JobStatus.TRACKING_STORAGE)) {
76             mTrackedTasks.remove(taskStatus);
77         }
78     }
79 
maybeReportNewStorageState()80     private void maybeReportNewStorageState() {
81         final long nowElapsed = sElapsedRealtimeClock.millis();
82         final boolean storageNotLow = mStorageTracker.isStorageNotLow();
83         boolean reportChange = false;
84         synchronized (mLock) {
85             for (int i = mTrackedTasks.size() - 1; i >= 0; i--) {
86                 final JobStatus ts = mTrackedTasks.valueAt(i);
87                 reportChange |= ts.setStorageNotLowConstraintSatisfied(nowElapsed, storageNotLow);
88             }
89         }
90         if (storageNotLow) {
91             // Tell the scheduler that any ready jobs should be flushed.
92             mStateChangedListener.onRunJobNow(null);
93         } else if (reportChange) {
94             // Let the scheduler know that state has changed. This may or may not result in an
95             // execution.
96             mStateChangedListener.onControllerStateChanged();
97         }
98     }
99 
100     public final class StorageTracker extends BroadcastReceiver {
101         /**
102          * Track whether storage is low.
103          */
104         private boolean mStorageLow;
105         /** Sequence number of last broadcast. */
106         private int mLastStorageSeq = -1;
107 
StorageTracker()108         public StorageTracker() {
109         }
110 
startTracking()111         public void startTracking() {
112             IntentFilter filter = new IntentFilter();
113 
114             // Storage status.  Just need to register, since STORAGE_LOW is a sticky
115             // broadcast we will receive that if it is currently active.
116             filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
117             filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
118             mContext.registerReceiver(this, filter);
119         }
120 
isStorageNotLow()121         public boolean isStorageNotLow() {
122             return !mStorageLow;
123         }
124 
getSeq()125         public int getSeq() {
126             return mLastStorageSeq;
127         }
128 
129         @Override
onReceive(Context context, Intent intent)130         public void onReceive(Context context, Intent intent) {
131             onReceiveInternal(intent);
132         }
133 
134         @VisibleForTesting
onReceiveInternal(Intent intent)135         public void onReceiveInternal(Intent intent) {
136             final String action = intent.getAction();
137             mLastStorageSeq = intent.getIntExtra(DeviceStorageMonitorService.EXTRA_SEQUENCE,
138                     mLastStorageSeq);
139             if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) {
140                 if (DEBUG) {
141                     Slog.d(TAG, "Available storage too low to do work. @ "
142                             + sElapsedRealtimeClock.millis());
143                 }
144                 mStorageLow = true;
145                 maybeReportNewStorageState();
146             } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
147                 if (DEBUG) {
148                     Slog.d(TAG, "Available storage high enough to do work. @ "
149                             + sElapsedRealtimeClock.millis());
150                 }
151                 mStorageLow = false;
152                 maybeReportNewStorageState();
153             }
154         }
155     }
156 
157     @Override
dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate)158     public void dumpControllerStateLocked(IndentingPrintWriter pw,
159             Predicate<JobStatus> predicate) {
160         pw.println("Not low: " + mStorageTracker.isStorageNotLow());
161         pw.println("Sequence: " + mStorageTracker.getSeq());
162         pw.println();
163 
164         for (int i = 0; i < mTrackedTasks.size(); i++) {
165             final JobStatus js = mTrackedTasks.valueAt(i);
166             if (!predicate.test(js)) {
167                 continue;
168             }
169             pw.print("#");
170             js.printUniqueId(pw);
171             pw.print(" from ");
172             UserHandle.formatUid(pw, js.getSourceUid());
173             pw.println();
174         }
175     }
176 
177     @Override
dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, Predicate<JobStatus> predicate)178     public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
179             Predicate<JobStatus> predicate) {
180         final long token = proto.start(fieldId);
181         final long mToken = proto.start(StateControllerProto.STORAGE);
182 
183         proto.write(StateControllerProto.StorageController.IS_STORAGE_NOT_LOW,
184                 mStorageTracker.isStorageNotLow());
185         proto.write(StateControllerProto.StorageController.LAST_BROADCAST_SEQUENCE_NUMBER,
186                 mStorageTracker.getSeq());
187 
188         for (int i = 0; i < mTrackedTasks.size(); i++) {
189             final JobStatus js = mTrackedTasks.valueAt(i);
190             if (!predicate.test(js)) {
191                 continue;
192             }
193             final long jsToken = proto.start(StateControllerProto.StorageController.TRACKED_JOBS);
194             js.writeToShortProto(proto, StateControllerProto.StorageController.TrackedJob.INFO);
195             proto.write(StateControllerProto.StorageController.TrackedJob.SOURCE_UID,
196                     js.getSourceUid());
197             proto.end(jsToken);
198         }
199 
200         proto.end(mToken);
201         proto.end(token);
202     }
203 }
204