1 /* <lambda>null2 * Copyright (C) 2024 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.qs.tiles.dialog 18 19 import android.app.AlertDialog 20 import android.content.Context 21 import android.content.DialogInterface 22 import android.graphics.drawable.Drawable 23 import android.net.Network 24 import android.net.NetworkCapabilities 25 import android.os.Handler 26 import android.telephony.ServiceState 27 import android.telephony.SignalStrength 28 import android.telephony.SubscriptionManager 29 import android.telephony.TelephonyDisplayInfo 30 import android.text.Html 31 import android.text.Layout 32 import android.text.TextUtils 33 import android.text.method.LinkMovementMethod 34 import android.util.Log 35 import android.view.View 36 import android.view.ViewStub 37 import android.view.WindowManager 38 import android.widget.Button 39 import android.widget.ImageView 40 import android.widget.LinearLayout 41 import android.widget.ProgressBar 42 import android.widget.Switch 43 import android.widget.TextView 44 import androidx.annotation.MainThread 45 import androidx.annotation.WorkerThread 46 import androidx.compose.runtime.getValue 47 import androidx.compose.runtime.mutableStateOf 48 import androidx.compose.runtime.setValue 49 import androidx.lifecycle.Lifecycle 50 import androidx.lifecycle.LifecycleOwner 51 import androidx.lifecycle.LifecycleRegistry 52 import androidx.lifecycle.MutableLiveData 53 import androidx.recyclerview.widget.LinearLayoutManager 54 import androidx.recyclerview.widget.RecyclerView 55 import com.android.internal.logging.UiEvent 56 import com.android.internal.logging.UiEventLogger 57 import com.android.settingslib.satellite.SatelliteDialogUtils.TYPE_IS_WIFI 58 import com.android.settingslib.satellite.SatelliteDialogUtils.mayStartSatelliteWarningDialog 59 import com.android.settingslib.wifi.WifiEnterpriseRestrictionUtils 60 import com.android.systemui.Prefs 61 import com.android.systemui.accessibility.floatingmenu.AnnotationLinkSpan 62 import com.android.systemui.dagger.qualifiers.Background 63 import com.android.systemui.dagger.qualifiers.Main 64 import com.android.systemui.res.R 65 import com.android.systemui.statusbar.phone.SystemUIDialog 66 import com.android.systemui.statusbar.policy.KeyguardStateController 67 import com.android.wifitrackerlib.WifiEntry 68 import com.google.common.annotations.VisibleForTesting 69 import dagger.assisted.Assisted 70 import dagger.assisted.AssistedFactory 71 import dagger.assisted.AssistedInject 72 import java.util.concurrent.Executor 73 import kotlinx.coroutines.CoroutineScope 74 import kotlinx.coroutines.Job 75 76 /** 77 * View content for the Internet tile details that handles all UI interactions and state management. 78 */ 79 class InternetDetailsContentManager 80 @AssistedInject 81 constructor( 82 private val internetDetailsContentController: InternetDetailsContentController, 83 @Assisted(CAN_CONFIG_MOBILE_DATA) private val canConfigMobileData: Boolean, 84 @Assisted(CAN_CONFIG_WIFI) private val canConfigWifi: Boolean, 85 private val uiEventLogger: UiEventLogger, 86 @Main private val handler: Handler, 87 @Background private val backgroundExecutor: Executor, 88 private val keyguard: KeyguardStateController, 89 ) { 90 // Lifecycle 91 private lateinit var lifecycleRegistry: LifecycleRegistry 92 @VisibleForTesting internal var lifecycleOwner: LifecycleOwner? = null 93 @VisibleForTesting internal val internetContentData = MutableLiveData<InternetContent>() 94 @VisibleForTesting internal var connectedWifiEntry: WifiEntry? = null 95 @VisibleForTesting internal var isProgressBarVisible = false 96 97 // UI Components 98 private lateinit var contentView: View 99 private lateinit var divider: View 100 private lateinit var progressBar: ProgressBar 101 private lateinit var ethernetLayout: LinearLayout 102 private lateinit var mobileNetworkLayout: LinearLayout 103 private var secondaryMobileNetworkLayout: LinearLayout? = null 104 private lateinit var turnWifiOnLayout: LinearLayout 105 private lateinit var wifiToggleTitleTextView: TextView 106 private lateinit var wifiScanNotifyLayout: LinearLayout 107 private lateinit var wifiScanNotifyTextView: TextView 108 private lateinit var connectedWifiListLayout: LinearLayout 109 private lateinit var connectedWifiIcon: ImageView 110 private lateinit var connectedWifiTitleTextView: TextView 111 private lateinit var connectedWifiSummaryTextView: TextView 112 private lateinit var wifiSettingsIcon: ImageView 113 private lateinit var wifiRecyclerView: RecyclerView 114 private lateinit var seeAllLayout: LinearLayout 115 private lateinit var signalIcon: ImageView 116 private lateinit var mobileTitleTextView: TextView 117 private lateinit var mobileSummaryTextView: TextView 118 private lateinit var airplaneModeSummaryTextView: TextView 119 private lateinit var mobileDataToggle: Switch 120 private lateinit var mobileToggleDivider: View 121 private lateinit var wifiToggle: Switch 122 private lateinit var shareWifiButton: Button 123 private lateinit var airplaneModeButton: Button 124 private var alertDialog: AlertDialog? = null 125 private var canChangeWifiState = false 126 private var wifiNetworkHeight = 0 127 private var backgroundOn: Drawable? = null 128 private var backgroundOff: Drawable? = null 129 private var clickJob: Job? = null 130 private var defaultDataSubId = internetDetailsContentController.defaultDataSubscriptionId 131 @VisibleForTesting internal lateinit var adapter: InternetAdapter 132 @VisibleForTesting internal var wifiEntriesCount: Int = 0 133 @VisibleForTesting internal var hasMoreWifiEntries: Boolean = false 134 private lateinit var context: Context 135 private lateinit var coroutineScope: CoroutineScope 136 137 var title by mutableStateOf("") 138 private set 139 140 var subTitle by mutableStateOf("") 141 private set 142 143 @AssistedFactory 144 interface Factory { 145 fun create( 146 @Assisted(CAN_CONFIG_MOBILE_DATA) canConfigMobileData: Boolean, 147 @Assisted(CAN_CONFIG_WIFI) canConfigWifi: Boolean, 148 ): InternetDetailsContentManager 149 } 150 151 /** 152 * Binds the content manager to the provided content view. 153 * 154 * This method initializes the lifecycle, views, click listeners, and UI of the details content. 155 * It also updates the UI with the current Wi-Fi network information. 156 * 157 * @param contentView The view to which the content manager should be bound. 158 */ 159 fun bind(contentView: View, coroutineScope: CoroutineScope) { 160 if (DEBUG) { 161 Log.d(TAG, "Bind InternetDetailsContentManager") 162 } 163 164 this.contentView = contentView 165 context = contentView.context 166 this.coroutineScope = coroutineScope 167 adapter = InternetAdapter(internetDetailsContentController, coroutineScope) 168 canChangeWifiState = WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(context) 169 170 initializeLifecycle() 171 initializeViews() 172 updateDetailsUI(getStartingInternetContent()) 173 initializeAndConfigure() 174 } 175 176 /** 177 * Initializes the LifecycleRegistry if it hasn't been initialized yet. It sets the initial 178 * state of the LifecycleRegistry to Lifecycle.State.CREATED. 179 */ 180 fun initializeLifecycle() { 181 if (!::lifecycleRegistry.isInitialized) { 182 lifecycleOwner = 183 object : LifecycleOwner { 184 override val lifecycle: Lifecycle 185 get() = lifecycleRegistry 186 } 187 lifecycleRegistry = LifecycleRegistry(lifecycleOwner!!) 188 } 189 lifecycleRegistry.currentState = Lifecycle.State.CREATED 190 } 191 192 private fun initializeViews() { 193 // Set accessibility properties 194 contentView.accessibilityPaneTitle = 195 context.getText(R.string.accessibility_desc_quick_settings) 196 197 // Get dimension resources 198 wifiNetworkHeight = 199 context.resources.getDimensionPixelSize(R.dimen.internet_dialog_wifi_network_height) 200 201 // Initialize LiveData observer 202 internetContentData.observe(lifecycleOwner!!) { internetContent -> 203 updateDetailsUI(internetContent) 204 } 205 206 // Network layouts 207 divider = contentView.requireViewById(R.id.divider) 208 progressBar = contentView.requireViewById(R.id.wifi_searching_progress) 209 210 // Set wifi, mobile and ethernet layouts 211 setWifiLayout() 212 setMobileLayout() 213 ethernetLayout = contentView.requireViewById(R.id.ethernet_layout) 214 215 // Share WiFi 216 shareWifiButton = contentView.requireViewById(R.id.share_wifi_button) 217 shareWifiButton.setOnClickListener { view -> 218 if ( 219 internetDetailsContentController.mayLaunchShareWifiSettings( 220 connectedWifiEntry, 221 view, 222 ) 223 ) { 224 uiEventLogger.log(InternetDetailsEvent.SHARE_WIFI_QS_BUTTON_CLICKED) 225 } 226 } 227 228 // Airplane mode 229 airplaneModeButton = contentView.requireViewById(R.id.apm_button) 230 airplaneModeButton.setOnClickListener { 231 internetDetailsContentController.setAirplaneModeDisabled() 232 } 233 airplaneModeSummaryTextView = contentView.requireViewById(R.id.airplane_mode_summary) 234 235 // Background drawables 236 backgroundOn = context.getDrawable(R.drawable.settingslib_switch_bar_bg_on) 237 backgroundOff = context.getDrawable(R.drawable.internet_dialog_selected_effect) 238 239 // Done button is only visible for the dialog view 240 contentView.findViewById<Button>(R.id.done_button).apply { visibility = View.GONE } 241 242 // Title and subtitle will be added in the `TileDetails` 243 contentView.findViewById<TextView>(R.id.internet_dialog_title).apply { 244 visibility = View.GONE 245 } 246 contentView.findViewById<TextView>(R.id.internet_dialog_subtitle).apply { 247 visibility = View.GONE 248 } 249 } 250 251 private fun setWifiLayout() { 252 // Initialize Wi-Fi related views 253 turnWifiOnLayout = contentView.requireViewById(R.id.turn_on_wifi_layout) 254 wifiToggleTitleTextView = contentView.requireViewById(R.id.wifi_toggle_title) 255 wifiScanNotifyLayout = contentView.requireViewById(R.id.wifi_scan_notify_layout) 256 wifiScanNotifyTextView = contentView.requireViewById(R.id.wifi_scan_notify_text) 257 connectedWifiListLayout = contentView.requireViewById(R.id.wifi_connected_layout) 258 connectedWifiIcon = contentView.requireViewById(R.id.wifi_connected_icon) 259 connectedWifiTitleTextView = contentView.requireViewById(R.id.wifi_connected_title) 260 connectedWifiSummaryTextView = contentView.requireViewById(R.id.wifi_connected_summary) 261 wifiSettingsIcon = contentView.requireViewById(R.id.wifi_settings_icon) 262 wifiToggle = contentView.requireViewById(R.id.wifi_toggle) 263 wifiRecyclerView = 264 contentView.requireViewById<RecyclerView>(R.id.wifi_list_layout).apply { 265 layoutManager = LinearLayoutManager(context) 266 adapter = this@InternetDetailsContentManager.adapter 267 } 268 seeAllLayout = contentView.requireViewById(R.id.see_all_layout) 269 270 // Set click listeners for Wi-Fi related views 271 wifiToggle.setOnClickListener { 272 val isChecked = wifiToggle.isChecked 273 handleWifiToggleClicked(isChecked) 274 } 275 connectedWifiListLayout.setOnClickListener(this::onClickConnectedWifi) 276 seeAllLayout.setOnClickListener(this::onClickSeeMoreButton) 277 } 278 279 private fun setMobileLayout() { 280 // Initialize mobile data related views 281 mobileNetworkLayout = contentView.requireViewById(R.id.mobile_network_layout) 282 signalIcon = contentView.requireViewById(R.id.signal_icon) 283 mobileTitleTextView = contentView.requireViewById(R.id.mobile_title) 284 mobileSummaryTextView = contentView.requireViewById(R.id.mobile_summary) 285 mobileDataToggle = contentView.requireViewById(R.id.mobile_toggle) 286 mobileToggleDivider = contentView.requireViewById(R.id.mobile_toggle_divider) 287 288 // Set click listeners for mobile data related views 289 mobileNetworkLayout.setOnClickListener { 290 val autoSwitchNonDdsSubId: Int = 291 internetDetailsContentController.getActiveAutoSwitchNonDdsSubId() 292 if (autoSwitchNonDdsSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { 293 showTurnOffAutoDataSwitchDialog(autoSwitchNonDdsSubId) 294 } 295 internetDetailsContentController.connectCarrierNetwork() 296 } 297 298 // Mobile data toggle 299 mobileDataToggle.setOnClickListener { 300 val isChecked = mobileDataToggle.isChecked 301 if (!isChecked && shouldShowMobileDialog()) { 302 mobileDataToggle.isChecked = true 303 showTurnOffMobileDialog() 304 } else if (internetDetailsContentController.isMobileDataEnabled != isChecked) { 305 internetDetailsContentController.setMobileDataEnabled( 306 context, 307 defaultDataSubId, 308 isChecked, 309 false, 310 ) 311 } 312 } 313 } 314 315 /** 316 * This function ensures the component is in the RESUMED state and sets up the internet details 317 * content controller. 318 * 319 * If the component is already in the RESUMED state, this function does nothing. 320 */ 321 fun initializeAndConfigure() { 322 // If the current state is RESUMED, it's already initialized. 323 if (lifecycleRegistry.currentState == Lifecycle.State.RESUMED) { 324 return 325 } 326 327 lifecycleRegistry.currentState = Lifecycle.State.RESUMED 328 internetDetailsContentController.onStart(internetDetailsCallback, canConfigWifi) 329 if (!canConfigWifi) { 330 hideWifiViews() 331 } 332 } 333 334 private fun getTitleText(): String { 335 return internetDetailsContentController.getDialogTitleText().toString() 336 } 337 338 private fun getSubtitleText(): String { 339 return internetDetailsContentController.getSubtitleText(isProgressBarVisible).toString() 340 } 341 342 private fun updateDetailsUI(internetContent: InternetContent) { 343 if (DEBUG) { 344 Log.d(TAG, "updateDetailsUI ") 345 } 346 347 if (!::context.isInitialized) { 348 return 349 } 350 351 title = getTitleText() 352 subTitle = getSubtitleText() 353 354 airplaneModeButton.visibility = 355 if (internetContent.isAirplaneModeEnabled) View.VISIBLE else View.GONE 356 357 updateEthernetUI(internetContent) 358 updateMobileUI(internetContent) 359 updateWifiUI(internetContent) 360 } 361 362 private fun getStartingInternetContent(): InternetContent { 363 return InternetContent( 364 isWifiEnabled = internetDetailsContentController.isWifiEnabled, 365 isDeviceLocked = internetDetailsContentController.isDeviceLocked, 366 ) 367 } 368 369 @VisibleForTesting 370 internal fun hideWifiViews() { 371 setProgressBarVisible(false) 372 turnWifiOnLayout.visibility = View.GONE 373 connectedWifiListLayout.visibility = View.GONE 374 wifiRecyclerView.visibility = View.GONE 375 seeAllLayout.visibility = View.GONE 376 shareWifiButton.visibility = View.GONE 377 } 378 379 private fun setProgressBarVisible(visible: Boolean) { 380 if (isProgressBarVisible == visible) { 381 return 382 } 383 384 // Set the indeterminate value from false to true each time to ensure that the progress bar 385 // resets its animation and starts at the leftmost starting point each time it is displayed. 386 isProgressBarVisible = visible 387 progressBar.visibility = if (visible) View.VISIBLE else View.GONE 388 progressBar.isIndeterminate = visible 389 divider.visibility = if (visible) View.GONE else View.VISIBLE 390 } 391 392 private fun showTurnOffAutoDataSwitchDialog(subId: Int) { 393 var carrierName: CharSequence? = getMobileNetworkTitle(defaultDataSubId) 394 if (TextUtils.isEmpty(carrierName)) { 395 carrierName = getDefaultCarrierName() 396 } 397 alertDialog = 398 AlertDialog.Builder(context) 399 .setTitle(context.getString(R.string.auto_data_switch_disable_title, carrierName)) 400 .setMessage(R.string.auto_data_switch_disable_message) 401 .setNegativeButton(R.string.auto_data_switch_dialog_negative_button) { _, _ -> } 402 .setPositiveButton(R.string.auto_data_switch_dialog_positive_button) { _, _ -> 403 internetDetailsContentController.setAutoDataSwitchMobileDataPolicy( 404 subId, 405 /* enable= */ false, 406 ) 407 secondaryMobileNetworkLayout?.visibility = View.GONE 408 } 409 .create() 410 alertDialog!!.window?.setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG) 411 SystemUIDialog.setShowForAllUsers(alertDialog, true) 412 SystemUIDialog.registerDismissListener(alertDialog) 413 SystemUIDialog.setWindowOnTop(alertDialog, keyguard.isShowing()) 414 alertDialog!!.show() 415 } 416 417 private fun shouldShowMobileDialog(): Boolean { 418 val mobileDataTurnedOff = 419 Prefs.getBoolean(context, Prefs.Key.QS_HAS_TURNED_OFF_MOBILE_DATA, false) 420 return internetDetailsContentController.isMobileDataEnabled && !mobileDataTurnedOff 421 } 422 423 private fun getMobileNetworkTitle(subId: Int): CharSequence { 424 return internetDetailsContentController.getMobileNetworkTitle(subId) 425 } 426 427 private fun showTurnOffMobileDialog() { 428 val context = contentView.context 429 var carrierName: CharSequence? = getMobileNetworkTitle(defaultDataSubId) 430 val isInService: Boolean = 431 internetDetailsContentController.isVoiceStateInService(defaultDataSubId) 432 if (TextUtils.isEmpty(carrierName) || !isInService) { 433 carrierName = getDefaultCarrierName() 434 } 435 alertDialog = 436 AlertDialog.Builder(context) 437 .setTitle(R.string.mobile_data_disable_title) 438 .setMessage(context.getString(R.string.mobile_data_disable_message, carrierName)) 439 .setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int -> } 440 .setPositiveButton( 441 com.android.internal.R.string.alert_windows_notification_turn_off_action 442 ) { _: DialogInterface?, _: Int -> 443 internetDetailsContentController.setMobileDataEnabled( 444 context, 445 defaultDataSubId, 446 false, 447 false, 448 ) 449 mobileDataToggle.isChecked = false 450 Prefs.putBoolean(context, Prefs.Key.QS_HAS_TURNED_OFF_MOBILE_DATA, true) 451 } 452 .create() 453 alertDialog!!.window?.setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG) 454 SystemUIDialog.setShowForAllUsers(alertDialog, true) 455 SystemUIDialog.registerDismissListener(alertDialog) 456 SystemUIDialog.setWindowOnTop(alertDialog, keyguard.isShowing()) 457 458 alertDialog!!.show() 459 } 460 461 private fun onClickConnectedWifi(view: View?) { 462 if (connectedWifiEntry == null) { 463 return 464 } 465 internetDetailsContentController.launchWifiDetailsSetting(connectedWifiEntry!!.key, view) 466 } 467 468 private fun onClickSeeMoreButton(view: View?) { 469 internetDetailsContentController.launchNetworkSetting(view) 470 } 471 472 private fun handleWifiToggleClicked(isChecked: Boolean) { 473 if (clickJob != null && !clickJob!!.isCompleted) { 474 return 475 } 476 clickJob = 477 mayStartSatelliteWarningDialog(contentView.context, coroutineScope, TYPE_IS_WIFI) { 478 isAllowClick: Boolean -> 479 if (isAllowClick) { 480 setWifiEnabled(isChecked) 481 } else { 482 wifiToggle.isChecked = !isChecked 483 } 484 } 485 } 486 487 private fun setWifiEnabled(isEnabled: Boolean) { 488 if (internetDetailsContentController.isWifiEnabled == isEnabled) { 489 return 490 } 491 internetDetailsContentController.isWifiEnabled = isEnabled 492 } 493 494 @MainThread 495 private fun updateEthernetUI(internetContent: InternetContent) { 496 ethernetLayout.visibility = if (internetContent.hasEthernet) View.VISIBLE else View.GONE 497 } 498 499 private fun updateWifiUI(internetContent: InternetContent) { 500 if (!canConfigWifi) { 501 return 502 } 503 504 updateWifiToggle(internetContent) 505 updateConnectedWifi(internetContent) 506 updateWifiListAndSeeAll(internetContent) 507 updateWifiScanNotify(internetContent) 508 } 509 510 private fun updateMobileUI(internetContent: InternetContent) { 511 if (!internetContent.shouldUpdateMobileNetwork) { 512 return 513 } 514 515 val isNetworkConnected = 516 internetContent.activeNetworkIsCellular || internetContent.isCarrierNetworkActive 517 // 1. Mobile network should be gone if airplane mode ON or the list of active 518 // subscriptionId is null. 519 // 2. Carrier network should be gone if airplane mode ON and Wi-Fi is OFF. 520 if (DEBUG) { 521 Log.d( 522 TAG, 523 /*msg = */ "updateMobileUI, isCarrierNetworkActive = " + 524 internetContent.isCarrierNetworkActive, 525 ) 526 } 527 528 if ( 529 !internetContent.hasActiveSubIdOnDds && 530 (!internetContent.isWifiEnabled || !internetContent.isCarrierNetworkActive) 531 ) { 532 mobileNetworkLayout.visibility = View.GONE 533 secondaryMobileNetworkLayout?.visibility = View.GONE 534 return 535 } 536 537 mobileNetworkLayout.visibility = View.VISIBLE 538 mobileDataToggle.setChecked(internetDetailsContentController.isMobileDataEnabled) 539 mobileTitleTextView.text = getMobileNetworkTitle(defaultDataSubId) 540 val summary = getMobileNetworkSummary(defaultDataSubId) 541 if (!TextUtils.isEmpty(summary)) { 542 mobileSummaryTextView.text = Html.fromHtml(summary, Html.FROM_HTML_MODE_LEGACY) 543 mobileSummaryTextView.setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) 544 mobileSummaryTextView.visibility = View.VISIBLE 545 } else { 546 mobileSummaryTextView.visibility = View.GONE 547 } 548 backgroundExecutor.execute { 549 val drawable = getSignalStrengthDrawable(defaultDataSubId) 550 handler.post { signalIcon.setImageDrawable(drawable) } 551 } 552 553 mobileDataToggle.visibility = if (canConfigMobileData) View.VISIBLE else View.INVISIBLE 554 mobileToggleDivider.visibility = if (canConfigMobileData) View.VISIBLE else View.INVISIBLE 555 val primaryColor = 556 if (isNetworkConnected) R.color.connected_network_primary_color 557 else R.color.disconnected_network_primary_color 558 mobileToggleDivider.setBackgroundColor(context.getColor(primaryColor)) 559 560 // Display the info for the non-DDS if it's actively being used 561 val autoSwitchNonDdsSubId: Int = internetContent.activeAutoSwitchNonDdsSubId 562 563 val nonDdsVisibility = 564 if (autoSwitchNonDdsSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) View.VISIBLE 565 else View.GONE 566 567 val secondaryRes = 568 if (isNetworkConnected) R.style.TextAppearance_InternetDialog_Secondary_Active 569 else R.style.TextAppearance_InternetDialog_Secondary 570 if (nonDdsVisibility == View.VISIBLE) { 571 // non DDS is the currently active sub, set primary visual for it 572 setNonDDSActive(autoSwitchNonDdsSubId) 573 } else { 574 mobileNetworkLayout.background = if (isNetworkConnected) backgroundOn else backgroundOff 575 mobileTitleTextView.setTextAppearance( 576 if (isNetworkConnected) R.style.TextAppearance_InternetDialog_Active 577 else R.style.TextAppearance_InternetDialog 578 ) 579 mobileSummaryTextView.setTextAppearance(secondaryRes) 580 } 581 582 secondaryMobileNetworkLayout?.visibility = nonDdsVisibility 583 584 // Set airplane mode to the summary for carrier network 585 if (internetContent.isAirplaneModeEnabled) { 586 airplaneModeSummaryTextView.apply { 587 visibility = View.VISIBLE 588 text = context.getText(R.string.airplane_mode) 589 setTextAppearance(secondaryRes) 590 } 591 } else { 592 airplaneModeSummaryTextView.visibility = View.GONE 593 } 594 } 595 596 private fun setNonDDSActive(autoSwitchNonDdsSubId: Int) { 597 val stub: ViewStub = contentView.findViewById(R.id.secondary_mobile_network_stub) 598 stub.inflate() 599 secondaryMobileNetworkLayout = 600 contentView.findViewById(R.id.secondary_mobile_network_layout) 601 secondaryMobileNetworkLayout?.setOnClickListener { view: View? -> 602 this.onClickConnectedSecondarySub(view) 603 } 604 secondaryMobileNetworkLayout?.background = backgroundOn 605 606 contentView.requireViewById<TextView>(R.id.secondary_mobile_title).apply { 607 text = getMobileNetworkTitle(autoSwitchNonDdsSubId) 608 setTextAppearance(R.style.TextAppearance_InternetDialog_Active) 609 } 610 611 val summary = getMobileNetworkSummary(autoSwitchNonDdsSubId) 612 contentView.requireViewById<TextView>(R.id.secondary_mobile_summary).apply { 613 if (!TextUtils.isEmpty(summary)) { 614 text = Html.fromHtml(summary, Html.FROM_HTML_MODE_LEGACY) 615 breakStrategy = Layout.BREAK_STRATEGY_SIMPLE 616 setTextAppearance(R.style.TextAppearance_InternetDialog_Active) 617 } 618 } 619 620 val secondarySignalIcon: ImageView = contentView.requireViewById(R.id.secondary_signal_icon) 621 backgroundExecutor.execute { 622 val drawable = getSignalStrengthDrawable(autoSwitchNonDdsSubId) 623 handler.post { secondarySignalIcon.setImageDrawable(drawable) } 624 } 625 626 contentView.requireViewById<ImageView>(R.id.secondary_settings_icon).apply { 627 setColorFilter(context.getColor(R.color.connected_network_primary_color)) 628 } 629 630 // set secondary visual for default data sub 631 mobileNetworkLayout.background = backgroundOff 632 mobileTitleTextView.setTextAppearance(R.style.TextAppearance_InternetDialog) 633 mobileSummaryTextView.setTextAppearance(R.style.TextAppearance_InternetDialog_Secondary) 634 signalIcon.setColorFilter(context.getColor(R.color.connected_network_secondary_color)) 635 } 636 637 @MainThread 638 private fun updateWifiToggle(internetContent: InternetContent) { 639 if (wifiToggle.isChecked != internetContent.isWifiEnabled) { 640 wifiToggle.isChecked = internetContent.isWifiEnabled 641 } 642 if (internetContent.isDeviceLocked) { 643 wifiToggleTitleTextView.setTextAppearance( 644 if ((connectedWifiEntry != null)) R.style.TextAppearance_InternetDialog_Active 645 else R.style.TextAppearance_InternetDialog 646 ) 647 } 648 turnWifiOnLayout.background = 649 if ((internetContent.isDeviceLocked && connectedWifiEntry != null)) backgroundOn 650 else null 651 652 if (!canChangeWifiState && wifiToggle.isEnabled) { 653 wifiToggle.isEnabled = false 654 wifiToggleTitleTextView.isEnabled = false 655 contentView.requireViewById<TextView>(R.id.wifi_toggle_summary).apply { 656 isEnabled = false 657 visibility = View.VISIBLE 658 } 659 } 660 } 661 662 @MainThread 663 private fun updateConnectedWifi(internetContent: InternetContent) { 664 if ( 665 !internetContent.isWifiEnabled || 666 connectedWifiEntry == null || 667 internetContent.isDeviceLocked 668 ) { 669 connectedWifiListLayout.visibility = View.GONE 670 shareWifiButton.visibility = View.GONE 671 return 672 } 673 connectedWifiListLayout.visibility = View.VISIBLE 674 connectedWifiTitleTextView.text = connectedWifiEntry!!.title 675 connectedWifiSummaryTextView.text = connectedWifiEntry!!.getSummary(false) 676 connectedWifiIcon.setImageDrawable( 677 internetDetailsContentController.getInternetWifiDrawable(connectedWifiEntry!!) 678 ) 679 wifiSettingsIcon.setColorFilter(context.getColor(R.color.connected_network_primary_color)) 680 681 val canShareWifi = 682 internetDetailsContentController.getConfiguratorQrCodeGeneratorIntentOrNull( 683 connectedWifiEntry 684 ) != null 685 shareWifiButton.visibility = if (canShareWifi) View.VISIBLE else View.GONE 686 687 secondaryMobileNetworkLayout?.visibility = View.GONE 688 } 689 690 @MainThread 691 private fun updateWifiListAndSeeAll(internetContent: InternetContent) { 692 if (!internetContent.isWifiEnabled || internetContent.isDeviceLocked) { 693 wifiRecyclerView.visibility = View.GONE 694 seeAllLayout.visibility = View.GONE 695 return 696 } 697 val wifiListMaxCount = getWifiListMaxCount() 698 if (adapter.itemCount > wifiListMaxCount) { 699 hasMoreWifiEntries = true 700 } 701 adapter.setMaxEntriesCount(wifiListMaxCount) 702 val wifiListMinHeight = wifiNetworkHeight * wifiListMaxCount 703 if (wifiRecyclerView.minimumHeight != wifiListMinHeight) { 704 wifiRecyclerView.minimumHeight = wifiListMinHeight 705 } 706 wifiRecyclerView.visibility = View.VISIBLE 707 seeAllLayout.visibility = if (hasMoreWifiEntries) View.VISIBLE else View.INVISIBLE 708 } 709 710 @MainThread 711 private fun updateWifiScanNotify(internetContent: InternetContent) { 712 if ( 713 internetContent.isWifiEnabled || 714 !internetContent.isWifiScanEnabled || 715 internetContent.isDeviceLocked 716 ) { 717 wifiScanNotifyLayout.visibility = View.GONE 718 return 719 } 720 721 if (TextUtils.isEmpty(wifiScanNotifyTextView.text)) { 722 val linkInfo = 723 AnnotationLinkSpan.LinkInfo(AnnotationLinkSpan.LinkInfo.DEFAULT_ANNOTATION) { 724 view: View? -> 725 internetDetailsContentController.launchWifiScanningSetting(view) 726 } 727 wifiScanNotifyTextView.text = 728 AnnotationLinkSpan.linkify( 729 context.getText(R.string.wifi_scan_notify_message), 730 linkInfo, 731 ) 732 wifiScanNotifyTextView.movementMethod = LinkMovementMethod.getInstance() 733 } 734 wifiScanNotifyLayout.visibility = View.VISIBLE 735 } 736 737 @VisibleForTesting 738 @MainThread 739 internal fun getWifiListMaxCount(): Int { 740 // Use the maximum count of networks to calculate the remaining count for Wi-Fi networks. 741 var count = MAX_NETWORK_COUNT 742 if (ethernetLayout.visibility == View.VISIBLE) { 743 count -= 1 744 } 745 if (mobileNetworkLayout.visibility == View.VISIBLE) { 746 count -= 1 747 } 748 749 // If the remaining count is greater than the maximum count of the Wi-Fi network, the 750 // maximum count of the Wi-Fi network is used. 751 if (count > InternetDetailsContentController.MAX_WIFI_ENTRY_COUNT) { 752 count = InternetDetailsContentController.MAX_WIFI_ENTRY_COUNT 753 } 754 if (connectedWifiListLayout.visibility == View.VISIBLE) { 755 count -= 1 756 } 757 return count 758 } 759 760 private fun getMobileNetworkSummary(subId: Int): String { 761 return internetDetailsContentController.getMobileNetworkSummary(subId) 762 } 763 764 /** For DSDS auto data switch */ 765 private fun onClickConnectedSecondarySub(view: View?) { 766 internetDetailsContentController.launchMobileNetworkSettings(view) 767 } 768 769 private fun getSignalStrengthDrawable(subId: Int): Drawable { 770 return internetDetailsContentController.getSignalStrengthDrawable(subId) 771 } 772 773 /** 774 * Unbinds all listeners and resources associated with the view. This method should be called 775 * when the view is no longer needed. 776 */ 777 fun unBind() { 778 if (DEBUG) { 779 Log.d(TAG, "unBind") 780 } 781 lifecycleRegistry.currentState = Lifecycle.State.DESTROYED 782 mobileNetworkLayout.setOnClickListener(null) 783 connectedWifiListLayout.setOnClickListener(null) 784 secondaryMobileNetworkLayout?.setOnClickListener(null) 785 seeAllLayout.setOnClickListener(null) 786 wifiToggle.setOnCheckedChangeListener(null) 787 shareWifiButton.setOnClickListener(null) 788 airplaneModeButton.setOnClickListener(null) 789 internetDetailsContentController.onStop() 790 } 791 792 /** 793 * Update the internet details content when receiving the callback. 794 * 795 * @param shouldUpdateMobileNetwork `true` for update the mobile network layout, otherwise 796 * `false`. 797 */ 798 @VisibleForTesting 799 internal fun updateContent(shouldUpdateMobileNetwork: Boolean) { 800 backgroundExecutor.execute { 801 internetContentData.postValue(getInternetContent(shouldUpdateMobileNetwork)) 802 } 803 } 804 805 private fun getInternetContent(shouldUpdateMobileNetwork: Boolean): InternetContent { 806 return InternetContent( 807 shouldUpdateMobileNetwork = shouldUpdateMobileNetwork, 808 activeNetworkIsCellular = 809 if (shouldUpdateMobileNetwork) 810 internetDetailsContentController.activeNetworkIsCellular() 811 else false, 812 isCarrierNetworkActive = 813 if (shouldUpdateMobileNetwork) 814 internetDetailsContentController.isCarrierNetworkActive() 815 else false, 816 isAirplaneModeEnabled = internetDetailsContentController.isAirplaneModeEnabled, 817 hasEthernet = internetDetailsContentController.hasEthernet(), 818 isWifiEnabled = internetDetailsContentController.isWifiEnabled, 819 hasActiveSubIdOnDds = internetDetailsContentController.hasActiveSubIdOnDds(), 820 isDeviceLocked = internetDetailsContentController.isDeviceLocked, 821 isWifiScanEnabled = internetDetailsContentController.isWifiScanEnabled(), 822 activeAutoSwitchNonDdsSubId = 823 internetDetailsContentController.getActiveAutoSwitchNonDdsSubId(), 824 ) 825 } 826 827 /** 828 * Handles window focus changes. If the activity loses focus and the system UI dialog is 829 * showing, it dismisses the current alert dialog to prevent it from persisting in the 830 * background. 831 * 832 * @param dialog The internet system UI dialog whose focus state has changed. 833 * @param hasFocus True if the window has gained focus, false otherwise. 834 */ 835 fun onWindowFocusChanged(dialog: SystemUIDialog, hasFocus: Boolean) { 836 if (alertDialog != null && !alertDialog!!.isShowing) { 837 if (!hasFocus && dialog.isShowing) { 838 dialog.dismiss() 839 } 840 } 841 } 842 843 private fun getDefaultCarrierName(): String? { 844 return context.getString(R.string.mobile_data_disable_message_default_carrier) 845 } 846 847 @VisibleForTesting 848 internal val internetDetailsCallback = 849 object : InternetDetailsContentController.InternetDialogCallback { 850 override fun onRefreshCarrierInfo() { 851 updateContent(shouldUpdateMobileNetwork = true) 852 } 853 854 override fun onSimStateChanged() { 855 updateContent(shouldUpdateMobileNetwork = true) 856 } 857 858 @WorkerThread 859 override fun onCapabilitiesChanged( 860 network: Network?, 861 networkCapabilities: NetworkCapabilities?, 862 ) { 863 updateContent(shouldUpdateMobileNetwork = true) 864 } 865 866 @WorkerThread 867 override fun onLost(network: Network) { 868 updateContent(shouldUpdateMobileNetwork = true) 869 } 870 871 override fun onSubscriptionsChanged(dataSubId: Int) { 872 defaultDataSubId = dataSubId 873 updateContent(shouldUpdateMobileNetwork = true) 874 } 875 876 override fun onServiceStateChanged(serviceState: ServiceState?) { 877 updateContent(shouldUpdateMobileNetwork = true) 878 } 879 880 @WorkerThread 881 override fun onDataConnectionStateChanged(state: Int, networkType: Int) { 882 updateContent(shouldUpdateMobileNetwork = true) 883 } 884 885 override fun onSignalStrengthsChanged(signalStrength: SignalStrength?) { 886 updateContent(shouldUpdateMobileNetwork = true) 887 } 888 889 override fun onUserMobileDataStateChanged(enabled: Boolean) { 890 updateContent(shouldUpdateMobileNetwork = true) 891 } 892 893 override fun onDisplayInfoChanged(telephonyDisplayInfo: TelephonyDisplayInfo?) { 894 updateContent(shouldUpdateMobileNetwork = true) 895 } 896 897 override fun onCarrierNetworkChange(active: Boolean) { 898 updateContent(shouldUpdateMobileNetwork = true) 899 } 900 901 override fun dismissDialog() { 902 if (DEBUG) { 903 Log.d(TAG, "dismissDialog") 904 } 905 // TODO: b/377388104 Close details view 906 } 907 908 override fun onAccessPointsChanged( 909 wifiEntries: MutableList<WifiEntry>?, 910 connectedEntry: WifiEntry?, 911 ifHasMoreWifiEntries: Boolean, 912 ) { 913 // Should update the carrier network layout when it is connected under airplane 914 // mode ON. 915 val shouldUpdateCarrierNetwork = 916 (mobileNetworkLayout.visibility == View.VISIBLE) && 917 internetDetailsContentController.isAirplaneModeEnabled 918 handler.post { 919 connectedWifiEntry = connectedEntry 920 wifiEntriesCount = wifiEntries?.size ?: 0 921 hasMoreWifiEntries = ifHasMoreWifiEntries 922 updateContent(shouldUpdateCarrierNetwork) 923 adapter.setWifiEntries(wifiEntries, wifiEntriesCount) 924 adapter.notifyDataSetChanged() 925 } 926 } 927 928 override fun onWifiScan(isScan: Boolean) { 929 setProgressBarVisible(isScan) 930 } 931 } 932 933 enum class InternetDetailsEvent(private val id: Int) : UiEventLogger.UiEventEnum { 934 @UiEvent(doc = "The Internet details became visible on the screen.") 935 INTERNET_DETAILS_VISIBLE(2071), 936 @UiEvent(doc = "The share wifi button is clicked.") SHARE_WIFI_QS_BUTTON_CLICKED(1462); 937 938 override fun getId(): Int { 939 return id 940 } 941 } 942 943 @VisibleForTesting 944 data class InternetContent( 945 val isAirplaneModeEnabled: Boolean = false, 946 val hasEthernet: Boolean = false, 947 val shouldUpdateMobileNetwork: Boolean = false, 948 val activeNetworkIsCellular: Boolean = false, 949 val isCarrierNetworkActive: Boolean = false, 950 val isWifiEnabled: Boolean = false, 951 val hasActiveSubIdOnDds: Boolean = false, 952 val isDeviceLocked: Boolean = false, 953 val isWifiScanEnabled: Boolean = false, 954 val activeAutoSwitchNonDdsSubId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID, 955 ) 956 957 companion object { 958 private const val TAG = "InternetDetailsContent" 959 private val DEBUG: Boolean = Log.isLoggable(TAG, Log.DEBUG) 960 private const val MAX_NETWORK_COUNT = 4 961 const val CAN_CONFIG_MOBILE_DATA = "can_config_mobile_data" 962 const val CAN_CONFIG_WIFI = "can_config_wifi" 963 } 964 } 965