Merge changes from topic "AutoSelectPreferenceController" into main

* changes:
  Clean up ServiceStateStatus
  Improve AutoSelectPreferenceController
This commit is contained in:
Chaohui Wang
2023-12-28 10:41:10 +00:00
committed by Android (Google) Code Review
18 changed files with 423 additions and 1878 deletions

View File

@@ -1,100 +0,0 @@
/*
* Copyright (C) 2022 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.network.helper;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
import java.util.concurrent.atomic.AtomicReference;
/**
* A {@link androidx.lifecycle.LifecycleObserver} implementation of adapter over callback.
*
* Which including:
* 1. Request to active callback when Lifecycle.State.STARTED
* 2. Request to inactive callback when Lifecycle.State.STOPPED
* 3. Close (no further resume) when Lifecycle.State.DESTROYED
*/
@VisibleForTesting
abstract class LifecycleCallbackAdapter implements LifecycleEventObserver, AutoCloseable {
private static final String TAG = "LifecycleCallbackAdapter";
private AtomicReference<Lifecycle> mLifecycle = new AtomicReference<Lifecycle>();
/**
* Constructor
* @param lifecycle {@link Lifecycle} to monitor
*/
@VisibleForTesting
protected LifecycleCallbackAdapter(@NonNull Lifecycle lifecycle) {
mLifecycle.set(lifecycle);
lifecycle.addObserver(this);
}
/**
* Get {@link Lifecycle} under monitor.
* @return {@link Lifecycle}. Return {@code null} when closed.
*/
@VisibleForTesting
public Lifecycle getLifecycle() {
return mLifecycle.get();
}
/**
* Check current callback status.
* @return true when callback is active.
*/
public abstract boolean isCallbackActive();
/**
* Change callback status.
* @param isActive true to active callback, otherwise inactive.
*/
public abstract void setCallbackActive(boolean isActive);
/**
* Implementation of LifecycleEventObserver.
*/
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
if (mLifecycle.get() == null) {
return;
}
Lifecycle.State state = event.getTargetState();
boolean expectCallbackActive = state.isAtLeast(Lifecycle.State.STARTED);
if (expectCallbackActive != isCallbackActive()) {
setCallbackActive(expectCallbackActive);
}
if (state == Lifecycle.State.DESTROYED) {
close();
}
}
/**
* Implementation of AutoCloseable.
*/
@MainThread
public void close() {
Lifecycle lifecycle = mLifecycle.getAndSet(null);
if (lifecycle != null) {
lifecycle.removeObserver(this);
}
}
}

View File

