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 package com.android.media.tests; 17 18 import com.android.media.tests.AudioLoopbackImageAnalyzer.Result; 19 import com.android.tradefed.device.DeviceNotAvailableException; 20 import com.android.tradefed.device.ITestDevice; 21 import com.android.tradefed.log.LogUtil.CLog; 22 import com.android.tradefed.util.Pair; 23 24 import com.google.common.io.Files; 25 26 import java.io.BufferedReader; 27 import java.io.File; 28 import java.io.FileNotFoundException; 29 import java.io.IOException; 30 import java.io.PrintWriter; 31 import java.io.UnsupportedEncodingException; 32 import java.nio.charset.StandardCharsets; 33 import java.time.Instant; 34 import java.util.ArrayList; 35 import java.util.Arrays; 36 import java.util.Collections; 37 import java.util.Comparator; 38 import java.util.HashMap; 39 import java.util.List; 40 import java.util.Map; 41 42 /** Helper class for AudioLoopbackTest. It keeps runtime data, analytics, */ 43 public class AudioLoopbackTestHelper { 44 45 private StatisticsData mLatencyStats = null; 46 private StatisticsData mConfidenceStats = null; 47 private ArrayList<ResultData> mAllResults; 48 private ArrayList<ResultData> mGoodResults = new ArrayList<ResultData>(); 49 private ArrayList<ResultData> mBadResults = new ArrayList<ResultData>(); 50 private ArrayList<Map<String, String>> mResultDictionaries = 51 new ArrayList<Map<String, String>>(); 52 53 // Controls acceptable tolerance in ms around median latency 54 private static final double TOLERANCE = 2.0; 55 56 //=================================================================== 57 // ENUMS 58 //=================================================================== 59 public enum LogFileType { 60 RESULT, 61 WAVE, 62 GRAPH, 63 PLAYER_BUFFER, 64 PLAYER_BUFFER_HISTOGRAM, 65 PLAYER_BUFFER_PERIOD_TIMES, 66 RECORDER_BUFFER, 67 RECORDER_BUFFER_HISTOGRAM, 68 RECORDER_BUFFER_PERIOD_TIMES, 69 GLITCHES_MILLIS, 70 HEAT_MAP, 71 LOGCAT 72 } 73 74 //=================================================================== 75 // INNER CLASSES 76 //=================================================================== 77 private class StatisticsData { 78 double mMin = 0; 79 double mMax = 0; 80 double mMean = 0; 81 double mMedian = 0; 82 83 @Override toString()84 public String toString() { 85 return String.format( 86 "min = %1$f, max = %2$f, median=%3$f, mean = %4$f", mMin, mMax, mMedian, mMean); 87 } 88 } 89 90 /** ResultData is an inner class that holds results and logfile info from each test run */ 91 public static class ResultData { 92 private Float mLatencyMs; 93 private Float mLatencyConfidence; 94 private Integer mAudioLevel; 95 private Integer mIteration; 96 private Long mDeviceTestStartTime; 97 private boolean mIsTimedOut = false; 98 private HashMap<LogFileType, String> mLogs = new HashMap<LogFileType, String>(); 99 private Result mImageAnalyzerResult = Result.UNKNOWN; 100 private String mFailureReason = null; 101 102 // Optional 103 private Float mPeriodConfidence = null; 104 private Float mRms = null; 105 private Float mRmsAverage = null; 106 private Integer mBblockSize = null; 107 getLatency()108 public float getLatency() { 109 return mLatencyMs.floatValue(); 110 } 111 setLatency(float latencyMs)112 public void setLatency(float latencyMs) { 113 this.mLatencyMs = Float.valueOf(latencyMs); 114 } 115 hasLatency()116 public boolean hasLatency() { 117 return mLatencyMs != null; 118 } 119 getConfidence()120 public float getConfidence() { 121 return mLatencyConfidence.floatValue(); 122 } 123 setConfidence(float latencyConfidence)124 public void setConfidence(float latencyConfidence) { 125 this.mLatencyConfidence = Float.valueOf(latencyConfidence); 126 } 127 hasConfidence()128 public boolean hasConfidence() { 129 return mLatencyConfidence != null; 130 } 131 getPeriodConfidence()132 public float getPeriodConfidence() { 133 return mPeriodConfidence.floatValue(); 134 } 135 setPeriodConfidence(float periodConfidence)136 public void setPeriodConfidence(float periodConfidence) { 137 this.mPeriodConfidence = Float.valueOf(periodConfidence); 138 } 139 hasPeriodConfidence()140 public boolean hasPeriodConfidence() { 141 return mPeriodConfidence != null; 142 } 143 getRMS()144 public float getRMS() { 145 return mRms.floatValue(); 146 } 147 setRMS(float rms)148 public void setRMS(float rms) { 149 this.mRms = Float.valueOf(rms); 150 } 151 hasRMS()152 public boolean hasRMS() { 153 return mRms != null; 154 } 155 getRMSAverage()156 public float getRMSAverage() { 157 return mRmsAverage.floatValue(); 158 } 159 setRMSAverage(float rmsAverage)160 public void setRMSAverage(float rmsAverage) { 161 this.mRmsAverage = Float.valueOf(rmsAverage); 162 } 163 hasRMSAverage()164 public boolean hasRMSAverage() { 165 return mRmsAverage != null; 166 } 167 getAudioLevel()168 public int getAudioLevel() { 169 return mAudioLevel.intValue(); 170 } 171 setAudioLevel(int audioLevel)172 public void setAudioLevel(int audioLevel) { 173 this.mAudioLevel = Integer.valueOf(audioLevel); 174 } 175 hasAudioLevel()176 public boolean hasAudioLevel() { 177 return mAudioLevel != null; 178 } 179 getBlockSize()180 public int getBlockSize() { 181 return mBblockSize.intValue(); 182 } 183 setBlockSize(int blockSize)184 public void setBlockSize(int blockSize) { 185 this.mBblockSize = Integer.valueOf(blockSize); 186 } 187 hasBlockSize()188 public boolean hasBlockSize() { 189 return mBblockSize != null; 190 } 191 getIteration()192 public int getIteration() { 193 return mIteration.intValue(); 194 } 195 setIteration(int iteration)196 public void setIteration(int iteration) { 197 this.mIteration = Integer.valueOf(iteration); 198 } 199 getDeviceTestStartTime()200 public long getDeviceTestStartTime() { 201 return mDeviceTestStartTime.longValue(); 202 } 203 setDeviceTestStartTime(long deviceTestStartTime)204 public void setDeviceTestStartTime(long deviceTestStartTime) { 205 this.mDeviceTestStartTime = Long.valueOf(deviceTestStartTime); 206 } 207 getImageAnalyzerResult()208 public Result getImageAnalyzerResult() { 209 return mImageAnalyzerResult; 210 } 211 setImageAnalyzerResult(Result imageAnalyzerResult)212 public void setImageAnalyzerResult(Result imageAnalyzerResult) { 213 this.mImageAnalyzerResult = imageAnalyzerResult; 214 } 215 getFailureReason()216 public String getFailureReason() { 217 return mFailureReason; 218 } 219 setFailureReason(String failureReason)220 public void setFailureReason(String failureReason) { 221 this.mFailureReason = failureReason; 222 } 223 isTimedOut()224 public boolean isTimedOut() { 225 return mIsTimedOut; 226 } 227 setIsTimedOut(boolean isTimedOut)228 public void setIsTimedOut(boolean isTimedOut) { 229 this.mIsTimedOut = isTimedOut; 230 } 231 getLogFile(LogFileType log)232 public String getLogFile(LogFileType log) { 233 return mLogs.get(log); 234 } 235 setLogFile(LogFileType log, String filename)236 public void setLogFile(LogFileType log, String filename) { 237 CLog.i("setLogFile: type=" + log.name() + ", filename=" + filename); 238 if (!mLogs.containsKey(log) && filename != null && !filename.isEmpty()) { 239 mLogs.put(log, filename); 240 } 241 } 242 hasBadResults()243 public boolean hasBadResults() { 244 return hasTimedOut() 245 || hasNoTestResults() 246 || !hasLatency() 247 || !hasConfidence() 248 || mImageAnalyzerResult == Result.FAIL; 249 } 250 hasTimedOut()251 public boolean hasTimedOut() { 252 return mIsTimedOut; 253 } 254 hasLogFile(LogFileType log)255 public boolean hasLogFile(LogFileType log) { 256 return mLogs.containsKey(log); 257 } 258 hasNoTestResults()259 public boolean hasNoTestResults() { 260 return !hasConfidence() && !hasLatency(); 261 } 262 263 public static Comparator<ResultData> latencyComparator = 264 new Comparator<ResultData>() { 265 @Override 266 public int compare(ResultData o1, ResultData o2) { 267 return o1.mLatencyMs.compareTo(o2.mLatencyMs); 268 } 269 }; 270 271 public static Comparator<ResultData> confidenceComparator = 272 new Comparator<ResultData>() { 273 @Override 274 public int compare(ResultData o1, ResultData o2) { 275 return o1.mLatencyConfidence.compareTo(o2.mLatencyConfidence); 276 } 277 }; 278 279 public static Comparator<ResultData> iteratorComparator = 280 new Comparator<ResultData>() { 281 @Override 282 public int compare(ResultData o1, ResultData o2) { 283 return Integer.compare(o1.mIteration, o2.mIteration); 284 } 285 }; 286 287 @Override toString()288 public String toString() { 289 final String NL = "\n"; 290 final StringBuilder sb = new StringBuilder(512); 291 sb.append("{").append(NL); 292 sb.append("{\nlatencyMs=").append(mLatencyMs).append(NL); 293 sb.append("latencyConfidence=").append(mLatencyConfidence).append(NL); 294 sb.append("isTimedOut=").append(mIsTimedOut).append(NL); 295 sb.append("iteration=").append(mIteration).append(NL); 296 sb.append("logs=").append(Arrays.toString(mLogs.values().toArray())).append(NL); 297 sb.append("audioLevel=").append(mAudioLevel).append(NL); 298 sb.append("deviceTestStartTime=").append(mDeviceTestStartTime).append(NL); 299 sb.append("rms=").append(mRms).append(NL); 300 sb.append("rmsAverage=").append(mRmsAverage).append(NL); 301 sb.append("}").append(NL); 302 return sb.toString(); 303 } 304 } 305 AudioLoopbackTestHelper(int iterations)306 public AudioLoopbackTestHelper(int iterations) { 307 mAllResults = new ArrayList<ResultData>(iterations); 308 } 309 addTestData(ResultData data, Map<String, String> resultDictionary, boolean useImageAnalyzer)310 public void addTestData(ResultData data, 311 Map<String, 312 String> resultDictionary, 313 boolean useImageAnalyzer) { 314 mResultDictionaries.add(data.getIteration(), resultDictionary); 315 mAllResults.add(data); 316 317 if (useImageAnalyzer && data.hasLogFile(LogFileType.GRAPH)) { 318 // Analyze captured screenshot to see if wave form is within reason 319 final String screenshot = data.getLogFile(LogFileType.GRAPH); 320 final Pair<Result, String> result = AudioLoopbackImageAnalyzer.analyzeImage(screenshot); 321 data.setImageAnalyzerResult(result.first); 322 data.setFailureReason(result.second); 323 } 324 } 325 getAllTestData()326 public final List<ResultData> getAllTestData() { 327 return mAllResults; 328 } 329 getResultDictionaryForIteration(int i)330 public Map<String, String> getResultDictionaryForIteration(int i) { 331 return mResultDictionaries.get(i); 332 } 333 334 /** 335 * Returns a list of the worst test result objects, up to maxNrOfWorstResults 336 * 337 * <p> 338 * 339 * <ol> 340 * <li> Tests in the bad results list are added first 341 * <li> If still space, add test results based on low confidence and then tests that are 342 * outside tolerance boundaries 343 * </ol> 344 * 345 * @param maxNrOfWorstResults 346 * @return list of worst test result objects 347 */ getWorstResults(int maxNrOfWorstResults)348 public List<ResultData> getWorstResults(int maxNrOfWorstResults) { 349 int counter = 0; 350 final ArrayList<ResultData> worstResults = new ArrayList<ResultData>(maxNrOfWorstResults); 351 352 for (final ResultData data : mBadResults) { 353 if (counter < maxNrOfWorstResults) { 354 worstResults.add(data); 355 counter++; 356 } 357 } 358 359 for (final ResultData data : mGoodResults) { 360 if (counter < maxNrOfWorstResults) { 361 boolean failed = false; 362 if (data.getConfidence() < 1.0f) { 363 data.setFailureReason("Low confidence"); 364 failed = true; 365 } else if (data.getLatency() < (mLatencyStats.mMedian - TOLERANCE) 366 || data.getLatency() > (mLatencyStats.mMedian + TOLERANCE)) { 367 data.setFailureReason("Latency not within tolerance from median"); 368 failed = true; 369 } 370 371 if (failed) { 372 worstResults.add(data); 373 counter++; 374 } 375 } 376 } 377 378 return worstResults; 379 } 380 parseKeyValuePairFromFile( File result, final Map<String, String> dictionary, final String resultKeyPrefix, final String splitOn, final String keyValueFormat)381 public static Map<String, String> parseKeyValuePairFromFile( 382 File result, 383 final Map<String, String> dictionary, 384 final String resultKeyPrefix, 385 final String splitOn, 386 final String keyValueFormat) 387 throws IOException { 388 389 final Map<String, String> resultMap = new HashMap<String, String>(); 390 final BufferedReader br = Files.newReader(result, StandardCharsets.UTF_8); 391 392 try { 393 String line = br.readLine(); 394 while (line != null) { 395 line = line.trim().replaceAll(" +", " "); 396 final String[] tokens = line.split(splitOn); 397 if (tokens.length >= 2) { 398 final String key = tokens[0].trim(); 399 final String value = tokens[1].trim(); 400 if (dictionary.containsKey(key)) { 401 CLog.i(String.format(keyValueFormat, key, value)); 402 resultMap.put(resultKeyPrefix + dictionary.get(key), value); 403 } 404 } 405 line = br.readLine(); 406 } 407 } finally { 408 br.close(); 409 } 410 return resultMap; 411 } 412 processTestData()413 public int processTestData() { 414 415 // Collect statistics about the test run 416 int nrOfValidResults = 0; 417 double sumLatency = 0; 418 double sumConfidence = 0; 419 420 final int totalNrOfTests = mAllResults.size(); 421 mLatencyStats = new StatisticsData(); 422 mConfidenceStats = new StatisticsData(); 423 mBadResults = new ArrayList<ResultData>(); 424 mGoodResults = new ArrayList<ResultData>(totalNrOfTests); 425 426 // Copy all results into Good results list 427 mGoodResults.addAll(mAllResults); 428 429 for (final ResultData data : mAllResults) { 430 if (data.hasBadResults()) { 431 mBadResults.add(data); 432 continue; 433 } 434 // Get mean values 435 sumLatency += data.getLatency(); 436 sumConfidence += data.getConfidence(); 437 } 438 439 if (!mBadResults.isEmpty()) { 440 analyzeBadResults(mBadResults, mAllResults.size()); 441 } 442 443 // Remove bad runs from result array 444 mGoodResults.removeAll(mBadResults); 445 446 // Fail test immediately if we don't have ANY good results 447 if (mGoodResults.isEmpty()) { 448 return 0; 449 } 450 451 nrOfValidResults = mGoodResults.size(); 452 453 // ---- LATENCY: Get Median, Min and Max values ---- 454 Collections.sort(mGoodResults, ResultData.latencyComparator); 455 456 mLatencyStats.mMin = mGoodResults.get(0).mLatencyMs; 457 mLatencyStats.mMax = mGoodResults.get(nrOfValidResults - 1).mLatencyMs; 458 mLatencyStats.mMean = sumLatency / nrOfValidResults; 459 // Is array even or odd numbered 460 if (nrOfValidResults % 2 == 0) { 461 final int middle = nrOfValidResults / 2; 462 final float middleLeft = mGoodResults.get(middle - 1).mLatencyMs; 463 final float middleRight = mGoodResults.get(middle).mLatencyMs; 464 mLatencyStats.mMedian = (middleLeft + middleRight) / 2.0f; 465 } else { 466 // It's and odd numbered array, just grab the middle value 467 mLatencyStats.mMedian = mGoodResults.get(nrOfValidResults / 2).mLatencyMs; 468 } 469 470 // ---- CONFIDENCE: Get Median, Min and Max values ---- 471 Collections.sort(mGoodResults, ResultData.confidenceComparator); 472 473 mConfidenceStats.mMin = mGoodResults.get(0).mLatencyConfidence; 474 mConfidenceStats.mMax = mGoodResults.get(nrOfValidResults - 1).mLatencyConfidence; 475 mConfidenceStats.mMean = sumConfidence / nrOfValidResults; 476 // Is array even or odd numbered 477 if (nrOfValidResults % 2 == 0) { 478 final int middle = nrOfValidResults / 2; 479 final float middleLeft = mGoodResults.get(middle - 1).mLatencyConfidence; 480 final float middleRight = mGoodResults.get(middle).mLatencyConfidence; 481 mConfidenceStats.mMedian = (middleLeft + middleRight) / 2.0f; 482 } else { 483 // It's and odd numbered array, just grab the middle value 484 mConfidenceStats.mMedian = mGoodResults.get(nrOfValidResults / 2).mLatencyConfidence; 485 } 486 487 for (final ResultData data : mGoodResults) { 488 // Check if within Latency Tolerance 489 if (data.getConfidence() < 1.0f) { 490 data.setFailureReason("Low confidence"); 491 } else if (data.getLatency() < (mLatencyStats.mMedian - TOLERANCE) 492 || data.getLatency() > (mLatencyStats.mMedian + TOLERANCE)) { 493 data.setFailureReason("Latency not within tolerance from median"); 494 } 495 } 496 497 // Create histogram 498 // Strategy: Create buckets based on whole ints, like 16 ms, 17 ms, 18 ms etc. Count how 499 // many tests fall into each bucket. Just cast the float to an int, no rounding up/down 500 // required. 501 final int[] histogram = new int[(int) mLatencyStats.mMax + 1]; 502 for (final ResultData rd : mGoodResults) { 503 // Increase value in bucket 504 histogram[(int) (rd.mLatencyMs.floatValue())]++; 505 } 506 507 CLog.i("========== VALID RESULTS ============================================"); 508 CLog.i(String.format("Valid tests: %1$d of %2$d", nrOfValidResults, totalNrOfTests)); 509 CLog.i("Latency: " + mLatencyStats.toString()); 510 CLog.i("Confidence: " + mConfidenceStats.toString()); 511 CLog.i("========== HISTOGRAM ================================================"); 512 for (int i = 0; i < histogram.length; i++) { 513 if (histogram[i] > 0) { 514 CLog.i(String.format("%1$01d ms => %2$d", i, histogram[i])); 515 } 516 } 517 518 // VERIFY the good results by running image analysis on the 519 // screenshot of the incoming audio waveform 520 521 return nrOfValidResults; 522 } 523 writeAllResultsToCSVFile(File csvFile, ITestDevice device)524 public void writeAllResultsToCSVFile(File csvFile, ITestDevice device) 525 throws DeviceNotAvailableException, FileNotFoundException, 526 UnsupportedEncodingException { 527 528 final String deviceType = device.getProperty("ro.build.product"); 529 final String buildId = device.getBuildAlias(); 530 final String serialNumber = device.getSerialNumber(); 531 532 // Sort data on iteration 533 Collections.sort(mAllResults, ResultData.iteratorComparator); 534 535 final StringBuilder sb = new StringBuilder(256); 536 final PrintWriter writer = new PrintWriter(csvFile, StandardCharsets.UTF_8.name()); 537 final String SEPARATOR = ","; 538 539 // Write column labels 540 writer.println( 541 "Device Time,Device Type,Build Id,Serial Number,Iteration,Latency," 542 + "Confidence,Period Confidence,Block Size,Audio Level,RMS,RMS Average," 543 + "Image Analysis,Failure Reason"); 544 for (final ResultData data : mAllResults) { 545 final Instant instant = Instant.ofEpochSecond(data.mDeviceTestStartTime); 546 547 sb.append(instant).append(SEPARATOR); 548 sb.append(deviceType).append(SEPARATOR); 549 sb.append(buildId).append(SEPARATOR); 550 sb.append(serialNumber).append(SEPARATOR); 551 sb.append(data.getIteration()).append(SEPARATOR); 552 sb.append(data.hasLatency() ? data.getLatency() : "").append(SEPARATOR); 553 sb.append(data.hasConfidence() ? data.getConfidence() : "").append(SEPARATOR); 554 sb.append(data.hasPeriodConfidence() ? data.getPeriodConfidence() : "").append(SEPARATOR); 555 sb.append(data.hasBlockSize() ? data.getBlockSize() : "").append(SEPARATOR); 556 sb.append(data.hasAudioLevel() ? data.getAudioLevel() : "").append(SEPARATOR); 557 sb.append(data.hasRMS() ? data.getRMS() : "").append(SEPARATOR); 558 sb.append(data.hasRMSAverage() ? data.getRMSAverage() : "").append(SEPARATOR); 559 sb.append(data.getImageAnalyzerResult().name()).append(SEPARATOR); 560 sb.append(data.getFailureReason()); 561 562 writer.println(sb.toString()); 563 564 sb.setLength(0); 565 } 566 writer.close(); 567 } 568 analyzeBadResults(ArrayList<ResultData> badResults, int totalNrOfTests)569 private void analyzeBadResults(ArrayList<ResultData> badResults, int totalNrOfTests) { 570 int testNoData = 0; 571 int testTimeoutCounts = 0; 572 int testResultsNotFoundCounts = 0; 573 int testWithoutLatencyResultCount = 0; 574 int testWithoutConfidenceResultCount = 0; 575 576 for (final ResultData data : badResults) { 577 if (data.hasTimedOut()) { 578 testTimeoutCounts++; 579 testNoData++; 580 continue; 581 } 582 583 if (data.hasNoTestResults()) { 584 testResultsNotFoundCounts++; 585 testNoData++; 586 continue; 587 } 588 589 if (!data.hasLatency()) { 590 testWithoutLatencyResultCount++; 591 testNoData++; 592 continue; 593 } 594 595 if (!data.hasConfidence()) { 596 testWithoutConfidenceResultCount++; 597 testNoData++; 598 continue; 599 } 600 } 601 602 CLog.i("========== BAD RESULTS ============================================"); 603 CLog.i(String.format("No Data: %1$d of %2$d", testNoData, totalNrOfTests)); 604 CLog.i(String.format("Timed out: %1$d of %2$d", testTimeoutCounts, totalNrOfTests)); 605 CLog.i( 606 String.format( 607 "No results: %1$d of %2$d", testResultsNotFoundCounts, totalNrOfTests)); 608 CLog.i( 609 String.format( 610 "No Latency results: %1$d of %2$d", 611 testWithoutLatencyResultCount, totalNrOfTests)); 612 CLog.i( 613 String.format( 614 "No Confidence results: %1$d of %2$d", 615 testWithoutConfidenceResultCount, totalNrOfTests)); 616 } 617 618 /** Generates metrics dictionary for stress test */ populateStressTestMetrics( Map<String, String> metrics, final String resultKeyPrefix)619 public void populateStressTestMetrics( 620 Map<String, String> metrics, final String resultKeyPrefix) { 621 metrics.put(resultKeyPrefix + "total_nr_of_tests", Integer.toString(mAllResults.size())); 622 metrics.put(resultKeyPrefix + "nr_of_good_tests", Integer.toString(mGoodResults.size())); 623 metrics.put(resultKeyPrefix + "latency_max", Double.toString(mLatencyStats.mMax)); 624 metrics.put(resultKeyPrefix + "latency_min", Double.toString(mLatencyStats.mMin)); 625 metrics.put(resultKeyPrefix + "latency_mean", Double.toString(mLatencyStats.mMean)); 626 metrics.put(resultKeyPrefix + "latency_median", Double.toString(mLatencyStats.mMedian)); 627 metrics.put(resultKeyPrefix + "confidence_max", Double.toString(mConfidenceStats.mMax)); 628 metrics.put(resultKeyPrefix + "confidence_min", Double.toString(mConfidenceStats.mMin)); 629 metrics.put(resultKeyPrefix + "confidence_mean", Double.toString(mConfidenceStats.mMean)); 630 metrics.put( 631 resultKeyPrefix + "confidence_median", Double.toString(mConfidenceStats.mMedian)); 632 } 633 } 634