From a396a4d8b9d24cb7efbf4ec2b4f639c97f56ca1e Mon Sep 17 00:00:00 2001 From: Weng Su Date: Wed, 4 Jan 2023 04:22:57 +0800 Subject: [PATCH] Refine TetherSettings with ViewModel - Use ViewModel to handle the TetheringManager's lifecycle. - Use LiveData to observe TetheredInterfaces's changes instead of callbacks. Bug: 246531382 Test: manual test make RunSettingsRoboTests ROBOTEST_FILTER=TetherSettingsTest atest -c TetheringManagerModelTest Change-Id: Ib1a710a19a5340f63401a37b5c2b0bc307ac82f0 --- .../network/tether/TetherSettings.java | 13 +- .../network/tether/TetheringHelper.java | 157 ------------------ .../network/tether/TetheringManagerModel.java | 75 +++++++++ .../network/tether/TetheringHelperTest.java | 150 ----------------- .../tether/TetheringManagerModelTest.java | 95 +++++++++++ 5 files changed, 176 insertions(+), 314 deletions(-) delete mode 100644 src/com/android/settings/network/tether/TetheringHelper.java create mode 100644 src/com/android/settings/network/tether/TetheringManagerModel.java delete mode 100644 tests/robotests/src/com/android/settings/network/tether/TetheringHelperTest.java create mode 100644 tests/robotests/src/com/android/settings/network/tether/TetheringManagerModelTest.java diff --git a/src/com/android/settings/network/tether/TetherSettings.java b/src/com/android/settings/network/tether/TetherSettings.java index 118d407941d..6ec15a72de4 100644 --- a/src/com/android/settings/network/tether/TetherSettings.java +++ b/src/com/android/settings/network/tether/TetherSettings.java @@ -50,6 +50,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.ViewModelProvider; import androidx.preference.Preference; import androidx.preference.SwitchPreference; @@ -77,7 +78,7 @@ import java.util.concurrent.atomic.AtomicReference; */ @SearchIndexable public class TetherSettings extends RestrictedSettingsFragment - implements DataSaverBackend.Listener, TetheringManager.TetheringEventCallback { + implements DataSaverBackend.Listener { @VisibleForTesting static final String KEY_TETHER_PREFS_SCREEN = "tether_prefs_screen"; @@ -132,7 +133,6 @@ public class TetherSettings extends RestrictedSettingsFragment Context mContext; @VisibleForTesting TetheringManager mTm; - private TetheringHelper mTetheringHelper; @Override public int getMetricsCategory() { @@ -148,8 +148,9 @@ public class TetherSettings extends RestrictedSettingsFragment super.onAttach(context); mWifiTetherPreferenceController = new WifiTetherPreferenceController(context, getSettingsLifecycle()); - mTetheringHelper = TetheringHelper.getInstance(context, this /* TetheringEventCallback */, - getSettingsLifecycle()); + TetheringManagerModel model = new ViewModelProvider(this).get(TetheringManagerModel.class); + mTm = model.mTetheringManager; + model.getTetheredInterfaces().observe(this, this::onTetheredInterfacesChanged); } @Override @@ -188,7 +189,6 @@ public class TetherSettings extends RestrictedSettingsFragment mDataSaverBackend.addListener(this); mCm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - mTm = mTetheringHelper.getTetheringManager(); // Some devices do not have available EthernetManager. In that case getSystemService will // return null. mEm = mContext.getSystemService(EthernetManager.class); @@ -687,8 +687,7 @@ public class TetherSettings extends RestrictedSettingsFragment } } - @Override - public void onTetheredInterfacesChanged(List interfaces) { + protected void onTetheredInterfacesChanged(List interfaces) { Log.d(TAG, "onTetheredInterfacesChanged() interfaces : " + interfaces.toString()); final String[] tethered = interfaces.toArray(new String[interfaces.size()]); updateUsbState(tethered); diff --git a/src/com/android/settings/network/tether/TetheringHelper.java b/src/com/android/settings/network/tether/TetheringHelper.java deleted file mode 100644 index da559047f5d..00000000000 --- a/src/com/android/settings/network/tether/TetheringHelper.java +++ /dev/null @@ -1,157 +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.tether; - -import android.annotation.NonNull; -import android.annotation.TestApi; -import android.content.Context; -import android.net.TetheringManager; -import android.util.Log; - -import androidx.annotation.VisibleForTesting; -import androidx.lifecycle.DefaultLifecycleObserver; -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleOwner; - -import com.android.internal.annotations.GuardedBy; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Helper class for TetheringManager. - */ -public class TetheringHelper implements DefaultLifecycleObserver { - private static final String TAG = "TetheringHelper"; - - private static final Object sInstanceLock = new Object(); - @TestApi - @GuardedBy("sInstanceLock") - private static Map sTestInstances; - - protected static Context sAppContext; - protected static TetheringManager sTetheringManager; - protected static TetheringEventCallback sTetheringEventCallback = new TetheringEventCallback(); - protected static ArrayList sEventCallbacks = - new ArrayList<>(); - - protected TetheringManager.TetheringEventCallback mCallback; - - /** - * Static method to create a singleton class for TetheringHelper. - * - * @param context The Context this is associated with. - * @return an instance of {@link TetheringHelper} object. - */ - @NonNull - public static TetheringHelper getInstance(@NonNull Context context, - @NonNull TetheringManager.TetheringEventCallback callback, - @NonNull Lifecycle lifecycle) { - synchronized (sInstanceLock) { - if (sTestInstances != null && sTestInstances.containsKey(context)) { - TetheringHelper testInstance = sTestInstances.get(context); - Log.w(TAG, "The context owner use a test instance:" + testInstance); - return testInstance; - } - - if (sAppContext == null) { - sAppContext = context.getApplicationContext(); - sTetheringManager = sAppContext.getSystemService(TetheringManager.class); - } - TetheringHelper helper = new TetheringHelper(); - helper.mCallback = callback; - lifecycle.addObserver(helper); - return helper; - } - } - - /** - * A convenience method to set pre-prepared instance or mock class for testing. - * - * @param context The Context this is associated with. - * @param instance of {@link TetheringHelper} object. - * @hide - */ - @TestApi - @VisibleForTesting - public static void setTestInstance(@NonNull Context context, TetheringHelper instance) { - synchronized (sInstanceLock) { - if (sTestInstances == null) sTestInstances = new ConcurrentHashMap<>(); - Log.w(TAG, "Set a test instance by context:" + context); - sTestInstances.put(context, instance); - } - } - - /** - * The constructor can only be accessed from static method inside the class itself, this is - * to avoid creating a class by adding a private constructor. - */ - private TetheringHelper() { - // Do nothing. - } - - /** - * Returns the TetheringManager If the system service is successfully obtained. - */ - public TetheringManager getTetheringManager() { - return sTetheringManager; - } - - @Override - public void onStart(@NonNull LifecycleOwner owner) { - if (sEventCallbacks.contains(mCallback)) { - Log.w(TAG, "The callback already contains, don't register callback:" + mCallback); - return; - } - sEventCallbacks.add(mCallback); - if (sEventCallbacks.size() == 1) { - sTetheringManager.registerTetheringEventCallback(sAppContext.getMainExecutor(), - sTetheringEventCallback); - } - } - - @Override - public void onStop(@NonNull LifecycleOwner owner) { - if (!sEventCallbacks.remove(mCallback)) { - Log.w(TAG, "The callback does not contain, don't unregister callback:" + mCallback); - return; - } - if (sEventCallbacks.size() == 0) { - sTetheringManager.unregisterTetheringEventCallback(sTetheringEventCallback); - } - } - - @Override - public void onDestroy(@NonNull LifecycleOwner owner) { - mCallback = null; - } - - protected static final class TetheringEventCallback implements - TetheringManager.TetheringEventCallback { - @Override - public void onTetheredInterfacesChanged(List interfaces) { - for (TetheringManager.TetheringEventCallback callback : sEventCallbacks) { - if (callback == null) continue; - callback.onTetheredInterfacesChanged(interfaces); - } - } - } -} - - diff --git a/src/com/android/settings/network/tether/TetheringManagerModel.java b/src/com/android/settings/network/tether/TetheringManagerModel.java new file mode 100644 index 00000000000..7506c975c32 --- /dev/null +++ b/src/com/android/settings/network/tether/TetheringManagerModel.java @@ -0,0 +1,75 @@ +/* + * 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.tether; + +import android.app.Application; +import android.net.TetheringManager; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Transformations; + +import java.util.List; + +/** + * TetheringManager view model implementation + */ +public class TetheringManagerModel extends AndroidViewModel { + protected TetheringManager mTetheringManager; + protected EventCallback mEventCallback = new EventCallback(); + protected MutableLiveData> mTetheredInterfaces = new MutableLiveData<>(); + + public TetheringManagerModel(@NonNull Application application) { + super(application); + mTetheringManager = application.getSystemService(TetheringManager.class); + mTetheringManager + .registerTetheringEventCallback(application.getMainExecutor(), mEventCallback); + } + + @Override + protected void onCleared() { + mTetheringManager.unregisterTetheringEventCallback(mEventCallback); + } + + /** + * Gets the TetheringManager If the system service is successfully obtained. + */ + public TetheringManager getTetheringManager() { + return mTetheringManager; + } + + /** + * Gets the TetheredInterfaces wrapped by LiveData. + */ + @NonNull + public LiveData> getTetheredInterfaces() { + return Transformations.distinctUntilChanged(mTetheredInterfaces); + } + + /** + * Callback for use with {@link TetheringManager#registerTetheringEventCallback} to find out + * tethering upstream status. + */ + protected class EventCallback implements TetheringManager.TetheringEventCallback { + @Override + public void onTetheredInterfacesChanged(List interfaces) { + mTetheredInterfaces.setValue(interfaces); + } + } +} diff --git a/tests/robotests/src/com/android/settings/network/tether/TetheringHelperTest.java b/tests/robotests/src/com/android/settings/network/tether/TetheringHelperTest.java deleted file mode 100644 index 5b9ace8ef7a..00000000000 --- a/tests/robotests/src/com/android/settings/network/tether/TetheringHelperTest.java +++ /dev/null @@ -1,150 +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.tether; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -import android.content.Context; -import android.net.TetheringManager; - -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleOwner; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Spy; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; - -import java.util.List; -import java.util.concurrent.Executor; - -@RunWith(AndroidJUnit4.class) -public class TetheringHelperTest { - @Rule - public final MockitoRule mMockitoRule = MockitoJUnit.rule(); - @Spy - Context mContext = ApplicationProvider.getApplicationContext(); - @Mock - Lifecycle mLifecycle; - @Mock - LifecycleOwner mLifecycleOwner; - @Mock - TetheringManager mTetheringManager; - @Mock - TetheringManager.TetheringEventCallback mTetheringEventCallback; - @Mock - List mInterfaces; - - TetheringHelper mHelper; - - @Before - public void setUp() { - mHelper = TetheringHelper.getInstance(mContext, mTetheringEventCallback, mLifecycle); - mHelper.sTetheringManager = mTetheringManager; - } - - @Test - public void constructor_propertiesShouldBeReady() { - assertThat(mHelper.sAppContext).isNotNull(); - assertThat(mHelper.sTetheringManager).isNotNull(); - assertThat(mHelper.mCallback).isNotNull(); - verify(mLifecycle).addObserver(any()); - } - - @Test - public void getTetheringManager_isNotNull() { - assertThat(mHelper.getTetheringManager()).isNotNull(); - } - - @Test - public void onStart_eventCallbacksClear_addAndRegisterCallback() { - mHelper.sEventCallbacks.clear(); - - mHelper.onStart(mLifecycleOwner); - - assertThat(mHelper.sEventCallbacks.contains(mTetheringEventCallback)).isTrue(); - assertThat(mHelper.sEventCallbacks.size()).isEqualTo(1); - verify(mTetheringManager).registerTetheringEventCallback(any(Executor.class), - eq(TetheringHelper.sTetheringEventCallback)); - } - - @Test - public void onStart_eventCallbacksContains_doNotRegisterCallback() { - mHelper.sEventCallbacks.clear(); - mHelper.sEventCallbacks.add(mTetheringEventCallback); - - mHelper.onStart(mLifecycleOwner); - - verify(mTetheringManager, never()).registerTetheringEventCallback(any(Executor.class), - eq(TetheringHelper.sTetheringEventCallback)); - } - - @Test - public void onStop_eventCallbacksContains_removeAndUnregisterCallback() { - mHelper.sEventCallbacks.clear(); - mHelper.sEventCallbacks.add(mTetheringEventCallback); - - mHelper.onStop(mLifecycleOwner); - - assertThat(mHelper.sEventCallbacks.contains(mTetheringEventCallback)).isFalse(); - assertThat(mHelper.sEventCallbacks.size()).isEqualTo(0); - verify(mTetheringManager).unregisterTetheringEventCallback( - eq(TetheringHelper.sTetheringEventCallback)); - } - - @Test - public void onStop_eventCallbacksClear_doNotUnregisterCallback() { - mHelper.sEventCallbacks.clear(); - - mHelper.onStop(mLifecycleOwner); - - assertThat(mHelper.sEventCallbacks.contains(mTetheringEventCallback)).isFalse(); - assertThat(mHelper.sEventCallbacks.size()).isEqualTo(0); - verify(mTetheringManager, never()).unregisterTetheringEventCallback( - eq(TetheringHelper.sTetheringEventCallback)); - } - - @Test - public void onTetheredInterfacesChanged_eventCallbacksContains_doCallback() { - mHelper.sEventCallbacks.clear(); - mHelper.sEventCallbacks.add(mTetheringEventCallback); - - mHelper.sTetheringEventCallback.onTetheredInterfacesChanged(mInterfaces); - - verify(mTetheringEventCallback).onTetheredInterfacesChanged(eq(mInterfaces)); - } - - @Test - public void onTetheredInterfacesChanged_eventCallbacksClear_doNotCallback() { - mHelper.sEventCallbacks.clear(); - - mHelper.sTetheringEventCallback.onTetheredInterfacesChanged(mInterfaces); - - verify(mTetheringEventCallback, never()).onTetheredInterfacesChanged(eq(mInterfaces)); - } -} diff --git a/tests/robotests/src/com/android/settings/network/tether/TetheringManagerModelTest.java b/tests/robotests/src/com/android/settings/network/tether/TetheringManagerModelTest.java new file mode 100644 index 00000000000..0e8f00afe05 --- /dev/null +++ b/tests/robotests/src/com/android/settings/network/tether/TetheringManagerModelTest.java @@ -0,0 +1,95 @@ +/* + * 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.tether; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Application; +import android.net.TetheringManager; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.List; +import java.util.concurrent.Executor; + +@RunWith(AndroidJUnit4.class) +public class TetheringManagerModelTest { + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Mock + Application mApplication; + @Mock + Executor mExecutor; + @Mock + TetheringManager mTetheringManager; + @Mock + List mInterfaces; + + TetheringManagerModel mModel; + + @Before + public void setUp() { + when(mApplication.getMainExecutor()).thenReturn(mExecutor); + when(mApplication.getSystemService(TetheringManager.class)).thenReturn(mTetheringManager); + + mModel = new TetheringManagerModel(mApplication); + } + + @Test + public void constructor_registerCallback() { + verify(mTetheringManager).registerTetheringEventCallback(any(), eq(mModel.mEventCallback)); + } + + @Test + public void onCleared_unregisterCallback() { + mModel.onCleared(); + + verify(mTetheringManager).unregisterTetheringEventCallback(eq(mModel.mEventCallback)); + } + + @Test + public void getTetheringManager_isNotNull() { + assertThat(mModel.getTetheringManager()).isNotNull(); + } + + @Test + public void getTetheredInterfaces_isNotNull() { + assertThat(mModel.getTetheredInterfaces()).isNotNull(); + } + + @Test + public void onTetheredInterfacesChanged_updateTetheredInterfaces() { + mModel.mTetheredInterfaces.setValue(null); + + mModel.mEventCallback.onTetheredInterfacesChanged(mInterfaces); + + assertThat(mModel.mTetheredInterfaces.getValue()).isEqualTo(mInterfaces); + } +}