@@ -1,132 +0,0 @@
/*
* Copyright (C) 2022 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.network.helper;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
import com.android.settingslib.utils.ThreadUtils;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
/**
* A {@link LifecycleCallbackAdapter} which support carrying a result from any threads back to UI
* thread through {@link #postResult(T)}.
*
* A {@link Consumer<T>} would be invoked from UI thread for further processing on the result.
*
* Note: Result not in STARTED or RESUMED stage will be discarded silently.
* This is to align with the criteria set within
* {@link LifecycleCallbackAdapter#onStateChanged()}.
*/
@VisibleForTesting
public class LifecycleCallbackConverter<T> extends LifecycleCallbackAdapter {
private static final String TAG = "LifecycleCallbackConverter";
private final Thread mUiThread;
private final Consumer<T> mResultCallback;
/**
* A record of number of active status change.
* Even numbers (0, 2, 4, 6 ...) are inactive status.
* Odd numbers (1, 3, 5, 7 ...) are active status.
*/
private final AtomicLong mNumberOfActiveStatusChange = new AtomicLong();
/**
* Constructor
*
* @param lifecycle {@link Lifecycle} to monitor
* @param resultCallback for further processing the result
*/
@VisibleForTesting
@UiThread
public LifecycleCallbackConverter(
@NonNull Lifecycle lifecycle, @NonNull Consumer<T> resultCallback) {
super(lifecycle);
mUiThread = Thread.currentThread();
mResultCallback = resultCallback;
}
/**
* Post a result (from any thread) back to UI thread.
*
* @param result the object ready to be passed back to {@link Consumer<T>}.
*/
@AnyThread
@VisibleForTesting
public void postResult(T result) {
/**
* Since mNumberOfActiveStatusChange only increase, it is a concept of sequence number.
* Carry it when sending data in between different threads allow to verify if the data
* has arrived on time. And drop the data when expired.
*/
long currentNumberOfChange = mNumberOfActiveStatusChange.get();
if (Thread.currentThread() == mUiThread) {
dispatchExtResult(currentNumberOfChange, result); // Dispatch directly
} else {
postResultToUiThread(currentNumberOfChange, result);
}
}
@AnyThread
protected void postResultToUiThread(long numberOfStatusChange, T result) {
ThreadUtils.postOnMainThread(() -> dispatchExtResult(numberOfStatusChange, result));
}
@UiThread
protected void dispatchExtResult(long numberOfStatusChange, T result) {
/**
* For a postResult() sending in between different threads, not only create a latency
* but also enqueued into main UI thread for dispatch.
*
* To align behavior within {@link LifecycleCallbackAdapter#onStateChanged()},
* some checking on both numberOfStatusChange and {@link Lifecycle} status are required.
*/
if (isActiveStatus(numberOfStatusChange)
&& (numberOfStatusChange == mNumberOfActiveStatusChange.get())
&& getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
mResultCallback.accept(result);
}
}
private static final boolean isActiveStatus(long numberOfStatusChange) {
return ((numberOfStatusChange & 1L) != 0L);
}
/* Implementation of LifecycleCallbackAdapter */
@UiThread
public boolean isCallbackActive() {
return isActiveStatus(mNumberOfActiveStatusChange.get());
}
/* Implementation of LifecycleCallbackAdapter */
@UiThread
public void setCallbackActive(boolean updatedActiveStatus) {
/**
* Make sure only increase when active status got changed.
* This is to implement the definition of mNumberOfActiveStatusChange.
*/
if (isCallbackActive() != updatedActiveStatus) {
mNumberOfActiveStatusChange.getAndIncrement();
}
}
}

View File

@@ -1,104 +0,0 @@
/*
* Copyright (C) 2022 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.network.helper;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
import java.util.function.Consumer;
/**
* A {@link BroadcastReceiver} for {@link Intent}.
*
* This is {@link BroadcastReceiver} supported by {@link LifecycleCallbackConverter},
* and only register when state is either START or RESUME.
*/
@VisibleForTesting
public class LifecycleCallbackIntentReceiver extends LifecycleCallbackConverter<Intent> {
private static final String TAG = "LifecycleCallbackIntentReceiver";
@VisibleForTesting
protected final BroadcastReceiver mReceiver;
private final Runnable mRegisterCallback;
private final Runnable mUnRegisterCallback;
/**
* Constructor
* @param lifecycle {@link Lifecycle} to monitor
* @param context for this BroadcastReceiver
* @param filter the IntentFilter for BroadcastReceiver
* @param broadcastPermission for permission when listening
* @param scheduler for running in background thread
* @param resultCallback for the Intent from BroadcastReceiver
*/
@VisibleForTesting
public LifecycleCallbackIntentReceiver(@NonNull Lifecycle lifecycle,
@NonNull Context context, @NonNull IntentFilter filter,
String broadcastPermission, Handler scheduler,
@NonNull Consumer<Intent> resultCallback) {
super(lifecycle, resultCallback);
// BroadcastReceiver
mReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
if (isInitialStickyBroadcast()) {
return;
}
final String action = intent.getAction();
if ((action == null) || (action.length() <= 0)) {
return;
}
postResult(intent);
}
};
// Register operation
mRegisterCallback = () -> {
Intent initIntent = context.registerReceiver(mReceiver,
filter, broadcastPermission, scheduler);
if (initIntent != null) {
postResult(initIntent);
}
};
// Un-Register operation
mUnRegisterCallback = () -> {
context.unregisterReceiver(mReceiver);
};
}
@Override
public void setCallbackActive(boolean isActive) {
super.setCallbackActive(isActive);
Runnable op = (isActive) ? mRegisterCallback : mUnRegisterCallback;
op.run();
}
@Override
public void close() {
super.close();
if (isCallbackActive()) {
setCallbackActive(false);
}
}
}

