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.systemui.shade.carrier;
18 
19 import static android.telephony.SubscriptionManager.INVALID_SIM_SLOT_INDEX;
20 import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
21 
22 import android.annotation.MainThread;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.os.Handler;
28 import android.os.Looper;
29 import android.os.Message;
30 import android.provider.Settings;
31 import android.telephony.SubscriptionManager;
32 import android.text.TextUtils;
33 import android.util.Log;
34 import android.view.View;
35 import android.widget.TextView;
36 
37 import androidx.annotation.VisibleForTesting;
38 
39 import com.android.keyguard.CarrierTextManager;
40 import com.android.settingslib.AccessibilityContentDescriptions;
41 import com.android.settingslib.mobile.TelephonyIcons;
42 import com.android.systemui.R;
43 import com.android.systemui.dagger.SysUISingleton;
44 import com.android.systemui.dagger.qualifiers.Background;
45 import com.android.systemui.dagger.qualifiers.Main;
46 import com.android.systemui.plugins.ActivityStarter;
47 import com.android.systemui.statusbar.connectivity.MobileDataIndicators;
48 import com.android.systemui.statusbar.connectivity.NetworkController;
49 import com.android.systemui.statusbar.connectivity.SignalCallback;
50 import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider;
51 import com.android.systemui.statusbar.phone.StatusBarLocation;
52 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
53 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
54 import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder;
55 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView;
56 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
57 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel;
58 import com.android.systemui.util.CarrierConfigTracker;
59 
60 import java.util.List;
61 import java.util.function.Consumer;
62 
63 import javax.inject.Inject;
64 
65 public class ShadeCarrierGroupController {
66     private static final String TAG = "ShadeCarrierGroup";
67 
68     /**
69      * Support up to 3 slots which is what's supported by {@link TelephonyManager#getPhoneCount}
70      */
71     private static final int SIM_SLOTS = 3;
72 
73     private final ActivityStarter mActivityStarter;
74     private final Handler mBgHandler;
75     private final Context mContext;
76     private final NetworkController mNetworkController;
77     private final CarrierTextManager mCarrierTextManager;
78     private final TextView mNoSimTextView;
79     // Non final for testing
80     private H mMainHandler;
81     private final Callback mCallback;
82     private final MobileIconsViewModel mMobileIconsViewModel;
83     private final MobileContextProvider mMobileContextProvider;
84     private final StatusBarPipelineFlags mStatusBarPipelineFlags;
85     private boolean mListening;
86     private final CellSignalState[] mInfos =
87             new CellSignalState[SIM_SLOTS];
88     private View[] mCarrierDividers = new View[SIM_SLOTS - 1];
89     private ShadeCarrier[] mCarrierGroups = new ShadeCarrier[SIM_SLOTS];
90     private int[] mLastSignalLevel = new int[SIM_SLOTS];
91     private String[] mLastSignalLevelDescription = new String[SIM_SLOTS];
92     private final CarrierConfigTracker mCarrierConfigTracker;
93 
94     private boolean mIsSingleCarrier;
95     @Nullable
96     private OnSingleCarrierChangedListener mOnSingleCarrierChangedListener;
97 
98     private final SlotIndexResolver mSlotIndexResolver;
99 
100     private final SignalCallback mSignalCallback = new SignalCallback() {
101                 @Override
102                 public void setMobileDataIndicators(@NonNull MobileDataIndicators indicators) {
103                     int slotIndex = getSlotIndex(indicators.subId);
104                     if (slotIndex >= SIM_SLOTS) {
105                         Log.w(TAG, "setMobileDataIndicators - slot: " + slotIndex);
106                         return;
107                     }
108                     if (slotIndex == INVALID_SIM_SLOT_INDEX) {
109                         Log.e(TAG, "Invalid SIM slot index for subscription: " + indicators.subId);
110                         return;
111                     }
112                     mInfos[slotIndex] = new CellSignalState(
113                             indicators.statusIcon.visible,
114                             indicators.statusIcon.icon,
115                             indicators.statusIcon.contentDescription,
116                             indicators.typeContentDescription.toString(),
117                             indicators.roaming
118                     );
119                     mMainHandler.obtainMessage(H.MSG_UPDATE_STATE).sendToTarget();
120                 }
121 
122                 @Override
123                 public void setNoSims(boolean hasNoSims, boolean simDetected) {
124                     if (hasNoSims) {
125                         for (int i = 0; i < SIM_SLOTS; i++) {
126                             mInfos[i] = mInfos[i].changeVisibility(false);
127                         }
128                     }
129                     mMainHandler.obtainMessage(H.MSG_UPDATE_STATE).sendToTarget();
130                 }
131             };
132 
133     private static class Callback implements CarrierTextManager.CarrierTextCallback {
134         private H mHandler;
135 
Callback(H handler)136         Callback(H handler) {
137             mHandler = handler;
138         }
139 
140         @Override
updateCarrierInfo(CarrierTextManager.CarrierTextCallbackInfo info)141         public void updateCarrierInfo(CarrierTextManager.CarrierTextCallbackInfo info) {
142             mHandler.obtainMessage(H.MSG_UPDATE_CARRIER_INFO, info).sendToTarget();
143         }
144     }
145 
ShadeCarrierGroupController( ShadeCarrierGroup view, ActivityStarter activityStarter, @Background Handler bgHandler, @Main Looper mainLooper, NetworkController networkController, CarrierTextManager.Builder carrierTextManagerBuilder, Context context, CarrierConfigTracker carrierConfigTracker, SlotIndexResolver slotIndexResolver, MobileUiAdapter mobileUiAdapter, MobileContextProvider mobileContextProvider, StatusBarPipelineFlags statusBarPipelineFlags )146     private ShadeCarrierGroupController(
147             ShadeCarrierGroup view,
148             ActivityStarter activityStarter,
149             @Background Handler bgHandler,
150             @Main Looper mainLooper,
151             NetworkController networkController,
152             CarrierTextManager.Builder carrierTextManagerBuilder,
153             Context context,
154             CarrierConfigTracker carrierConfigTracker,
155             SlotIndexResolver slotIndexResolver,
156             MobileUiAdapter mobileUiAdapter,
157             MobileContextProvider mobileContextProvider,
158             StatusBarPipelineFlags statusBarPipelineFlags
159     ) {
160         mContext = context;
161         mActivityStarter = activityStarter;
162         mBgHandler = bgHandler;
163         mNetworkController = networkController;
164         mStatusBarPipelineFlags = statusBarPipelineFlags;
165         mCarrierTextManager = carrierTextManagerBuilder
166                 .setShowAirplaneMode(false)
167                 .setShowMissingSim(false)
168                 .setDebugLocationString("Shade")
169                 .build();
170         mCarrierConfigTracker = carrierConfigTracker;
171         mSlotIndexResolver = slotIndexResolver;
172         View.OnClickListener onClickListener = v -> {
173             if (!v.isVisibleToUser()) {
174                 return;
175             }
176             mActivityStarter.postStartActivityDismissingKeyguard(
177                     new Intent(Settings.ACTION_WIRELESS_SETTINGS), 0);
178         };
179 
180         mNoSimTextView = view.getNoSimTextView();
181         mNoSimTextView.setOnClickListener(onClickListener);
182         mMainHandler = new H(mainLooper, this::handleUpdateCarrierInfo, this::handleUpdateState);
183         mCallback = new Callback(mMainHandler);
184 
185         mCarrierGroups[0] = view.getCarrier1View();
186         mCarrierGroups[1] = view.getCarrier2View();
187         mCarrierGroups[2] = view.getCarrier3View();
188 
189         mMobileContextProvider = mobileContextProvider;
190         mMobileIconsViewModel = mobileUiAdapter.getMobileIconsViewModel();
191 
192         if (mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()) {
193             mobileUiAdapter.setShadeCarrierGroupController(this);
194             MobileIconsBinder.bind(view, mMobileIconsViewModel);
195         }
196 
197         mCarrierDividers[0] = view.getCarrierDivider1();
198         mCarrierDividers[1] = view.getCarrierDivider2();
199 
200         for (int i = 0; i < SIM_SLOTS; i++) {
201             mInfos[i] = new CellSignalState(
202                     true,
203                     R.drawable.ic_shade_no_calling_sms,
204                     context.getText(AccessibilityContentDescriptions.NO_CALLING).toString(),
205                     "",
206                     false);
207             mLastSignalLevel[i] = TelephonyIcons.MOBILE_CALL_STRENGTH_ICONS[0];
208             mLastSignalLevelDescription[i] =
209                     context.getText(AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0])
210                             .toString();
211             mCarrierGroups[i].setOnClickListener(onClickListener);
212         }
213         mIsSingleCarrier = computeIsSingleCarrier();
214         view.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
215 
216         view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
217             @Override
218             public void onViewAttachedToWindow(View v) {
219             }
220 
221             @Override
222             public void onViewDetachedFromWindow(View v) {
223                 setListening(false);
224             }
225         });
226     }
227 
228     /** Updates the number of visible mobile icons using the new pipeline. */
updateModernMobileIcons(List<Integer> subIds)229     public void updateModernMobileIcons(List<Integer> subIds) {
230         if (!mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()) {
231             Log.d(TAG, "ignoring new pipeline callback because new mobile icon is disabled");
232             return;
233         }
234 
235         for (ShadeCarrier carrier : mCarrierGroups) {
236             carrier.removeModernMobileView();
237         }
238 
239         List<IconData> iconDataList = processSubIdList(subIds);
240 
241         for (IconData iconData : iconDataList) {
242             ShadeCarrier carrier = mCarrierGroups[iconData.slotIndex];
243 
244             Context mobileContext =
245                     mMobileContextProvider.getMobileContextForSub(iconData.subId, mContext);
246             ModernShadeCarrierGroupMobileView modernMobileView = ModernShadeCarrierGroupMobileView
247                     .constructAndBind(
248                         mobileContext,
249                         mMobileIconsViewModel.getLogger(),
250                         "mobile_carrier_shade_group",
251                         (ShadeCarrierGroupMobileIconViewModel) mMobileIconsViewModel
252                                 .viewModelForSub(iconData.subId,
253                                     StatusBarLocation.SHADE_CARRIER_GROUP)
254                     );
255             carrier.addModernMobileView(modernMobileView);
256         }
257     }
258 
259     @VisibleForTesting
processSubIdList(List<Integer> subIds)260     List<IconData> processSubIdList(List<Integer> subIds) {
261         return subIds
262                 .stream()
263                 .limit(SIM_SLOTS)
264                 .map(subId -> new IconData(subId, getSlotIndex(subId)))
265                 .filter(iconData ->
266                         iconData.slotIndex < SIM_SLOTS
267                                 && iconData.slotIndex != INVALID_SIM_SLOT_INDEX
268                 )
269                 .toList();
270     }
271 
272     @VisibleForTesting
getSlotIndex(int subscriptionId)273     protected int getSlotIndex(int subscriptionId) {
274         return mSlotIndexResolver.getSlotIndex(subscriptionId);
275     }
276 
277     @VisibleForTesting
getShadeCarrierVisibility(int index)278     protected int getShadeCarrierVisibility(int index) {
279         return mCarrierGroups[index].getVisibility();
280     }
281 
282     /**
283      * Sets a {@link OnSingleCarrierChangedListener}.
284      *
285      * This will get notified when the number of carriers changes between 1 and "not one".
286      * @param listener
287      */
setOnSingleCarrierChangedListener( @ullable OnSingleCarrierChangedListener listener)288     public void setOnSingleCarrierChangedListener(
289             @Nullable OnSingleCarrierChangedListener listener) {
290         mOnSingleCarrierChangedListener = listener;
291     }
292 
isSingleCarrier()293     public boolean isSingleCarrier() {
294         return mIsSingleCarrier;
295     }
296 
computeIsSingleCarrier()297     private boolean computeIsSingleCarrier() {
298         int carrierCount = 0;
299         for (int i = 0; i < SIM_SLOTS; i++) {
300 
301             if (mInfos[i].visible) {
302                 carrierCount++;
303             }
304         }
305         return carrierCount == 1;
306     }
307 
setListening(boolean listening)308     public void setListening(boolean listening) {
309         if (listening == mListening) {
310             return;
311         }
312         mListening = listening;
313 
314         mBgHandler.post(this::updateListeners);
315     }
316 
updateListeners()317     private void updateListeners() {
318         if (mListening) {
319             if (mNetworkController.hasVoiceCallingFeature()) {
320                 mNetworkController.addCallback(mSignalCallback);
321             }
322             mCarrierTextManager.setListening(mCallback);
323         } else {
324             mNetworkController.removeCallback(mSignalCallback);
325             mCarrierTextManager.setListening(null);
326         }
327     }
328 
329 
330     @MainThread
handleUpdateState()331     private void handleUpdateState() {
332         if (!mMainHandler.getLooper().isCurrentThread()) {
333             mMainHandler.obtainMessage(H.MSG_UPDATE_STATE).sendToTarget();
334             return;
335         }
336 
337         boolean singleCarrier = computeIsSingleCarrier();
338 
339         if (singleCarrier) {
340             for (int i = 0; i < SIM_SLOTS; i++) {
341                 if (mInfos[i].visible
342                         && mInfos[i].mobileSignalIconId == R.drawable.ic_shade_sim_card) {
343                     mInfos[i] = new CellSignalState(true, R.drawable.ic_blank, "", "", false);
344                 }
345             }
346         }
347 
348         if (mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()) {
349             Log.d(TAG, "ignoring old pipeline callback because new mobile icon is enabled");
350         } else {
351             for (int i = 0; i < SIM_SLOTS; i++) {
352                 mCarrierGroups[i].updateState(mInfos[i], singleCarrier);
353             }
354         }
355 
356         mCarrierDividers[0].setVisibility(
357                 mInfos[0].visible && mInfos[1].visible ? View.VISIBLE : View.GONE);
358         // This tackles the case of slots 2 being available as well as at least one other.
359         // In that case we show the second divider. Note that if both dividers are visible, it means
360         // all three slots are in use, and that is correct.
361         mCarrierDividers[1].setVisibility(
362                 (mInfos[1].visible && mInfos[2].visible)
363                         || (mInfos[0].visible && mInfos[2].visible) ? View.VISIBLE : View.GONE);
364         if (mIsSingleCarrier != singleCarrier) {
365             mIsSingleCarrier = singleCarrier;
366             if (mOnSingleCarrierChangedListener != null) {
367                 mOnSingleCarrierChangedListener.onSingleCarrierChanged(singleCarrier);
368             }
369         }
370     }
371 
372     @MainThread
handleUpdateCarrierInfo(CarrierTextManager.CarrierTextCallbackInfo info)373     private void handleUpdateCarrierInfo(CarrierTextManager.CarrierTextCallbackInfo info) {
374         if (!mMainHandler.getLooper().isCurrentThread()) {
375             mMainHandler.obtainMessage(H.MSG_UPDATE_CARRIER_INFO, info).sendToTarget();
376             return;
377         }
378 
379         mNoSimTextView.setVisibility(View.GONE);
380         if (!info.airplaneMode && info.anySimReady) {
381             boolean[] slotSeen = new boolean[SIM_SLOTS];
382             if (info.listOfCarriers.length == info.subscriptionIds.length) {
383                 for (int i = 0; i < SIM_SLOTS && i < info.listOfCarriers.length; i++) {
384                     int slot = getSlotIndex(info.subscriptionIds[i]);
385                     if (slot >= SIM_SLOTS) {
386                         Log.w(TAG, "updateInfoCarrier - slot: " + slot);
387                         continue;
388                     }
389                     if (slot == INVALID_SIM_SLOT_INDEX) {
390                         Log.e(TAG,
391                                 "Invalid SIM slot index for subscription: "
392                                         + info.subscriptionIds[i]);
393                         continue;
394                     }
395                     String carrierText = info.listOfCarriers[i].toString().trim();
396                     if (!TextUtils.isEmpty(carrierText)) {
397                         mInfos[slot] = mInfos[slot].changeVisibility(true);
398                         slotSeen[slot] = true;
399                         mCarrierGroups[slot].setCarrierText(carrierText);
400                         mCarrierGroups[slot].setVisibility(View.VISIBLE);
401                     }
402                 }
403                 for (int i = 0; i < SIM_SLOTS; i++) {
404                     if (!slotSeen[i]) {
405                         mInfos[i] = mInfos[i].changeVisibility(false);
406                         mCarrierGroups[i].setVisibility(View.GONE);
407                     }
408                 }
409             } else {
410                 Log.e(TAG, "Carrier information arrays not of same length");
411             }
412         } else {
413             // No sims or airplane mode (but not WFC). Do not show ShadeCarrierGroup,
414             // instead just show info.carrierText in a different view.
415             for (int i = 0; i < SIM_SLOTS; i++) {
416                 mInfos[i] = mInfos[i].changeVisibility(false);
417                 mCarrierGroups[i].setCarrierText("");
418                 mCarrierGroups[i].setVisibility(View.GONE);
419             }
420             mNoSimTextView.setText(info.carrierText);
421             if (!TextUtils.isEmpty(info.carrierText)) {
422                 mNoSimTextView.setVisibility(View.VISIBLE);
423             }
424         }
425         handleUpdateState(); // handleUpdateCarrierInfo is always called from main thread.
426     }
427 
428     private static class H extends Handler {
429         private Consumer<CarrierTextManager.CarrierTextCallbackInfo> mUpdateCarrierInfo;
430         private Runnable mUpdateState;
431         static final int MSG_UPDATE_CARRIER_INFO = 0;
432         static final int MSG_UPDATE_STATE = 1;
433 
H(Looper looper, Consumer<CarrierTextManager.CarrierTextCallbackInfo> updateCarrierInfo, Runnable updateState)434         H(Looper looper,
435                 Consumer<CarrierTextManager.CarrierTextCallbackInfo> updateCarrierInfo,
436                 Runnable updateState) {
437             super(looper);
438             mUpdateCarrierInfo = updateCarrierInfo;
439             mUpdateState = updateState;
440         }
441 
442         @Override
handleMessage(Message msg)443         public void handleMessage(Message msg) {
444             switch (msg.what) {
445                 case MSG_UPDATE_CARRIER_INFO:
446                     mUpdateCarrierInfo.accept(
447                             (CarrierTextManager.CarrierTextCallbackInfo) msg.obj);
448                     break;
449                 case MSG_UPDATE_STATE:
450                     mUpdateState.run();
451                     break;
452                 default:
453                     super.handleMessage(msg);
454             }
455         }
456     }
457 
458     public static class Builder {
459         private ShadeCarrierGroup mView;
460         private final ActivityStarter mActivityStarter;
461         private final Handler mHandler;
462         private final Looper mLooper;
463         private final NetworkController mNetworkController;
464         private final CarrierTextManager.Builder mCarrierTextControllerBuilder;
465         private final Context mContext;
466         private final CarrierConfigTracker mCarrierConfigTracker;
467         private final SlotIndexResolver mSlotIndexResolver;
468         private final MobileUiAdapter mMobileUiAdapter;
469         private final MobileContextProvider mMobileContextProvider;
470         private final StatusBarPipelineFlags mStatusBarPipelineFlags;
471 
472         @Inject
Builder( ActivityStarter activityStarter, @Background Handler handler, @Main Looper looper, NetworkController networkController, CarrierTextManager.Builder carrierTextControllerBuilder, Context context, CarrierConfigTracker carrierConfigTracker, SlotIndexResolver slotIndexResolver, MobileUiAdapter mobileUiAdapter, MobileContextProvider mobileContextProvider, StatusBarPipelineFlags statusBarPipelineFlags )473         public Builder(
474                 ActivityStarter activityStarter,
475                 @Background Handler handler,
476                 @Main Looper looper,
477                 NetworkController networkController,
478                 CarrierTextManager.Builder carrierTextControllerBuilder,
479                 Context context,
480                 CarrierConfigTracker carrierConfigTracker,
481                 SlotIndexResolver slotIndexResolver,
482                 MobileUiAdapter mobileUiAdapter,
483                 MobileContextProvider mobileContextProvider,
484                 StatusBarPipelineFlags statusBarPipelineFlags
485         ) {
486             mActivityStarter = activityStarter;
487             mHandler = handler;
488             mLooper = looper;
489             mNetworkController = networkController;
490             mCarrierTextControllerBuilder = carrierTextControllerBuilder;
491             mContext = context;
492             mCarrierConfigTracker = carrierConfigTracker;
493             mSlotIndexResolver = slotIndexResolver;
494             mMobileUiAdapter = mobileUiAdapter;
495             mMobileContextProvider = mobileContextProvider;
496             mStatusBarPipelineFlags = statusBarPipelineFlags;
497         }
498 
setShadeCarrierGroup(ShadeCarrierGroup view)499         public Builder setShadeCarrierGroup(ShadeCarrierGroup view) {
500             mView = view;
501             return this;
502         }
503 
build()504         public ShadeCarrierGroupController build() {
505             return new ShadeCarrierGroupController(
506                     mView,
507                     mActivityStarter,
508                     mHandler,
509                     mLooper,
510                     mNetworkController,
511                     mCarrierTextControllerBuilder,
512                     mContext,
513                     mCarrierConfigTracker,
514                     mSlotIndexResolver,
515                     mMobileUiAdapter,
516                     mMobileContextProvider,
517                     mStatusBarPipelineFlags
518             );
519         }
520     }
521 
522     /**
523      * Notify when the state changes from 1 carrier to "not one" and viceversa
524      */
525     @FunctionalInterface
526     public interface OnSingleCarrierChangedListener {
onSingleCarrierChanged(boolean isSingleCarrier)527         void onSingleCarrierChanged(boolean isSingleCarrier);
528     }
529 
530     /**
531      * Interface for resolving slot index from subscription ID.
532      */
533     @FunctionalInterface
534     public interface SlotIndexResolver {
535         /**
536          * Get slot index for given sub id.
537          */
getSlotIndex(int subscriptionId)538         int getSlotIndex(int subscriptionId);
539     }
540 
541     /**
542      * Default implementation for {@link SlotIndexResolver}.
543      *
544      * It retrieves the slot index using {@link SubscriptionManager#getSlotIndex}.
545      */
546     @SysUISingleton
547     public static class SubscriptionManagerSlotIndexResolver implements SlotIndexResolver {
548 
549         @Inject
SubscriptionManagerSlotIndexResolver()550         public SubscriptionManagerSlotIndexResolver() {}
551 
552         @Override
getSlotIndex(int subscriptionId)553         public int getSlotIndex(int subscriptionId) {
554             return SubscriptionManager.getSlotIndex(subscriptionId);
555         }
556     }
557 
558     @VisibleForTesting
559     static class IconData {
560         public final int subId;
561         public final int slotIndex;
562 
IconData(int subId, int slotIndex)563         IconData(int subId, int slotIndex) {
564             this.subId = subId;
565             this.slotIndex = slotIndex;
566         }
567     }
568 }
569