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