View File

@@ -1,72 +0,0 @@
/*
* Copyright (C) 2022 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.network.helper;
import android.telephony.TelephonyCallback;
import android.telephony.TelephonyManager;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
* A {@link LifecycleCallbackConverter} for supporting the register/unregister work for
* {@link TelephonyCallback}.
*/
@VisibleForTesting
public class LifecycleCallbackTelephonyAdapter<T> extends LifecycleCallbackConverter<T> {
private static final String TAG = "LifecycleCallbackTelephony";
private final Runnable mRegisterCallback;
private final Runnable mUnRegisterCallback;
/**
* Constructor
* @param lifecycle {@link Lifecycle} to monitor
* @param telephonyManager {@link TelephonyManager} to interact with
* @param telephonyCallback {@link TelephonyCallback}
* @param executor {@link Executor} for receiving the notify from telephony framework.
* @param resultCallback for the result from {@link TelephonyCallback}
*/
@VisibleForTesting
public LifecycleCallbackTelephonyAdapter(@NonNull Lifecycle lifecycle,
@NonNull TelephonyManager telephonyManager,
@NonNull TelephonyCallback telephonyCallback,
Executor executor, @NonNull Consumer<T> resultCallback) {
super(lifecycle, resultCallback);
// Register operation
mRegisterCallback = () -> {
telephonyManager.registerTelephonyCallback(executor, telephonyCallback);
};
// Un-Register operation
mUnRegisterCallback = () -> {
telephonyManager.unregisterTelephonyCallback(telephonyCallback);
};
}
@Override
public void setCallbackActive(boolean isActive) {
super.setCallbackActive(isActive);
Runnable op = (isActive) ? mRegisterCallback : mUnRegisterCallback;
op.run();
}
}

View File

@@ -1,105 +0,0 @@
/*
* Copyright (C) 2022 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.network.helper;
import android.telephony.ServiceState;
import android.telephony.TelephonyCallback;
import android.telephony.TelephonyManager;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LiveData;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
* A {@link LiveData} as a mapping of allowed network types reported from {@link TelephonyCallback}.
* Only got update when Lifecycle.State is considered as STARTED or RESUMED.
*
* {@code null} when status unknown. Other values are {@link ServiceState}.
*
* @deprecated Please us {@link com.android.settings.network.telephony.ServiceStateFlowKt} instead.
*/
@Deprecated
public class ServiceStateStatus extends LiveData<ServiceState> {
private static final String TAG = "ServiceStateStatus";
@VisibleForTesting
protected ServiceStateProducer mServiceStateProducer;
@VisibleForTesting
protected LifecycleCallbackTelephonyAdapter mAdapter;
@VisibleForTesting
protected Consumer<ServiceState> mLiveDataUpdater = status -> setValue(status);
/**
* Constructor
* @param lifecycle {@link Lifecycle} to monitor
* @param telephonyManager {@link TelephonyManager} to interact with
* @param executor {@link Executor} for receiving the notify from telephony framework.
*/
@VisibleForTesting
public ServiceStateStatus(@NonNull Lifecycle lifecycle,
@NonNull TelephonyManager telephonyManager, Executor executor) {
super();
mServiceStateProducer = new ServiceStateProducer(this);
mAdapter = new LifecycleCallbackTelephonyAdapter<ServiceState>(lifecycle,
telephonyManager, mServiceStateProducer, executor, mLiveDataUpdater) {
@Override
public void setCallbackActive(boolean isActive) {
super.setCallbackActive(isActive);
if (!isActive) {
/**
* Set to unknown status when no longer actively monitoring
* {@link TelephonyCallback}.
*/
mLiveDataUpdater.accept(null);
}
}
};
}
/**
* An implementation of TelephonyCallback.
*
* Change of allowed network type will be forward to
* {@link LifecycleCallbackTelephonyAdapter}.
*/
@VisibleForTesting
protected static class ServiceStateProducer extends TelephonyCallback
implements TelephonyCallback.ServiceStateListener {
private final ServiceStateStatus mStatus;
/**
* Constructor
* @param status {@link ServiceStateStatus}
*/
public ServiceStateProducer(ServiceStateStatus status) {
mStatus = status;
}
@Override
public void onServiceStateChanged(ServiceState serviceState) {
mStatus.mAdapter.postResult(serviceState);
}
}
}

