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