1 /* 2 * Copyright (C) 2018 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.providers.settings; 18 19 import static android.provider.Settings.Config.SYNC_DISABLED_MODE_NONE; 20 import static android.provider.Settings.Config.SYNC_DISABLED_MODE_PERSISTENT; 21 import static android.provider.Settings.Config.SYNC_DISABLED_MODE_UNTIL_REBOOT; 22 23 import android.aconfig.Aconfig.parsed_flag; 24 import android.aconfig.Aconfig.parsed_flags; 25 import android.annotation.SuppressLint; 26 import android.app.ActivityManager; 27 import android.content.AttributionSource; 28 import android.content.IContentProvider; 29 import android.os.Binder; 30 import android.os.Build; 31 import android.os.Bundle; 32 import android.os.ParcelFileDescriptor; 33 import android.os.Process; 34 import android.os.RemoteException; 35 import android.os.ResultReceiver; 36 import android.os.ShellCallback; 37 import android.os.ShellCommand; 38 import android.provider.DeviceConfig; 39 import android.provider.DeviceConfigShellCommandHandler; 40 import android.provider.Settings; 41 import android.provider.Settings.Config.SyncDisabledMode; 42 import android.provider.UpdatableDeviceConfigServiceReadiness; 43 import android.util.Slog; 44 45 import com.android.internal.util.FastPrintWriter; 46 47 import java.io.File; 48 import java.io.FileDescriptor; 49 import java.io.FileInputStream; 50 import java.io.FileOutputStream; 51 import java.io.IOException; 52 import java.io.PrintWriter; 53 import java.lang.reflect.Field; 54 import java.lang.reflect.Modifier; 55 import java.util.ArrayList; 56 import java.util.Collections; 57 import java.util.HashMap; 58 import java.util.HashSet; 59 import java.util.List; 60 import java.util.Map; 61 62 /** 63 * Receives shell commands from the command line related to device config flags, and dispatches them 64 * to the SettingsProvider. 65 */ 66 public final class DeviceConfigService extends Binder { 67 private static final List<String> sAconfigTextProtoFilesOnDevice = List.of( 68 "/system/etc/aconfig_flags.pb", 69 "/system_ext/etc/aconfig_flags.pb", 70 "/product/etc/aconfig_flags.pb", 71 "/vendor/etc/aconfig_flags.pb"); 72 73 private static final List<String> PRIVATE_NAMESPACES = List.of( 74 "device_config_overrides", 75 "staged", 76 "token_staged"); 77 78 final SettingsProvider mProvider; 79 80 private static final String TAG = "DeviceConfigService"; 81 DeviceConfigService(SettingsProvider provider)82 public DeviceConfigService(SettingsProvider provider) { 83 mProvider = provider; 84 } 85 86 @Override onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver resultReceiver)87 public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, 88 String[] args, ShellCallback callback, ResultReceiver resultReceiver) 89 throws RemoteException { 90 if (UpdatableDeviceConfigServiceReadiness.shouldStartUpdatableService()) { 91 callUpdableDeviceConfigShellCommandHandler(in, out, err, args, resultReceiver); 92 } else { 93 (new MyShellCommand(mProvider)) 94 .exec(this, in, out, err, args, callback, resultReceiver); 95 } 96 } 97 98 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)99 protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 100 final IContentProvider iprovider = mProvider.getIContentProvider(); 101 pw.println("DeviceConfig flags:"); 102 for (String line : MyShellCommand.listAll(iprovider)) { 103 pw.println(line); 104 } 105 106 ArrayList<String> missingFiles = new ArrayList<String>(); 107 for (String fileName : sAconfigTextProtoFilesOnDevice) { 108 File aconfigFile = new File(fileName); 109 if (!aconfigFile.exists()) { 110 missingFiles.add(fileName); 111 } 112 } 113 114 if (missingFiles.isEmpty()) { 115 pw.println("\nAconfig flags:"); 116 for (String name : MyShellCommand.listAllAconfigFlags(iprovider)) { 117 pw.println(name); 118 } 119 } else { 120 pw.println("\nFailed to dump aconfig flags due to missing files:"); 121 for (String fileName : missingFiles) { 122 pw.println(fileName); 123 } 124 } 125 } 126 getAconfigFlagNamesInDeviceConfig()127 private static HashSet<String> getAconfigFlagNamesInDeviceConfig() { 128 HashSet<String> nameSet = new HashSet<String>(); 129 try { 130 for (String fileName : sAconfigTextProtoFilesOnDevice) { 131 byte[] contents = (new FileInputStream(fileName)).readAllBytes(); 132 parsed_flags parsedFlags = parsed_flags.parseFrom(contents); 133 if (parsedFlags == null) { 134 Slog.e(TAG, "failed to parse aconfig protobuf from " + fileName); 135 continue; 136 } 137 138 for (parsed_flag flag : parsedFlags.getParsedFlagList()) { 139 String namespace = flag.getNamespace(); 140 String packageName = flag.getPackage(); 141 String name = flag.getName(); 142 nameSet.add(namespace + "/" + packageName + "." + name); 143 } 144 } 145 } catch (IOException e) { 146 Slog.e(TAG, "failed to read aconfig protobuf", e); 147 } 148 return nameSet; 149 } 150 callUpdableDeviceConfigShellCommandHandler(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ResultReceiver resultReceiver)151 private void callUpdableDeviceConfigShellCommandHandler(FileDescriptor in, FileDescriptor out, 152 FileDescriptor err, String[] args, ResultReceiver resultReceiver) { 153 int result = -1; 154 try ( 155 ParcelFileDescriptor inPfd = ParcelFileDescriptor.dup(in); 156 ParcelFileDescriptor outPfd = ParcelFileDescriptor.dup(out); 157 ParcelFileDescriptor errPfd = ParcelFileDescriptor.dup(err)) { 158 result = DeviceConfigShellCommandHandler.handleShellCommand(inPfd, outPfd, errPfd, 159 args); 160 } catch (IOException e) { 161 PrintWriter pw = new FastPrintWriter(new FileOutputStream(err)); 162 pw.println("dup() failed: " + e.getMessage()); 163 pw.flush(); 164 } finally { 165 resultReceiver.send(result, null); 166 } 167 } 168 169 static final class MyShellCommand extends ShellCommand { 170 final SettingsProvider mProvider; 171 172 enum CommandVerb { 173 GET, 174 PUT, 175 OVERRIDE, 176 CLEAR_OVERRIDE, 177 DELETE, 178 LIST, 179 LIST_NAMESPACES, 180 LIST_LOCAL_OVERRIDES, 181 RESET, 182 SET_SYNC_DISABLED_FOR_TESTS, 183 GET_SYNC_DISABLED_FOR_TESTS, 184 } 185 MyShellCommand(SettingsProvider provider)186 MyShellCommand(SettingsProvider provider) { 187 mProvider = provider; 188 } 189 getAllFlags(IContentProvider provider)190 public static HashMap<String, String> getAllFlags(IContentProvider provider) { 191 HashMap<String, String> allFlags = new HashMap<String, String>(); 192 try { 193 Bundle args = new Bundle(); 194 args.putInt(Settings.CALL_METHOD_USER_KEY, 195 ActivityManager.getService().getCurrentUser().id); 196 Bundle b = provider.call(new AttributionSource(Process.myUid(), 197 resolveCallingPackage(), null), Settings.AUTHORITY, 198 Settings.CALL_METHOD_LIST_CONFIG, null, args); 199 if (b != null) { 200 Map<String, String> flagsToValues = 201 (HashMap) b.getSerializable(Settings.NameValueTable.VALUE); 202 allFlags.putAll(flagsToValues); 203 } 204 } catch (RemoteException e) { 205 throw new RuntimeException("Failed in IPC", e); 206 } 207 208 return allFlags; 209 } 210 listAll(IContentProvider provider)211 public static List<String> listAll(IContentProvider provider) { 212 HashMap<String, String> allFlags = getAllFlags(provider); 213 final ArrayList<String> lines = new ArrayList<>(); 214 for (String key : allFlags.keySet()) { 215 lines.add(key + "=" + allFlags.get(key)); 216 } 217 Collections.sort(lines); 218 return lines; 219 } 220 log(String msg)221 private static void log(String msg) { 222 if (Build.IS_DEBUGGABLE) { 223 Slog.wtf(TAG, msg); 224 } else { 225 Slog.e(TAG, msg); 226 } 227 } 228 listAllAconfigFlags(IContentProvider provider)229 public static List<String> listAllAconfigFlags(IContentProvider provider) { 230 HashMap<String, String> allFlags = getAllFlags(provider); 231 HashSet<String> aconfigFlagNames = getAconfigFlagNamesInDeviceConfig(); 232 final ArrayList<String> lines = new ArrayList<>(); 233 for (String aconfigFlag : aconfigFlagNames) { 234 String val = allFlags.get(aconfigFlag); 235 if (val != null) { 236 // aconfigFlag is in the form of [namespace]/[package].[flag_name] 237 int idx = aconfigFlag.indexOf("/"); 238 if (idx == -1 || idx == aconfigFlag.length() - 1 || idx == 0) { 239 log("invalid flag entry in device config: " + aconfigFlag); 240 continue; 241 } 242 243 // we intend to print out [package].[flag_name] [namespace]=val 244 String aconfigFlagNameByPackage = aconfigFlag.substring(idx + 1); 245 String namespace = aconfigFlag.substring(0, idx); 246 lines.add("flag:" + aconfigFlagNameByPackage + " namespace:" + namespace + 247 " value:" + val); 248 } 249 } 250 Collections.sort(lines); 251 return lines; 252 } 253 254 @SuppressLint("AndroidFrameworkRequiresPermission") 255 @Override onCommand(String cmd)256 public int onCommand(String cmd) { 257 if (cmd == null || "help".equals(cmd) || "-h".equals(cmd)) { 258 onHelp(); 259 return -1; 260 } 261 262 final PrintWriter perr = getErrPrintWriter(); 263 boolean isValid = false; 264 265 CommandVerb verb; 266 if ("get".equalsIgnoreCase(cmd)) { 267 verb = CommandVerb.GET; 268 } else if ("put".equalsIgnoreCase(cmd)) { 269 verb = CommandVerb.PUT; 270 } else if ("override".equalsIgnoreCase(cmd)) { 271 verb = CommandVerb.OVERRIDE; 272 } else if ("clear_override".equalsIgnoreCase(cmd)) { 273 verb = CommandVerb.CLEAR_OVERRIDE; 274 } else if ("delete".equalsIgnoreCase(cmd)) { 275 verb = CommandVerb.DELETE; 276 } else if ("list".equalsIgnoreCase(cmd)) { 277 verb = CommandVerb.LIST; 278 if (peekNextArg() == null) { 279 isValid = true; 280 } 281 } else if ("list_namespaces".equalsIgnoreCase(cmd)) { 282 verb = CommandVerb.LIST_NAMESPACES; 283 if (peekNextArg() == null) { 284 isValid = true; 285 } 286 } else if ("list_local_overrides".equalsIgnoreCase(cmd)) { 287 verb = CommandVerb.LIST_LOCAL_OVERRIDES; 288 if (peekNextArg() == null) { 289 isValid = true; 290 } 291 } else if ("reset".equalsIgnoreCase(cmd)) { 292 verb = CommandVerb.RESET; 293 } else if ("set_sync_disabled_for_tests".equalsIgnoreCase(cmd)) { 294 verb = CommandVerb.SET_SYNC_DISABLED_FOR_TESTS; 295 } else if ("get_sync_disabled_for_tests".equalsIgnoreCase(cmd)) { 296 verb = CommandVerb.GET_SYNC_DISABLED_FOR_TESTS; 297 if (peekNextArg() != null) { 298 perr.println("Bad arguments"); 299 return -1; 300 } 301 isValid = true; 302 } else { 303 // invalid 304 perr.println("Invalid command: " + cmd); 305 return -1; 306 } 307 308 // Parse args for those commands that have them. 309 int syncDisabledModeArg = -1; 310 int resetMode = -1; 311 boolean makeDefault = false; 312 String namespace = null; 313 String key = null; 314 String value = null; 315 String arg; 316 boolean publicOnly = false; 317 while ((arg = getNextArg()) != null) { 318 if (verb == CommandVerb.RESET) { 319 if (resetMode == -1) { 320 // RESET 1st arg (required) 321 if ("untrusted_defaults".equalsIgnoreCase(arg)) { 322 resetMode = Settings.RESET_MODE_UNTRUSTED_DEFAULTS; 323 } else if ("untrusted_clear".equalsIgnoreCase(arg)) { 324 resetMode = Settings.RESET_MODE_UNTRUSTED_CHANGES; 325 } else if ("trusted_defaults".equalsIgnoreCase(arg)) { 326 resetMode = Settings.RESET_MODE_TRUSTED_DEFAULTS; 327 } else { 328 // invalid 329 perr.println("Invalid reset mode: " + arg); 330 return -1; 331 } 332 if (peekNextArg() == null) { 333 isValid = true; 334 } 335 } else { 336 // RESET 2nd arg (optional) 337 namespace = arg; 338 if (peekNextArg() == null) { 339 isValid = true; 340 } else { 341 // invalid 342 perr.println("Too many arguments"); 343 return -1; 344 } 345 } 346 } else if (verb == CommandVerb.SET_SYNC_DISABLED_FOR_TESTS) { 347 if (syncDisabledModeArg == -1) { 348 // SET_SYNC_DISABLED_FOR_TESTS 1st arg (required) 349 syncDisabledModeArg = parseSyncDisabledMode(arg); 350 if (syncDisabledModeArg == -1) { 351 // invalid 352 perr.println("Invalid sync disabled mode: " + arg); 353 return -1; 354 } 355 if (peekNextArg() == null) { 356 isValid = true; 357 } 358 } 359 } else if (verb == CommandVerb.LIST_NAMESPACES) { 360 if (arg.equals("--public")) { 361 isValid = true; 362 publicOnly = true; 363 } 364 } else if (namespace == null) { 365 // GET, PUT, OVERRIDE, DELETE, LIST 1st arg 366 namespace = arg; 367 if (verb == CommandVerb.LIST) { 368 if (peekNextArg() == null) { 369 isValid = true; 370 } else { 371 // invalid 372 perr.println("Too many arguments"); 373 return -1; 374 } 375 } 376 } else if (key == null) { 377 // GET, PUT, OVERRIDE, DELETE 2nd arg 378 key = arg; 379 boolean validVerb = verb == CommandVerb.GET 380 || verb == CommandVerb.DELETE 381 || verb == CommandVerb.CLEAR_OVERRIDE; 382 if (validVerb) { 383 // GET, DELETE only have 2 args 384 if (peekNextArg() == null) { 385 isValid = true; 386 } else { 387 // invalid 388 perr.println("Too many arguments"); 389 return -1; 390 } 391 } 392 } else if (value == null) { 393 // PUT, OVERRIDE 3rd arg (required) 394 value = arg; 395 boolean validVerb = verb == CommandVerb.PUT 396 || verb == CommandVerb.OVERRIDE; 397 if (validVerb && peekNextArg() == null) { 398 isValid = true; 399 } 400 } else if ("default".equalsIgnoreCase(arg)) { 401 // PUT 4th arg (optional) 402 makeDefault = true; 403 if (verb == CommandVerb.PUT && peekNextArg() == null) { 404 isValid = true; 405 } else { 406 // invalid 407 perr.println("Too many arguments"); 408 return -1; 409 } 410 } 411 } 412 413 if (!isValid) { 414 perr.println("Bad arguments"); 415 return -1; 416 } 417 418 final IContentProvider iprovider = mProvider.getIContentProvider(); 419 final PrintWriter pout = getOutPrintWriter(); 420 switch (verb) { 421 case GET: 422 pout.println(DeviceConfig.getProperty(namespace, key)); 423 break; 424 case PUT: 425 DeviceConfig.setProperty(namespace, key, value, makeDefault); 426 break; 427 case OVERRIDE: 428 DeviceConfig.setLocalOverride(namespace, key, value); 429 break; 430 case CLEAR_OVERRIDE: 431 DeviceConfig.clearLocalOverride(namespace, key); 432 break; 433 case DELETE: 434 pout.println(delete(iprovider, namespace, key) 435 ? "Successfully deleted " + key + " from " + namespace 436 : "Failed to delete " + key + " from " + namespace); 437 break; 438 case LIST: 439 if (namespace != null) { 440 DeviceConfig.Properties properties = 441 DeviceConfig.getProperties(namespace); 442 List<String> keys = new ArrayList<>(properties.getKeyset()); 443 Collections.sort(keys); 444 for (String name : keys) { 445 pout.println(name + "=" + properties.getString(name, null)); 446 } 447 } else { 448 for (String line : listAll(iprovider)) { 449 boolean isPrivate = false; 450 for (String privateNamespace : PRIVATE_NAMESPACES) { 451 if (line.startsWith(privateNamespace)) { 452 isPrivate = true; 453 break; 454 } 455 } 456 457 if (!isPrivate) { 458 pout.println(line); 459 } 460 } 461 } 462 break; 463 case LIST_NAMESPACES: 464 List<String> namespaces; 465 if (publicOnly) { 466 namespaces = DeviceConfig.getPublicNamespaces(); 467 } else { 468 Field[] fields = DeviceConfig.class.getDeclaredFields(); 469 namespaces = new ArrayList<>(fields.length); 470 // TODO(b/265948913): once moved to mainline, it should call a hidden method 471 // directly 472 for (Field field : fields) { 473 int modifiers = field.getModifiers(); 474 try { 475 if (Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers) 476 && field.getType().equals(String.class) 477 && field.getName().startsWith("NAMESPACE_")) { 478 namespaces.add((String) field.get(null)); 479 } 480 } catch (IllegalAccessException ignored) { } 481 } 482 } 483 for (int i = 0; i < namespaces.size(); i++) { 484 pout.println(namespaces.get(i)); 485 } 486 break; 487 case LIST_LOCAL_OVERRIDES: 488 Map<String, Map<String, String>> underlyingValues = 489 DeviceConfig.getUnderlyingValuesForOverriddenFlags(); 490 for (String overrideNamespace : underlyingValues.keySet()) { 491 Map<String, String> flagToValue = 492 underlyingValues.get(overrideNamespace); 493 for (String flag : flagToValue.keySet()) { 494 String flagText = overrideNamespace + "/" + flag; 495 String valueText = 496 DeviceConfig.getProperty(overrideNamespace, flag); 497 pout.println(flagText + "=" + valueText); 498 } 499 } 500 break; 501 case RESET: 502 DeviceConfig.resetToDefaults(resetMode, namespace); 503 break; 504 case SET_SYNC_DISABLED_FOR_TESTS: 505 DeviceConfig.setSyncDisabledMode(syncDisabledModeArg); 506 break; 507 case GET_SYNC_DISABLED_FOR_TESTS: 508 int syncDisabledModeInt = DeviceConfig.getSyncDisabledMode(); 509 String syncDisabledModeString = formatSyncDisabledMode(syncDisabledModeInt); 510 if (syncDisabledModeString == null) { 511 perr.println("Unknown mode: " + syncDisabledModeInt); 512 return -1; 513 } 514 pout.println(syncDisabledModeString); 515 break; 516 default: 517 perr.println("Unspecified command"); 518 return -1; 519 } 520 return 0; 521 } 522 523 @Override onHelp()524 public void onHelp() { 525 PrintWriter pw = getOutPrintWriter(); 526 pw.println("Device Config (device_config) commands:"); 527 pw.println(" help"); 528 pw.println(" Print this help text."); 529 pw.println(" get NAMESPACE KEY"); 530 pw.println(" Retrieve the current value of KEY from the given NAMESPACE."); 531 pw.println(" put NAMESPACE KEY VALUE [default]"); 532 pw.println(" Change the contents of KEY to VALUE for the given NAMESPACE."); 533 pw.println(" {default} to set as the default value."); 534 pw.println(" override NAMESPACE KEY VALUE"); 535 pw.println(" Set flag NAMESPACE/KEY to the given VALUE, and ignores " 536 + "server-updates for"); 537 pw.println(" this flag. This can still be called even if there is no underlying " 538 + "value set."); 539 pw.println(" delete NAMESPACE KEY"); 540 pw.println(" Delete the entry for KEY for the given NAMESPACE."); 541 pw.println(" clear_override NAMESPACE KEY"); 542 pw.println(" Clear local sticky flag override for KEY in the given NAMESPACE."); 543 pw.println(" list_namespaces [--public]"); 544 pw.println(" Prints the name of all (or just the public) namespaces."); 545 pw.println(" list [NAMESPACE]"); 546 pw.println(" Print all keys and values defined, optionally for the given " 547 + "NAMESPACE."); 548 pw.println(" list_local_overrides"); 549 pw.println(" Print all flags that have been overridden."); 550 pw.println(" reset RESET_MODE [NAMESPACE]"); 551 pw.println(" Reset all flag values, optionally for a NAMESPACE, according to " 552 + "RESET_MODE."); 553 pw.println(" RESET_MODE is one of {untrusted_defaults, untrusted_clear, " 554 + "trusted_defaults}"); 555 pw.println(" NAMESPACE limits which flags are reset if provided, otherwise all " 556 + "flags are reset"); 557 pw.println(" set_sync_disabled_for_tests SYNC_DISABLED_MODE"); 558 pw.println(" Modifies bulk property setting behavior for tests. When in one of the" 559 + " disabled modes"); 560 pw.println(" this ensures that config isn't overwritten. SYNC_DISABLED_MODE is " 561 + "one of:"); 562 pw.println(" none: Sync is not disabled. A reboot may be required to restart" 563 + " syncing."); 564 pw.println(" persistent: Sync is disabled, this state will survive a reboot."); 565 pw.println(" until_reboot: Sync is disabled until the next reboot."); 566 pw.println(" get_sync_disabled_for_tests"); 567 pw.println(" Prints one of the SYNC_DISABLED_MODE values, see" 568 + " set_sync_disabled_for_tests"); 569 } 570 delete(IContentProvider provider, String namespace, String key)571 private boolean delete(IContentProvider provider, String namespace, String key) { 572 String compositeKey = namespace + "/" + key; 573 boolean success; 574 575 try { 576 Bundle args = new Bundle(); 577 args.putInt(Settings.CALL_METHOD_USER_KEY, 578 ActivityManager.getService().getCurrentUser().id); 579 Bundle b = provider.call(new AttributionSource(Process.myUid(), 580 resolveCallingPackage(), null), Settings.AUTHORITY, 581 Settings.CALL_METHOD_DELETE_CONFIG, compositeKey, args); 582 success = (b != null && b.getInt(SettingsProvider.RESULT_ROWS_DELETED) == 1); 583 } catch (RemoteException e) { 584 throw new RuntimeException("Failed in IPC", e); 585 } 586 return success; 587 } 588 resolveCallingPackage()589 private static String resolveCallingPackage() { 590 switch (Binder.getCallingUid()) { 591 case Process.ROOT_UID: { 592 return "root"; 593 } 594 595 case Process.SHELL_UID: { 596 return "com.android.shell"; 597 } 598 599 default: { 600 return null; 601 } 602 } 603 } 604 } 605 parseSyncDisabledMode(String arg)606 private static @SyncDisabledMode int parseSyncDisabledMode(String arg) { 607 int syncDisabledMode; 608 if ("none".equalsIgnoreCase(arg)) { 609 syncDisabledMode = SYNC_DISABLED_MODE_NONE; 610 } else if ("persistent".equalsIgnoreCase(arg)) { 611 syncDisabledMode = SYNC_DISABLED_MODE_PERSISTENT; 612 } else if ("until_reboot".equalsIgnoreCase(arg)) { 613 syncDisabledMode = SYNC_DISABLED_MODE_UNTIL_REBOOT; 614 } else { 615 syncDisabledMode = -1; 616 } 617 return syncDisabledMode; 618 } 619 formatSyncDisabledMode(@yncDisabledMode int syncDisabledMode)620 private static String formatSyncDisabledMode(@SyncDisabledMode int syncDisabledMode) { 621 switch (syncDisabledMode) { 622 case SYNC_DISABLED_MODE_NONE: 623 return "none"; 624 case SYNC_DISABLED_MODE_PERSISTENT: 625 return "persistent"; 626 case SYNC_DISABLED_MODE_UNTIL_REBOOT: 627 return "until_reboot"; 628 default: 629 return null; 630 } 631 } 632 } 633