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.timezone; 18 19 import static android.app.timezone.RulesState.DISTRO_STATUS_INSTALLED; 20 import static android.app.timezone.RulesState.DISTRO_STATUS_NONE; 21 import static android.app.timezone.RulesState.DISTRO_STATUS_UNKNOWN; 22 import static android.app.timezone.RulesState.STAGED_OPERATION_INSTALL; 23 import static android.app.timezone.RulesState.STAGED_OPERATION_NONE; 24 import static android.app.timezone.RulesState.STAGED_OPERATION_UNINSTALL; 25 import static android.app.timezone.RulesState.STAGED_OPERATION_UNKNOWN; 26 27 import android.app.timezone.Callback; 28 import android.app.timezone.DistroFormatVersion; 29 import android.app.timezone.DistroRulesVersion; 30 import android.app.timezone.ICallback; 31 import android.app.timezone.IRulesManager; 32 import android.app.timezone.RulesManager; 33 import android.app.timezone.RulesState; 34 import android.content.Context; 35 import android.icu.util.TimeZone; 36 import android.os.ParcelFileDescriptor; 37 import android.os.RemoteException; 38 import android.util.Slog; 39 40 import com.android.i18n.timezone.TimeZoneDataFiles; 41 import com.android.i18n.timezone.TimeZoneFinder; 42 import com.android.i18n.timezone.TzDataSetVersion; 43 import com.android.i18n.timezone.ZoneInfoDb; 44 import com.android.internal.annotations.VisibleForTesting; 45 import com.android.server.EventLogTags; 46 import com.android.server.SystemService; 47 import com.android.timezone.distro.DistroException; 48 import com.android.timezone.distro.DistroVersion; 49 import com.android.timezone.distro.StagedDistroOperation; 50 import com.android.timezone.distro.TimeZoneDistro; 51 import com.android.timezone.distro.installer.TimeZoneDistroInstaller; 52 53 54 import java.io.File; 55 import java.io.FileDescriptor; 56 import java.io.FileInputStream; 57 import java.io.IOException; 58 import java.io.InputStream; 59 import java.io.PrintWriter; 60 import java.util.Arrays; 61 import java.util.concurrent.Executor; 62 import java.util.concurrent.atomic.AtomicBoolean; 63 64 public final class RulesManagerService extends IRulesManager.Stub { 65 66 private static final String TAG = "timezone.RulesManagerService"; 67 68 /** The distro format supported by this device. */ 69 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) 70 static final DistroFormatVersion DISTRO_FORMAT_VERSION_SUPPORTED = 71 new DistroFormatVersion( 72 TzDataSetVersion.currentFormatMajorVersion(), 73 TzDataSetVersion.currentFormatMinorVersion()); 74 75 public static class Lifecycle extends SystemService { Lifecycle(Context context)76 public Lifecycle(Context context) { 77 super(context); 78 } 79 80 @Override onStart()81 public void onStart() { 82 RulesManagerService service = RulesManagerService.create(getContext()); 83 service.start(); 84 85 // Publish the binder service so it can be accessed from other (appropriately 86 // permissioned) processes. 87 publishBinderService(Context.TIME_ZONE_RULES_MANAGER_SERVICE, service); 88 89 // Publish the service instance locally so we can use it directly from within the system 90 // server from TimeZoneUpdateIdler. 91 publishLocalService(RulesManagerService.class, service); 92 } 93 } 94 95 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) 96 static final String REQUIRED_UPDATER_PERMISSION = 97 android.Manifest.permission.UPDATE_TIME_ZONE_RULES; 98 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) 99 static final String REQUIRED_QUERY_PERMISSION = 100 android.Manifest.permission.QUERY_TIME_ZONE_RULES; 101 102 private final AtomicBoolean mOperationInProgress = new AtomicBoolean(false); 103 private final PermissionHelper mPermissionHelper; 104 private final PackageTracker mPackageTracker; 105 private final Executor mExecutor; 106 private final RulesManagerIntentHelper mIntentHelper; 107 private final TimeZoneDistroInstaller mInstaller; 108 create(Context context)109 private static RulesManagerService create(Context context) { 110 RulesManagerServiceHelperImpl helper = new RulesManagerServiceHelperImpl(context); 111 File baseVersionFile = new File(TimeZoneDataFiles.getTimeZoneModuleTzVersionFile()); 112 File tzDataDir = new File(TimeZoneDataFiles.getDataTimeZoneRootDir()); 113 return new RulesManagerService( 114 helper /* permissionHelper */, 115 helper /* executor */, 116 helper /* intentHelper */, 117 PackageTracker.create(context), 118 new TimeZoneDistroInstaller(TAG, baseVersionFile, tzDataDir)); 119 } 120 121 // A constructor that can be used by tests to supply mocked / faked dependencies. 122 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) RulesManagerService(PermissionHelper permissionHelper, Executor executor, RulesManagerIntentHelper intentHelper, PackageTracker packageTracker, TimeZoneDistroInstaller timeZoneDistroInstaller)123 RulesManagerService(PermissionHelper permissionHelper, Executor executor, 124 RulesManagerIntentHelper intentHelper, PackageTracker packageTracker, 125 TimeZoneDistroInstaller timeZoneDistroInstaller) { 126 mPermissionHelper = permissionHelper; 127 mExecutor = executor; 128 mIntentHelper = intentHelper; 129 mPackageTracker = packageTracker; 130 mInstaller = timeZoneDistroInstaller; 131 } 132 start()133 public void start() { 134 // Return value deliberately ignored: no action required on failure to start. 135 mPackageTracker.start(); 136 } 137 138 @Override // Binder call getRulesState()139 public RulesState getRulesState() { 140 mPermissionHelper.enforceCallerHasPermission(REQUIRED_QUERY_PERMISSION); 141 142 return getRulesStateInternal(); 143 } 144 145 /** Like {@link #getRulesState()} without the permission check. */ getRulesStateInternal()146 private RulesState getRulesStateInternal() { 147 synchronized(this) { 148 TzDataSetVersion baseVersion; 149 try { 150 baseVersion = mInstaller.readBaseVersion(); 151 } catch (IOException e) { 152 Slog.w(TAG, "Failed to read base rules version", e); 153 return null; 154 } 155 156 // Determine the installed distro state. This should be possible regardless of whether 157 // there's an operation in progress. 158 DistroVersion installedDistroVersion; 159 int distroStatus = DISTRO_STATUS_UNKNOWN; 160 DistroRulesVersion installedDistroRulesVersion = null; 161 try { 162 installedDistroVersion = mInstaller.getInstalledDistroVersion(); 163 if (installedDistroVersion == null) { 164 distroStatus = DISTRO_STATUS_NONE; 165 installedDistroRulesVersion = null; 166 } else { 167 distroStatus = DISTRO_STATUS_INSTALLED; 168 installedDistroRulesVersion = new DistroRulesVersion( 169 installedDistroVersion.rulesVersion, 170 installedDistroVersion.revision); 171 } 172 } catch (DistroException | IOException e) { 173 Slog.w(TAG, "Failed to read installed distro.", e); 174 } 175 176 boolean operationInProgress = this.mOperationInProgress.get(); 177 178 // Determine the staged operation status, if possible. 179 DistroRulesVersion stagedDistroRulesVersion = null; 180 int stagedOperationStatus = STAGED_OPERATION_UNKNOWN; 181 if (!operationInProgress) { 182 StagedDistroOperation stagedDistroOperation; 183 try { 184 stagedDistroOperation = mInstaller.getStagedDistroOperation(); 185 if (stagedDistroOperation == null) { 186 stagedOperationStatus = STAGED_OPERATION_NONE; 187 } else if (stagedDistroOperation.isUninstall) { 188 stagedOperationStatus = STAGED_OPERATION_UNINSTALL; 189 } else { 190 // Must be an install. 191 stagedOperationStatus = STAGED_OPERATION_INSTALL; 192 DistroVersion stagedDistroVersion = stagedDistroOperation.distroVersion; 193 stagedDistroRulesVersion = new DistroRulesVersion( 194 stagedDistroVersion.rulesVersion, 195 stagedDistroVersion.revision); 196 } 197 } catch (DistroException | IOException e) { 198 Slog.w(TAG, "Failed to read staged distro.", e); 199 } 200 } 201 return new RulesState(baseVersion.getRulesVersion(), DISTRO_FORMAT_VERSION_SUPPORTED, 202 operationInProgress, stagedOperationStatus, stagedDistroRulesVersion, 203 distroStatus, installedDistroRulesVersion); 204 } 205 } 206 207 @Override requestInstall(ParcelFileDescriptor distroParcelFileDescriptor, byte[] checkTokenBytes, ICallback callback)208 public int requestInstall(ParcelFileDescriptor distroParcelFileDescriptor, 209 byte[] checkTokenBytes, ICallback callback) { 210 211 boolean closeParcelFileDescriptorOnExit = true; 212 try { 213 mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION); 214 215 CheckToken checkToken = null; 216 if (checkTokenBytes != null) { 217 checkToken = createCheckTokenOrThrow(checkTokenBytes); 218 } 219 EventLogTags.writeTimezoneRequestInstall(toStringOrNull(checkToken)); 220 221 synchronized (this) { 222 if (distroParcelFileDescriptor == null) { 223 throw new NullPointerException("distroParcelFileDescriptor == null"); 224 } 225 if (callback == null) { 226 throw new NullPointerException("observer == null"); 227 } 228 if (mOperationInProgress.get()) { 229 return RulesManager.ERROR_OPERATION_IN_PROGRESS; 230 } 231 mOperationInProgress.set(true); 232 233 // Execute the install asynchronously. 234 mExecutor.execute( 235 new InstallRunnable(distroParcelFileDescriptor, checkToken, callback)); 236 237 // The InstallRunnable now owns the ParcelFileDescriptor, so it will close it after 238 // it executes (and we do not have to). 239 closeParcelFileDescriptorOnExit = false; 240 241 return RulesManager.SUCCESS; 242 } 243 } finally { 244 // We should close() the local ParcelFileDescriptor we were passed if it hasn't been 245 // passed to another thread to handle. 246 if (distroParcelFileDescriptor != null && closeParcelFileDescriptorOnExit) { 247 try { 248 distroParcelFileDescriptor.close(); 249 } catch (IOException e) { 250 Slog.w(TAG, "Failed to close distroParcelFileDescriptor", e); 251 } 252 } 253 } 254 } 255 256 private class InstallRunnable implements Runnable { 257 258 private final ParcelFileDescriptor mDistroParcelFileDescriptor; 259 private final CheckToken mCheckToken; 260 private final ICallback mCallback; 261 InstallRunnable(ParcelFileDescriptor distroParcelFileDescriptor, CheckToken checkToken, ICallback callback)262 InstallRunnable(ParcelFileDescriptor distroParcelFileDescriptor, CheckToken checkToken, 263 ICallback callback) { 264 mDistroParcelFileDescriptor = distroParcelFileDescriptor; 265 mCheckToken = checkToken; 266 mCallback = callback; 267 } 268 269 @Override run()270 public void run() { 271 EventLogTags.writeTimezoneInstallStarted(toStringOrNull(mCheckToken)); 272 273 boolean success = false; 274 // Adopt the ParcelFileDescriptor into this try-with-resources so it is closed 275 // when we are done. 276 try (ParcelFileDescriptor pfd = mDistroParcelFileDescriptor) { 277 // The ParcelFileDescriptor owns the underlying FileDescriptor and we'll close 278 // it at the end of the try-with-resources. 279 final boolean isFdOwner = false; 280 InputStream is = new FileInputStream(pfd.getFileDescriptor(), isFdOwner); 281 282 TimeZoneDistro distro = new TimeZoneDistro(is); 283 int installerResult = mInstaller.stageInstallWithErrorCode(distro); 284 285 // Notify interested parties that something is staged. 286 sendInstallNotificationIntentIfRequired(installerResult); 287 288 int resultCode = mapInstallerResultToApiCode(installerResult); 289 EventLogTags.writeTimezoneInstallComplete(toStringOrNull(mCheckToken), resultCode); 290 sendFinishedStatus(mCallback, resultCode); 291 292 // All the installer failure modes are currently non-recoverable and won't be 293 // improved by trying again. Therefore success = true. 294 success = true; 295 } catch (Exception e) { 296 Slog.w(TAG, "Failed to install distro.", e); 297 EventLogTags.writeTimezoneInstallComplete( 298 toStringOrNull(mCheckToken), Callback.ERROR_UNKNOWN_FAILURE); 299 sendFinishedStatus(mCallback, Callback.ERROR_UNKNOWN_FAILURE); 300 } finally { 301 // Notify the package tracker that the operation is now complete. 302 mPackageTracker.recordCheckResult(mCheckToken, success); 303 304 mOperationInProgress.set(false); 305 } 306 } 307 sendInstallNotificationIntentIfRequired(int installerResult)308 private void sendInstallNotificationIntentIfRequired(int installerResult) { 309 if (installerResult == TimeZoneDistroInstaller.INSTALL_SUCCESS) { 310 mIntentHelper.sendTimeZoneOperationStaged(); 311 } 312 } 313 mapInstallerResultToApiCode(int installerResult)314 private int mapInstallerResultToApiCode(int installerResult) { 315 switch (installerResult) { 316 case TimeZoneDistroInstaller.INSTALL_SUCCESS: 317 return Callback.SUCCESS; 318 case TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE: 319 return Callback.ERROR_INSTALL_BAD_DISTRO_STRUCTURE; 320 case TimeZoneDistroInstaller.INSTALL_FAIL_RULES_TOO_OLD: 321 return Callback.ERROR_INSTALL_RULES_TOO_OLD; 322 case TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_FORMAT_VERSION: 323 return Callback.ERROR_INSTALL_BAD_DISTRO_FORMAT_VERSION; 324 case TimeZoneDistroInstaller.INSTALL_FAIL_VALIDATION_ERROR: 325 return Callback.ERROR_INSTALL_VALIDATION_ERROR; 326 default: 327 return Callback.ERROR_UNKNOWN_FAILURE; 328 } 329 } 330 } 331 332 @Override requestUninstall(byte[] checkTokenBytes, ICallback callback)333 public int requestUninstall(byte[] checkTokenBytes, ICallback callback) { 334 mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION); 335 336 CheckToken checkToken = null; 337 if (checkTokenBytes != null) { 338 checkToken = createCheckTokenOrThrow(checkTokenBytes); 339 } 340 EventLogTags.writeTimezoneRequestUninstall(toStringOrNull(checkToken)); 341 synchronized(this) { 342 if (callback == null) { 343 throw new NullPointerException("callback == null"); 344 } 345 346 if (mOperationInProgress.get()) { 347 return RulesManager.ERROR_OPERATION_IN_PROGRESS; 348 } 349 mOperationInProgress.set(true); 350 351 // Execute the uninstall asynchronously. 352 mExecutor.execute(new UninstallRunnable(checkToken, callback)); 353 354 return RulesManager.SUCCESS; 355 } 356 } 357 358 private class UninstallRunnable implements Runnable { 359 360 private final CheckToken mCheckToken; 361 private final ICallback mCallback; 362 UninstallRunnable(CheckToken checkToken, ICallback callback)363 UninstallRunnable(CheckToken checkToken, ICallback callback) { 364 mCheckToken = checkToken; 365 mCallback = callback; 366 } 367 368 @Override run()369 public void run() { 370 EventLogTags.writeTimezoneUninstallStarted(toStringOrNull(mCheckToken)); 371 boolean packageTrackerStatus = false; 372 try { 373 int uninstallResult = mInstaller.stageUninstall(); 374 375 // Notify interested parties that something is staged. 376 sendUninstallNotificationIntentIfRequired(uninstallResult); 377 378 packageTrackerStatus = (uninstallResult == TimeZoneDistroInstaller.UNINSTALL_SUCCESS 379 || uninstallResult == TimeZoneDistroInstaller.UNINSTALL_NOTHING_INSTALLED); 380 381 // Right now we just have Callback.SUCCESS / Callback.ERROR_UNKNOWN_FAILURE for 382 // uninstall. All clients should be checking against SUCCESS. More granular failures 383 // may be added in future. 384 int callbackResultCode = 385 packageTrackerStatus ? Callback.SUCCESS : Callback.ERROR_UNKNOWN_FAILURE; 386 EventLogTags.writeTimezoneUninstallComplete( 387 toStringOrNull(mCheckToken), callbackResultCode); 388 sendFinishedStatus(mCallback, callbackResultCode); 389 } catch (Exception e) { 390 EventLogTags.writeTimezoneUninstallComplete( 391 toStringOrNull(mCheckToken), Callback.ERROR_UNKNOWN_FAILURE); 392 Slog.w(TAG, "Failed to uninstall distro.", e); 393 sendFinishedStatus(mCallback, Callback.ERROR_UNKNOWN_FAILURE); 394 } finally { 395 // Notify the package tracker that the operation is now complete. 396 mPackageTracker.recordCheckResult(mCheckToken, packageTrackerStatus); 397 398 mOperationInProgress.set(false); 399 } 400 } 401 sendUninstallNotificationIntentIfRequired(int uninstallResult)402 private void sendUninstallNotificationIntentIfRequired(int uninstallResult) { 403 switch (uninstallResult) { 404 case TimeZoneDistroInstaller.UNINSTALL_SUCCESS: 405 mIntentHelper.sendTimeZoneOperationStaged(); 406 break; 407 case TimeZoneDistroInstaller.UNINSTALL_NOTHING_INSTALLED: 408 mIntentHelper.sendTimeZoneOperationUnstaged(); 409 break; 410 case TimeZoneDistroInstaller.UNINSTALL_FAIL: 411 default: 412 // No-op - unknown or nothing to notify about. 413 } 414 } 415 } 416 sendFinishedStatus(ICallback callback, int resultCode)417 private void sendFinishedStatus(ICallback callback, int resultCode) { 418 try { 419 callback.onFinished(resultCode); 420 } catch (RemoteException e) { 421 Slog.e(TAG, "Unable to notify observer of result", e); 422 } 423 } 424 425 @Override requestNothing(byte[] checkTokenBytes, boolean success)426 public void requestNothing(byte[] checkTokenBytes, boolean success) { 427 mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION); 428 CheckToken checkToken = null; 429 if (checkTokenBytes != null) { 430 checkToken = createCheckTokenOrThrow(checkTokenBytes); 431 } 432 EventLogTags.writeTimezoneRequestNothing(toStringOrNull(checkToken)); 433 mPackageTracker.recordCheckResult(checkToken, success); 434 EventLogTags.writeTimezoneNothingComplete(toStringOrNull(checkToken)); 435 } 436 437 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)438 protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 439 if (!mPermissionHelper.checkDumpPermission(TAG, pw)) { 440 return; 441 } 442 443 RulesState rulesState = getRulesStateInternal(); 444 if (args != null && args.length == 2) { 445 // Formatting options used for automated tests. The format is less free-form than 446 // the -format options, which are intended to be easier to parse. 447 if ("-format_state".equals(args[0]) && args[1] != null) { 448 for (char c : args[1].toCharArray()) { 449 switch (c) { 450 case 'p': { 451 // Report operation in progress 452 String value = "Unknown"; 453 if (rulesState != null) { 454 value = Boolean.toString(rulesState.isOperationInProgress()); 455 } 456 pw.println("Operation in progress: " + value); 457 break; 458 } 459 case 'b': { 460 // Report base rules version 461 String value = "Unknown"; 462 if (rulesState != null) { 463 value = rulesState.getBaseRulesVersion(); 464 } 465 pw.println("Base rules version: " + value); 466 break; 467 } 468 case 'c': { 469 // Report current installation state 470 String value = "Unknown"; 471 if (rulesState != null) { 472 value = distroStatusToString(rulesState.getDistroStatus()); 473 } 474 pw.println("Current install state: " + value); 475 break; 476 } 477 case 'i': { 478 // Report currently installed version 479 String value = "Unknown"; 480 if (rulesState != null) { 481 DistroRulesVersion installedRulesVersion = 482 rulesState.getInstalledDistroRulesVersion(); 483 if (installedRulesVersion == null) { 484 value = "<None>"; 485 } else { 486 value = installedRulesVersion.toDumpString(); 487 } 488 } 489 pw.println("Installed rules version: " + value); 490 break; 491 } 492 case 'o': { 493 // Report staged operation type 494 String value = "Unknown"; 495 if (rulesState != null) { 496 int stagedOperationType = rulesState.getStagedOperationType(); 497 value = stagedOperationToString(stagedOperationType); 498 } 499 pw.println("Staged operation: " + value); 500 break; 501 } 502 case 't': { 503 // Report staged version (i.e. the one that will be installed next boot 504 // if the staged operation is an install). 505 String value = "Unknown"; 506 if (rulesState != null) { 507 DistroRulesVersion stagedDistroRulesVersion = 508 rulesState.getStagedDistroRulesVersion(); 509 if (stagedDistroRulesVersion == null) { 510 value = "<None>"; 511 } else { 512 value = stagedDistroRulesVersion.toDumpString(); 513 } 514 } 515 pw.println("Staged rules version: " + value); 516 break; 517 } 518 case 'a': { 519 // Report the active rules version (i.e. the rules in use by the current 520 // process). 521 pw.println("Active rules version (ICU, ZoneInfoDb, TimeZoneFinder): " 522 + TimeZone.getTZDataVersion() + "," 523 + ZoneInfoDb.getInstance().getVersion() + "," 524 + TimeZoneFinder.getInstance().getIanaVersion()); 525 break; 526 } 527 default: { 528 pw.println("Unknown option: " + c); 529 } 530 } 531 } 532 return; 533 } 534 } 535 536 pw.println("RulesManagerService state: " + toString()); 537 pw.println("Active rules version (ICU, ZoneInfoDB, TimeZoneFinder): " 538 + TimeZone.getTZDataVersion() + "," 539 + ZoneInfoDb.getInstance().getVersion() + "," 540 + TimeZoneFinder.getInstance().getIanaVersion()); 541 pw.println("Distro state: " + rulesState.toString()); 542 mPackageTracker.dump(pw); 543 } 544 545 /** 546 * Called when the device is considered idle. 547 */ notifyIdle()548 void notifyIdle() { 549 // No package has changed: we are just triggering because the device is idle and there 550 // *might* be work to do. 551 final boolean packageChanged = false; 552 mPackageTracker.triggerUpdateIfNeeded(packageChanged); 553 } 554 555 @Override toString()556 public String toString() { 557 return "RulesManagerService{" + 558 "mOperationInProgress=" + mOperationInProgress + 559 '}'; 560 } 561 createCheckTokenOrThrow(byte[] checkTokenBytes)562 private static CheckToken createCheckTokenOrThrow(byte[] checkTokenBytes) { 563 CheckToken checkToken; 564 try { 565 checkToken = CheckToken.fromByteArray(checkTokenBytes); 566 } catch (IOException e) { 567 throw new IllegalArgumentException("Unable to read token bytes " 568 + Arrays.toString(checkTokenBytes), e); 569 } 570 return checkToken; 571 } 572 distroStatusToString(int distroStatus)573 private static String distroStatusToString(int distroStatus) { 574 switch(distroStatus) { 575 case DISTRO_STATUS_NONE: 576 return "None"; 577 case DISTRO_STATUS_INSTALLED: 578 return "Installed"; 579 case DISTRO_STATUS_UNKNOWN: 580 default: 581 return "Unknown"; 582 } 583 } 584 stagedOperationToString(int stagedOperationType)585 private static String stagedOperationToString(int stagedOperationType) { 586 switch(stagedOperationType) { 587 case STAGED_OPERATION_NONE: 588 return "None"; 589 case STAGED_OPERATION_UNINSTALL: 590 return "Uninstall"; 591 case STAGED_OPERATION_INSTALL: 592 return "Install"; 593 case STAGED_OPERATION_UNKNOWN: 594 default: 595 return "Unknown"; 596 } 597 } 598 toStringOrNull(Object obj)599 private static String toStringOrNull(Object obj) { 600 return obj == null ? null : obj.toString(); 601 } 602 } 603