1 /*
2  * Copyright (C) 2019 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.car.dialer.telecom;
18 
19 import android.content.Context;
20 import android.content.Intent;
21 import android.content.SharedPreferences;
22 import android.telecom.Call;
23 
24 import androidx.annotation.MainThread;
25 
26 import com.android.car.dialer.Constants;
27 import com.android.car.dialer.R;
28 import com.android.car.dialer.log.L;
29 import com.android.car.dialer.notification.InCallNotificationController;
30 import com.android.car.dialer.ui.activecall.InCallActivity;
31 import com.android.car.dialer.ui.activecall.InCallViewModel;
32 
33 import java.util.ArrayList;
34 
35 import javax.inject.Inject;
36 
37 import dagger.hilt.android.qualifiers.ApplicationContext;
38 import dagger.hilt.android.scopes.ServiceScoped;
39 
40 /**
41  * Routes a call to different path depending on its state. If there is any {@link
42  * InCallServiceImpl.ActiveCallListChangedCallback} that already handles the call, i.e. the {@link
43  * InCallViewModel} that actively updates the in call page, then we don't show HUN for the ringing
44  * call or attempt to start the in call page again.
45  */
46 @ServiceScoped
47 class InCallRouter {
48 
49     private static final String TAG = "CD.InCallRouter";
50 
51     private final Context mContext;
52     private final SharedPreferences mSharedPreferences;
53     private final InCallNotificationController mInCallNotificationController;
54     private final ArrayList<InCallServiceImpl.ActiveCallListChangedCallback>
55             mActiveCallListChangedCallbacks = new ArrayList<>();
56     private final ProjectionCallHandler mProjectionCallHandler;
57 
58     @Inject
InCallRouter( @pplicationContext Context context, SharedPreferences sharedPreferences, InCallNotificationController inCallNotificationController, ProjectionCallHandler projectionCallHandler)59     InCallRouter(
60             @ApplicationContext Context context,
61             SharedPreferences sharedPreferences,
62             InCallNotificationController inCallNotificationController,
63             ProjectionCallHandler projectionCallHandler) {
64         mContext = context;
65         mSharedPreferences = sharedPreferences;
66         mInCallNotificationController = inCallNotificationController;
67         mProjectionCallHandler = projectionCallHandler;
68     }
69 
start()70     void start() {
71         mProjectionCallHandler.start();
72         mActiveCallListChangedCallbacks.add(mProjectionCallHandler);
73     }
74 
stop()75     void stop() {
76         mActiveCallListChangedCallbacks.remove(mProjectionCallHandler);
77         mProjectionCallHandler.stop();
78     }
79 
80     /**
81      * Routes the added call to the correct path:
82      * <ul>
83      * <li> First dispatches it to the {@link InCallServiceImpl.ActiveCallListChangedCallback}s.
84      * <li> If the ringing call is not handled by callbacks, it will show a HUN.
85      * <li> If the call is in other state and not handled by callbacks, it will try to launch the in
86      * call page.
87      */
onCallAdded(Call call)88     void onCallAdded(Call call) {
89         boolean isHandled = routeToActiveCallListChangedCallback(call);
90         if (isHandled) {
91             return;
92         }
93 
94         int state = call.getState();
95         if (state == Call.STATE_RINGING) {
96             routeToNotification(call);
97             // Otherwise, no operations. Incoming call will be displayed outside of Dialer app
98             // such as cluster.
99         } else if (state != Call.STATE_DISCONNECTED) {
100             // Don't launch the in call page if state is disconnected.
101             // Otherwise, the InCallActivity finishes right after onCreate() and flashes.
102             routeToFullScreenIncomingCallPage(false);
103         }
104     }
105 
106     /**
107      * Called by {@link InCallServiceImpl#onCallRemoved(Call)}. It notifies the {@link
108      * InCallServiceImpl.ActiveCallListChangedCallback}s to update the active call list.
109      */
onCallRemoved(Call call)110     void onCallRemoved(Call call) {
111         for (InCallServiceImpl.ActiveCallListChangedCallback callback :
112                 mActiveCallListChangedCallbacks) {
113             callback.onTelecomCallRemoved(call);
114         }
115     }
116 
117     @MainThread
registerActiveCallListChangedCallback( InCallServiceImpl.ActiveCallListChangedCallback callback)118     void registerActiveCallListChangedCallback(
119             InCallServiceImpl.ActiveCallListChangedCallback callback) {
120         mActiveCallListChangedCallbacks.add(callback);
121     }
122 
123     @MainThread
unregisterActiveCallHandler(InCallServiceImpl.ActiveCallListChangedCallback callback)124     void unregisterActiveCallHandler(InCallServiceImpl.ActiveCallListChangedCallback callback) {
125         mActiveCallListChangedCallbacks.remove(callback);
126     }
127 
128     /**
129      * Dispatches the call to {@link InCallServiceImpl.ActiveCallListChangedCallback}.
130      */
routeToActiveCallListChangedCallback(Call call)131     private boolean routeToActiveCallListChangedCallback(Call call) {
132         boolean isHandled = false;
133         for (InCallServiceImpl.ActiveCallListChangedCallback callback :
134                 mActiveCallListChangedCallbacks) {
135             if (callback.onTelecomCallAdded(call)) {
136                 isHandled = true;
137             }
138         }
139 
140         return isHandled;
141     }
142 
143     /**
144      * Presents the ringing call in HUN.
145      */
routeToNotification(Call call)146     private void routeToNotification(Call call) {
147         if (shouldShowIncomingCallHun()) {
148             mInCallNotificationController.showInCallNotification(call);
149         }
150         call.registerCallback(new Call.Callback() {
151             @Override
152             public void onStateChanged(Call call, int state) {
153                 L.d(TAG, "Ringing call state changed to %d", state);
154                 if (call.getState() != Call.STATE_DISCONNECTED) {
155                     // Don't launch the in call page if state is disconnected. Otherwise, the
156                     // InCallActivity finishes right after onCreate() and flashes.
157                     routeToFullScreenIncomingCallPage(false);
158                 }
159                 mInCallNotificationController.cancelInCallNotification(call);
160                 call.unregisterCallback(this);
161             }
162         });
163     }
164 
165     /**
166      * Launches {@link InCallActivity} and presents the on going call in the in call page.
167      */
routeToFullScreenIncomingCallPage(boolean showDialpad)168     void routeToFullScreenIncomingCallPage(boolean showDialpad) {
169         // It has been configured not to show the fullscreen incall ui.
170         if (!shouldShowFullScreenUi()) {
171             return;
172         }
173 
174         Intent launchIntent = new Intent(mContext, InCallActivity.class);
175         launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
176         launchIntent.putExtra(Constants.Intents.EXTRA_SHOW_DIALPAD, showDialpad);
177         mContext.startActivity(launchIntent);
178     }
179 
shouldShowIncomingCallHun()180     private boolean shouldShowIncomingCallHun() {
181         boolean shouldSuppressHunByDefault =
182                 mContext.getResources().getBoolean(R.bool.config_should_suppress_incoming_call_hun);
183         return !mSharedPreferences
184                 .getBoolean(mContext.getString(R.string.pref_no_incoming_call_hun_key),
185                         shouldSuppressHunByDefault);
186     }
187 
shouldShowFullScreenUi()188     private boolean shouldShowFullScreenUi() {
189         boolean shouldShowFullScreenUiByDefault =
190                 mContext.getResources().getBoolean(R.bool.config_show_fullscreen_incall_ui);
191         return mSharedPreferences
192                 .getBoolean(mContext.getString(R.string.pref_show_fullscreen_active_call_ui_key),
193                         shouldShowFullScreenUiByDefault);
194     }
195 }
196