1 /* 2 * Copyright (C) 2022 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.telecom.voip; 18 19 import android.os.Handler; 20 import android.os.HandlerThread; 21 import android.telecom.CallException; 22 import android.telecom.Log; 23 24 import com.android.internal.annotations.VisibleForTesting; 25 import com.android.server.telecom.LoggedHandlerExecutor; 26 import com.android.server.telecom.TelecomSystem; 27 import com.android.server.telecom.flags.Flags; 28 29 import java.time.LocalDateTime; 30 import java.util.List; 31 import java.util.concurrent.CompletableFuture; 32 import java.util.concurrent.CompletionStage; 33 import java.util.concurrent.TimeUnit; 34 import java.util.concurrent.atomic.AtomicBoolean; 35 import java.util.function.Function; 36 37 public class VoipCallTransaction { 38 //TODO: add log events 39 private static final long DEFAULT_TRANSACTION_TIMEOUT_MS = 5000L; 40 41 /** 42 * Tracks stats about a transaction for logging purposes. 43 */ 44 public static class Stats { 45 // the logging visible timestamp for ease of debugging 46 public final LocalDateTime addedTimeStamp; 47 // the time in nS that the transaction was first created 48 private final long mCreatedTimeNs; 49 // the time that the transaction was started. 50 private long mStartedTimeNs = -1L; 51 // the time that the transaction was finished. 52 private long mFinishedTimeNs = -1L; 53 // If finished, did this transaction finish because it timed out? 54 private boolean mIsTimedOut = false; 55 private VoipCallTransactionResult mTransactionResult = null; 56 Stats()57 public Stats() { 58 addedTimeStamp = LocalDateTime.now(); 59 mCreatedTimeNs = System.nanoTime(); 60 } 61 62 /** 63 * Mark the transaction as started and record the time. 64 */ markStarted()65 public void markStarted() { 66 if (mStartedTimeNs > -1) return; 67 mStartedTimeNs = System.nanoTime(); 68 } 69 70 /** 71 * Mark the transaction as completed and record the time. 72 */ markComplete(boolean isTimedOut, VoipCallTransactionResult result)73 public void markComplete(boolean isTimedOut, VoipCallTransactionResult result) { 74 if (mFinishedTimeNs > -1) return; 75 mFinishedTimeNs = System.nanoTime(); 76 mIsTimedOut = isTimedOut; 77 mTransactionResult = result; 78 } 79 80 /** 81 * @return Time in mS since the transaction was created. 82 */ measureTimeSinceCreatedMs()83 public long measureTimeSinceCreatedMs() { 84 return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - mCreatedTimeNs); 85 } 86 87 /** 88 * @return Time in mS between when transaction was created and when it was marked as 89 * started. Returns -1 if the transaction was not started yet. 90 */ measureCreatedToStartedMs()91 public long measureCreatedToStartedMs() { 92 return mStartedTimeNs > 0 ? 93 TimeUnit.NANOSECONDS.toMillis(mStartedTimeNs - mCreatedTimeNs) : -1; 94 } 95 96 /** 97 * @return Time in mS since the transaction was marked started to the TransactionManager. 98 * Returns -1 if the transaction hasn't been started yet. 99 */ measureTimeSinceStartedMs()100 public long measureTimeSinceStartedMs() { 101 return mStartedTimeNs > 0 ? 102 TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - mStartedTimeNs) : -1; 103 } 104 105 /** 106 * @return Time in mS between when the transaction was marked as started and when it was 107 * marked as completed. Returns -1 if the transaction hasn't started or finished yet. 108 */ measureStartedToCompletedMs()109 public long measureStartedToCompletedMs() { 110 return (mStartedTimeNs > 0 && mFinishedTimeNs > 0) ? 111 TimeUnit.NANOSECONDS.toMillis(mFinishedTimeNs - mStartedTimeNs) : -1; 112 113 } 114 115 /** 116 * @return true if this transaction completed due to timing out, false if the transaction 117 * hasn't completed yet or it completed and did not time out. 118 */ isTimedOut()119 public boolean isTimedOut() { 120 return mIsTimedOut; 121 } 122 123 /** 124 * @return the result if the transaction completed, null if it timed out or hasn't completed 125 * yet. 126 */ getTransactionResult()127 public VoipCallTransactionResult getTransactionResult() { 128 return mTransactionResult; 129 } 130 } 131 132 protected final AtomicBoolean mCompleted = new AtomicBoolean(false); 133 protected final String mTransactionName = this.getClass().getSimpleName(); 134 private final HandlerThread mHandlerThread; 135 protected final Handler mHandler; 136 protected TransactionManager.TransactionCompleteListener mCompleteListener; 137 protected final List<VoipCallTransaction> mSubTransactions; 138 protected final TelecomSystem.SyncRoot mLock; 139 protected final long mTransactionTimeoutMs; 140 protected final Stats mStats; 141 VoipCallTransaction( List<VoipCallTransaction> subTransactions, TelecomSystem.SyncRoot lock, long timeoutMs)142 public VoipCallTransaction( 143 List<VoipCallTransaction> subTransactions, TelecomSystem.SyncRoot lock, 144 long timeoutMs) { 145 mSubTransactions = subTransactions; 146 mHandlerThread = new HandlerThread(this.toString()); 147 mHandlerThread.start(); 148 mHandler = new Handler(mHandlerThread.getLooper()); 149 mLock = lock; 150 mTransactionTimeoutMs = timeoutMs; 151 mStats = Flags.enableCallSequencing() ? new Stats() : null; 152 } 153 VoipCallTransaction(List<VoipCallTransaction> subTransactions, TelecomSystem.SyncRoot lock)154 public VoipCallTransaction(List<VoipCallTransaction> subTransactions, 155 TelecomSystem.SyncRoot lock) { 156 this(subTransactions, lock, DEFAULT_TRANSACTION_TIMEOUT_MS); 157 } VoipCallTransaction(TelecomSystem.SyncRoot lock, long timeoutMs)158 public VoipCallTransaction(TelecomSystem.SyncRoot lock, long timeoutMs) { 159 this(null /* mSubTransactions */, lock, timeoutMs); 160 } 161 VoipCallTransaction(TelecomSystem.SyncRoot lock)162 public VoipCallTransaction(TelecomSystem.SyncRoot lock) { 163 this(null /* mSubTransactions */, lock); 164 } 165 start()166 public final void start() { 167 if (mStats != null) mStats.markStarted(); 168 // post timeout work 169 CompletableFuture<Void> future = new CompletableFuture<>(); 170 mHandler.postDelayed(() -> future.complete(null), mTransactionTimeoutMs); 171 future.thenApplyAsync((x) -> { 172 timeout(); 173 return null; 174 }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode() 175 + ".s", mLock)); 176 177 processTransactions(); 178 } 179 180 /** 181 * By default, this processes this transaction. For VoipCallTransactions with sub-transactions, 182 * this implementation should be overwritten to handle also processing sub-transactions. 183 */ processTransactions()184 protected void processTransactions() { 185 scheduleTransaction(); 186 } 187 188 /** 189 * This method is called when the transaction has finished either successfully or exceptionally. 190 * VoipCallTransactions that are extending this class should override this method to clean up 191 * any leftover state. 192 */ finishTransaction()193 protected void finishTransaction() { 194 195 } 196 scheduleTransaction()197 protected final void scheduleTransaction() { 198 LoggedHandlerExecutor executor = new LoggedHandlerExecutor(mHandler, 199 mTransactionName + "@" + hashCode() + ".sT", mLock); 200 CompletableFuture<Void> future = CompletableFuture.completedFuture(null); 201 future.thenComposeAsync(this::processTransaction, executor) 202 .thenApplyAsync((Function<VoipCallTransactionResult, Void>) result -> { 203 notifyListenersOfResult(result); 204 return null; 205 }, executor) 206 .exceptionally((throwable -> { 207 // Do NOT wait for the timeout in order to finish this failed transaction. 208 // Instead, propagate the failure to the other transactions immediately! 209 String errorMessage = throwable != null ? throwable.getMessage() : 210 "encountered an exception while processing " + mTransactionName; 211 notifyListenersOfResult(new VoipCallTransactionResult( 212 CallException.CODE_ERROR_UNKNOWN, errorMessage)); 213 Log.e(this, throwable, "Error while executing transaction."); 214 return null; 215 })); 216 } 217 notifyListenersOfResult(VoipCallTransactionResult result)218 protected void notifyListenersOfResult(VoipCallTransactionResult result){ 219 mCompleted.set(true); 220 finish(result); 221 if (mCompleteListener != null) { 222 mCompleteListener.onTransactionCompleted(result, mTransactionName); 223 } 224 } 225 processTransaction(Void v)226 protected CompletionStage<VoipCallTransactionResult> processTransaction(Void v) { 227 return CompletableFuture.completedFuture( 228 new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED, null)); 229 } 230 setCompleteListener(TransactionManager.TransactionCompleteListener listener)231 public final void setCompleteListener(TransactionManager.TransactionCompleteListener listener) { 232 mCompleteListener = listener; 233 } 234 235 @VisibleForTesting timeout()236 public final void timeout() { 237 if (mCompleted.getAndSet(true)) { 238 return; 239 } 240 finish(true, null); 241 if (mCompleteListener != null) { 242 mCompleteListener.onTransactionTimeout(mTransactionName); 243 } 244 } 245 246 @VisibleForTesting getHandler()247 public final Handler getHandler() { 248 return mHandler; 249 } 250 finish(VoipCallTransactionResult result)251 public final void finish(VoipCallTransactionResult result) { 252 finish(false, result); 253 } 254 finish(boolean isTimedOut, VoipCallTransactionResult result)255 private void finish(boolean isTimedOut, VoipCallTransactionResult result) { 256 if (mStats != null) mStats.markComplete(isTimedOut, result); 257 finishTransaction(); 258 // finish all sub transactions 259 if (mSubTransactions != null && !mSubTransactions.isEmpty()) { 260 mSubTransactions.forEach( t -> t.finish(isTimedOut, result)); 261 } 262 mHandlerThread.quitSafely(); 263 } 264 265 /** 266 * @return Stats related to this transaction if stats are enabled, null otherwise. 267 */ getStats()268 public final Stats getStats() { 269 return mStats; 270 } 271 } 272