Merge "Refine TetherSettings to avoid activity leaks"
This commit is contained in:
committed by
Android (Google) Code Review
commit
2451afc0df
@@ -77,7 +77,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||||||
*/
|
*/
|
||||||
@SearchIndexable
|
@SearchIndexable
|
||||||
public class TetherSettings extends RestrictedSettingsFragment
|
public class TetherSettings extends RestrictedSettingsFragment
|
||||||
implements DataSaverBackend.Listener {
|
implements DataSaverBackend.Listener, TetheringManager.TetheringEventCallback {
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final String KEY_TETHER_PREFS_SCREEN = "tether_prefs_screen";
|
static final String KEY_TETHER_PREFS_SCREEN = "tether_prefs_screen";
|
||||||
@@ -111,7 +111,6 @@ public class TetherSettings extends RestrictedSettingsFragment
|
|||||||
private OnStartTetheringCallback mStartTetheringCallback;
|
private OnStartTetheringCallback mStartTetheringCallback;
|
||||||
private ConnectivityManager mCm;
|
private ConnectivityManager mCm;
|
||||||
private EthernetManager mEm;
|
private EthernetManager mEm;
|
||||||
private TetheringEventCallback mTetheringEventCallback;
|
|
||||||
private EthernetListener mEthernetListener;
|
private EthernetListener mEthernetListener;
|
||||||
private final HashSet<String> mAvailableInterfaces = new HashSet<>();
|
private final HashSet<String> mAvailableInterfaces = new HashSet<>();
|
||||||
|
|
||||||
@@ -133,6 +132,7 @@ 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,6 +148,8 @@ 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 */,
|
||||||
|
getSettingsLifecycle());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -186,7 +188,7 @@ public class TetherSettings extends RestrictedSettingsFragment
|
|||||||
mDataSaverBackend.addListener(this);
|
mDataSaverBackend.addListener(this);
|
||||||
|
|
||||||
mCm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
mCm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
mTm = (TetheringManager) getSystemService(Context.TETHERING_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);
|
||||||
@@ -364,15 +366,14 @@ public class TetherSettings extends RestrictedSettingsFragment
|
|||||||
|
|
||||||
|
|
||||||
mStartTetheringCallback = new OnStartTetheringCallback(this);
|
mStartTetheringCallback = new OnStartTetheringCallback(this);
|
||||||
mTetheringEventCallback = new TetheringEventCallback(this);
|
|
||||||
mTm.registerTetheringEventCallback(r -> mHandler.post(r), mTetheringEventCallback);
|
|
||||||
|
|
||||||
mMassStorageActive = Environment.MEDIA_SHARED.equals(Environment.getExternalStorageState());
|
mMassStorageActive = Environment.MEDIA_SHARED.equals(Environment.getExternalStorageState());
|
||||||
registerReceiver();
|
registerReceiver();
|
||||||
|
|
||||||
mEthernetListener = new EthernetListener(this);
|
mEthernetListener = new EthernetListener(this);
|
||||||
if (mEm != null) {
|
if (mEm != null) {
|
||||||
mEm.addInterfaceStateListener(r -> mHandler.post(r), mEthernetListener);
|
mEm.addInterfaceStateListener(mContext.getApplicationContext().getMainExecutor(),
|
||||||
|
mEthernetListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUsbState();
|
updateUsbState();
|
||||||
@@ -387,13 +388,11 @@ public class TetherSettings extends RestrictedSettingsFragment
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getActivity().unregisterReceiver(mTetherChangeReceiver);
|
getActivity().unregisterReceiver(mTetherChangeReceiver);
|
||||||
mTm.unregisterTetheringEventCallback(mTetheringEventCallback);
|
|
||||||
if (mEm != null) {
|
if (mEm != null) {
|
||||||
mEm.removeInterfaceStateListener(mEthernetListener);
|
mEm.removeInterfaceStateListener(mEthernetListener);
|
||||||
}
|
}
|
||||||
mTetherChangeReceiver = null;
|
mTetherChangeReceiver = null;
|
||||||
mStartTetheringCallback = null;
|
mStartTetheringCallback = null;
|
||||||
mTetheringEventCallback = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
@@ -688,25 +687,8 @@ public class TetherSettings extends RestrictedSettingsFragment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class TetheringEventCallback implements
|
|
||||||
TetheringManager.TetheringEventCallback {
|
|
||||||
final WeakReference<TetherSettings> mTetherSettings;
|
|
||||||
|
|
||||||
TetheringEventCallback(TetherSettings settings) {
|
|
||||||
mTetherSettings = new WeakReference<>(settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTetheredInterfacesChanged(List<String> interfaces) {
|
public void onTetheredInterfacesChanged(List<String> interfaces) {
|
||||||
final TetherSettings tetherSettings = mTetherSettings.get();
|
|
||||||
if (tetherSettings == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tetherSettings.onTetheredInterfacesChanged(interfaces);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
|
157
src/com/android/settings/network/tether/TetheringHelper.java
Normal file
157
src/com/android/settings/network/tether/TetheringHelper.java
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@@ -0,0 +1,150 @@
|
|||||||
|
/*
|
||||||
|
* 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));
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user