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