View File

@@ -1,102 +0,0 @@
/*
* Copyright (C) 2022 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.network.helper;
import android.telephony.TelephonyCallback;
import android.telephony.TelephonyManager;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LiveData;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
* A {@link LiveData} as a mapping of voice call state reported from {@link TelephonyCallback}.
* Only got update when Lifecycle.State is considered as STARTED or RESUMED.
*
* {@code null} when status unknown. Other values are TelephonyManager#CALL_STATE_IDLE,
* TelephonyManager#CALL_STATE_RINGING and TelephonyManager#CALL_STATE_OFFHOOK.
*/
@VisibleForTesting
public class VoiceCallStatus extends LiveData<Integer> {
private static final String TAG = "VoiceCallStatus";
@VisibleForTesting
protected CallStateProducer mCallStateProducer;
@VisibleForTesting
protected LifecycleCallbackTelephonyAdapter mAdapter;
@VisibleForTesting
protected Consumer<Integer> mLiveDataUpdater = status -> setValue(status);
/**
* Constructor
* @param lifecycle {@link Lifecycle} to monitor
* @param telephonyManager {@link TelephonyManager} to interact with
* @param executor {@link Executor} for receiving the notify from telephony framework.
*/
@VisibleForTesting
public VoiceCallStatus(@NonNull Lifecycle lifecycle,
@NonNull TelephonyManager telephonyManager, Executor executor) {
super();
mCallStateProducer = new CallStateProducer(this);
mAdapter = new LifecycleCallbackTelephonyAdapter<Integer>(lifecycle,
telephonyManager, mCallStateProducer, executor, mLiveDataUpdater) {
@Override
public void setCallbackActive(boolean isActive) {
super.setCallbackActive(isActive);
if (!isActive) {
/**
* Set to unknown status when no longer actively monitoring
* {@link TelephonyCallback}.
*/
mLiveDataUpdater.accept(null);
}
}
};
}
/**
* An implementation of TelephonyCallback.
*
* Status of voice call will be forward to {@link LifecycleCallbackTelephonyAdapter}
*/
@VisibleForTesting
protected static class CallStateProducer extends TelephonyCallback
implements TelephonyCallback.CallStateListener {
private final VoiceCallStatus mStatus;
/**
* Constructor
* @param status {@link VoiceCallStatus}
*/
public CallStateProducer(VoiceCallStatus status) {
mStatus = status;
}
@Override
public void onCallStateChanged(int state) {
mStatus.mAdapter.postResult(state);
}
}
}

View File

@@ -276,7 +276,7 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme
use(OpenNetworkSelectPagePreferenceController.class).init(mSubId);
final AutoSelectPreferenceController autoSelectPreferenceController =
use(AutoSelectPreferenceController.class)
.init(getLifecycle(), mSubId)
.init(mSubId)
.addListener(openNetworkSelectPagePreferenceController);
use(NetworkPreferenceCategoryController.class).init(mSubId)
.setChildren(Arrays.asList(autoSelectPreferenceController));

View File

