diff options
Diffstat (limited to 'src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt')
-rw-r--r-- | src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt new file mode 100644 index 00000000000..10e3f849905 --- /dev/null +++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.connecteddevice.threadnetwork + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.net.thread.ThreadNetworkController +import android.net.thread.ThreadNetworkController.StateCallback +import android.net.thread.ThreadNetworkException +import android.net.thread.ThreadNetworkManager +import android.os.OutcomeReceiver +import android.provider.Settings +import android.util.Log +import androidx.annotation.VisibleForTesting +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.preference.Preference +import androidx.preference.PreferenceScreen +import com.android.net.thread.platform.flags.Flags +import com.android.settings.R +import com.android.settings.core.TogglePreferenceController +import java.util.concurrent.Executor + +/** Controller for the "Thread" toggle in "Connected devices > Connection preferences". */ +class ThreadNetworkPreferenceController @VisibleForTesting constructor( + context: Context, + key: String, + private val executor: Executor, + private val threadController: BaseThreadNetworkController? +) : TogglePreferenceController(context, key), LifecycleEventObserver { + private val stateCallback: StateCallback + private val airplaneModeReceiver: BroadcastReceiver + private var threadEnabled = false + private var airplaneModeOn = false + private var preference: Preference? = null + + /** + * A testable interface for [ThreadNetworkController] which is `final`. + * + * We are in a awkward situation that Android API guideline suggest `final` for API classes + * while Robolectric test is being deprecated for platform testing (See + * tests/robotests/new_tests_hook.sh). This force us to use "mockito-target-extended" but it's + * conflicting with the default "mockito-target" which is somehow indirectly depended by the + * `SettingsUnitTests` target. + */ + @VisibleForTesting + interface BaseThreadNetworkController { + fun setEnabled( + enabled: Boolean, + executor: Executor, + receiver: OutcomeReceiver<Void?, ThreadNetworkException> + ) + + fun registerStateCallback(executor: Executor, callback: StateCallback) + + fun unregisterStateCallback(callback: StateCallback) + } + + constructor(context: Context, key: String) : this( + context, + key, + ContextCompat.getMainExecutor(context), + getThreadNetworkController(context) + ) + + init { + stateCallback = newStateCallback() + airplaneModeReceiver = newAirPlaneModeReceiver() + } + + val isThreadSupportedOnDevice: Boolean + get() = threadController != null + + private fun newStateCallback(): StateCallback { + return object : StateCallback { + override fun onThreadEnableStateChanged(enabledState: Int) { + threadEnabled = enabledState == ThreadNetworkController.STATE_ENABLED + } + + override fun onDeviceRoleChanged(role: Int) {} + } + } + + private fun newAirPlaneModeReceiver(): BroadcastReceiver { + return object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + airplaneModeOn = isAirplaneModeOn(context) + Log.i(TAG, "Airplane mode is " + if (airplaneModeOn) "ON" else "OFF") + preference?.let { preference -> updateState(preference) } + } + } + } + + override fun getAvailabilityStatus(): Int { + return if (!Flags.threadEnabledPlatform()) { + CONDITIONALLY_UNAVAILABLE + } else if (!isThreadSupportedOnDevice) { + UNSUPPORTED_ON_DEVICE + } else if (airplaneModeOn) { + DISABLED_DEPENDENT_SETTING + } else { + AVAILABLE + } + } + + override fun displayPreference(screen: PreferenceScreen) { + super.displayPreference(screen) + preference = screen.findPreference(preferenceKey) + } + + override fun isChecked(): Boolean { + // TODO (b/322742298): + // Check airplane mode here because it's planned to disable Thread state in airplane mode + // (code in the mainline module). But it's currently not implemented yet (b/322742298). + // By design, the toggle should be unchecked in airplane mode, so explicitly check the + // airplane mode here to acchieve the same UX. + return !airplaneModeOn && threadEnabled + } + + override fun setChecked(isChecked: Boolean): Boolean { + if (threadController == null) { + return false + } + val action = if (isChecked) "enable" else "disable" + threadController.setEnabled( + isChecked, + executor, + object : OutcomeReceiver<Void?, ThreadNetworkException> { + override fun onError(e: ThreadNetworkException) { + // TODO(b/327549838): gracefully handle the failure by resetting the UI state + Log.e(TAG, "Failed to $action Thread", e) + } + + override fun onResult(unused: Void?) { + Log.d(TAG, "Successfully $action Thread") + } + }) + return true + } + + override fun onStateChanged(lifecycleOwner: LifecycleOwner, event: Lifecycle.Event) { + if (threadController == null) { + return + } + + when (event) { + Lifecycle.Event.ON_START -> { + threadController.registerStateCallback(executor, stateCallback) + airplaneModeOn = isAirplaneModeOn(mContext) + mContext.registerReceiver( + airplaneModeReceiver, + IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED) + ) + preference?.let { preference -> updateState(preference) } + } + Lifecycle.Event.ON_STOP -> { + threadController.unregisterStateCallback(stateCallback) + mContext.unregisterReceiver(airplaneModeReceiver) + } + else -> {} + } + } + + override fun updateState(preference: Preference) { + super.updateState(preference) + preference.isEnabled = !airplaneModeOn + refreshSummary(preference) + } + + override fun getSummary(): CharSequence { + val resId: Int = if (airplaneModeOn) { + R.string.thread_network_settings_summary_airplane_mode + } else { + R.string.thread_network_settings_summary + } + return mContext.getResources().getString(resId) + } + + override fun getSliceHighlightMenuRes(): Int { + return R.string.menu_key_connected_devices + } + + companion object { + private const val TAG = "ThreadNetworkSettings" + private fun getThreadNetworkController(context: Context): BaseThreadNetworkController? { + if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_THREAD_NETWORK)) { + return null + } + val manager = context.getSystemService(ThreadNetworkManager::class.java) ?: return null + val controller = manager.allThreadNetworkControllers[0] + return object : BaseThreadNetworkController { + override fun setEnabled( + enabled: Boolean, + executor: Executor, + receiver: OutcomeReceiver<Void?, ThreadNetworkException> + ) { + controller.setEnabled(enabled, executor, receiver) + } + + override fun registerStateCallback(executor: Executor, callback: StateCallback) { + controller.registerStateCallback(executor, callback) + } + + override fun unregisterStateCallback(callback: StateCallback) { + controller.unregisterStateCallback(callback) + } + } + } + + private fun isAirplaneModeOn(context: Context): Boolean { + return Settings.Global.getInt( + context.contentResolver, + Settings.Global.AIRPLANE_MODE_ON, + 0 + ) == 1 + } + } +} |