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
This commit is contained in:
Weng Su
2023-01-04 04:22:57 +08:00
parent 2451afc0df
commit a396a4d8b9
5 changed files with 176 additions and 314 deletions

View File

@@ -50,6 +50,7 @@ import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.SwitchPreference; import androidx.preference.SwitchPreference;
@@ -77,7 +78,7 @@ import java.util.concurrent.atomic.AtomicReference;
*/ */
@SearchIndexable @SearchIndexable
public class TetherSettings extends RestrictedSettingsFragment public class TetherSettings extends RestrictedSettingsFragment
implements DataSaverBackend.Listener, TetheringManager.TetheringEventCallback { implements DataSaverBackend.Listener {
@VisibleForTesting @VisibleForTesting
static final String KEY_TETHER_PREFS_SCREEN = "tether_prefs_screen"; static final String KEY_TETHER_PREFS_SCREEN = "tether_prefs_screen";
@@ -132,7 +133,6 @@ public class TetherSettings extends RestrictedSettingsFragment
Context mContext; Context mContext;
@VisibleForTesting @VisibleForTesting
TetheringManager mTm; TetheringManager mTm;
private TetheringHelper mTetheringHelper;
@Override @Override
public int getMetricsCategory() { public int getMetricsCategory() {
@@ -148,8 +148,9 @@ public class TetherSettings extends RestrictedSettingsFragment
super.onAttach(context); super.onAttach(context);
mWifiTetherPreferenceController = mWifiTetherPreferenceController =
new WifiTetherPreferenceController(context, getSettingsLifecycle()); new WifiTetherPreferenceController(context, getSettingsLifecycle());
mTetheringHelper = TetheringHelper.getInstance(context, this /* TetheringEventCallback */, TetheringManagerModel model = new ViewModelProvider(this).get(TetheringManagerModel.class);
getSettingsLifecycle()); mTm = model.mTetheringManager;
model.getTetheredInterfaces().observe(this, this::onTetheredInterfacesChanged);
} }
@Override @Override
@@ -188,7 +189,6 @@ public class TetherSettings extends RestrictedSettingsFragment
mDataSaverBackend.addListener(this); mDataSaverBackend.addListener(this);
mCm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); mCm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
mTm = mTetheringHelper.getTetheringManager();
// Some devices do not have available EthernetManager. In that case getSystemService will // Some devices do not have available EthernetManager. In that case getSystemService will
// return null. // return null.
mEm = mContext.getSystemService(EthernetManager.class); mEm = mContext.getSystemService(EthernetManager.class);
@@ -687,8 +687,7 @@ public class TetherSettings extends RestrictedSettingsFragment
} }
} }
@Override protected void onTetheredInterfacesChanged(List<String> interfaces) {
public void onTetheredInterfacesChanged(List<String> interfaces) {
Log.d(TAG, "onTetheredInterfacesChanged() interfaces : " + interfaces.toString()); Log.d(TAG, "onTetheredInterfacesChanged() interfaces : " + interfaces.toString());
final String[] tethered = interfaces.toArray(new String[interfaces.size()]); final String[] tethered = interfaces.toArray(new String[interfaces.size()]);
updateUsbState(tethered); updateUsbState(tethered);

View File

@@ -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<Context, TetheringHelper> sTestInstances;
protected static Context sAppContext;
protected static TetheringManager sTetheringManager;
protected static TetheringEventCallback sTetheringEventCallback = new TetheringEventCallback();
protected static ArrayList<TetheringManager.TetheringEventCallback> 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<String> interfaces) {
for (TetheringManager.TetheringEventCallback callback : sEventCallbacks) {
if (callback == null) continue;
callback.onTetheredInterfacesChanged(interfaces);
}
}
}
}

View File

@@ -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<List<String>> 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<List<String>> 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<String> interfaces) {
mTetheredInterfaces.setValue(interfaces);
}
}
}

View File

@@ -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<String> 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));
}
}

View File

@@ -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<String> 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);
}
}