1 /* 2 * Copyright (C) 2012 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.ota.tests; 18 19 import static org.hamcrest.CoreMatchers.equalTo; 20 import static org.hamcrest.CoreMatchers.not; 21 import static org.junit.Assert.assertThat; 22 23 import com.android.tradefed.build.IBuildInfo; 24 import com.android.tradefed.build.IDeviceBuildInfo; 25 import com.android.tradefed.build.OtaDeviceBuildInfo; 26 import com.android.tradefed.config.IConfiguration; 27 import com.android.tradefed.config.IConfigurationReceiver; 28 import com.android.tradefed.config.Option; 29 import com.android.tradefed.config.OptionClass; 30 import com.android.tradefed.device.DeviceNotAvailableException; 31 import com.android.tradefed.device.IManagedTestDevice; 32 import com.android.tradefed.device.ITestDevice; 33 import com.android.tradefed.device.TestDeviceState; 34 import com.android.tradefed.log.LogReceiver; 35 import com.android.tradefed.log.LogUtil.CLog; 36 import com.android.tradefed.result.FileInputStreamSource; 37 import com.android.tradefed.result.ITestInvocationListener; 38 import com.android.tradefed.result.InputStreamSource; 39 import com.android.tradefed.result.LogDataType; 40 import com.android.tradefed.result.TestDescription; 41 import com.android.tradefed.targetprep.BuildError; 42 import com.android.tradefed.targetprep.ITargetPreparer; 43 import com.android.tradefed.targetprep.TargetSetupError; 44 import com.android.tradefed.testtype.IBuildReceiver; 45 import com.android.tradefed.testtype.IDeviceTest; 46 import com.android.tradefed.testtype.IResumableTest; 47 import com.android.tradefed.util.DeviceRecoveryModeUtil; 48 import com.android.tradefed.util.FileUtil; 49 import com.android.tradefed.util.StreamUtil; 50 import com.android.tradefed.util.proto.TfMetricProtoUtil; 51 52 import org.junit.Assert; 53 54 import java.io.DataInputStream; 55 import java.io.DataOutputStream; 56 import java.io.File; 57 import java.io.IOException; 58 import java.net.InetSocketAddress; 59 import java.net.ServerSocket; 60 import java.net.Socket; 61 import java.net.SocketAddress; 62 import java.util.HashMap; 63 import java.util.Map; 64 65 /** 66 * A test that will perform repeated flash + install OTA actions on a device. 67 * <p/> 68 * adb must have root. 69 * <p/> 70 * Expects a {@link OtaDeviceBuildInfo}. 71 * <p/> 72 * Note: this test assumes that the {@link ITargetPreparer}s included in this test's 73 * {@link IConfiguration} will flash the device back to a baseline build, and prepare the device to 74 * receive the OTA to a new build. 75 */ 76 @OptionClass(alias = "ota-stability") 77 public class SideloadOtaStabilityTest implements IDeviceTest, IBuildReceiver, 78 IConfigurationReceiver, IResumableTest { 79 80 private static final String UNCRYPT_FILE_PATH = "/cache/recovery/uncrypt_file"; 81 private static final String UNCRYPT_STATUS_FILE = "/cache/recovery/uncrypt_status"; 82 private static final String BLOCK_MAP_PATH = "@/cache/recovery/block.map"; 83 private static final String RECOVERY_COMMAND_PATH = "/cache/recovery/command"; 84 private static final String LOG_RECOV = "/cache/recovery/last_log"; 85 private static final String LOG_KMSG = "/cache/recovery/last_kmsg"; 86 private static final String KMSG_CMD = "cat /proc/kmsg"; 87 private static final long SOCKET_RETRY_CT = 30; 88 private static final long UNCRYPT_TIMEOUT = 15 * 60 * 1000; 89 90 private static final long SHORT_WAIT_UNCRYPT = 3 * 1000; 91 92 private OtaDeviceBuildInfo mOtaDeviceBuild; 93 private IConfiguration mConfiguration; 94 private ITestDevice mDevice; 95 96 @Option(name = "run-name", description = 97 "The name of the ota stability test run. Used to report metrics.") 98 private String mRunName = "ota-stability"; 99 100 @Option(name = "iterations", description = 101 "Number of ota stability 'flash + wait for ota' iterations to run.") 102 private int mIterations = 20; 103 104 @Option(name = "resume", description = "Resume the ota test run if an device setup error " 105 + "stopped the previous test run.") 106 private boolean mResumeMode = false; 107 108 @Option(name = "max-install-time", description = 109 "The maximum time to wait for an ota to install in seconds.") 110 private int mMaxInstallOnlineTimeSec = 5 * 60; 111 112 @Option(name = "package-data-path", description = 113 "path on /data for the package to be saved to") 114 /* This is currently the only path readable by uncrypt on the userdata partition */ 115 private String mPackageDataPath = "/data/data/com.google.android.gsf/app_download/update.zip"; 116 117 @Option(name = "max-reboot-time", description = 118 "The maximum time to wait for a device to reboot out of recovery if it fails") 119 private long mMaxRebootTimeSec = 5 * 60; 120 121 @Option(name = "uncrypt-only", description = "if true, only uncrypt then exit") 122 private boolean mUncryptOnly = false; 123 124 /** controls if this test should be resumed. Only used if mResumeMode is enabled */ 125 private boolean mResumable = true; 126 127 private long mUncryptDuration; 128 private LogReceiver mKmsgReceiver; 129 130 /** 131 * {@inheritDoc} 132 */ 133 @Override 134 public void setConfiguration(IConfiguration configuration) { 135 mConfiguration = configuration; 136 } 137 138 /** 139 * {@inheritDoc} 140 */ 141 @Override 142 public void setBuild(IBuildInfo buildInfo) { 143 mOtaDeviceBuild = (OtaDeviceBuildInfo) buildInfo; 144 } 145 146 /** 147 * {@inheritDoc} 148 */ 149 @Override 150 public void setDevice(ITestDevice device) { 151 mDevice = device; 152 } 153 154 /** 155 * {@inheritDoc} 156 */ 157 @Override 158 public ITestDevice getDevice() { 159 return mDevice; 160 } 161 162 /** 163 * Set the run name 164 */ 165 void setRunName(String runName) { 166 mRunName = runName; 167 } 168 169 /** 170 * Return the number of iterations. 171 * <p/> 172 * Exposed for unit testing 173 */ 174 public int getIterations() { 175 return mIterations; 176 } 177 178 /** 179 * Set the iterations 180 */ 181 void setIterations(int iterations) { 182 mIterations = iterations; 183 } 184 185 /** 186 * {@inheritDoc} 187 */ 188 @Override 189 public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { 190 // started run, turn to off 191 mResumable = false; 192 mKmsgReceiver = new LogReceiver(getDevice(), KMSG_CMD, "kmsg"); 193 checkFields(); 194 195 CLog.i("Starting OTA sideload test from %s to %s, for %d iterations", 196 mOtaDeviceBuild.getDeviceImageVersion(), 197 mOtaDeviceBuild.getOtaBuild().getOtaPackageVersion(), mIterations); 198 199 long startTime = System.currentTimeMillis(); 200 listener.testRunStarted(mRunName, 0); 201 int actualIterations = 0; 202 BootTimeInfo lastBootTime = null; 203 try { 204 while (actualIterations < mIterations) { 205 if (actualIterations != 0) { 206 // don't need to flash device on first iteration 207 flashDevice(); 208 } 209 lastBootTime = installOta(listener, mOtaDeviceBuild.getOtaBuild()); 210 actualIterations++; 211 CLog.i("Device %s successfully OTA-ed to build %s. Iteration: %d of %d", 212 mDevice.getSerialNumber(), 213 mOtaDeviceBuild.getOtaBuild().getOtaPackageVersion(), 214 actualIterations, mIterations); 215 } 216 } catch (AssertionError | BuildError e) { 217 CLog.e(e); 218 } catch (TargetSetupError e) { 219 CLog.i("Encountered TargetSetupError, marking this test as resumable"); 220 mResumable = true; 221 CLog.e(e); 222 // throw up an exception so this test can be resumed 223 Assert.fail(e.toString()); 224 } finally { 225 // if the device is down, we need to recover it so we can safely pull logs 226 IManagedTestDevice managedDevice = (IManagedTestDevice) mDevice; 227 if (!managedDevice.getDeviceState().equals(TestDeviceState.ONLINE)) { 228 // not all IDeviceRecovery implementations can handle getting out of recovery mode, 229 // so we should just reboot in that case since we no longer need to be in 230 // recovery 231 CLog.i("Device is not online, attempting to recover before capturing logs"); 232 DeviceRecoveryModeUtil.bootOutOfRecovery((IManagedTestDevice) mDevice, 233 mMaxInstallOnlineTimeSec * 1000); 234 } 235 double updateTime = sendRecoveryLog(listener); 236 Map<String, String> metrics = new HashMap<>(1); 237 metrics.put("iterations", Integer.toString(actualIterations)); 238 metrics.put("failed_iterations", Integer.toString(mIterations - actualIterations)); 239 metrics.put("update_time", Double.toString(updateTime)); 240 metrics.put("uncrypt_time", Long.toString(mUncryptDuration)); 241 if (lastBootTime != null) { 242 metrics.put("boot_time_online", Double.toString(lastBootTime.mOnlineTime)); 243 metrics.put("boot_time_available", Double.toString(lastBootTime.mAvailTime)); 244 } 245 long endTime = System.currentTimeMillis() - startTime; 246 listener.testRunEnded(endTime, TfMetricProtoUtil.upgradeConvert(metrics)); 247 } 248 } 249 250 /** 251 * Flash the device back to baseline build. 252 * <p/> 253 * Currently does this by re-running {@link ITargetPreparer#setUp(ITestDevice, IBuildInfo)} 254 */ 255 private void flashDevice() throws TargetSetupError, BuildError, DeviceNotAvailableException { 256 // assume the target preparers will flash the device back to device build 257 for (ITargetPreparer preparer : mConfiguration.getTargetPreparers()) { 258 preparer.setUp(mDevice, mOtaDeviceBuild); 259 } 260 } 261 262 private void checkFields() { 263 if (mDevice == null) { 264 throw new IllegalArgumentException("missing device"); 265 } 266 if (mConfiguration == null) { 267 throw new IllegalArgumentException("missing configuration"); 268 } 269 if (mOtaDeviceBuild == null) { 270 throw new IllegalArgumentException("missing build info"); 271 } 272 } 273 274 /** 275 * {@inheritDoc} 276 */ 277 @Override 278 public boolean isResumable() { 279 return mResumeMode && mResumable; 280 } 281 282 /** 283 * Actually install the OTA. 284 * @param listener 285 * @param otaBuild 286 * @return the amount of time in ms that the device took to boot after installing. 287 * @throws DeviceNotAvailableException 288 */ 289 private BootTimeInfo installOta(ITestInvocationListener listener, IDeviceBuildInfo otaBuild) 290 throws DeviceNotAvailableException { 291 TestDescription test = 292 new TestDescription(getClass().getName(), String.format("apply_ota[%s]", mRunName)); 293 Map<String, String> metrics = new HashMap<String, String>(); 294 listener.testStarted(test); 295 try { 296 mKmsgReceiver.start(); 297 try { 298 CLog.i("Pushing OTA package %s", otaBuild.getOtaPackageFile().getAbsolutePath()); 299 Assert.assertTrue(mDevice.pushFile(otaBuild.getOtaPackageFile(), mPackageDataPath)); 300 // this file needs to be uncrypted, since /data isn't mounted in recovery 301 // block.map should be empty since cache should be cleared 302 mDevice.pushString(mPackageDataPath + "\n", UNCRYPT_FILE_PATH); 303 // Flushing the file to flash. 304 mDevice.executeShellCommand("sync"); 305 306 mUncryptDuration = doUncrypt(SocketFactory.getInstance(), listener); 307 metrics.put("uncrypt_duration", Long.toString(mUncryptDuration)); 308 String installOtaCmd = String.format("--update_package=%s\n", BLOCK_MAP_PATH); 309 mDevice.pushString(installOtaCmd, RECOVERY_COMMAND_PATH); 310 CLog.i("Rebooting to install OTA"); 311 } finally { 312 // Kmsg contents during the OTA will be capture in last_kmsg, so we can turn off the 313 // kmsg receiver now 314 mKmsgReceiver.postLog(listener); 315 mKmsgReceiver.stop(); 316 } 317 // uncrypt is complete 318 if (mUncryptOnly) { 319 return new BootTimeInfo(-1, -1); 320 } 321 try { 322 mDevice.rebootIntoRecovery(); 323 } catch (DeviceNotAvailableException e) { 324 // The device will only enter the RECOVERY state if it hits the recovery menu. 325 // Since we added a command to /cache/recovery/command, recovery mode executes the 326 // command rather than booting into the menu. While applying the update as a result 327 // of the installed command, the device reports its state as NOT_AVAILABLE. If the 328 // device *actually* becomes unavailable, we will catch the resulting DNAE in the 329 // next call to waitForDeviceOnline. 330 CLog.i("Didn't go to recovery, went straight to update"); 331 } 332 333 mDevice.waitForDeviceNotAvailable(mMaxInstallOnlineTimeSec * 1000); 334 long start = System.currentTimeMillis(); 335 336 try { 337 mDevice.waitForDeviceOnline(mMaxInstallOnlineTimeSec * 1000); 338 } catch (DeviceNotAvailableException e) { 339 CLog.e("Device %s did not come back online after recovery", mDevice.getSerialNumber()); 340 listener.testFailed(test, e.getLocalizedMessage()); 341 listener.testRunFailed("Device did not come back online after recovery"); 342 sendUpdatePackage(listener, otaBuild); 343 throw new AssertionError("Device did not come back online after recovery"); 344 } 345 double onlineTime = (System.currentTimeMillis() - start) / 1000.0; 346 try { 347 mDevice.waitForDeviceAvailable(); 348 } catch (DeviceNotAvailableException e) { 349 CLog.e("Device %s did not boot up successfully after installing OTA", 350 mDevice.getSerialNumber()); 351 listener.testFailed(test, e.getLocalizedMessage()); 352 listener.testRunFailed("Device failed to boot after OTA"); 353 sendUpdatePackage(listener, otaBuild); 354 throw new AssertionError("Device failed to boot after OTA"); 355 } 356 double availTime = (System.currentTimeMillis() - start) / 1000.0; 357 return new BootTimeInfo(availTime, onlineTime); 358 } finally { 359 listener.testEnded(test, TfMetricProtoUtil.upgradeConvert(metrics)); 360 } 361 } 362 363 private InputStreamSource pullLogFile(String location) 364 throws DeviceNotAvailableException { 365 try { 366 // get recovery log 367 File destFile = FileUtil.createTempFile("recovery", "log"); 368 boolean gotFile = mDevice.pullFile(location, destFile); 369 if (gotFile) { 370 return new FileInputStreamSource(destFile, true /* delete */); 371 } 372 } catch (IOException e) { 373 CLog.e("Failed to get recovery log from device %s", mDevice.getSerialNumber()); 374 CLog.e(e); 375 } 376 return null; 377 } 378 379 protected void sendUpdatePackage(ITestInvocationListener listener, IDeviceBuildInfo otaBuild) { 380 InputStreamSource pkgSource = null; 381 try { 382 pkgSource = new FileInputStreamSource(otaBuild.getOtaPackageFile()); 383 listener.testLog(mRunName + "_package", LogDataType.ZIP, pkgSource); 384 } catch (NullPointerException e) { 385 CLog.w("Couldn't save update package due to exception"); 386 CLog.e(e); 387 return; 388 } finally { 389 StreamUtil.cancel(pkgSource); 390 } 391 } 392 393 protected double sendRecoveryLog(ITestInvocationListener listener) 394 throws DeviceNotAvailableException { 395 InputStreamSource lastLog = pullLogFile(LOG_RECOV); 396 InputStreamSource lastKmsg = pullLogFile(LOG_KMSG); 397 InputStreamSource blockMap = pullLogFile(BLOCK_MAP_PATH.substring(1)); 398 double elapsedTime = 0; 399 // last_log contains a timing metric in its last line, capture it here and return it 400 // for the metrics map to report 401 try { 402 if (lastLog == null || lastKmsg == null) { 403 CLog.w( 404 "Could not find last_log at directory %s, or last_kmsg at directory %s", 405 LOG_RECOV, LOG_KMSG); 406 return elapsedTime; 407 } 408 409 try { 410 String[] lastLogLines = StreamUtil.getStringFromSource(lastLog).split("\n"); 411 String endLine = lastLogLines[lastLogLines.length - 1]; 412 elapsedTime = Double.parseDouble( 413 endLine.substring(endLine.indexOf('[') + 1, endLine.indexOf(']')).trim()); 414 } catch (IOException | NumberFormatException | NullPointerException e) { 415 CLog.w("Couldn't get elapsed time from last_log due to exception"); 416 CLog.e(e); 417 } 418 listener.testLog(this.mRunName + "_recovery_log", LogDataType.TEXT, 419 lastLog); 420 listener.testLog(this.mRunName + "_recovery_kmsg", LogDataType.TEXT, 421 lastKmsg); 422 if (blockMap == null) { 423 CLog.w("Could not find block.map"); 424 } else { 425 listener.testLog(this.mRunName + "_block_map", LogDataType.TEXT, 426 blockMap); 427 } 428 return elapsedTime; 429 } finally { 430 StreamUtil.cancel(lastLog); 431 StreamUtil.cancel(lastKmsg); 432 StreamUtil.cancel(blockMap); 433 } 434 } 435 436 /** 437 * Uncrypt needs to attach to a socket before it will actually begin work, so we need to attach 438 * a socket to it. 439 * 440 * @return Elapse time of uncrypt 441 */ 442 public long doUncrypt( 443 ISocketFactory sockets, @SuppressWarnings("unused") ITestInvocationListener listener) 444 throws DeviceNotAvailableException { 445 // init has to start uncrypt or the socket will not be allocated 446 CLog.i("Starting uncrypt service"); 447 mDevice.executeShellCommand("setprop ctl.start uncrypt"); 448 try { 449 // This is a workaround for known issue with f2fs system. 450 CLog.i("Sleeping %d for uncrypt to be ready for socket connection.", 451 SHORT_WAIT_UNCRYPT); 452 Thread.sleep(SHORT_WAIT_UNCRYPT); 453 } catch (InterruptedException e) { 454 CLog.i("Got interrupted when waiting to uncrypt file."); 455 Thread.currentThread().interrupt(); 456 } 457 // MNC version of uncrypt does not require socket connection 458 if (mDevice.getApiLevel() < 24) { 459 CLog.i("Waiting for MNC uncrypt service finish"); 460 return waitForUncrypt(); 461 } 462 int port; 463 try { 464 port = getFreePort(); 465 } catch (IOException e) { 466 throw new RuntimeException(e); 467 } 468 // The socket uncrypt wants to run on is a local unix socket, so we can forward a tcp 469 // port to connect to it. 470 CLog.i("Connecting to uncrypt on port %d", port); 471 mDevice.executeAdbCommand("forward", "tcp:" + port, "localreserved:uncrypt"); 472 // connect to uncrypt! 473 String hostname = "localhost"; 474 long start = System.currentTimeMillis(); 475 try (Socket uncrypt = sockets.createClientSocket(hostname, port)) { 476 connectSocket(uncrypt, hostname, port); 477 try (DataInputStream dis = new DataInputStream(uncrypt.getInputStream()); 478 DataOutputStream dos = new DataOutputStream(uncrypt.getOutputStream())) { 479 int status = Integer.MIN_VALUE; 480 while (true) { 481 status = dis.readInt(); 482 if (isUncryptSuccess(status)) { 483 dos.writeInt(0); 484 break; 485 } 486 } 487 CLog.i("Final uncrypt status: %d", status); 488 return System.currentTimeMillis() - start; 489 } 490 } catch (IOException e) { 491 CLog.e("Lost connection with uncrypt due to IOException:"); 492 CLog.e(e); 493 return waitForUncrypt(); 494 } 495 } 496 497 private long waitForUncrypt() throws DeviceNotAvailableException { 498 CLog.i("Continuing to watch uncrypt progress for %d ms", UNCRYPT_TIMEOUT); 499 long time = 0; 500 int lastStatus = -1; 501 long start = System.currentTimeMillis(); 502 while ((time = System.currentTimeMillis() - start) < UNCRYPT_TIMEOUT) { 503 int status = readUncryptStatusFromFile(); 504 if (isUncryptSuccess(status)) { 505 return time; 506 } 507 if (status != lastStatus) { 508 lastStatus = status; 509 CLog.d("uncrypt status: %d", status); 510 } 511 try { 512 Thread.sleep(1000); 513 } catch (InterruptedException unused) { 514 Thread.currentThread().interrupt(); 515 } 516 } 517 CLog.e("Uncrypt didn't finish, last status was %d", lastStatus); 518 throw new RuntimeException("Uncrypt didn't succeed after timeout"); 519 } 520 521 private boolean isUncryptSuccess(int status) { 522 if (status == 100) { 523 CLog.i("Uncrypt finished successfully"); 524 return true; 525 } else if (status > 100 || status < 0) { 526 CLog.e("Uncrypt returned error status %d", status); 527 assertThat(status, not(equalTo(100))); 528 } 529 return false; 530 } 531 532 protected int getFreePort() throws IOException { 533 try (ServerSocket sock = new ServerSocket(0)) { 534 return sock.getLocalPort(); 535 } 536 } 537 538 /** 539 * Read the error status from uncrypt_status on device. 540 * 541 * @return 0 if uncrypt succeed, -1 if file not exists, specific error code otherwise. 542 */ 543 protected int readUncryptStatusFromFile() 544 throws DeviceNotAvailableException { 545 try { 546 InputStreamSource status = pullLogFile(UNCRYPT_STATUS_FILE); 547 if (status == null) { 548 return -1; 549 } 550 String[] lastLogLines = StreamUtil.getStringFromSource(status).split("\n"); 551 String endLine = lastLogLines[lastLogLines.length - 1]; 552 if (endLine.toLowerCase().startsWith("uncrypt_error:")) { 553 String[] elements = endLine.split(":"); 554 return Integer.parseInt(elements[1].trim()); 555 } else { 556 // MNC device case 557 return Integer.parseInt(endLine.trim()); 558 } 559 } catch (IOException e) { 560 throw new RuntimeException(e); 561 } 562 } 563 564 protected void connectSocket(Socket s, String host, int port) { 565 SocketAddress address = new InetSocketAddress(host, port); 566 boolean connected = false; 567 for (int i = 0; i < SOCKET_RETRY_CT; i++) { 568 try { 569 if (!s.isConnected()) { 570 s.connect(address); 571 } 572 connected = true; 573 break; 574 } catch (IOException unused) { 575 try { 576 Thread.sleep(1000); 577 CLog.d("Uncrypt socket was not ready on iteration %d of %d, retrying", i, 578 SOCKET_RETRY_CT); 579 } catch (InterruptedException e) { 580 CLog.w("Interrupted while connecting uncrypt socket on iteration %d", i); 581 Thread.currentThread().interrupt(); 582 } 583 } 584 } 585 if (!connected) { 586 throw new RuntimeException("failed to connect uncrypt socket"); 587 } 588 } 589 590 /** 591 * Provides a client socket. Allows for providing mock sockets to doUncrypt in unit testing. 592 */ 593 public interface ISocketFactory { 594 public Socket createClientSocket(String host, int port) throws IOException; 595 } 596 597 /** 598 * Default implementation of {@link ISocketFactory}, which provides a {@link Socket}. 599 */ 600 protected static class SocketFactory implements ISocketFactory { 601 private static SocketFactory sInstance; 602 603 private SocketFactory() { 604 } 605 606 public static SocketFactory getInstance() { 607 if (sInstance == null) { 608 sInstance = new SocketFactory(); 609 } 610 return sInstance; 611 } 612 613 @Override 614 public Socket createClientSocket(String host, int port) throws IOException { 615 return new Socket(host, port); 616 } 617 } 618 619 private static class BootTimeInfo { 620 /** 621 * Time (s) until device is completely available 622 */ 623 public double mAvailTime; 624 /** 625 * Time (s) until device is in "ONLINE" state 626 */ 627 public double mOnlineTime; 628 629 public BootTimeInfo(double avail, double online) { 630 mAvailTime = avail; 631 mOnlineTime = online; 632 } 633 } 634 } 635