@@ -1,339 +0,0 @@
/*
* Copyright (C) 2018 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.network.telephony.gsm;
import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.Looper;
import android.os.PersistableBundle;
import android.os.SystemClock;
import android.provider.Settings;
import android.telephony.CarrierConfigManager;
import android.telephony.ServiceState;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import androidx.preference.TwoStatePreference;
import com.android.settings.R;
import com.android.settings.network.AllowedNetworkTypesListener;
import com.android.settings.network.CarrierConfigCache;
import com.android.settings.network.helper.ServiceStateStatus;
import com.android.settings.network.telephony.MobileNetworkUtils;
import com.android.settings.network.telephony.TelephonyTogglePreferenceController;
import com.android.settingslib.utils.ThreadUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
/**
* Preference controller for "Auto Select Network"
*/
public class AutoSelectPreferenceController extends TelephonyTogglePreferenceController
implements LifecycleEventObserver{
private static final long MINIMUM_DIALOG_TIME_MILLIS = TimeUnit.SECONDS.toMillis(1);
private static final String LOG_TAG = "AutoSelectPreferenceController";
private static final String INTERNAL_LOG_TAG_ONRESUME = "OnResume";
private static final String INTERNAL_LOG_TAG_AFTERSET = "AfterSet";
private final Handler mUiHandler;
private PreferenceScreen mPreferenceScreen;
private AllowedNetworkTypesListener mAllowedNetworkTypesListener;
private TelephonyManager mTelephonyManager;
private boolean mOnlyAutoSelectInHome;
private List<OnNetworkSelectModeListener> mListeners;
@VisibleForTesting
ProgressDialog mProgressDialog;
@VisibleForTesting
TwoStatePreference mSwitchPreference;
private AtomicBoolean mUpdatingConfig;
private int mCacheOfModeStatus;
private AtomicLong mRecursiveUpdate;
ServiceStateStatus mServiceStateStatus;
public AutoSelectPreferenceController(Context context, String key) {
super(context, key);
mTelephonyManager = context.getSystemService(TelephonyManager.class);
mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
mRecursiveUpdate = new AtomicLong();
mUpdatingConfig = new AtomicBoolean();
mCacheOfModeStatus = TelephonyManager.NETWORK_SELECTION_MODE_UNKNOWN;
mListeners = new ArrayList<>();
mUiHandler = new Handler(Looper.getMainLooper());
mAllowedNetworkTypesListener = new AllowedNetworkTypesListener(
new HandlerExecutor(mUiHandler));
mAllowedNetworkTypesListener.setAllowedNetworkTypesListener(
() -> updatePreference());
}
private void updatePreference() {
if (mPreferenceScreen != null) {
displayPreference(mPreferenceScreen);
}
if (mSwitchPreference != null) {
mRecursiveUpdate.getAndIncrement();
updateState(mSwitchPreference);
mRecursiveUpdate.decrementAndGet();
}
}
/**
* Implementation of LifecycleEventObserver.
*/
@SuppressWarnings("FutureReturnValueIgnored")
public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner,
@NonNull Lifecycle.Event event) {
switch (event) {
case ON_START:
mAllowedNetworkTypesListener.register(mContext, mSubId);
break;
case ON_RESUME:
ThreadUtils.postOnBackgroundThread(() -> {
queryNetworkSelectionMode(INTERNAL_LOG_TAG_ONRESUME);
//Update UI in UI thread
mUiHandler.post(() -> {
if (mSwitchPreference != null) {
mRecursiveUpdate.getAndIncrement();
mSwitchPreference.setChecked(isChecked());
mRecursiveUpdate.decrementAndGet();
updateListenerValue();
}
});
});
break;
case ON_STOP:
mAllowedNetworkTypesListener.unregister(mContext, mSubId);
break;
default:
// Do nothing
break;
}
}
@Override
public int getAvailabilityStatus(int subId) {
return MobileNetworkUtils.shouldDisplayNetworkSelectOptions(mContext, subId)
? AVAILABLE
: CONDITIONALLY_UNAVAILABLE;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPreferenceScreen = screen;
mSwitchPreference = screen.findPreference(getPreferenceKey());
}
@Override
public boolean isChecked() {
return mCacheOfModeStatus == TelephonyManager.NETWORK_SELECTION_MODE_AUTO;
}
@Override
public void updateState(Preference preference) {
super.updateState(preference);
preference.setSummary(null);
final ServiceState serviceState = mTelephonyManager.getServiceState();
if (serviceState == null) {
preference.setEnabled(false);
return;
}
if (serviceState.getRoaming()) {
preference.setEnabled(true);
} else {
preference.setEnabled(!mOnlyAutoSelectInHome);
if (mOnlyAutoSelectInHome) {
preference.setSummary(mContext.getString(
R.string.manual_mode_disallowed_summary,
mTelephonyManager.getSimOperatorName()));
}
}
}
@Override
public boolean setChecked(boolean isChecked) {
if (mRecursiveUpdate.get() != 0) {
// Changing from software are allowed and changing presentation only.
return true;
}
if (isChecked) {
setAutomaticSelectionMode();
} else {
if (mSwitchPreference != null) {
Intent intent = new Intent();
intent.setClassName(SETTINGS_PACKAGE_NAME,
SETTINGS_PACKAGE_NAME + ".Settings$NetworkSelectActivity");
intent.putExtra(Settings.EXTRA_SUB_ID, mSubId);
mSwitchPreference.setIntent(intent);
}
}
return false;
}
@VisibleForTesting
Future setAutomaticSelectionMode() {
final long startMillis = SystemClock.elapsedRealtime();
showAutoSelectProgressBar();
if (mSwitchPreference != null) {
mSwitchPreference.setIntent(null);
mSwitchPreference.setEnabled(false);
}
return ThreadUtils.postOnBackgroundThread(() -> {
// set network selection mode in background
mUpdatingConfig.set(true);
mTelephonyManager.setNetworkSelectionModeAutomatic();
mUpdatingConfig.set(false);
//Update UI in UI thread
final long durationMillis = SystemClock.elapsedRealtime() - startMillis;
mUiHandler.postDelayed(() -> {
ThreadUtils.postOnBackgroundThread(() -> {
queryNetworkSelectionMode(INTERNAL_LOG_TAG_AFTERSET);
//Update UI in UI thread
mUiHandler.post(() -> {
mRecursiveUpdate.getAndIncrement();
if (mSwitchPreference != null) {
mSwitchPreference.setEnabled(true);
mSwitchPreference.setChecked(isChecked());
}
mRecursiveUpdate.decrementAndGet();
updateListenerValue();
dismissProgressBar();
});
});
}, Math.max(MINIMUM_DIALOG_TIME_MILLIS - durationMillis, 0));
});
}
/**
* Initialization based on given subscription id.
**/
public AutoSelectPreferenceController init(Lifecycle lifecycle, int subId) {
mSubId = subId;
mTelephonyManager = mContext.getSystemService(TelephonyManager.class)
.createForSubscriptionId(mSubId);
final PersistableBundle carrierConfig =
CarrierConfigCache.getInstance(mContext).getConfigForSubId(mSubId);
mOnlyAutoSelectInHome = carrierConfig != null
? carrierConfig.getBoolean(
CarrierConfigManager.KEY_ONLY_AUTO_SELECT_IN_HOME_NETWORK_BOOL)
: false;
mServiceStateStatus = new ServiceStateStatus(lifecycle, mTelephonyManager,
new HandlerExecutor(mUiHandler)) {
@Override
protected void setValue(ServiceState status) {
if (status == null) {
return;
}
updateUiAutoSelectValue(status);
}
};
return this;
}
public AutoSelectPreferenceController addListener(OnNetworkSelectModeListener lsn) {
mListeners.add(lsn);
return this;
}
private void queryNetworkSelectionMode(String tag) {
mCacheOfModeStatus = mTelephonyManager.getNetworkSelectionMode();
Log.d(LOG_TAG, tag + ": query command done. mCacheOfModeStatus: " + mCacheOfModeStatus);
}
@VisibleForTesting
void updateUiAutoSelectValue(ServiceState status) {
if (status == null) {
return;
}
if (!mUpdatingConfig.get()) {
int networkSelectionMode = status.getIsManualSelection()
? TelephonyManager.NETWORK_SELECTION_MODE_MANUAL
: TelephonyManager.NETWORK_SELECTION_MODE_AUTO;
if (mCacheOfModeStatus == networkSelectionMode) {
return;
}
mCacheOfModeStatus = networkSelectionMode;
Log.d(LOG_TAG, "updateUiAutoSelectValue: mCacheOfModeStatus: " + mCacheOfModeStatus);
mRecursiveUpdate.getAndIncrement();
updateState(mSwitchPreference);
mRecursiveUpdate.decrementAndGet();
updateListenerValue();
}
}
private void updateListenerValue() {
for (OnNetworkSelectModeListener lsn : mListeners) {
lsn.onNetworkSelectModeUpdated(mCacheOfModeStatus);
}
}
private void showAutoSelectProgressBar() {
if (mProgressDialog == null) {
mProgressDialog = new ProgressDialog(mContext);
mProgressDialog.setMessage(
mContext.getResources().getString(R.string.register_automatically));
mProgressDialog.setCanceledOnTouchOutside(false);
mProgressDialog.setCancelable(false);
mProgressDialog.setIndeterminate(true);
}
mProgressDialog.show();
}
private void dismissProgressBar() {
if (mProgressDialog != null && mProgressDialog.isShowing()) {
try {
mProgressDialog.dismiss();
} catch (IllegalArgumentException e) {
// Ignore exception since the dialog will be gone anyway.
}
}
}
/**
* Callback when network select mode might get updated
*
* @see TelephonyManager#getNetworkSelectionMode()
*/
public interface OnNetworkSelectModeListener {
void onNetworkSelectModeUpdated(int mode);
}
}

