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