1 /**
2  * Copyright (C) 2023 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.soundtrigger;
18 
19 import android.telephony.Annotation;
20 import android.telephony.SubscriptionInfo;
21 import android.telephony.SubscriptionManager;
22 import android.telephony.TelephonyCallback;
23 import android.telephony.TelephonyManager;
24 import android.util.Slog;
25 
26 import com.android.internal.annotations.GuardedBy;
27 
28 import java.util.ArrayList;
29 import java.util.List;
30 import java.util.Objects;
31 import java.util.concurrent.ExecutorService;
32 import java.util.concurrent.Executors;
33 import java.util.concurrent.atomic.AtomicBoolean;
34 
35 /**
36  * Handles monitoring telephony call state across active subscriptions.
37  *
38  * @hide
39  */
40 public class PhoneCallStateHandler {
41 
42     public interface Callback {
onPhoneCallStateChanged(boolean isInPhoneCall)43         void onPhoneCallStateChanged(boolean isInPhoneCall);
44     }
45 
46     private final Object mLock = new Object();
47 
48     // Actually never contended due to executor.
49     @GuardedBy("mLock")
50     private final List<MyCallStateListener> mListenerList = new ArrayList<>();
51 
52     private final AtomicBoolean mIsPhoneCallOngoing = new AtomicBoolean(false);
53 
54     private final SubscriptionManager mSubscriptionManager;
55     private final TelephonyManager mTelephonyManager;
56     private final Callback mCallback;
57 
58     private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
59 
PhoneCallStateHandler( SubscriptionManager subscriptionManager, TelephonyManager telephonyManager, Callback callback)60     public PhoneCallStateHandler(
61             SubscriptionManager subscriptionManager,
62             TelephonyManager telephonyManager,
63             Callback callback) {
64         mSubscriptionManager = Objects.requireNonNull(subscriptionManager);
65         mTelephonyManager = Objects.requireNonNull(telephonyManager);
66         mCallback = Objects.requireNonNull(callback);
67         mSubscriptionManager.addOnSubscriptionsChangedListener(
68                 mExecutor,
69                 new SubscriptionManager.OnSubscriptionsChangedListener() {
70                     @Override
71                     public void onSubscriptionsChanged() {
72                         updateTelephonyListeners();
73                     }
74 
75                     @Override
76                     public void onAddListenerFailed() {
77                         Slog.wtf(
78                                 "SoundTriggerPhoneCallStateHandler",
79                                 "Failed to add a telephony listener");
80                     }
81                 });
82     }
83 
84     private final class MyCallStateListener extends TelephonyCallback
85             implements TelephonyCallback.CallStateListener {
86 
87         final TelephonyManager mTelephonyManagerForSubId;
88 
89         // Manager corresponding to the sub-id
MyCallStateListener(TelephonyManager telephonyManager)90         MyCallStateListener(TelephonyManager telephonyManager) {
91             mTelephonyManagerForSubId = telephonyManager;
92         }
93 
cleanup()94         void cleanup() {
95             mExecutor.execute(() -> mTelephonyManagerForSubId.unregisterTelephonyCallback(this));
96         }
97 
98         @Override
onCallStateChanged(int unused)99         public void onCallStateChanged(int unused) {
100             updateCallStatus();
101         }
102     }
103 
104     /** Compute the current call status, and dispatch callback if it has changed. */
updateCallStatus()105     private void updateCallStatus() {
106         boolean callStatus = checkCallStatus();
107         if (mIsPhoneCallOngoing.compareAndSet(!callStatus, callStatus)) {
108             mCallback.onPhoneCallStateChanged(callStatus);
109         }
110     }
111 
112     /**
113      * Synchronously query the current telephony call state across all subscriptions
114      *
115      * @return - {@code true} if in call, {@code false} if not in call.
116      */
checkCallStatus()117     private boolean checkCallStatus() {
118         List<SubscriptionInfo> infoList = mSubscriptionManager.getActiveSubscriptionInfoList();
119         if (infoList == null) return false;
120         return infoList.stream()
121                 .filter(s -> (s.getSubscriptionId() != SubscriptionManager.INVALID_SUBSCRIPTION_ID))
122                 .anyMatch(s -> isCallOngoingFromState(
123                                         mTelephonyManager
124                                                 .createForSubscriptionId(s.getSubscriptionId())
125                                                 .getCallStateForSubscription()));
126     }
127 
updateTelephonyListeners()128     private void updateTelephonyListeners() {
129         synchronized (mLock) {
130             for (var listener : mListenerList) {
131                 listener.cleanup();
132             }
133             mListenerList.clear();
134             List<SubscriptionInfo> infoList = mSubscriptionManager.getActiveSubscriptionInfoList();
135             if (infoList == null) return;
136             infoList.stream()
137                     .filter(s -> s.getSubscriptionId()
138                                             != SubscriptionManager.INVALID_SUBSCRIPTION_ID)
139                     .map(s -> mTelephonyManager.createForSubscriptionId(s.getSubscriptionId()))
140                     .forEach(manager -> {
141                         synchronized (mLock) {
142                             var listener = new MyCallStateListener(manager);
143                             mListenerList.add(listener);
144                             manager.registerTelephonyCallback(mExecutor, listener);
145                         }
146                     });
147         }
148     }
149 
isCallOngoingFromState(@nnotation.CallState int callState)150     private static boolean isCallOngoingFromState(@Annotation.CallState int callState) {
151         return switch (callState) {
152             case TelephonyManager.CALL_STATE_IDLE, TelephonyManager.CALL_STATE_RINGING -> false;
153             case TelephonyManager.CALL_STATE_OFFHOOK -> true;
154             default -> throw new IllegalStateException(
155                     "Received unexpected call state from Telephony Manager: " + callState);
156         };
157     }
158 }
159