blob: 1c5aa600580af1419fb5b6e753d406f67ce842f0 [file] [log] [blame]
Neil Fuller68f66662017-03-16 18:32:21 +00001/*
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
17package com.android.server.timezone;
18
19import com.android.internal.annotations.VisibleForTesting;
Neil Fullerd857f672017-07-20 11:00:35 +010020import com.android.server.EventLogTags;
Neil Fuller68f66662017-03-16 18:32:21 +000021import com.android.server.SystemService;
Neil Fullera6a71d02017-06-13 15:12:17 +010022import com.android.timezone.distro.DistroException;
23import com.android.timezone.distro.DistroVersion;
24import com.android.timezone.distro.StagedDistroOperation;
Neil Fuller54525bf2017-06-22 14:10:29 +010025import com.android.timezone.distro.TimeZoneDistro;
Neil Fuller5ca8b5b2017-06-29 12:39:49 +010026import com.android.timezone.distro.installer.TimeZoneDistroInstaller;
Neil Fuller68f66662017-03-16 18:32:21 +000027
28import android.app.timezone.Callback;
29import android.app.timezone.DistroFormatVersion;
30import android.app.timezone.DistroRulesVersion;
31import android.app.timezone.ICallback;
32import android.app.timezone.IRulesManager;
33import android.app.timezone.RulesManager;
34import android.app.timezone.RulesState;
35import android.content.Context;
36import android.os.ParcelFileDescriptor;
37import android.os.RemoteException;
38import android.util.Slog;
39
40import java.io.File;
Neil Fuller87b11282017-06-23 16:43:45 +010041import java.io.FileDescriptor;
Neil Fuller54525bf2017-06-22 14:10:29 +010042import java.io.FileInputStream;
Neil Fuller68f66662017-03-16 18:32:21 +000043import java.io.IOException;
Neil Fuller54525bf2017-06-22 14:10:29 +010044import java.io.InputStream;
Neil Fuller87b11282017-06-23 16:43:45 +010045import java.io.PrintWriter;
Neil Fuller68f66662017-03-16 18:32:21 +000046import java.util.Arrays;
47import java.util.concurrent.Executor;
48import java.util.concurrent.atomic.AtomicBoolean;
Neil Fuller87b11282017-06-23 16:43:45 +010049import libcore.icu.ICU;
50import libcore.util.ZoneInfoDB;
51
52import static android.app.timezone.RulesState.DISTRO_STATUS_INSTALLED;
53import static android.app.timezone.RulesState.DISTRO_STATUS_NONE;
54import static android.app.timezone.RulesState.DISTRO_STATUS_UNKNOWN;
55import static android.app.timezone.RulesState.STAGED_OPERATION_INSTALL;
56import static android.app.timezone.RulesState.STAGED_OPERATION_NONE;
57import static android.app.timezone.RulesState.STAGED_OPERATION_UNINSTALL;
58import static android.app.timezone.RulesState.STAGED_OPERATION_UNKNOWN;
Neil Fuller68f66662017-03-16 18:32:21 +000059
Neil Fuller68f66662017-03-16 18:32:21 +000060// TODO(nfuller) Check error handling best practices in the system server.
61public final class RulesManagerService extends IRulesManager.Stub {
62
63 private static final String TAG = "timezone.RulesManagerService";
64
65 /** The distro format supported by this device. */
66 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
67 static final DistroFormatVersion DISTRO_FORMAT_VERSION_SUPPORTED =
68 new DistroFormatVersion(
69 DistroVersion.CURRENT_FORMAT_MAJOR_VERSION,
70 DistroVersion.CURRENT_FORMAT_MINOR_VERSION);
71
72 public static class Lifecycle extends SystemService {
73 private RulesManagerService mService;
74
75 public Lifecycle(Context context) {
76 super(context);
77 }
78
79 @Override
80 public void onStart() {
81 mService = RulesManagerService.create(getContext());
82 mService.start();
83
84 publishBinderService(Context.TIME_ZONE_RULES_MANAGER_SERVICE, mService);
85 }
86 }
87
88 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
89 static final String REQUIRED_UPDATER_PERMISSION =
90 android.Manifest.permission.UPDATE_TIME_ZONE_RULES;
91 private static final File SYSTEM_TZ_DATA_FILE = new File("/system/usr/share/zoneinfo/tzdata");
92 private static final File TZ_DATA_DIR = new File("/data/misc/zoneinfo");
93
94 private final AtomicBoolean mOperationInProgress = new AtomicBoolean(false);
95 private final PermissionHelper mPermissionHelper;
96 private final PackageTracker mPackageTracker;
97 private final Executor mExecutor;
98 private final TimeZoneDistroInstaller mInstaller;
Neil Fuller68f66662017-03-16 18:32:21 +000099
100 private static RulesManagerService create(Context context) {
101 RulesManagerServiceHelperImpl helper = new RulesManagerServiceHelperImpl(context);
102 return new RulesManagerService(
103 helper /* permissionHelper */,
104 helper /* executor */,
Neil Fuller68f66662017-03-16 18:32:21 +0000105 PackageTracker.create(context),
106 new TimeZoneDistroInstaller(TAG, SYSTEM_TZ_DATA_FILE, TZ_DATA_DIR));
107 }
108
109 // A constructor that can be used by tests to supply mocked / faked dependencies.
110 RulesManagerService(PermissionHelper permissionHelper,
Neil Fuller54525bf2017-06-22 14:10:29 +0100111 Executor executor, PackageTracker packageTracker,
Neil Fuller68f66662017-03-16 18:32:21 +0000112 TimeZoneDistroInstaller timeZoneDistroInstaller) {
113 mPermissionHelper = permissionHelper;
114 mExecutor = executor;
Neil Fuller68f66662017-03-16 18:32:21 +0000115 mPackageTracker = packageTracker;
116 mInstaller = timeZoneDistroInstaller;
117 }
118
119 public void start() {
120 mPackageTracker.start();
121 }
122
123 @Override // Binder call
124 public RulesState getRulesState() {
125 mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION);
126
Neil Fuller87b11282017-06-23 16:43:45 +0100127 return getRulesStateInternal();
128 }
129
130 /** Like {@link #getRulesState()} without the permission check. */
131 private RulesState getRulesStateInternal() {
Neil Fuller68f66662017-03-16 18:32:21 +0000132 synchronized(this) {
133 String systemRulesVersion;
134 try {
135 systemRulesVersion = mInstaller.getSystemRulesVersion();
136 } catch (IOException e) {
137 Slog.w(TAG, "Failed to read system rules", e);
138 return null;
139 }
140
141 boolean operationInProgress = this.mOperationInProgress.get();
142
143 // Determine the staged operation status, if possible.
144 DistroRulesVersion stagedDistroRulesVersion = null;
Neil Fuller87b11282017-06-23 16:43:45 +0100145 int stagedOperationStatus = STAGED_OPERATION_UNKNOWN;
Neil Fuller68f66662017-03-16 18:32:21 +0000146 if (!operationInProgress) {
147 StagedDistroOperation stagedDistroOperation;
148 try {
149 stagedDistroOperation = mInstaller.getStagedDistroOperation();
150 if (stagedDistroOperation == null) {
Neil Fuller87b11282017-06-23 16:43:45 +0100151 stagedOperationStatus = STAGED_OPERATION_NONE;
Neil Fuller68f66662017-03-16 18:32:21 +0000152 } else if (stagedDistroOperation.isUninstall) {
Neil Fuller87b11282017-06-23 16:43:45 +0100153 stagedOperationStatus = STAGED_OPERATION_UNINSTALL;
Neil Fuller68f66662017-03-16 18:32:21 +0000154 } else {
155 // Must be an install.
Neil Fuller87b11282017-06-23 16:43:45 +0100156 stagedOperationStatus = STAGED_OPERATION_INSTALL;
Neil Fuller68f66662017-03-16 18:32:21 +0000157 DistroVersion stagedDistroVersion = stagedDistroOperation.distroVersion;
158 stagedDistroRulesVersion = new DistroRulesVersion(
159 stagedDistroVersion.rulesVersion,
160 stagedDistroVersion.revision);
161 }
162 } catch (DistroException | IOException e) {
163 Slog.w(TAG, "Failed to read staged distro.", e);
164 }
165 }
166
167 // Determine the installed distro state, if possible.
168 DistroVersion installedDistroVersion;
Neil Fuller87b11282017-06-23 16:43:45 +0100169 int distroStatus = DISTRO_STATUS_UNKNOWN;
Neil Fuller68f66662017-03-16 18:32:21 +0000170 DistroRulesVersion installedDistroRulesVersion = null;
171 if (!operationInProgress) {
172 try {
173 installedDistroVersion = mInstaller.getInstalledDistroVersion();
174 if (installedDistroVersion == null) {
Neil Fuller87b11282017-06-23 16:43:45 +0100175 distroStatus = DISTRO_STATUS_NONE;
Neil Fuller68f66662017-03-16 18:32:21 +0000176 installedDistroRulesVersion = null;
177 } else {
Neil Fuller87b11282017-06-23 16:43:45 +0100178 distroStatus = DISTRO_STATUS_INSTALLED;
Neil Fuller68f66662017-03-16 18:32:21 +0000179 installedDistroRulesVersion = new DistroRulesVersion(
180 installedDistroVersion.rulesVersion,
181 installedDistroVersion.revision);
182 }
183 } catch (DistroException | IOException e) {
184 Slog.w(TAG, "Failed to read installed distro.", e);
185 }
186 }
187 return new RulesState(systemRulesVersion, DISTRO_FORMAT_VERSION_SUPPORTED,
188 operationInProgress, stagedOperationStatus, stagedDistroRulesVersion,
189 distroStatus, installedDistroRulesVersion);
190 }
191 }
192
193 @Override
Neil Fuller54525bf2017-06-22 14:10:29 +0100194 public int requestInstall(ParcelFileDescriptor distroParcelFileDescriptor,
195 byte[] checkTokenBytes, ICallback callback) {
Neil Fuller68f66662017-03-16 18:32:21 +0000196
Neil Fuller54525bf2017-06-22 14:10:29 +0100197 boolean closeParcelFileDescriptorOnExit = true;
198 try {
199 mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION);
Neil Fuller68f66662017-03-16 18:32:21 +0000200
Neil Fuller54525bf2017-06-22 14:10:29 +0100201 CheckToken checkToken = null;
202 if (checkTokenBytes != null) {
203 checkToken = createCheckTokenOrThrow(checkTokenBytes);
204 }
Neil Fullerd857f672017-07-20 11:00:35 +0100205 EventLogTags.writeTimezoneRequestInstall(toStringOrNull(checkToken));
Neil Fuller68f66662017-03-16 18:32:21 +0000206
Neil Fuller54525bf2017-06-22 14:10:29 +0100207 synchronized (this) {
208 if (distroParcelFileDescriptor == null) {
209 throw new NullPointerException("distroParcelFileDescriptor == null");
210 }
211 if (callback == null) {
212 throw new NullPointerException("observer == null");
213 }
214 if (mOperationInProgress.get()) {
215 return RulesManager.ERROR_OPERATION_IN_PROGRESS;
216 }
217 mOperationInProgress.set(true);
218
219 // Execute the install asynchronously.
220 mExecutor.execute(
221 new InstallRunnable(distroParcelFileDescriptor, checkToken, callback));
222
223 // The InstallRunnable now owns the ParcelFileDescriptor, so it will close it after
224 // it executes (and we do not have to).
225 closeParcelFileDescriptorOnExit = false;
226
227 return RulesManager.SUCCESS;
228 }
229 } finally {
230 // We should close() the local ParcelFileDescriptor we were passed if it hasn't been
231 // passed to another thread to handle.
232 if (distroParcelFileDescriptor != null && closeParcelFileDescriptorOnExit) {
233 try {
234 distroParcelFileDescriptor.close();
235 } catch (IOException e) {
236 Slog.w(TAG, "Failed to close distroParcelFileDescriptor", e);
237 }
238 }
Neil Fuller68f66662017-03-16 18:32:21 +0000239 }
240 }
241
242 private class InstallRunnable implements Runnable {
243
Neil Fuller54525bf2017-06-22 14:10:29 +0100244 private final ParcelFileDescriptor mDistroParcelFileDescriptor;
Neil Fuller68f66662017-03-16 18:32:21 +0000245 private final CheckToken mCheckToken;
246 private final ICallback mCallback;
247
Neil Fuller54525bf2017-06-22 14:10:29 +0100248 InstallRunnable(ParcelFileDescriptor distroParcelFileDescriptor, CheckToken checkToken,
249 ICallback callback) {
250 mDistroParcelFileDescriptor = distroParcelFileDescriptor;
Neil Fuller68f66662017-03-16 18:32:21 +0000251 mCheckToken = checkToken;
252 mCallback = callback;
253 }
254
255 @Override
256 public void run() {
Neil Fullerd857f672017-07-20 11:00:35 +0100257 EventLogTags.writeTimezoneInstallStarted(toStringOrNull(mCheckToken));
258
Neil Fuller54525bf2017-06-22 14:10:29 +0100259 boolean success = false;
Neil Fuller68f66662017-03-16 18:32:21 +0000260 // Adopt the ParcelFileDescriptor into this try-with-resources so it is closed
261 // when we are done.
Neil Fuller54525bf2017-06-22 14:10:29 +0100262 try (ParcelFileDescriptor pfd = mDistroParcelFileDescriptor) {
263 // The ParcelFileDescriptor owns the underlying FileDescriptor and we'll close
264 // it at the end of the try-with-resources.
265 final boolean isFdOwner = false;
266 InputStream is = new FileInputStream(pfd.getFileDescriptor(), isFdOwner);
267
268 TimeZoneDistro distro = new TimeZoneDistro(is);
Neil Fullerfe3b1182017-06-19 12:56:08 +0100269 int installerResult = mInstaller.stageInstallWithErrorCode(distro);
Neil Fuller68f66662017-03-16 18:32:21 +0000270 int resultCode = mapInstallerResultToApiCode(installerResult);
Neil Fullerd857f672017-07-20 11:00:35 +0100271 EventLogTags.writeTimezoneInstallComplete(toStringOrNull(mCheckToken), resultCode);
Neil Fuller68f66662017-03-16 18:32:21 +0000272 sendFinishedStatus(mCallback, resultCode);
273
274 // All the installer failure modes are currently non-recoverable and won't be
275 // improved by trying again. Therefore success = true.
276 success = true;
277 } catch (Exception e) {
278 Slog.w(TAG, "Failed to install distro.", e);
Neil Fullerd857f672017-07-20 11:00:35 +0100279 EventLogTags.writeTimezoneInstallComplete(
280 toStringOrNull(mCheckToken), Callback.ERROR_UNKNOWN_FAILURE);
Neil Fuller68f66662017-03-16 18:32:21 +0000281 sendFinishedStatus(mCallback, Callback.ERROR_UNKNOWN_FAILURE);
282 } finally {
283 // Notify the package tracker that the operation is now complete.
284 mPackageTracker.recordCheckResult(mCheckToken, success);
285
286 mOperationInProgress.set(false);
287 }
288 }
289
290 private int mapInstallerResultToApiCode(int installerResult) {
291 switch (installerResult) {
292 case TimeZoneDistroInstaller.INSTALL_SUCCESS:
293 return Callback.SUCCESS;
294 case TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE:
295 return Callback.ERROR_INSTALL_BAD_DISTRO_STRUCTURE;
296 case TimeZoneDistroInstaller.INSTALL_FAIL_RULES_TOO_OLD:
297 return Callback.ERROR_INSTALL_RULES_TOO_OLD;
298 case TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_FORMAT_VERSION:
299 return Callback.ERROR_INSTALL_BAD_DISTRO_FORMAT_VERSION;
300 case TimeZoneDistroInstaller.INSTALL_FAIL_VALIDATION_ERROR:
301 return Callback.ERROR_INSTALL_VALIDATION_ERROR;
302 default:
303 return Callback.ERROR_UNKNOWN_FAILURE;
304 }
305 }
306 }
307
308 @Override
309 public int requestUninstall(byte[] checkTokenBytes, ICallback callback) {
310 mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION);
311
312 CheckToken checkToken = null;
313 if (checkTokenBytes != null) {
314 checkToken = createCheckTokenOrThrow(checkTokenBytes);
315 }
Neil Fullerd857f672017-07-20 11:00:35 +0100316 EventLogTags.writeTimezoneRequestUninstall(toStringOrNull(checkToken));
Neil Fuller68f66662017-03-16 18:32:21 +0000317 synchronized(this) {
318 if (callback == null) {
319 throw new NullPointerException("callback == null");
320 }
321
322 if (mOperationInProgress.get()) {
323 return RulesManager.ERROR_OPERATION_IN_PROGRESS;
324 }
325 mOperationInProgress.set(true);
326
327 // Execute the uninstall asynchronously.
328 mExecutor.execute(new UninstallRunnable(checkToken, callback));
329
330 return RulesManager.SUCCESS;
331 }
332 }
333
334 private class UninstallRunnable implements Runnable {
335
336 private final CheckToken mCheckToken;
337 private final ICallback mCallback;
338
339 public UninstallRunnable(CheckToken checkToken, ICallback callback) {
340 mCheckToken = checkToken;
341 mCallback = callback;
342 }
343
344 @Override
345 public void run() {
Neil Fullerd857f672017-07-20 11:00:35 +0100346 EventLogTags.writeTimezoneUninstallStarted(toStringOrNull(mCheckToken));
Neil Fuller68f66662017-03-16 18:32:21 +0000347 boolean success = false;
348 try {
349 success = mInstaller.stageUninstall();
350 // Right now we just have success (0) / failure (1). All clients should be checking
351 // against SUCCESS. More granular failures may be added in future.
352 int resultCode = success ? Callback.SUCCESS
353 : Callback.ERROR_UNKNOWN_FAILURE;
Neil Fullerd857f672017-07-20 11:00:35 +0100354 EventLogTags.writeTimezoneUninstallComplete(
355 toStringOrNull(mCheckToken), resultCode);
Neil Fuller68f66662017-03-16 18:32:21 +0000356 sendFinishedStatus(mCallback, resultCode);
357 } catch (Exception e) {
Neil Fullerd857f672017-07-20 11:00:35 +0100358 EventLogTags.writeTimezoneUninstallComplete(
359 toStringOrNull(mCheckToken), Callback.ERROR_UNKNOWN_FAILURE);
Neil Fuller68f66662017-03-16 18:32:21 +0000360 Slog.w(TAG, "Failed to uninstall distro.", e);
361 sendFinishedStatus(mCallback, Callback.ERROR_UNKNOWN_FAILURE);
362 } finally {
363 // Notify the package tracker that the operation is now complete.
364 mPackageTracker.recordCheckResult(mCheckToken, success);
365
366 mOperationInProgress.set(false);
367 }
368 }
369 }
370
371 private void sendFinishedStatus(ICallback callback, int resultCode) {
372 try {
373 callback.onFinished(resultCode);
374 } catch (RemoteException e) {
375 Slog.e(TAG, "Unable to notify observer of result", e);
376 }
377 }
378
379 @Override
380 public void requestNothing(byte[] checkTokenBytes, boolean success) {
381 mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION);
382 CheckToken checkToken = null;
383 if (checkTokenBytes != null) {
384 checkToken = createCheckTokenOrThrow(checkTokenBytes);
385 }
Neil Fullerd857f672017-07-20 11:00:35 +0100386 EventLogTags.writeTimezoneRequestNothing(toStringOrNull(checkToken));
Neil Fuller68f66662017-03-16 18:32:21 +0000387 mPackageTracker.recordCheckResult(checkToken, success);
Neil Fullerd857f672017-07-20 11:00:35 +0100388 EventLogTags.writeTimezoneNothingComplete(toStringOrNull(checkToken));
Neil Fuller68f66662017-03-16 18:32:21 +0000389 }
390
Neil Fuller87b11282017-06-23 16:43:45 +0100391 @Override
392 protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
393 if (!mPermissionHelper.checkDumpPermission(TAG, pw)) {
394 return;
395 }
396
397 RulesState rulesState = getRulesStateInternal();
398 if (args != null && args.length == 2) {
399 // Formatting options used for automated tests. The format is less free-form than
400 // the -format options, which are intended to be easier to parse.
401 if ("-format_state".equals(args[0]) && args[1] != null) {
402 for (char c : args[1].toCharArray()) {
403 switch (c) {
404 case 'p': // Report operation in progress
405 pw.println("Operation in progress: "
406 + rulesState.isOperationInProgress());
407 break;
408 case 's': // Report system image rules version
409 pw.println("System rules version: "
410 + rulesState.getSystemRulesVersion());
411 break;
412 case 'c': // Report current installation state
413 pw.println("Current install state: "
414 + distroStatusToString(rulesState.getDistroStatus()));
415 break;
416 case 'i': // Report currently installed version
417 DistroRulesVersion installedRulesVersion =
418 rulesState.getInstalledDistroRulesVersion();
419 pw.print("Installed rules version: ");
420 if (installedRulesVersion == null) {
421 pw.println("<None>");
422 } else {
423 pw.println(installedRulesVersion.toDumpString());
424 }
425 break;
426 case 'o': // Report staged operation type
427 int stagedOperationType = rulesState.getStagedOperationType();
428 pw.println("Staged operation: "
429 + stagedOperationToString(stagedOperationType));
430 break;
431 case 't':
432 // Report staged version (i.e. the one that will be installed next boot
433 // if the staged operation is an install).
434 pw.print("Staged rules version: ");
435 DistroRulesVersion stagedDistroRulesVersion =
436 rulesState.getStagedDistroRulesVersion();
437 if (stagedDistroRulesVersion == null) {
438 pw.println("<None>");
439 } else {
Neil Fuller3e8c5952017-07-07 16:09:49 +0100440 pw.println(stagedDistroRulesVersion.toDumpString());
Neil Fuller87b11282017-06-23 16:43:45 +0100441 }
442 break;
443 case 'a':
444 // Report the active rules version (i.e. the rules in use by the current
445 // process).
446 pw.println("Active rules version (ICU, libcore): "
447 + ICU.getTZDataVersion() + ","
448 + ZoneInfoDB.getInstance().getVersion());
449 break;
450 default:
451 pw.println("Unknown option: " + c);
452 }
453 }
454 return;
455 }
456 }
457
458 pw.println("RulesManagerService state: " + toString());
459 pw.println("Active rules version (ICU, libcore): " + ICU.getTZDataVersion() + ","
460 + ZoneInfoDB.getInstance().getVersion());
Neil Fullerd857f672017-07-20 11:00:35 +0100461 pw.println("Distro state: " + rulesState.toString());
Neil Fuller87b11282017-06-23 16:43:45 +0100462 mPackageTracker.dump(pw);
463 }
464
465 @Override
466 public String toString() {
467 return "RulesManagerService{" +
468 "mOperationInProgress=" + mOperationInProgress +
469 '}';
470 }
471
Neil Fuller68f66662017-03-16 18:32:21 +0000472 private static CheckToken createCheckTokenOrThrow(byte[] checkTokenBytes) {
473 CheckToken checkToken;
474 try {
475 checkToken = CheckToken.fromByteArray(checkTokenBytes);
476 } catch (IOException e) {
477 throw new IllegalArgumentException("Unable to read token bytes "
478 + Arrays.toString(checkTokenBytes), e);
479 }
480 return checkToken;
481 }
Neil Fuller87b11282017-06-23 16:43:45 +0100482
483 private static String distroStatusToString(int distroStatus) {
484 switch(distroStatus) {
485 case DISTRO_STATUS_NONE:
486 return "None";
487 case DISTRO_STATUS_INSTALLED:
488 return "Installed";
489 case DISTRO_STATUS_UNKNOWN:
490 default:
491 return "Unknown";
492 }
493 }
494
495 private static String stagedOperationToString(int stagedOperationType) {
496 switch(stagedOperationType) {
497 case STAGED_OPERATION_NONE:
498 return "None";
499 case STAGED_OPERATION_UNINSTALL:
500 return "Uninstall";
501 case STAGED_OPERATION_INSTALL:
502 return "Install";
503 case STAGED_OPERATION_UNKNOWN:
504 default:
505 return "Unknown";
506 }
507 }
Neil Fullerd857f672017-07-20 11:00:35 +0100508
509 private static String toStringOrNull(Object obj) {
510 return obj == null ? null : obj.toString();
511 }
Neil Fuller68f66662017-03-16 18:32:21 +0000512}