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.callsequencing; 18 19 import static android.telecom.CallException.CODE_OPERATION_TIMED_OUT; 20 21 import android.os.OutcomeReceiver; 22 import android.telecom.TelecomManager; 23 import android.telecom.CallException; 24 import android.util.IndentingPrintWriter; 25 import android.util.Log; 26 27 import com.android.internal.annotations.VisibleForTesting; 28 import com.android.server.telecom.AnomalyReporterAdapter; 29 import com.android.server.telecom.flags.FeatureFlags; 30 import com.android.server.telecom.flags.Flags; 31 import java.util.ArrayDeque; 32 import java.util.ArrayList; 33 import java.util.Deque; 34 import java.util.List; 35 import java.util.Locale; 36 import java.util.Queue; 37 import java.util.UUID; 38 import java.util.concurrent.CompletableFuture; 39 40 public class TransactionManager { 41 private static final String TAG = "CallTransactionManager"; 42 private static final int TRANSACTION_HISTORY_SIZE = 20; 43 private static TransactionManager INSTANCE = null; 44 private static final Object sLock = new Object(); 45 private final Queue<CallTransaction> mTransactions; 46 private final Deque<CallTransaction> mCompletedTransactions; 47 private CallTransaction mCurrentTransaction; 48 private boolean mProcessingCallSequencing; 49 private AnomalyReporterAdapter mAnomalyReporter; 50 private FeatureFlags mFeatureFlags; 51 public static final UUID TRANSACTION_MANAGER_TIMEOUT_UUID = 52 UUID.fromString("9ccce52e-6694-4357-9e5e-516a9531b062"); 53 public static final String TRANSACTION_MANAGER_TIMEOUT_MSG = 54 "TransactionManager hit a timeout while processing a transaction"; 55 56 public interface TransactionCompleteListener { onTransactionCompleted(CallTransactionResult result, String transactionName)57 void onTransactionCompleted(CallTransactionResult result, String transactionName); onTransactionTimeout(String transactionName)58 void onTransactionTimeout(String transactionName); 59 } 60 TransactionManager()61 private TransactionManager() { 62 mTransactions = new ArrayDeque<>(); 63 mCurrentTransaction = null; 64 if (Flags.enableCallSequencing()) { 65 mCompletedTransactions = new ArrayDeque<>(); 66 } else 67 mCompletedTransactions = null; 68 } 69 getInstance()70 public static TransactionManager getInstance() { 71 synchronized (sLock) { 72 if (INSTANCE == null) { 73 INSTANCE = new TransactionManager(); 74 } 75 } 76 return INSTANCE; 77 } 78 setFeatureFlag(FeatureFlags flag)79 public void setFeatureFlag(FeatureFlags flag){ 80 mFeatureFlags = flag; 81 } 82 setAnomalyReporter(AnomalyReporterAdapter callAnomalyReporter)83 public void setAnomalyReporter(AnomalyReporterAdapter callAnomalyReporter){ 84 mAnomalyReporter = callAnomalyReporter; 85 } 86 87 @VisibleForTesting getTestInstance()88 public static TransactionManager getTestInstance() { 89 return new TransactionManager(); 90 } 91 addTransaction(CallTransaction transaction, OutcomeReceiver<CallTransactionResult, CallException> receiver)92 public CompletableFuture<Boolean> addTransaction(CallTransaction transaction, 93 OutcomeReceiver<CallTransactionResult, CallException> receiver) { 94 CompletableFuture<Boolean> transactionCompleteFuture = new CompletableFuture<>(); 95 synchronized (sLock) { 96 mTransactions.add(transaction); 97 } 98 transaction.setCompleteListener(new TransactionCompleteListener() { 99 @Override 100 public void onTransactionCompleted(CallTransactionResult result, 101 String transactionName) { 102 Log.i(TAG, String.format("transaction %s completed: with result=[%d]", 103 transactionName, result.getResult())); 104 try { 105 if (result.getResult() == TelecomManager.TELECOM_TRANSACTION_SUCCESS) { 106 receiver.onResult(result); 107 transactionCompleteFuture.complete(true); 108 } else { 109 receiver.onError( 110 new CallException(result.getMessage(), 111 result.getResult())); 112 transactionCompleteFuture.complete(false); 113 } 114 } catch (Exception e) { 115 Log.e(TAG, String.format("onTransactionCompleted: Notifying transaction result" 116 + " %s resulted in an Exception.", result), e); 117 transactionCompleteFuture.complete(false); 118 } 119 finishTransaction(); 120 } 121 122 @Override 123 public void onTransactionTimeout(String transactionName){ 124 Log.i(TAG, String.format("transaction %s timeout", transactionName)); 125 try { 126 receiver.onError(new CallException(transactionName + " timeout", 127 CODE_OPERATION_TIMED_OUT)); 128 transactionCompleteFuture.complete(false); 129 if (mFeatureFlags != null && mAnomalyReporter != null && 130 mFeatureFlags.enableCallExceptionAnomReports()) { 131 mAnomalyReporter.reportAnomaly( 132 TRANSACTION_MANAGER_TIMEOUT_UUID, 133 TRANSACTION_MANAGER_TIMEOUT_MSG); 134 } 135 } catch (Exception e) { 136 Log.e(TAG, String.format("onTransactionTimeout: Notifying transaction " 137 + " %s resulted in an Exception.", transactionName), e); 138 transactionCompleteFuture.complete(false); 139 } 140 finishTransaction(); 141 } 142 }); 143 144 startTransactions(); 145 return transactionCompleteFuture; 146 } 147 startTransactions()148 private void startTransactions() { 149 synchronized (sLock) { 150 if (mTransactions.isEmpty()) { 151 // No transaction waiting for process 152 return; 153 } 154 155 if (mCurrentTransaction != null) { 156 // Ongoing transaction 157 return; 158 } 159 mCurrentTransaction = mTransactions.poll(); 160 } 161 mCurrentTransaction.start(); 162 } 163 finishTransaction()164 private void finishTransaction() { 165 synchronized (sLock) { 166 if (mCurrentTransaction != null) { 167 addTransactionToHistory(mCurrentTransaction); 168 mCurrentTransaction = null; 169 } 170 } 171 startTransactions(); 172 } 173 174 @VisibleForTesting clear()175 public void clear() { 176 List<CallTransaction> pendingTransactions; 177 synchronized (sLock) { 178 pendingTransactions = new ArrayList<>(mTransactions); 179 } 180 for (CallTransaction t : pendingTransactions) { 181 t.finish(new CallTransactionResult(CallException.CODE_ERROR_UNKNOWN 182 /* TODO:: define error b/335703584 */, "clear called")); 183 } 184 } 185 addTransactionToHistory(CallTransaction t)186 private void addTransactionToHistory(CallTransaction t) { 187 if (!Flags.enableCallSequencing()) return; 188 189 mCompletedTransactions.add(t); 190 if (mCompletedTransactions.size() > TRANSACTION_HISTORY_SIZE) { 191 mCompletedTransactions.poll(); 192 } 193 } 194 195 /** 196 * Called when the dumpsys is created for telecom to capture the current state. 197 */ dump(IndentingPrintWriter pw)198 public void dump(IndentingPrintWriter pw) { 199 if (!Flags.enableCallSequencing()) { 200 pw.println("<<Flag not enabled>>"); 201 return; 202 } 203 synchronized (sLock) { 204 pw.println("Pending Transactions:"); 205 pw.increaseIndent(); 206 for (CallTransaction t : mTransactions) { 207 printPendingTransactionStats(t, pw); 208 } 209 pw.decreaseIndent(); 210 211 pw.println("Ongoing Transaction:"); 212 pw.increaseIndent(); 213 if (mCurrentTransaction != null) { 214 printPendingTransactionStats(mCurrentTransaction, pw); 215 } 216 pw.decreaseIndent(); 217 218 pw.println("Completed Transactions:"); 219 pw.increaseIndent(); 220 for (CallTransaction t : mCompletedTransactions) { 221 printCompleteTransactionStats(t, pw); 222 } 223 pw.decreaseIndent(); 224 } 225 } 226 227 /** 228 * Recursively print the pending {@link CallTransaction} stats for logging purposes. 229 * @param t The transaction that stats should be printed for 230 * @param pw The IndentingPrintWriter to print the result to 231 */ printPendingTransactionStats(CallTransaction t, IndentingPrintWriter pw)232 private void printPendingTransactionStats(CallTransaction t, IndentingPrintWriter pw) { 233 CallTransaction.Stats s = t.getStats(); 234 if (s == null) { 235 pw.println(String.format(Locale.getDefault(), "%s: <NO STATS>", t.mTransactionName)); 236 return; 237 } 238 pw.println(String.format(Locale.getDefault(), 239 "[%s] %s: (result=[%s]), (created -> now : [%+d] mS)," 240 + " (created -> started : [%+d] mS)," 241 + " (started -> now : [%+d] mS)", 242 s.addedTimeStamp, t.mTransactionName, parseTransactionResult(s), 243 s.measureTimeSinceCreatedMs(), s.measureCreatedToStartedMs(), 244 s.measureTimeSinceStartedMs())); 245 246 if (t.mSubTransactions == null || t.mSubTransactions.isEmpty()) { 247 return; 248 } 249 pw.increaseIndent(); 250 for (CallTransaction subTransaction : t.mSubTransactions) { 251 printPendingTransactionStats(subTransaction, pw); 252 } 253 pw.decreaseIndent(); 254 } 255 256 /** 257 * Recursively print the complete Transaction stats for logging purposes. 258 * @param t The transaction that stats should be printed for 259 * @param pw The IndentingPrintWriter to print the result to 260 */ printCompleteTransactionStats(CallTransaction t, IndentingPrintWriter pw)261 private void printCompleteTransactionStats(CallTransaction t, IndentingPrintWriter pw) { 262 CallTransaction.Stats s = t.getStats(); 263 if (s == null) { 264 pw.println(String.format(Locale.getDefault(), "%s: <NO STATS>", t.mTransactionName)); 265 return; 266 } 267 pw.println(String.format(Locale.getDefault(), 268 "[%s] %s: (result=[%s]), (created -> started : [%+d] mS), " 269 + "(started -> completed : [%+d] mS)", 270 s.addedTimeStamp, t.mTransactionName, parseTransactionResult(s), 271 s.measureCreatedToStartedMs(), s.measureStartedToCompletedMs())); 272 273 if (t.mSubTransactions == null || t.mSubTransactions.isEmpty()) { 274 return; 275 } 276 pw.increaseIndent(); 277 for (CallTransaction subTransaction : t.mSubTransactions) { 278 printCompleteTransactionStats(subTransaction, pw); 279 } 280 pw.decreaseIndent(); 281 } 282 parseTransactionResult(CallTransaction.Stats s)283 private String parseTransactionResult(CallTransaction.Stats s) { 284 if (s.isTimedOut()) return "TIMED OUT"; 285 if (s.getTransactionResult() == null) return "PENDING"; 286 if (s.getTransactionResult().getResult() == CallTransactionResult.RESULT_SUCCEED) { 287 return "SUCCESS"; 288 } 289 return s.getTransactionResult().toString(); 290 } 291 } 292