1 /*
2  * Copyright 2020 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 package com.android.server.blob;
17 
18 import static android.provider.DeviceConfig.NAMESPACE_BLOBSTORE;
19 import static android.text.format.Formatter.FLAG_IEC_UNITS;
20 import static android.text.format.Formatter.formatFileSize;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.content.Context;
25 import android.os.Environment;
26 import android.provider.DeviceConfig;
27 import android.provider.DeviceConfig.Properties;
28 import android.text.TextUtils;
29 import android.util.DataUnit;
30 import android.util.IndentingPrintWriter;
31 import android.util.Log;
32 import android.util.Slog;
33 import android.util.TimeUtils;
34 
35 import java.io.File;
36 import java.util.concurrent.TimeUnit;
37 
38 class BlobStoreConfig {
39     public static final String TAG = "BlobStore";
40     public static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE);
41 
42     // Initial version.
43     public static final int XML_VERSION_INIT = 1;
44     // Added a string variant of lease description.
45     public static final int XML_VERSION_ADD_STRING_DESC = 2;
46     public static final int XML_VERSION_ADD_DESC_RES_NAME = 3;
47     public static final int XML_VERSION_ADD_COMMIT_TIME = 4;
48     public static final int XML_VERSION_ADD_SESSION_CREATION_TIME = 5;
49     public static final int XML_VERSION_ALLOW_ACCESS_ACROSS_USERS = 6;
50 
51     public static final int XML_VERSION_CURRENT = XML_VERSION_ALLOW_ACCESS_ACROSS_USERS;
52 
53     public static final long INVALID_BLOB_ID = 0;
54     public static final long INVALID_BLOB_SIZE = 0;
55 
56     private static final String ROOT_DIR_NAME = "blobstore";
57     private static final String BLOBS_DIR_NAME = "blobs";
58     private static final String SESSIONS_INDEX_FILE_NAME = "sessions_index.xml";
59     private static final String BLOBS_INDEX_FILE_NAME = "blobs_index.xml";
60 
61     /**
62      * Job Id for idle maintenance job ({@link BlobStoreIdleJobService}).
63      */
64     public static final int IDLE_JOB_ID = 0xB70B1D7; // 191934935L
65 
66     public static class DeviceConfigProperties {
67         /**
68          * Denotes the max time period (in millis) between each idle maintenance job run.
69          */
70         public static final String KEY_IDLE_JOB_PERIOD_MS = "idle_job_period_ms";
71         public static final long DEFAULT_IDLE_JOB_PERIOD_MS = TimeUnit.DAYS.toMillis(1);
72         public static long IDLE_JOB_PERIOD_MS = DEFAULT_IDLE_JOB_PERIOD_MS;
73 
74         /**
75          * Denotes the timeout in millis after which sessions with no updates will be deleted.
76          */
77         public static final String KEY_SESSION_EXPIRY_TIMEOUT_MS =
78                 "session_expiry_timeout_ms";
79         public static final long DEFAULT_SESSION_EXPIRY_TIMEOUT_MS = TimeUnit.DAYS.toMillis(7);
80         public static long SESSION_EXPIRY_TIMEOUT_MS = DEFAULT_SESSION_EXPIRY_TIMEOUT_MS;
81 
82         /**
83          * Denotes how low the limit for the amount of data, that an app will be allowed to acquire
84          * a lease on, can be.
85          */
86         public static final String KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR =
87                 "total_bytes_per_app_limit_floor";
88         public static final long DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR =
89                 DataUnit.MEBIBYTES.toBytes(300); // 300 MiB
90         public static long TOTAL_BYTES_PER_APP_LIMIT_FLOOR =
91                 DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR;
92 
93         /**
94          * Denotes the maximum amount of data an app can acquire a lease on, in terms of fraction
95          * of total disk space.
96          */
97         public static final String KEY_TOTAL_BYTES_PER_APP_LIMIT_FRACTION =
98                 "total_bytes_per_app_limit_fraction";
99         public static final float DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FRACTION = 0.01f;
100         public static float TOTAL_BYTES_PER_APP_LIMIT_FRACTION =
101                 DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FRACTION;
102 
103         /**
104          * Denotes the duration from the time a blob is committed that we wait for a lease to
105          * be acquired before deciding to delete the blob for having no leases.
106          */
107         public static final String KEY_LEASE_ACQUISITION_WAIT_DURATION_MS =
108                 "lease_acquisition_wait_time_ms";
109         public static final long DEFAULT_LEASE_ACQUISITION_WAIT_DURATION_MS =
110                 TimeUnit.HOURS.toMillis(6);
111         public static long LEASE_ACQUISITION_WAIT_DURATION_MS =
112                 DEFAULT_LEASE_ACQUISITION_WAIT_DURATION_MS;
113 
114         /**
115          * Denotes the duration from the time a blob is committed that any new commits of the same
116          * data blob from the same committer will be treated as if they occurred at the earlier
117          * commit time.
118          */
119         public static final String KEY_COMMIT_COOL_OFF_DURATION_MS =
120                 "commit_cool_off_duration_ms";
121         public static final long DEFAULT_COMMIT_COOL_OFF_DURATION_MS =
122                 TimeUnit.HOURS.toMillis(48);
123         public static long COMMIT_COOL_OFF_DURATION_MS =
124                 DEFAULT_COMMIT_COOL_OFF_DURATION_MS;
125 
126         /**
127          * Denotes whether to use RevocableFileDescriptor when apps try to read session/blob data.
128          */
129         public static final String KEY_USE_REVOCABLE_FD_FOR_READS =
130                 "use_revocable_fd_for_reads";
131         public static final boolean DEFAULT_USE_REVOCABLE_FD_FOR_READS = false;
132         public static boolean USE_REVOCABLE_FD_FOR_READS =
133                 DEFAULT_USE_REVOCABLE_FD_FOR_READS;
134 
135         /**
136          * Denotes how long before a blob is deleted, once the last lease on it is released.
137          */
138         public static final String KEY_DELETE_ON_LAST_LEASE_DELAY_MS =
139                 "delete_on_last_lease_delay_ms";
140         public static final long DEFAULT_DELETE_ON_LAST_LEASE_DELAY_MS =
141                 TimeUnit.HOURS.toMillis(6);
142         public static long DELETE_ON_LAST_LEASE_DELAY_MS =
143                 DEFAULT_DELETE_ON_LAST_LEASE_DELAY_MS;
144 
145         /**
146          * Denotes the maximum number of active sessions per app at any time.
147          */
148         public static final String KEY_MAX_ACTIVE_SESSIONS = "max_active_sessions";
149         public static int DEFAULT_MAX_ACTIVE_SESSIONS = 250;
150         public static int MAX_ACTIVE_SESSIONS = DEFAULT_MAX_ACTIVE_SESSIONS;
151 
152         /**
153          * Denotes the maximum number of committed blobs per app at any time.
154          */
155         public static final String KEY_MAX_COMMITTED_BLOBS = "max_committed_blobs";
156         public static int DEFAULT_MAX_COMMITTED_BLOBS = 1000;
157         public static int MAX_COMMITTED_BLOBS = DEFAULT_MAX_COMMITTED_BLOBS;
158 
159         /**
160          * Denotes the maximum number of leased blobs per app at any time.
161          */
162         public static final String KEY_MAX_LEASED_BLOBS = "max_leased_blobs";
163         public static int DEFAULT_MAX_LEASED_BLOBS = 500;
164         public static int MAX_LEASED_BLOBS = DEFAULT_MAX_LEASED_BLOBS;
165 
166         /**
167          * Denotes the maximum number of packages explicitly permitted to access a blob
168          * (permitted as part of creating a {@link BlobAccessMode}).
169          */
170         public static final String KEY_MAX_BLOB_ACCESS_PERMITTED_PACKAGES = "max_permitted_pks";
171         public static int DEFAULT_MAX_BLOB_ACCESS_PERMITTED_PACKAGES = 300;
172         public static int MAX_BLOB_ACCESS_PERMITTED_PACKAGES =
173                 DEFAULT_MAX_BLOB_ACCESS_PERMITTED_PACKAGES;
174 
175         /**
176          * Denotes the maximum number of characters that a lease description can have.
177          */
178         public static final String KEY_LEASE_DESC_CHAR_LIMIT = "lease_desc_char_limit";
179         public static int DEFAULT_LEASE_DESC_CHAR_LIMIT = 300;
180         public static int LEASE_DESC_CHAR_LIMIT = DEFAULT_LEASE_DESC_CHAR_LIMIT;
181 
refresh(Properties properties)182         static void refresh(Properties properties) {
183             if (!NAMESPACE_BLOBSTORE.equals(properties.getNamespace())) {
184                 return;
185             }
186             properties.getKeyset().forEach(key -> {
187                 switch (key) {
188                     case KEY_IDLE_JOB_PERIOD_MS:
189                         IDLE_JOB_PERIOD_MS = properties.getLong(key, DEFAULT_IDLE_JOB_PERIOD_MS);
190                         break;
191                     case KEY_SESSION_EXPIRY_TIMEOUT_MS:
192                         SESSION_EXPIRY_TIMEOUT_MS = properties.getLong(key,
193                                 DEFAULT_SESSION_EXPIRY_TIMEOUT_MS);
194                         break;
195                     case KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR:
196                         TOTAL_BYTES_PER_APP_LIMIT_FLOOR = properties.getLong(key,
197                                 DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR);
198                         break;
199                     case KEY_TOTAL_BYTES_PER_APP_LIMIT_FRACTION:
200                         TOTAL_BYTES_PER_APP_LIMIT_FRACTION = properties.getFloat(key,
201                                 DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FRACTION);
202                         break;
203                     case KEY_LEASE_ACQUISITION_WAIT_DURATION_MS:
204                         LEASE_ACQUISITION_WAIT_DURATION_MS = properties.getLong(key,
205                                 DEFAULT_LEASE_ACQUISITION_WAIT_DURATION_MS);
206                         break;
207                     case KEY_COMMIT_COOL_OFF_DURATION_MS:
208                         COMMIT_COOL_OFF_DURATION_MS = properties.getLong(key,
209                                 DEFAULT_COMMIT_COOL_OFF_DURATION_MS);
210                         break;
211                     case KEY_USE_REVOCABLE_FD_FOR_READS:
212                         USE_REVOCABLE_FD_FOR_READS = properties.getBoolean(key,
213                                 DEFAULT_USE_REVOCABLE_FD_FOR_READS);
214                         break;
215                     case KEY_DELETE_ON_LAST_LEASE_DELAY_MS:
216                         DELETE_ON_LAST_LEASE_DELAY_MS = properties.getLong(key,
217                                 DEFAULT_DELETE_ON_LAST_LEASE_DELAY_MS);
218                         break;
219                     case KEY_MAX_ACTIVE_SESSIONS:
220                         MAX_ACTIVE_SESSIONS = properties.getInt(key, DEFAULT_MAX_ACTIVE_SESSIONS);
221                         break;
222                     case KEY_MAX_COMMITTED_BLOBS:
223                         MAX_COMMITTED_BLOBS = properties.getInt(key, DEFAULT_MAX_COMMITTED_BLOBS);
224                         break;
225                     case KEY_MAX_LEASED_BLOBS:
226                         MAX_LEASED_BLOBS = properties.getInt(key, DEFAULT_MAX_LEASED_BLOBS);
227                         break;
228                     case KEY_MAX_BLOB_ACCESS_PERMITTED_PACKAGES:
229                         MAX_BLOB_ACCESS_PERMITTED_PACKAGES = properties.getInt(key,
230                                 DEFAULT_MAX_BLOB_ACCESS_PERMITTED_PACKAGES);
231                         break;
232                     case KEY_LEASE_DESC_CHAR_LIMIT:
233                         LEASE_DESC_CHAR_LIMIT = properties.getInt(key,
234                                 DEFAULT_LEASE_DESC_CHAR_LIMIT);
235                         break;
236                     default:
237                         Slog.wtf(TAG, "Unknown key in device config properties: " + key);
238                 }
239             });
240         }
241 
dump(IndentingPrintWriter fout, Context context)242         static void dump(IndentingPrintWriter fout, Context context) {
243             final String dumpFormat = "%s: [cur: %s, def: %s]";
244             fout.println(String.format(dumpFormat, KEY_IDLE_JOB_PERIOD_MS,
245                     TimeUtils.formatDuration(IDLE_JOB_PERIOD_MS),
246                     TimeUtils.formatDuration(DEFAULT_IDLE_JOB_PERIOD_MS)));
247             fout.println(String.format(dumpFormat, KEY_SESSION_EXPIRY_TIMEOUT_MS,
248                     TimeUtils.formatDuration(SESSION_EXPIRY_TIMEOUT_MS),
249                     TimeUtils.formatDuration(DEFAULT_SESSION_EXPIRY_TIMEOUT_MS)));
250             fout.println(String.format(dumpFormat, KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR,
251                     formatFileSize(context, TOTAL_BYTES_PER_APP_LIMIT_FLOOR, FLAG_IEC_UNITS),
252                     formatFileSize(context, DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR,
253                             FLAG_IEC_UNITS)));
254             fout.println(String.format(dumpFormat, KEY_TOTAL_BYTES_PER_APP_LIMIT_FRACTION,
255                     TOTAL_BYTES_PER_APP_LIMIT_FRACTION,
256                     DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FRACTION));
257             fout.println(String.format(dumpFormat, KEY_LEASE_ACQUISITION_WAIT_DURATION_MS,
258                     TimeUtils.formatDuration(LEASE_ACQUISITION_WAIT_DURATION_MS),
259                     TimeUtils.formatDuration(DEFAULT_LEASE_ACQUISITION_WAIT_DURATION_MS)));
260             fout.println(String.format(dumpFormat, KEY_COMMIT_COOL_OFF_DURATION_MS,
261                     TimeUtils.formatDuration(COMMIT_COOL_OFF_DURATION_MS),
262                     TimeUtils.formatDuration(DEFAULT_COMMIT_COOL_OFF_DURATION_MS)));
263             fout.println(String.format(dumpFormat, KEY_USE_REVOCABLE_FD_FOR_READS,
264                     USE_REVOCABLE_FD_FOR_READS, DEFAULT_USE_REVOCABLE_FD_FOR_READS));
265             fout.println(String.format(dumpFormat, KEY_DELETE_ON_LAST_LEASE_DELAY_MS,
266                     TimeUtils.formatDuration(DELETE_ON_LAST_LEASE_DELAY_MS),
267                     TimeUtils.formatDuration(DEFAULT_DELETE_ON_LAST_LEASE_DELAY_MS)));
268             fout.println(String.format(dumpFormat, KEY_MAX_ACTIVE_SESSIONS,
269                     MAX_ACTIVE_SESSIONS, DEFAULT_MAX_ACTIVE_SESSIONS));
270             fout.println(String.format(dumpFormat, KEY_MAX_COMMITTED_BLOBS,
271                     MAX_COMMITTED_BLOBS, DEFAULT_MAX_COMMITTED_BLOBS));
272             fout.println(String.format(dumpFormat, KEY_MAX_LEASED_BLOBS,
273                     MAX_LEASED_BLOBS, DEFAULT_MAX_LEASED_BLOBS));
274             fout.println(String.format(dumpFormat, KEY_MAX_BLOB_ACCESS_PERMITTED_PACKAGES,
275                     MAX_BLOB_ACCESS_PERMITTED_PACKAGES,
276                     DEFAULT_MAX_BLOB_ACCESS_PERMITTED_PACKAGES));
277             fout.println(String.format(dumpFormat, KEY_LEASE_DESC_CHAR_LIMIT,
278                     LEASE_DESC_CHAR_LIMIT, DEFAULT_LEASE_DESC_CHAR_LIMIT));
279         }
280     }
281 
initialize(Context context)282     public static void initialize(Context context) {
283         DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_BLOBSTORE,
284                 context.getMainExecutor(),
285                 properties -> DeviceConfigProperties.refresh(properties));
286         DeviceConfigProperties.refresh(DeviceConfig.getProperties(NAMESPACE_BLOBSTORE));
287     }
288 
289     /**
290      * Returns the max time period (in millis) between each idle maintenance job run.
291      */
getIdleJobPeriodMs()292     public static long getIdleJobPeriodMs() {
293         return DeviceConfigProperties.IDLE_JOB_PERIOD_MS;
294     }
295 
296     /**
297      * Returns whether a session is expired or not. A session is considered expired if the session
298      * has not been modified in a while (i.e. SESSION_EXPIRY_TIMEOUT_MS).
299      */
hasSessionExpired(long sessionLastModifiedMs)300     public static boolean hasSessionExpired(long sessionLastModifiedMs) {
301         return sessionLastModifiedMs
302                 < System.currentTimeMillis() - DeviceConfigProperties.SESSION_EXPIRY_TIMEOUT_MS;
303     }
304 
305     /**
306      * Returns the maximum amount of data that an app can acquire a lease on.
307      */
getAppDataBytesLimit()308     public static long getAppDataBytesLimit() {
309         final long totalBytesLimit = (long) (Environment.getDataSystemDirectory().getTotalSpace()
310                 * DeviceConfigProperties.TOTAL_BYTES_PER_APP_LIMIT_FRACTION);
311         return Math.max(DeviceConfigProperties.TOTAL_BYTES_PER_APP_LIMIT_FLOOR, totalBytesLimit);
312     }
313 
314     /**
315      * Returns whether the wait time for lease acquisition for a blob has elapsed.
316      */
hasLeaseWaitTimeElapsed(long commitTimeMs)317     public static boolean hasLeaseWaitTimeElapsed(long commitTimeMs) {
318         return commitTimeMs + DeviceConfigProperties.LEASE_ACQUISITION_WAIT_DURATION_MS
319                 < System.currentTimeMillis();
320     }
321 
322     /**
323      * Returns an adjusted commit time depending on whether commit cool-off period has elapsed.
324      *
325      * If this is the initial commit or the earlier commit cool-off period has elapsed, then
326      * the new commit time is used. Otherwise, the earlier commit time is used.
327      */
getAdjustedCommitTimeMs(long oldCommitTimeMs, long newCommitTimeMs)328     public static long getAdjustedCommitTimeMs(long oldCommitTimeMs, long newCommitTimeMs) {
329         if (oldCommitTimeMs == 0 || hasCommitCoolOffPeriodElapsed(oldCommitTimeMs)) {
330             return newCommitTimeMs;
331         }
332         return oldCommitTimeMs;
333     }
334 
335     /**
336      * Returns whether the commit cool-off period has elapsed.
337      */
hasCommitCoolOffPeriodElapsed(long commitTimeMs)338     private static boolean hasCommitCoolOffPeriodElapsed(long commitTimeMs) {
339         return commitTimeMs + DeviceConfigProperties.COMMIT_COOL_OFF_DURATION_MS
340                 < System.currentTimeMillis();
341     }
342 
343     /**
344      * Return whether to use RevocableFileDescriptor when apps try to read session/blob data.
345      */
shouldUseRevocableFdForReads()346     public static boolean shouldUseRevocableFdForReads() {
347         return DeviceConfigProperties.USE_REVOCABLE_FD_FOR_READS;
348     }
349 
350     /**
351      * Returns the duration to wait before a blob is deleted, once the last lease on it is released.
352      */
getDeletionOnLastLeaseDelayMs()353     public static long getDeletionOnLastLeaseDelayMs() {
354         return DeviceConfigProperties.DELETE_ON_LAST_LEASE_DELAY_MS;
355     }
356 
357     /**
358      * Returns the maximum number of active sessions per app.
359      */
getMaxActiveSessions()360     public static int getMaxActiveSessions() {
361         return DeviceConfigProperties.MAX_ACTIVE_SESSIONS;
362     }
363 
364     /**
365      * Returns the maximum number of committed blobs per app.
366      */
getMaxCommittedBlobs()367     public static int getMaxCommittedBlobs() {
368         return DeviceConfigProperties.MAX_COMMITTED_BLOBS;
369     }
370 
371     /**
372      * Returns the maximum number of leased blobs per app.
373      */
getMaxLeasedBlobs()374     public static int getMaxLeasedBlobs() {
375         return DeviceConfigProperties.MAX_LEASED_BLOBS;
376     }
377 
378     /**
379      * Returns the maximum number of packages explicitly permitted to access a blob.
380      */
getMaxPermittedPackages()381     public static int getMaxPermittedPackages() {
382         return DeviceConfigProperties.MAX_BLOB_ACCESS_PERMITTED_PACKAGES;
383     }
384 
385     /**
386      * Returns the lease description truncated to
387      * {@link DeviceConfigProperties#LEASE_DESC_CHAR_LIMIT} characters.
388      */
getTruncatedLeaseDescription(CharSequence description)389     public static CharSequence getTruncatedLeaseDescription(CharSequence description) {
390         if (TextUtils.isEmpty(description)) {
391             return description;
392         }
393         return TextUtils.trimToLengthWithEllipsis(description,
394                 DeviceConfigProperties.LEASE_DESC_CHAR_LIMIT);
395     }
396 
397     @Nullable
prepareBlobFile(long sessionId)398     public static File prepareBlobFile(long sessionId) {
399         final File blobsDir = prepareBlobsDir();
400         return blobsDir == null ? null : getBlobFile(blobsDir, sessionId);
401     }
402 
403     @NonNull
getBlobFile(long sessionId)404     public static File getBlobFile(long sessionId) {
405         return getBlobFile(getBlobsDir(), sessionId);
406     }
407 
408     @NonNull
getBlobFile(File blobsDir, long sessionId)409     private static File getBlobFile(File blobsDir, long sessionId) {
410         return new File(blobsDir, String.valueOf(sessionId));
411     }
412 
413     @Nullable
prepareBlobsDir()414     public static File prepareBlobsDir() {
415         final File blobsDir = getBlobsDir(prepareBlobStoreRootDir());
416         if (!blobsDir.exists() && !blobsDir.mkdir()) {
417             Slog.e(TAG, "Failed to mkdir(): " + blobsDir);
418             return null;
419         }
420         return blobsDir;
421     }
422 
423     @NonNull
getBlobsDir()424     public static File getBlobsDir() {
425         return getBlobsDir(getBlobStoreRootDir());
426     }
427 
428     @NonNull
getBlobsDir(File blobsRootDir)429     private static File getBlobsDir(File blobsRootDir) {
430         return new File(blobsRootDir, BLOBS_DIR_NAME);
431     }
432 
433     @Nullable
prepareSessionIndexFile()434     public static File prepareSessionIndexFile() {
435         final File blobStoreRootDir = prepareBlobStoreRootDir();
436         if (blobStoreRootDir == null) {
437             return null;
438         }
439         return new File(blobStoreRootDir, SESSIONS_INDEX_FILE_NAME);
440     }
441 
442     @Nullable
prepareBlobsIndexFile()443     public static File prepareBlobsIndexFile() {
444         final File blobsStoreRootDir = prepareBlobStoreRootDir();
445         if (blobsStoreRootDir == null) {
446             return null;
447         }
448         return new File(blobsStoreRootDir, BLOBS_INDEX_FILE_NAME);
449     }
450 
451     @Nullable
prepareBlobStoreRootDir()452     public static File prepareBlobStoreRootDir() {
453         final File blobStoreRootDir = getBlobStoreRootDir();
454         if (!blobStoreRootDir.exists() && !blobStoreRootDir.mkdir()) {
455             Slog.e(TAG, "Failed to mkdir(): " + blobStoreRootDir);
456             return null;
457         }
458         return blobStoreRootDir;
459     }
460 
461     @NonNull
getBlobStoreRootDir()462     public static File getBlobStoreRootDir() {
463         return new File(Environment.getDataSystemDirectory(), ROOT_DIR_NAME);
464     }
465 
dump(IndentingPrintWriter fout, Context context)466     public static void dump(IndentingPrintWriter fout, Context context) {
467         fout.println("XML current version: " + XML_VERSION_CURRENT);
468 
469         fout.println("Idle job ID: " + IDLE_JOB_ID);
470 
471         fout.println("Total bytes per app limit: " + formatFileSize(context,
472                 getAppDataBytesLimit(), FLAG_IEC_UNITS));
473 
474         fout.println("Device config properties:");
475         fout.increaseIndent();
476         DeviceConfigProperties.dump(fout, context);
477         fout.decreaseIndent();
478     }
479 }
480