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