Analytics.java revision 874c0f8fa95a5da5a82e67c1fe39697883d753eb
1/* 2 * Copyright (C) 2015 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.telecom; 18 19import android.telecom.DisconnectCause; 20import android.telecom.ParcelableCallAnalytics; 21import android.telecom.TelecomAnalytics; 22import android.util.SparseArray; 23 24import com.android.internal.annotations.VisibleForTesting; 25import com.android.internal.util.IndentingPrintWriter; 26 27import java.util.ArrayList; 28import java.util.Collections; 29import java.util.HashMap; 30import java.util.LinkedList; 31import java.util.List; 32import java.util.Map; 33import java.util.stream.Collectors; 34 35import static android.telecom.ParcelableCallAnalytics.AnalyticsEvent; 36import static android.telecom.TelecomAnalytics.SessionTiming; 37 38/** 39 * A class that collects and stores data on how calls are being made, in order to 40 * aggregate these into useful statistics. 41 */ 42public class Analytics { 43 public static final Map<String, Integer> sLogEventToAnalyticsEvent = 44 new HashMap<String, Integer>() {{ 45 put(Log.Events.SET_SELECT_PHONE_ACCOUNT, AnalyticsEvent.SET_SELECT_PHONE_ACCOUNT); 46 put(Log.Events.REQUEST_HOLD, AnalyticsEvent.REQUEST_HOLD); 47 put(Log.Events.REQUEST_UNHOLD, AnalyticsEvent.REQUEST_UNHOLD); 48 put(Log.Events.SWAP, AnalyticsEvent.SWAP); 49 put(Log.Events.SKIP_RINGING, AnalyticsEvent.SKIP_RINGING); 50 put(Log.Events.CONFERENCE_WITH, AnalyticsEvent.CONFERENCE_WITH); 51 put(Log.Events.SPLIT_FROM_CONFERENCE, AnalyticsEvent.SPLIT_CONFERENCE); 52 put(Log.Events.SET_PARENT, AnalyticsEvent.SET_PARENT); 53 put(Log.Events.MUTE, AnalyticsEvent.MUTE); 54 put(Log.Events.UNMUTE, AnalyticsEvent.UNMUTE); 55 put(Log.Events.AUDIO_ROUTE_BT, AnalyticsEvent.AUDIO_ROUTE_BT); 56 put(Log.Events.AUDIO_ROUTE_EARPIECE, AnalyticsEvent.AUDIO_ROUTE_EARPIECE); 57 put(Log.Events.AUDIO_ROUTE_HEADSET, AnalyticsEvent.AUDIO_ROUTE_HEADSET); 58 put(Log.Events.AUDIO_ROUTE_SPEAKER, AnalyticsEvent.AUDIO_ROUTE_SPEAKER); 59 put(Log.Events.SILENCE, AnalyticsEvent.SILENCE); 60 put(Log.Events.SCREENING_COMPLETED, AnalyticsEvent.SCREENING_COMPLETED); 61 put(Log.Events.BLOCK_CHECK_FINISHED, AnalyticsEvent.BLOCK_CHECK_FINISHED); 62 put(Log.Events.DIRECT_TO_VM_FINISHED, AnalyticsEvent.DIRECT_TO_VM_FINISHED); 63 put(Log.Events.REMOTELY_HELD, AnalyticsEvent.REMOTELY_HELD); 64 put(Log.Events.REMOTELY_UNHELD, AnalyticsEvent.REMOTELY_UNHELD); 65 put(Log.Events.REQUEST_PULL, AnalyticsEvent.REQUEST_PULL); 66 put(Log.Events.REQUEST_ACCEPT, AnalyticsEvent.REQUEST_ACCEPT); 67 put(Log.Events.REQUEST_REJECT, AnalyticsEvent.REQUEST_REJECT); 68 put(Log.Events.SET_ACTIVE, AnalyticsEvent.SET_ACTIVE); 69 put(Log.Events.SET_DISCONNECTED, AnalyticsEvent.SET_DISCONNECTED); 70 put(Log.Events.SET_HOLD, AnalyticsEvent.SET_HOLD); 71 put(Log.Events.SET_DIALING, AnalyticsEvent.SET_DIALING); 72 put(Log.Events.START_CONNECTION, AnalyticsEvent.START_CONNECTION); 73 put(Log.Events.BIND_CS, AnalyticsEvent.BIND_CS); 74 put(Log.Events.CS_BOUND, AnalyticsEvent.CS_BOUND); 75 put(Log.Events.SCREENING_SENT, AnalyticsEvent.SCREENING_SENT); 76 put(Log.Events.DIRECT_TO_VM_INITIATED, AnalyticsEvent.DIRECT_TO_VM_INITIATED); 77 put(Log.Events.BLOCK_CHECK_INITIATED, AnalyticsEvent.BLOCK_CHECK_INITIATED); 78 put(Log.Events.FILTERING_INITIATED, AnalyticsEvent.FILTERING_INITIATED); 79 put(Log.Events.FILTERING_COMPLETED, AnalyticsEvent.FILTERING_COMPLETED); 80 put(Log.Events.FILTERING_TIMED_OUT, AnalyticsEvent.FILTERING_TIMED_OUT); 81 }}; 82 83 public static final Map<String, Integer> sLogSessionToSessionId = 84 new HashMap<String, Integer> () {{ 85 put(Log.Sessions.ICA_ANSWER_CALL, SessionTiming.ICA_ANSWER_CALL); 86 put(Log.Sessions.ICA_REJECT_CALL, SessionTiming.ICA_REJECT_CALL); 87 put(Log.Sessions.ICA_DISCONNECT_CALL, SessionTiming.ICA_DISCONNECT_CALL); 88 put(Log.Sessions.ICA_HOLD_CALL, SessionTiming.ICA_HOLD_CALL); 89 put(Log.Sessions.ICA_UNHOLD_CALL, SessionTiming.ICA_UNHOLD_CALL); 90 put(Log.Sessions.ICA_MUTE, SessionTiming.ICA_MUTE); 91 put(Log.Sessions.ICA_SET_AUDIO_ROUTE, SessionTiming.ICA_SET_AUDIO_ROUTE); 92 put(Log.Sessions.ICA_CONFERENCE, SessionTiming.ICA_CONFERENCE); 93 put(Log.Sessions.CSW_HANDLE_CREATE_CONNECTION_COMPLETE, 94 SessionTiming.CSW_HANDLE_CREATE_CONNECTION_COMPLETE); 95 put(Log.Sessions.CSW_SET_ACTIVE, SessionTiming.CSW_SET_ACTIVE); 96 put(Log.Sessions.CSW_SET_RINGING, SessionTiming.CSW_SET_RINGING); 97 put(Log.Sessions.CSW_SET_DIALING, SessionTiming.CSW_SET_DIALING); 98 put(Log.Sessions.CSW_SET_DISCONNECTED, SessionTiming.CSW_SET_DISCONNECTED); 99 put(Log.Sessions.CSW_SET_ON_HOLD, SessionTiming.CSW_SET_ON_HOLD); 100 put(Log.Sessions.CSW_REMOVE_CALL, SessionTiming.CSW_REMOVE_CALL); 101 put(Log.Sessions.CSW_SET_IS_CONFERENCED, SessionTiming.CSW_SET_IS_CONFERENCED); 102 put(Log.Sessions.CSW_ADD_CONFERENCE_CALL, SessionTiming.CSW_ADD_CONFERENCE_CALL); 103 104 }}; 105 106 public static final Map<String, Integer> sLogEventTimingToAnalyticsEventTiming = 107 new HashMap<String, Integer>() {{ 108 put(Log.Events.Timings.ACCEPT_TIMING, 109 ParcelableCallAnalytics.EventTiming.ACCEPT_TIMING); 110 put(Log.Events.Timings.REJECT_TIMING, 111 ParcelableCallAnalytics.EventTiming.REJECT_TIMING); 112 put(Log.Events.Timings.DISCONNECT_TIMING, 113 ParcelableCallAnalytics.EventTiming.DISCONNECT_TIMING); 114 put(Log.Events.Timings.HOLD_TIMING, 115 ParcelableCallAnalytics.EventTiming.HOLD_TIMING); 116 put(Log.Events.Timings.UNHOLD_TIMING, 117 ParcelableCallAnalytics.EventTiming.UNHOLD_TIMING); 118 put(Log.Events.Timings.OUTGOING_TIME_TO_DIALING_TIMING, 119 ParcelableCallAnalytics.EventTiming.OUTGOING_TIME_TO_DIALING_TIMING); 120 put(Log.Events.Timings.BIND_CS_TIMING, 121 ParcelableCallAnalytics.EventTiming.BIND_CS_TIMING); 122 put(Log.Events.Timings.SCREENING_COMPLETED_TIMING, 123 ParcelableCallAnalytics.EventTiming.SCREENING_COMPLETED_TIMING); 124 put(Log.Events.Timings.DIRECT_TO_VM_FINISHED_TIMING, 125 ParcelableCallAnalytics.EventTiming.DIRECT_TO_VM_FINISHED_TIMING); 126 put(Log.Events.Timings.BLOCK_CHECK_FINISHED_TIMING, 127 ParcelableCallAnalytics.EventTiming.BLOCK_CHECK_FINISHED_TIMING); 128 put(Log.Events.Timings.FILTERING_COMPLETED_TIMING, 129 ParcelableCallAnalytics.EventTiming.FILTERING_COMPLETED_TIMING); 130 put(Log.Events.Timings.FILTERING_TIMED_OUT_TIMING, 131 ParcelableCallAnalytics.EventTiming.FILTERING_TIMED_OUT_TIMING); 132 }}; 133 134 public static final Map<Integer, String> sSessionIdToLogSession = new HashMap<>(); 135 static { 136 for (Map.Entry<String, Integer> e : sLogSessionToSessionId.entrySet()) { 137 sSessionIdToLogSession.put(e.getValue(), e.getKey()); 138 } 139 } 140 141 public static class CallInfo { 142 public void setCallStartTime(long startTime) { 143 } 144 145 public void setCallEndTime(long endTime) { 146 } 147 148 public void setCallIsAdditional(boolean isAdditional) { 149 } 150 151 public void setCallIsInterrupted(boolean isInterrupted) { 152 } 153 154 public void setCallDisconnectCause(DisconnectCause disconnectCause) { 155 } 156 157 public void addCallTechnology(int callTechnology) { 158 } 159 160 public void setCreatedFromExistingConnection(boolean createdFromExistingConnection) { 161 } 162 163 public void setCallConnectionService(String connectionServiceName) { 164 } 165 166 public void setCallEvents(Log.CallEventRecord records) { 167 } 168 } 169 170 /** 171 * A class that holds data associated with a call. 172 */ 173 @VisibleForTesting 174 public static class CallInfoImpl extends CallInfo { 175 public String callId; 176 public long startTime; // start time in milliseconds since the epoch. 0 if not yet set. 177 public long endTime; // end time in milliseconds since the epoch. 0 if not yet set. 178 public int callDirection; // one of UNKNOWN_DIRECTION, INCOMING_DIRECTION, 179 // or OUTGOING_DIRECTION. 180 public boolean isAdditionalCall = false; // true if the call came in while another call was 181 // in progress or if the user dialed this call 182 // while in the middle of another call. 183 public boolean isInterrupted = false; // true if the call was interrupted by an incoming 184 // or outgoing call. 185 public int callTechnologies; // bitmask denoting which technologies a call used. 186 187 // true if the Telecom Call object was created from an existing connection via 188 // CallsManager#createCallForExistingConnection, for example, by ImsConference. 189 public boolean createdFromExistingConnection = false; 190 191 public DisconnectCause callTerminationReason; 192 public String connectionService; 193 public boolean isEmergency = false; 194 195 public Log.CallEventRecord callEvents; 196 197 CallInfoImpl(String callId, int callDirection) { 198 this.callId = callId; 199 startTime = 0; 200 endTime = 0; 201 this.callDirection = callDirection; 202 callTechnologies = 0; 203 connectionService = ""; 204 } 205 206 CallInfoImpl(CallInfoImpl other) { 207 this.callId = other.callId; 208 this.startTime = other.startTime; 209 this.endTime = other.endTime; 210 this.callDirection = other.callDirection; 211 this.isAdditionalCall = other.isAdditionalCall; 212 this.isInterrupted = other.isInterrupted; 213 this.callTechnologies = other.callTechnologies; 214 this.createdFromExistingConnection = other.createdFromExistingConnection; 215 this.connectionService = other.connectionService; 216 this.isEmergency = other.isEmergency; 217 this.callEvents = other.callEvents; 218 219 if (other.callTerminationReason != null) { 220 this.callTerminationReason = new DisconnectCause( 221 other.callTerminationReason.getCode(), 222 other.callTerminationReason.getLabel(), 223 other.callTerminationReason.getDescription(), 224 other.callTerminationReason.getReason(), 225 other.callTerminationReason.getTone()); 226 } else { 227 this.callTerminationReason = null; 228 } 229 } 230 231 @Override 232 public void setCallStartTime(long startTime) { 233 Log.d(TAG, "setting startTime for call " + callId + " to " + startTime); 234 this.startTime = startTime; 235 } 236 237 @Override 238 public void setCallEndTime(long endTime) { 239 Log.d(TAG, "setting endTime for call " + callId + " to " + endTime); 240 this.endTime = endTime; 241 } 242 243 @Override 244 public void setCallIsAdditional(boolean isAdditional) { 245 Log.d(TAG, "setting isAdditional for call " + callId + " to " + isAdditional); 246 this.isAdditionalCall = isAdditional; 247 } 248 249 @Override 250 public void setCallIsInterrupted(boolean isInterrupted) { 251 Log.d(TAG, "setting isInterrupted for call " + callId + " to " + isInterrupted); 252 this.isInterrupted = isInterrupted; 253 } 254 255 @Override 256 public void addCallTechnology(int callTechnology) { 257 Log.d(TAG, "adding callTechnology for call " + callId + ": " + callTechnology); 258 this.callTechnologies |= callTechnology; 259 } 260 261 @Override 262 public void setCallDisconnectCause(DisconnectCause disconnectCause) { 263 Log.d(TAG, "setting disconnectCause for call " + callId + " to " + disconnectCause); 264 this.callTerminationReason = disconnectCause; 265 } 266 267 @Override 268 public void setCreatedFromExistingConnection(boolean createdFromExistingConnection) { 269 Log.d(TAG, "setting createdFromExistingConnection for call " + callId + " to " 270 + createdFromExistingConnection); 271 this.createdFromExistingConnection = createdFromExistingConnection; 272 } 273 274 @Override 275 public void setCallConnectionService(String connectionServiceName) { 276 Log.d(TAG, "setting connection service for call " + callId + ": " 277 + connectionServiceName); 278 this.connectionService = connectionServiceName; 279 } 280 281 @Override 282 public void setCallEvents(Log.CallEventRecord records) { 283 this.callEvents = records; 284 } 285 286 @Override 287 public String toString() { 288 return "{\n" 289 + " startTime: " + startTime + '\n' 290 + " endTime: " + endTime + '\n' 291 + " direction: " + getCallDirectionString() + '\n' 292 + " isAdditionalCall: " + isAdditionalCall + '\n' 293 + " isInterrupted: " + isInterrupted + '\n' 294 + " callTechnologies: " + getCallTechnologiesAsString() + '\n' 295 + " callTerminationReason: " + getCallDisconnectReasonString() + '\n' 296 + " connectionService: " + connectionService + '\n' 297 + "}\n"; 298 } 299 300 public ParcelableCallAnalytics toParcelableAnalytics() { 301 // Rounds up to the nearest second. 302 long callDuration = (endTime == 0 || startTime == 0) ? 0 : endTime - startTime; 303 callDuration += (callDuration % MILLIS_IN_1_SECOND == 0) ? 304 0 : (MILLIS_IN_1_SECOND - callDuration % MILLIS_IN_1_SECOND); 305 306 List<AnalyticsEvent> events; 307 List<ParcelableCallAnalytics.EventTiming> timings; 308 if (callEvents != null) { 309 events = convertLogEventsToAnalyticsEvents(callEvents.getEvents()); 310 timings = callEvents.extractEventTimings().stream() 311 .map(Analytics::logEventTimingToAnalyticsEventTiming) 312 .collect(Collectors.toList()); 313 } else { 314 events = Collections.emptyList(); 315 timings = Collections.emptyList(); 316 } 317 return new ParcelableCallAnalytics( 318 // rounds down to nearest 5 minute mark 319 startTime - startTime % ParcelableCallAnalytics.MILLIS_IN_5_MINUTES, 320 callDuration, 321 callDirection, 322 isAdditionalCall, 323 isInterrupted, 324 callTechnologies, 325 callTerminationReason == null ? 326 ParcelableCallAnalytics.STILL_CONNECTED : 327 callTerminationReason.getCode(), 328 isEmergency, 329 connectionService, 330 createdFromExistingConnection, 331 events, 332 timings); 333 } 334 335 private String getCallDirectionString() { 336 switch (callDirection) { 337 case UNKNOWN_DIRECTION: 338 return "UNKNOWN"; 339 case INCOMING_DIRECTION: 340 return "INCOMING"; 341 case OUTGOING_DIRECTION: 342 return "OUTGOING"; 343 default: 344 return "UNKNOWN"; 345 } 346 } 347 348 private String getCallTechnologiesAsString() { 349 StringBuilder s = new StringBuilder(); 350 s.append('['); 351 if ((callTechnologies & CDMA_PHONE) != 0) s.append("CDMA "); 352 if ((callTechnologies & GSM_PHONE) != 0) s.append("GSM "); 353 if ((callTechnologies & SIP_PHONE) != 0) s.append("SIP "); 354 if ((callTechnologies & IMS_PHONE) != 0) s.append("IMS "); 355 if ((callTechnologies & THIRD_PARTY_PHONE) != 0) s.append("THIRD_PARTY "); 356 s.append(']'); 357 return s.toString(); 358 } 359 360 private String getCallDisconnectReasonString() { 361 if (callTerminationReason != null) { 362 return callTerminationReason.toString(); 363 } else { 364 return "NOT SET"; 365 } 366 } 367 } 368 public static final String TAG = "TelecomAnalytics"; 369 370 // Constants for call direction 371 public static final int UNKNOWN_DIRECTION = ParcelableCallAnalytics.CALLTYPE_UNKNOWN; 372 public static final int INCOMING_DIRECTION = ParcelableCallAnalytics.CALLTYPE_INCOMING; 373 public static final int OUTGOING_DIRECTION = ParcelableCallAnalytics.CALLTYPE_OUTGOING; 374 375 // Constants for call technology 376 public static final int CDMA_PHONE = ParcelableCallAnalytics.CDMA_PHONE; 377 public static final int GSM_PHONE = ParcelableCallAnalytics.GSM_PHONE; 378 public static final int IMS_PHONE = ParcelableCallAnalytics.IMS_PHONE; 379 public static final int SIP_PHONE = ParcelableCallAnalytics.SIP_PHONE; 380 public static final int THIRD_PARTY_PHONE = ParcelableCallAnalytics.THIRD_PARTY_PHONE; 381 382 public static final long MILLIS_IN_1_SECOND = ParcelableCallAnalytics.MILLIS_IN_1_SECOND; 383 384 private static final Object sLock = new Object(); // Coarse lock for all of analytics 385 private static final Map<String, CallInfoImpl> sCallIdToInfo = new HashMap<>(); 386 private static final List<SessionTiming> sSessionTimings = new LinkedList<>(); 387 388 public static void addSessionTiming(String sessionName, long time) { 389 if (sLogSessionToSessionId.containsKey(sessionName)) { 390 synchronized (sLock) { 391 sSessionTimings.add(new SessionTiming(sLogSessionToSessionId.get(sessionName), 392 time)); 393 } 394 } 395 } 396 397 public static CallInfo initiateCallAnalytics(String callId, int direction) { 398 Log.d(TAG, "Starting analytics for call " + callId); 399 CallInfoImpl callInfo = new CallInfoImpl(callId, direction); 400 synchronized (sLock) { 401 sCallIdToInfo.put(callId, callInfo); 402 } 403 return callInfo; 404 } 405 406 public static TelecomAnalytics dumpToParcelableAnalytics() { 407 List<ParcelableCallAnalytics> calls = new LinkedList<>(); 408 List<SessionTiming> sessionTimings = new LinkedList<>(); 409 synchronized (sLock) { 410 calls.addAll(sCallIdToInfo.values().stream() 411 .map(CallInfoImpl::toParcelableAnalytics) 412 .collect(Collectors.toList())); 413 sessionTimings.addAll(sSessionTimings); 414 sCallIdToInfo.clear(); 415 sSessionTimings.clear(); 416 } 417 return new TelecomAnalytics(sessionTimings, calls); 418 } 419 420 public static void dump(IndentingPrintWriter writer) { 421 synchronized (sLock) { 422 int prefixLength = CallsManager.TELECOM_CALL_ID_PREFIX.length(); 423 List<String> callIds = new ArrayList<>(sCallIdToInfo.keySet()); 424 // Sort the analytics in increasing order of call IDs 425 Collections.sort(callIds, (id1, id2) -> { 426 int i1, i2; 427 try { 428 i1 = Integer.valueOf(id1.substring(prefixLength)); 429 i2 = Integer.valueOf(id2.substring(prefixLength)); 430 } catch (NumberFormatException e) { 431 return 0; 432 } 433 return i1 - i2; 434 }); 435 436 for (String callId : callIds) { 437 writer.printf("Call %s: ", callId); 438 writer.println(sCallIdToInfo.get(callId).toString()); 439 } 440 441 Map<Integer, Double> averageTimings = SessionTiming.averageTimings(sSessionTimings); 442 averageTimings.entrySet().stream() 443 .filter(e -> sSessionIdToLogSession.containsKey(e.getKey())) 444 .forEach(e -> writer.printf("%s: %.2f\n", 445 sSessionIdToLogSession.get(e.getKey()), e.getValue())); 446 } 447 } 448 449 public static void reset() { 450 synchronized (sLock) { 451 sCallIdToInfo.clear(); 452 } 453 } 454 455 /** 456 * Returns a copy of callIdToInfo. Use only for testing. 457 */ 458 @VisibleForTesting 459 public static Map<String, CallInfoImpl> cloneData() { 460 synchronized (sLock) { 461 Map<String, CallInfoImpl> result = new HashMap<>(sCallIdToInfo.size()); 462 for (Map.Entry<String, CallInfoImpl> entry : sCallIdToInfo.entrySet()) { 463 result.put(entry.getKey(), new CallInfoImpl(entry.getValue())); 464 } 465 return result; 466 } 467 } 468 469 private static List<AnalyticsEvent> convertLogEventsToAnalyticsEvents( 470 List<Log.CallEvent> logEvents) { 471 long timeOfLastEvent = -1; 472 ArrayList<AnalyticsEvent> events = new ArrayList<>(logEvents.size()); 473 for (Log.CallEvent logEvent : logEvents) { 474 if (sLogEventToAnalyticsEvent.containsKey(logEvent.eventId)) { 475 int analyticsEventId = sLogEventToAnalyticsEvent.get(logEvent.eventId); 476 long timeSinceLastEvent = 477 timeOfLastEvent < 0 ? -1 : logEvent.time - timeOfLastEvent; 478 events.add(new AnalyticsEvent( 479 analyticsEventId, 480 roundToOneSigFig(timeSinceLastEvent) 481 )); 482 timeOfLastEvent = logEvent.time; 483 } 484 } 485 return events; 486 } 487 488 private static ParcelableCallAnalytics.EventTiming logEventTimingToAnalyticsEventTiming( 489 Log.CallEventRecord.EventTiming logEventTiming) { 490 int analyticsEventTimingName = 491 sLogEventTimingToAnalyticsEventTiming.containsKey(logEventTiming.name) ? 492 sLogEventTimingToAnalyticsEventTiming.get(logEventTiming.name) : 493 ParcelableCallAnalytics.EventTiming.INVALID; 494 return new ParcelableCallAnalytics.EventTiming(analyticsEventTimingName, 495 (long) logEventTiming.time); 496 } 497 498 @VisibleForTesting 499 public static long roundToOneSigFig(long val) { 500 if (val == 0) { 501 return val; 502 } 503 int logVal = (int) Math.floor(Math.log10(val < 0 ? -val : val)); 504 double s = Math.pow(10, logVal); 505 double dec = val / s; 506 return (long) (Math.round(dec) * s); 507 } 508} 509