View File

@@ -0,0 +1,224 @@
/*
* Copyright (C) 2023 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.network.telephony.gsm
import android.app.ProgressDialog
import android.content.Context
import android.content.Intent
import android.os.PersistableBundle
import android.provider.Settings
import android.telephony.CarrierConfigManager
import android.telephony.ServiceState
import android.telephony.TelephonyManager
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.preference.Preference
import androidx.preference.PreferenceScreen
import com.android.settings.R
import com.android.settings.Settings.NetworkSelectActivity
import com.android.settings.network.CarrierConfigCache
import com.android.settings.network.telephony.MobileNetworkUtils
import com.android.settings.network.telephony.allowedNetworkTypesFlow
import com.android.settings.network.telephony.serviceStateFlow
import com.android.settings.spa.preference.ComposePreferenceController
import com.android.settingslib.spa.framework.compose.OverridableFlow
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import com.android.settingslib.spa.widget.preference.SwitchPreference
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import kotlin.properties.Delegates.notNull
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Preference controller for "Auto Select Network"
*/
class AutoSelectPreferenceController @JvmOverloads constructor(
context: Context,
key: String,
private val allowedNetworkTypesFlowFactory: (subId: Int) -> Flow<Long> =
context::allowedNetworkTypesFlow,
private val serviceStateFlowFactory: (subId: Int) -> Flow<ServiceState> =
context::serviceStateFlow,
private val getConfigForSubId: (subId: Int) -> PersistableBundle = { subId ->
CarrierConfigCache.getInstance(context).getConfigForSubId(subId)
},
) : ComposePreferenceController(context, key) {
private lateinit var telephonyManager: TelephonyManager
private val listeners = mutableListOf<OnNetworkSelectModeListener>()
@VisibleForTesting
var progressDialog: ProgressDialog? = null
private lateinit var preference: Preference
private var subId by notNull<Int>()
/**
* Initialization based on given subscription id.
*/
fun init(subId: Int): AutoSelectPreferenceController {
this.subId = subId
telephonyManager = mContext.getSystemService(TelephonyManager::class.java)!!
.createForSubscriptionId(subId)
return this
}
override fun getAvailabilityStatus() =
if (MobileNetworkUtils.shouldDisplayNetworkSelectOptions(mContext, subId)) AVAILABLE
else CONDITIONALLY_UNAVAILABLE
override fun displayPreference(screen: PreferenceScreen) {
super.displayPreference(screen)
preference = screen.findPreference(preferenceKey)!!
}
@Composable
override fun Content() {
val coroutineScope = rememberCoroutineScope()
val serviceStateFlow = remember {
serviceStateFlowFactory(subId)
.stateIn(coroutineScope, SharingStarted.Lazily, null)
.filterNotNull()
}
val isAutoOverridableFlow = remember {
OverridableFlow(serviceStateFlow.map { !it.isManualSelection })
}
val isAuto by isAutoOverridableFlow.flow
.onEach(::updateListenerValue)
.collectAsStateWithLifecycle(initialValue = null)
val disallowedSummary by serviceStateFlow.map(::getDisallowedSummary)
.collectAsStateWithLifecycle(initialValue = "")
SwitchPreference(object : SwitchPreferenceModel {
override val title = stringResource(R.string.select_automatically)
override val summary = { disallowedSummary }
override val changeable = { disallowedSummary.isEmpty() }
override val checked = { isAuto }
override val onCheckedChange: (Boolean) -> Unit = { newChecked ->
if (newChecked) {
coroutineScope.launch { setAutomaticSelectionMode(isAutoOverridableFlow) }
} else {
mContext.startActivity(Intent().apply {
setClass(mContext, NetworkSelectActivity::class.java)
putExtra(Settings.EXTRA_SUB_ID, subId)
})
}
}
})
}
private suspend fun getDisallowedSummary(serviceState: ServiceState): String =
withContext(Dispatchers.Default) {
if (!serviceState.roaming && onlyAutoSelectInHome()) {
mContext.getString(
R.string.manual_mode_disallowed_summary,
telephonyManager.simOperatorName
)
} else ""
}
private fun onlyAutoSelectInHome(): Boolean =
getConfigForSubId(subId)
.getBoolean(CarrierConfigManager.KEY_ONLY_AUTO_SELECT_IN_HOME_NETWORK_BOOL)
private suspend fun setAutomaticSelectionMode(overrideChannel: OverridableFlow<Boolean>) {
showAutoSelectProgressBar()
withContext(Dispatchers.Default) {
val minimumDialogTimeDeferred = async { delay(MINIMUM_DIALOG_TIME) }
telephonyManager.setNetworkSelectionModeAutomatic()
minimumDialogTimeDeferred.await()
}
overrideChannel.override(true)
dismissProgressBar()
}
override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) {
allowedNetworkTypesFlowFactory(subId).collectLatestWithLifecycle(viewLifecycleOwner) {
preference.isVisible = withContext(Dispatchers.Default) {
MobileNetworkUtils.shouldDisplayNetworkSelectOptions(mContext, subId)
}
}
}
fun addListener(listener: OnNetworkSelectModeListener): AutoSelectPreferenceController {
listeners.add(listener)
return this
}
private fun updateListenerValue(isAuto: Boolean) {
for (listener in listeners) {
listener.onNetworkSelectModeUpdated(
if (isAuto) TelephonyManager.NETWORK_SELECTION_MODE_AUTO
else TelephonyManager.NETWORK_SELECTION_MODE_MANUAL
)
}
}
private fun showAutoSelectProgressBar() {
if (progressDialog == null) {
progressDialog = ProgressDialog(mContext).apply {
setMessage(mContext.resources.getString(R.string.register_automatically))
setCanceledOnTouchOutside(false)
setCancelable(false)
isIndeterminate = true
}
}
progressDialog?.show()
}
private fun dismissProgressBar() {
if (progressDialog?.isShowing == true) {
try {
progressDialog?.dismiss()
} catch (e: IllegalArgumentException) {
// Ignore exception since the dialog will be gone anyway.
}
}
}
/**
* Callback when network select mode might get updated
*
* @see TelephonyManager.getNetworkSelectionMode
*/
interface OnNetworkSelectModeListener {
fun onNetworkSelectModeUpdated(mode: Int)
}
companion object {
private val MINIMUM_DIALOG_TIME = 1.seconds
}
}