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