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