Merge "Deprecate TelephonyManager.getCellLocation()"
diff --git a/api/current.txt b/api/current.txt
index 0db3f28..4370505 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -23767,6 +23767,7 @@
method public android.net.Network getBoundNetworkForProcess();
method public android.net.ProxyInfo getDefaultProxy();
method public android.net.LinkProperties getLinkProperties(android.net.Network);
+ method public int getMultipathPreference(android.net.Network);
method public android.net.NetworkCapabilities getNetworkCapabilities(android.net.Network);
method public deprecated android.net.NetworkInfo getNetworkInfo(int);
method public android.net.NetworkInfo getNetworkInfo(android.net.Network);
@@ -23814,6 +23815,9 @@
field public static final java.lang.String EXTRA_NO_CONNECTIVITY = "noConnectivity";
field public static final java.lang.String EXTRA_OTHER_NETWORK_INFO = "otherNetwork";
field public static final java.lang.String EXTRA_REASON = "reason";
+ field public static final int MULTIPATH_PREFERENCE_HANDOVER = 1; // 0x1
+ field public static final int MULTIPATH_PREFERENCE_PERFORMANCE = 4; // 0x4
+ field public static final int MULTIPATH_PREFERENCE_RELIABILITY = 2; // 0x2
field public static final int RESTRICT_BACKGROUND_STATUS_DISABLED = 1; // 0x1
field public static final int RESTRICT_BACKGROUND_STATUS_ENABLED = 3; // 0x3
field public static final int RESTRICT_BACKGROUND_STATUS_WHITELISTED = 2; // 0x2
@@ -37455,6 +37459,7 @@
method public android.telecom.PhoneAccountHandle getDefaultOutgoingPhoneAccount(java.lang.String);
method public java.lang.String getLine1Number(android.telecom.PhoneAccountHandle);
method public android.telecom.PhoneAccount getPhoneAccount(android.telecom.PhoneAccountHandle);
+ method public java.util.List<android.telecom.PhoneAccountHandle> getSelfManagedPhoneAccounts();
method public android.telecom.PhoneAccountHandle getSimCallManager();
method public java.lang.String getVoiceMailNumber(android.telecom.PhoneAccountHandle);
method public boolean handleMmi(java.lang.String);
diff --git a/api/system-current.txt b/api/system-current.txt
index 0537325..d801448 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -25554,6 +25554,7 @@
method public java.lang.String getCaptivePortalServerUrl();
method public android.net.ProxyInfo getDefaultProxy();
method public android.net.LinkProperties getLinkProperties(android.net.Network);
+ method public int getMultipathPreference(android.net.Network);
method public android.net.NetworkCapabilities getNetworkCapabilities(android.net.Network);
method public deprecated android.net.NetworkInfo getNetworkInfo(int);
method public android.net.NetworkInfo getNetworkInfo(android.net.Network);
@@ -25605,6 +25606,9 @@
field public static final java.lang.String EXTRA_NO_CONNECTIVITY = "noConnectivity";
field public static final java.lang.String EXTRA_OTHER_NETWORK_INFO = "otherNetwork";
field public static final java.lang.String EXTRA_REASON = "reason";
+ field public static final int MULTIPATH_PREFERENCE_HANDOVER = 1; // 0x1
+ field public static final int MULTIPATH_PREFERENCE_PERFORMANCE = 4; // 0x4
+ field public static final int MULTIPATH_PREFERENCE_RELIABILITY = 2; // 0x2
field public static final int RESTRICT_BACKGROUND_STATUS_DISABLED = 1; // 0x1
field public static final int RESTRICT_BACKGROUND_STATUS_ENABLED = 3; // 0x3
field public static final int RESTRICT_BACKGROUND_STATUS_WHITELISTED = 2; // 0x2
@@ -40621,6 +40625,7 @@
method public android.telecom.PhoneAccount getPhoneAccount(android.telecom.PhoneAccountHandle);
method public java.util.List<android.telecom.PhoneAccountHandle> getPhoneAccountsForPackage();
method public java.util.List<android.telecom.PhoneAccountHandle> getPhoneAccountsSupportingScheme(java.lang.String);
+ method public java.util.List<android.telecom.PhoneAccountHandle> getSelfManagedPhoneAccounts();
method public android.telecom.PhoneAccountHandle getSimCallManager();
method public java.lang.String getVoiceMailNumber(android.telecom.PhoneAccountHandle);
method public boolean handleMmi(java.lang.String);
diff --git a/api/test-current.txt b/api/test-current.txt
index eff60ac..084fb36 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -23841,6 +23841,7 @@
method public android.net.Network getBoundNetworkForProcess();
method public android.net.ProxyInfo getDefaultProxy();
method public android.net.LinkProperties getLinkProperties(android.net.Network);
+ method public int getMultipathPreference(android.net.Network);
method public android.net.NetworkCapabilities getNetworkCapabilities(android.net.Network);
method public deprecated android.net.NetworkInfo getNetworkInfo(int);
method public android.net.NetworkInfo getNetworkInfo(android.net.Network);
@@ -23888,6 +23889,9 @@
field public static final java.lang.String EXTRA_NO_CONNECTIVITY = "noConnectivity";
field public static final java.lang.String EXTRA_OTHER_NETWORK_INFO = "otherNetwork";
field public static final java.lang.String EXTRA_REASON = "reason";
+ field public static final int MULTIPATH_PREFERENCE_HANDOVER = 1; // 0x1
+ field public static final int MULTIPATH_PREFERENCE_PERFORMANCE = 4; // 0x4
+ field public static final int MULTIPATH_PREFERENCE_RELIABILITY = 2; // 0x2
field public static final int RESTRICT_BACKGROUND_STATUS_DISABLED = 1; // 0x1
field public static final int RESTRICT_BACKGROUND_STATUS_ENABLED = 3; // 0x3
field public static final int RESTRICT_BACKGROUND_STATUS_WHITELISTED = 2; // 0x2
@@ -37554,6 +37558,7 @@
method public android.telecom.PhoneAccountHandle getDefaultOutgoingPhoneAccount(java.lang.String);
method public java.lang.String getLine1Number(android.telecom.PhoneAccountHandle);
method public android.telecom.PhoneAccount getPhoneAccount(android.telecom.PhoneAccountHandle);
+ method public java.util.List<android.telecom.PhoneAccountHandle> getSelfManagedPhoneAccounts();
method public android.telecom.PhoneAccountHandle getSimCallManager();
method public java.lang.String getVoiceMailNumber(android.telecom.PhoneAccountHandle);
method public boolean handleMmi(java.lang.String);
diff --git a/core/java/android/bluetooth/BluetoothHeadset.java b/core/java/android/bluetooth/BluetoothHeadset.java
index f46a3b3..2d25659 100644
--- a/core/java/android/bluetooth/BluetoothHeadset.java
+++ b/core/java/android/bluetooth/BluetoothHeadset.java
@@ -1010,6 +1010,18 @@
}
/**
+ * check if in-band ringing is supported for this platform.
+ *
+ * @return true if in-band ringing is supported
+ * false if in-band ringing is not supported
+ * @hide
+ */
+ public static boolean isInbandRingingSupported(Context context) {
+ return context.getResources().getBoolean(
+ com.android.internal.R.bool.config_bluetooth_hfp_inband_ringing_support);
+ }
+
+ /**
* Send Headset the BIND response from AG to report change in the status of the
* HF indicators to the headset
*
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index cfbd948..2a985e7 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -3407,6 +3407,75 @@
}
/**
+ * It is acceptable to briefly use multipath data to provide seamless connectivity for
+ * time-sensitive user-facing operations when the system default network is temporarily
+ * unresponsive. The amount of data should be limited (less than one megabyte), and the
+ * operation should be infrequent to ensure that data usage is limited.
+ *
+ * An example of such an operation might be a time-sensitive foreground activity, such as a
+ * voice command, that the user is performing while walking out of range of a Wi-Fi network.
+ */
+ public static final int MULTIPATH_PREFERENCE_HANDOVER = 1 << 0;
+
+ /**
+ * It is acceptable to use small amounts of multipath data on an ongoing basis to provide
+ * a backup channel for traffic that is primarily going over another network.
+ *
+ * An example might be maintaining backup connections to peers or servers for the purpose of
+ * fast fallback if the default network is temporarily unresponsive or disconnects. The traffic
+ * on backup paths should be negligible compared to the traffic on the main path.
+ */
+ public static final int MULTIPATH_PREFERENCE_RELIABILITY = 1 << 1;
+
+ /**
+ * It is acceptable to use metered data to improve network latency and performance.
+ */
+ public static final int MULTIPATH_PREFERENCE_PERFORMANCE = 1 << 2;
+
+ /**
+ * Return value to use for unmetered networks. On such networks we currently set all the flags
+ * to true.
+ * @hide
+ */
+ public static final int MULTIPATH_PREFERENCE_UNMETERED =
+ MULTIPATH_PREFERENCE_HANDOVER |
+ MULTIPATH_PREFERENCE_RELIABILITY |
+ MULTIPATH_PREFERENCE_PERFORMANCE;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {
+ MULTIPATH_PREFERENCE_HANDOVER,
+ MULTIPATH_PREFERENCE_RELIABILITY,
+ MULTIPATH_PREFERENCE_PERFORMANCE,
+ })
+ public @interface MultipathPreference {
+ }
+
+ /**
+ * Provides a hint to the calling application on whether it is desirable to use the
+ * multinetwork APIs (e.g., {@link Network#openConnection}, {@link Network#bindSocket}, etc.)
+ * for multipath data transfer on this network when it is not the system default network.
+ * Applications desiring to use multipath network protocols should call this method before
+ * each such operation.
+ * <p>
+ * This method requires the caller to hold the permission
+ * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+ *
+ * @param network The network on which the application desires to use multipath data.
+ * If {@code null}, this method will return the a preference that will generally
+ * apply to metered networks.
+ * @return a bitwise OR of zero or more of the {@code MULTIPATH_PREFERENCE_*} constants.
+ */
+ public @MultipathPreference int getMultipathPreference(Network network) {
+ try {
+ return mService.getMultipathPreference(network);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Resets all connectivity manager settings back to factory defaults.
* @hide
*/
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
index b123c28f..425e494 100644
--- a/core/java/android/net/IConnectivityManager.aidl
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -161,6 +161,8 @@
void setAcceptUnvalidated(in Network network, boolean accept, boolean always);
void setAvoidUnvalidated(in Network network);
+ int getMultipathPreference(in Network Network);
+
int getRestoreDefaultNetworkDelay(int networkType);
boolean addVpnAddress(String address, int prefixLength);
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 0a40703..8e55f4b 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -7582,6 +7582,16 @@
public static final String NETWORK_AVOID_BAD_WIFI = "network_avoid_bad_wifi";
/**
+ * User setting for ConnectivityManager.getMeteredMultipathPreference(). This value may be
+ * overridden by the system based on device or application state. If null, the value
+ * specified by config_networkMeteredMultipathPreference is used.
+ *
+ * @hide
+ */
+ public static final String NETWORK_METERED_MULTIPATH_PREFERENCE =
+ "network_metered_multipath_preference";
+
+ /**
* Whether Wifi display is enabled/disabled
* 0=disabled. 1=enabled.
* @hide
diff --git a/core/java/android/util/IntArray.java b/core/java/android/util/IntArray.java
index 9326203..3617aa7 100644
--- a/core/java/android/util/IntArray.java
+++ b/core/java/android/util/IntArray.java
@@ -17,7 +17,7 @@
package android.util;
import com.android.internal.util.ArrayUtils;
-
+import com.android.internal.util.Preconditions;
import java.util.Arrays;
import libcore.util.EmptyArray;
@@ -32,6 +32,11 @@
private int[] mValues;
private int mSize;
+ private IntArray(int[] array, int size) {
+ mValues = array;
+ mSize = Preconditions.checkArgumentInRange(size, 0, array.length, "size");
+ }
+
/**
* Creates an empty IntArray with the default initial capacity.
*/
@@ -52,6 +57,35 @@
}
/**
+ * Creates an IntArray wrapping the given primitive int array.
+ */
+ public static IntArray wrap(int[] array) {
+ return new IntArray(array, array.length);
+ }
+
+ /**
+ * Creates an IntArray from the given primitive int array, copying it.
+ */
+ public static IntArray fromArray(int[] array, int size) {
+ return wrap(Arrays.copyOf(array, size));
+ }
+
+ /**
+ * Changes the size of this IntArray. If this IntArray is shrinked, the backing array capacity
+ * is unchanged. If the new size is larger than backing array capacity, a new backing array is
+ * created from the current content of this IntArray padded with 0s.
+ */
+ public void resize(int newSize) {
+ Preconditions.checkArgumentNonnegative(newSize);
+ if (newSize <= mValues.length) {
+ Arrays.fill(mValues, newSize, mValues.length, 0);
+ } else {
+ ensureCapacity(newSize - mSize);
+ }
+ mSize = newSize;
+ }
+
+ /**
* Appends the specified value to the end of this array.
*/
public void add(int value) {
@@ -59,23 +93,23 @@
}
/**
- * Inserts a value at the specified position in this array.
+ * Inserts a value at the specified position in this array. If the specified index is equal to
+ * the length of the array, the value is added at the end.
*
* @throws IndexOutOfBoundsException when index < 0 || index > size()
*/
public void add(int index, int value) {
- if (index < 0 || index > mSize) {
- throw new IndexOutOfBoundsException();
- }
-
ensureCapacity(1);
+ int rightSegment = mSize - index;
+ mSize++;
+ checkBounds(index);
- if (mSize - index != 0) {
- System.arraycopy(mValues, index, mValues, index + 1, mSize - index);
+ if (rightSegment != 0) {
+ // Move by 1 all values from the right of 'index'
+ System.arraycopy(mValues, index, mValues, index + 1, rightSegment);
}
mValues[index] = value;
- mSize++;
}
/**
@@ -141,13 +175,19 @@
* Returns the value at the specified position in this array.
*/
public int get(int index) {
- if (index >= mSize) {
- throw new ArrayIndexOutOfBoundsException(mSize, index);
- }
+ checkBounds(index);
return mValues[index];
}
/**
+ * Sets the value at the specified position in this array.
+ */
+ public void set(int index, int value) {
+ checkBounds(index);
+ mValues[index] = value;
+ }
+
+ /**
* Returns the index of the first occurrence of the specified value in this
* array, or -1 if this array does not contain the value.
*/
@@ -165,9 +205,7 @@
* Removes the value at the specified index from this array.
*/
public void remove(int index) {
- if (index >= mSize) {
- throw new ArrayIndexOutOfBoundsException(mSize, index);
- }
+ checkBounds(index);
System.arraycopy(mValues, index + 1, mValues, index, mSize - index - 1);
mSize--;
}
@@ -185,4 +223,10 @@
public int[] toArray() {
return Arrays.copyOf(mValues, mSize);
}
+
+ private void checkBounds(int index) {
+ if (index < 0 || mSize <= index) {
+ throw new ArrayIndexOutOfBoundsException(mSize, index);
+ }
+ }
}
diff --git a/core/java/android/util/LongArray.java b/core/java/android/util/LongArray.java
index 54a6882..9b0489c 100644
--- a/core/java/android/util/LongArray.java
+++ b/core/java/android/util/LongArray.java
@@ -17,6 +17,8 @@
package android.util;
import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.Preconditions;
+import java.util.Arrays;
import libcore.util.EmptyArray;
/**
@@ -30,6 +32,11 @@
private long[] mValues;
private int mSize;
+ private LongArray(long[] array, int size) {
+ mValues = array;
+ mSize = Preconditions.checkArgumentInRange(size, 0, array.length, "size");
+ }
+
/**
* Creates an empty LongArray with the default initial capacity.
*/
@@ -50,6 +57,35 @@
}
/**
+ * Creates an LongArray wrapping the given primitive long array.
+ */
+ public static LongArray wrap(long[] array) {
+ return new LongArray(array, array.length);
+ }
+
+ /**
+ * Creates an LongArray from the given primitive long array, copying it.
+ */
+ public static LongArray fromArray(long[] array, int size) {
+ return wrap(Arrays.copyOf(array, size));
+ }
+
+ /**
+ * Changes the size of this LongArray. If this LongArray is shrinked, the backing array capacity
+ * is unchanged. If the new size is larger than backing array capacity, a new backing array is
+ * created from the current content of this LongArray padded with 0s.
+ */
+ public void resize(int newSize) {
+ Preconditions.checkArgumentNonnegative(newSize);
+ if (newSize <= mValues.length) {
+ Arrays.fill(mValues, newSize, mValues.length, 0);
+ } else {
+ ensureCapacity(newSize - mSize);
+ }
+ mSize = newSize;
+ }
+
+ /**
* Appends the specified value to the end of this array.
*/
public void add(long value) {
@@ -57,23 +93,23 @@
}
/**
- * Inserts a value at the specified position in this array.
+ * Inserts a value at the specified position in this array. If the specified index is equal to
+ * the length of the array, the value is added at the end.
*
* @throws IndexOutOfBoundsException when index < 0 || index > size()
*/
public void add(int index, long value) {
- if (index < 0 || index > mSize) {
- throw new IndexOutOfBoundsException();
- }
-
ensureCapacity(1);
+ int rightSegment = mSize - index;
+ mSize++;
+ checkBounds(index);
- if (mSize - index != 0) {
- System.arraycopy(mValues, index, mValues, index + 1, mSize - index);
+ if (rightSegment != 0) {
+ // Move by 1 all values from the right of 'index'
+ System.arraycopy(mValues, index, mValues, index + 1, rightSegment);
}
mValues[index] = value;
- mSize++;
}
/**
@@ -126,13 +162,19 @@
* Returns the value at the specified position in this array.
*/
public long get(int index) {
- if (index >= mSize) {
- throw new ArrayIndexOutOfBoundsException(mSize, index);
- }
+ checkBounds(index);
return mValues[index];
}
/**
+ * Sets the value at the specified position in this array.
+ */
+ public void set(int index, long value) {
+ checkBounds(index);
+ mValues[index] = value;
+ }
+
+ /**
* Returns the index of the first occurrence of the specified value in this
* array, or -1 if this array does not contain the value.
*/
@@ -150,9 +192,7 @@
* Removes the value at the specified index from this array.
*/
public void remove(int index) {
- if (index >= mSize) {
- throw new ArrayIndexOutOfBoundsException(mSize, index);
- }
+ checkBounds(index);
System.arraycopy(mValues, index + 1, mValues, index, mSize - index - 1);
mSize--;
}
@@ -163,4 +203,17 @@
public int size() {
return mSize;
}
+
+ /**
+ * Returns a new array with the contents of this LongArray.
+ */
+ public long[] toArray() {
+ return Arrays.copyOf(mValues, mSize);
+ }
+
+ private void checkBounds(int index) {
+ if (index < 0 || mSize <= index) {
+ throw new ArrayIndexOutOfBoundsException(mSize, index);
+ }
+ }
}
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 387c39b..589aa07 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -281,6 +281,11 @@
Settings.Global.NETWORK_AVOID_BAD_WIFI. This is the default value of that setting. -->
<integer translatable="false" name="config_networkAvoidBadWifi">1</integer>
+ <!-- Default value for ConnectivityManager.getMultipathPreference() on metered networks. Actual
+ device behaviour is controlled by Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE.
+ This is the default value of that setting. -->
+ <integer translatable="false" name="config_networkMeteredMultipathPreference">3</integer>
+
<!-- List of regexpressions describing the interface (if any) that represent tetherable
USB interfaces. If the device doesn't want to support tethering over USB this should
be empty. An example would be "usb.*" -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index f970909..4db748e 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1784,6 +1784,7 @@
<java-symbol type="integer" name="config_networkNotifySwitchType" />
<java-symbol type="array" name="config_networkNotifySwitches" />
<java-symbol type="integer" name="config_networkAvoidBadWifi" />
+ <java-symbol type="integer" name="config_networkMeteredMultipathPreference" />
<java-symbol type="integer" name="config_notificationsBatteryFullARGB" />
<java-symbol type="integer" name="config_notificationsBatteryLedOff" />
<java-symbol type="integer" name="config_notificationsBatteryLedOn" />
diff --git a/core/tests/utiltests/src/android/util/IntArrayTest.java b/core/tests/utiltests/src/android/util/IntArrayTest.java
new file mode 100644
index 0000000..a6120a1
--- /dev/null
+++ b/core/tests/utiltests/src/android/util/IntArrayTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2017 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 android.util;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IntArrayTest {
+
+ @Test
+ public void testIntArray() {
+ IntArray a = new IntArray();
+ a.add(1);
+ a.add(2);
+ a.add(3);
+ verify(new int[]{1, 2, 3}, a);
+
+ IntArray b = IntArray.fromArray(new int[]{4, 5, 6, 7, 8}, 3);
+ a.addAll(b);
+ verify(new int[]{1, 2, 3, 4, 5, 6}, a);
+
+ a.resize(2);
+ verify(new int[]{1, 2}, a);
+
+ a.resize(8);
+ verify(new int[]{1, 2, 0, 0, 0, 0, 0, 0}, a);
+
+ a.set(5, 10);
+ verify(new int[]{1, 2, 0, 0, 0, 10, 0, 0}, a);
+
+ a.add(5, 20);
+ assertEquals(20, a.get(5));
+ assertEquals(5, a.indexOf(20));
+ verify(new int[]{1, 2, 0, 0, 0, 20, 10, 0, 0}, a);
+
+ assertEquals(-1, a.indexOf(99));
+
+ a.resize(15);
+ a.set(14, 30);
+ verify(new int[]{1, 2, 0, 0, 0, 20, 10, 0, 0, 0, 0, 0, 0, 0, 30}, a);
+
+ int[] backingArray = new int[]{1, 2, 3, 4};
+ a = IntArray.wrap(backingArray);
+ a.set(0, 10);
+ assertEquals(10, backingArray[0]);
+ backingArray[1] = 20;
+ backingArray[2] = 30;
+ verify(backingArray, a);
+ assertEquals(2, a.indexOf(30));
+
+ a.resize(2);
+ assertEquals(0, backingArray[2]);
+ assertEquals(0, backingArray[3]);
+
+ a.add(50);
+ verify(new int[]{10, 20, 50}, a);
+ }
+
+ public void verify(int[] expected, IntArray intArray) {
+ assertEquals(expected.length, intArray.size());
+ assertArrayEquals(expected, intArray.toArray());
+ }
+}
diff --git a/core/tests/utiltests/src/android/util/LongArrayTest.java b/core/tests/utiltests/src/android/util/LongArrayTest.java
new file mode 100644
index 0000000..a7afcbd
--- /dev/null
+++ b/core/tests/utiltests/src/android/util/LongArrayTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2017 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 android.util;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class LongArrayTest {
+
+ @Test
+ public void testLongArray() {
+ LongArray a = new LongArray();
+ a.add(1);
+ a.add(2);
+ a.add(3);
+ verify(new long[]{1, 2, 3}, a);
+
+ LongArray b = LongArray.fromArray(new long[]{4, 5, 6, 7, 8}, 3);
+ a.addAll(b);
+ verify(new long[]{1, 2, 3, 4, 5, 6}, a);
+
+ a.resize(2);
+ verify(new long[]{1, 2}, a);
+
+ a.resize(8);
+ verify(new long[]{1, 2, 0, 0, 0, 0, 0, 0}, a);
+
+ a.set(5, 10);
+ verify(new long[]{1, 2, 0, 0, 0, 10, 0, 0}, a);
+
+ a.add(5, 20);
+ assertEquals(20, a.get(5));
+ assertEquals(5, a.indexOf(20));
+ verify(new long[]{1, 2, 0, 0, 0, 20, 10, 0, 0}, a);
+
+ assertEquals(-1, a.indexOf(99));
+
+ a.resize(15);
+ a.set(14, 30);
+ verify(new long[]{1, 2, 0, 0, 0, 20, 10, 0, 0, 0, 0, 0, 0, 0, 30}, a);
+
+ long[] backingArray = new long[]{1, 2, 3, 4};
+ a = LongArray.wrap(backingArray);
+ a.set(0, 10);
+ assertEquals(10, backingArray[0]);
+ backingArray[1] = 20;
+ backingArray[2] = 30;
+ verify(backingArray, a);
+ assertEquals(2, a.indexOf(30));
+
+ a.resize(2);
+ assertEquals(0, backingArray[2]);
+ assertEquals(0, backingArray[3]);
+
+ a.add(50);
+ verify(new long[]{10, 20, 50}, a);
+ }
+
+ public void verify(long[] expected, LongArray longArrays) {
+ assertEquals(expected.length, longArrays.size());
+ assertArrayEquals(expected, longArrays.toArray());
+ }
+}
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index c51f872..adbcfe8 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -429,6 +429,8 @@
<string name="mobile_data_always_on">Cellular data always active</string>
<!-- Setting Checkbox title for disabling Bluetooth absolute volume -->
<string name="bluetooth_disable_absolute_volume">Disable absolute volume</string>
+ <!-- Setting Checkbox title for enabling Bluetooth inband ringing -->
+ <string name="bluetooth_enable_inband_ringing">Enable in-band ringing</string>
<!-- UI debug setting: Select Bluetooth AVRCP Version -->
<string name="bluetooth_select_avrcp_version_string">Bluetooth AVRCP Version</string>
@@ -510,6 +512,9 @@
<string name="verify_apps_over_usb_summary">Check apps installed via ADB/ADT for harmful behavior.</string>
<!-- Summary of checkbox for disabling Bluetooth absolute volume -->
<string name="bluetooth_disable_absolute_volume_summary">Disables the Bluetooth absolute volume feature in case of volume issues with remote devices such as unacceptably loud volume or lack of control.</string>
+ <!-- Summary of checkbox for enabling Bluetooth inband ringing -->
+ <string name="bluetooth_enable_inband_ringing_summary">Allow ringtones on the phone to be played on Bluetooth headsets</string>
+
<!-- Title of checkbox setting that enables the terminal app. [CHAR LIMIT=32] -->
<string name="enable_terminal_title">Local terminal</string>
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 7031662..9dde3e2 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -2838,6 +2838,18 @@
}
}
+ @Override
+ public int getMultipathPreference(Network network) {
+ enforceAccessPermission();
+
+ NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+ if (nai != null && !nai.networkInfo.isMetered()) {
+ return ConnectivityManager.MULTIPATH_PREFERENCE_UNMETERED;
+ }
+
+ return mMultinetworkPolicyTracker.getMeteredMultipathPreference();
+ }
+
private class InternalHandler extends Handler {
public InternalHandler(Looper looper) {
super(looper);
diff --git a/services/core/java/com/android/server/connectivity/Tethering.java b/services/core/java/com/android/server/connectivity/Tethering.java
index 3bf55965..76c895c 100644
--- a/services/core/java/com/android/server/connectivity/Tethering.java
+++ b/services/core/java/com/android/server/connectivity/Tethering.java
@@ -90,7 +90,9 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
+import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
@@ -122,12 +124,23 @@
public final TetherInterfaceStateMachine stateMachine;
public int lastState;
public int lastError;
+
public TetherState(TetherInterfaceStateMachine sm) {
stateMachine = sm;
// Assume all state machines start out available and with no errors.
lastState = IControlsTethering.STATE_AVAILABLE;
lastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
}
+
+ public boolean isCurrentlyServing() {
+ switch (lastState) {
+ case IControlsTethering.STATE_TETHERED:
+ case IControlsTethering.STATE_LOCAL_HOTSPOT:
+ return true;
+ default:
+ return false;
+ }
+ }
}
// used to synchronize public access to members
@@ -143,11 +156,13 @@
private final StateMachine mTetherMasterSM;
private final OffloadController mOffloadController;
private final UpstreamNetworkMonitor mUpstreamNetworkMonitor;
+ private final HashSet<TetherInterfaceStateMachine> mForwardedDownstreams;
private volatile TetheringConfiguration mConfig;
private String mCurrentUpstreamIface;
private Notification.Builder mTetheredNotificationBuilder;
private int mLastNotificationId;
+
private boolean mRndisEnabled; // track the RNDIS function enabled state
private boolean mUsbTetherRequested; // true if USB tethering should be started
// when RNDIS is enabled
@@ -174,6 +189,7 @@
mOffloadController = new OffloadController(mTetherMasterSM.getHandler());
mUpstreamNetworkMonitor = new UpstreamNetworkMonitor(
mContext, mTetherMasterSM, TetherMasterSM.EVENT_UPSTREAM_CALLBACK);
+ mForwardedDownstreams = new HashSet<>();
mStateReceiver = new StateReceiver();
IntentFilter filter = new IntentFilter();
@@ -511,6 +527,10 @@
}
public int tether(String iface) {
+ return tether(iface, IControlsTethering.STATE_TETHERED);
+ }
+
+ private int tether(String iface, int requestedState) {
if (DBG) Log.d(TAG, "Tethering " + iface);
synchronized (mPublicSync) {
TetherState tetherState = mTetherStates.get(iface);
@@ -524,7 +544,13 @@
Log.e(TAG, "Tried to Tether an unavailable iface: " + iface + ", ignoring");
return ConnectivityManager.TETHER_ERROR_UNAVAIL_IFACE;
}
- tetherState.stateMachine.sendMessage(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
+ // NOTE: If a CMD_TETHER_REQUESTED message is already in the TISM's
+ // queue but not yet processed, this will be a no-op and it will not
+ // return an error.
+ //
+ // TODO: reexamine the threading and messaging model.
+ tetherState.stateMachine.sendMessage(
+ TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, requestedState);
return ConnectivityManager.TETHER_ERROR_NO_ERROR;
}
}
@@ -537,8 +563,8 @@
Log.e(TAG, "Tried to Untether an unknown iface :" + iface + ", ignoring");
return ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE;
}
- if (tetherState.lastState != IControlsTethering.STATE_TETHERED) {
- Log.e(TAG, "Tried to untether an untethered iface :" + iface + ", ignoring");
+ if (!tetherState.isCurrentlyServing()) {
+ Log.e(TAG, "Tried to untether an inactive iface :" + iface + ", ignoring");
return ConnectivityManager.TETHER_ERROR_UNAVAIL_IFACE;
}
tetherState.stateMachine.sendMessage(
@@ -565,6 +591,7 @@
}
}
+ // TODO: Figure out how to update for local hotspot mode interfaces.
private void sendTetherStateChangedBroadcast() {
if (!getConnectivityManager().isTetheringSupported()) return;
@@ -728,7 +755,9 @@
mRndisEnabled = rndisEnabled;
// start tethering if we have a request pending
if (usbConnected && mRndisEnabled && mUsbTetherRequested) {
- tetherMatchingInterfaces(true, ConnectivityManager.TETHERING_USB);
+ tetherMatchingInterfaces(
+ IControlsTethering.STATE_TETHERED,
+ ConnectivityManager.TETHERING_USB);
}
mUsbTetherRequested = false;
}
@@ -743,9 +772,11 @@
break;
case WifiManager.WIFI_AP_STATE_ENABLED:
// When the AP comes up and we've been requested to tether it, do so.
- if (mWifiTetherRequested) {
- tetherMatchingInterfaces(true, ConnectivityManager.TETHERING_WIFI);
- }
+ // Otherwise, assume it's a local-only hotspot request.
+ final int state = mWifiTetherRequested
+ ? IControlsTethering.STATE_TETHERED
+ : IControlsTethering.STATE_LOCAL_HOTSPOT;
+ tetherMatchingInterfaces(state, ConnectivityManager.TETHERING_WIFI);
break;
case WifiManager.WIFI_AP_STATE_DISABLED:
case WifiManager.WIFI_AP_STATE_DISABLING:
@@ -775,8 +806,16 @@
}
}
- private void tetherMatchingInterfaces(boolean enable, int interfaceType) {
- if (VDBG) Log.d(TAG, "tetherMatchingInterfaces(" + enable + ", " + interfaceType + ")");
+ // TODO: Consider renaming to something more accurate in its description.
+ // This method:
+ // - allows requesting either tethering or local hotspot serving states
+ // - handles both enabling and disabling serving states
+ // - only tethers the first matching interface in listInterfaces()
+ // order of a given type
+ private void tetherMatchingInterfaces(int requestedState, int interfaceType) {
+ if (VDBG) {
+ Log.d(TAG, "tetherMatchingInterfaces(" + requestedState + ", " + interfaceType + ")");
+ }
String[] ifaces = null;
try {
@@ -799,7 +838,20 @@
return;
}
- int result = (enable ? tether(chosenIface) : untether(chosenIface));
+ final int result;
+ switch (requestedState) {
+ case IControlsTethering.STATE_UNAVAILABLE:
+ case IControlsTethering.STATE_AVAILABLE:
+ result = untether(chosenIface);
+ break;
+ case IControlsTethering.STATE_TETHERED:
+ case IControlsTethering.STATE_LOCAL_HOTSPOT:
+ result = tether(chosenIface, requestedState);
+ break;
+ default:
+ Log.wtf(TAG, "Unknown interface state: " + requestedState);
+ return;
+ }
if (result != ConnectivityManager.TETHER_ERROR_NO_ERROR) {
Log.e(TAG, "unable start or stop tethering on iface " + chosenIface);
return;
@@ -844,7 +896,8 @@
if (mRndisEnabled) {
final long ident = Binder.clearCallingIdentity();
try {
- tetherMatchingInterfaces(true, ConnectivityManager.TETHERING_USB);
+ tetherMatchingInterfaces(IControlsTethering.STATE_TETHERED,
+ ConnectivityManager.TETHERING_USB);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -855,7 +908,8 @@
} else {
final long ident = Binder.clearCallingIdentity();
try {
- tetherMatchingInterfaces(false, ConnectivityManager.TETHERING_USB);
+ tetherMatchingInterfaces(IControlsTethering.STATE_AVAILABLE,
+ ConnectivityManager.TETHERING_USB);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -919,6 +973,14 @@
}
}
+ private boolean upstreamWanted() {
+ if (!mForwardedDownstreams.isEmpty()) return true;
+
+ synchronized (mPublicSync) {
+ return mUsbTetherRequested || mWifiTetherRequested;
+ }
+ }
+
// Needed because the canonical source of upstream truth is just the
// upstream interface name, |mCurrentUpstreamIface|. This is ripe for
// future simplification, once the upstream Network is canonical.
@@ -935,10 +997,10 @@
class TetherMasterSM extends StateMachine {
private static final int BASE_MASTER = Protocol.BASE_TETHERING;
- // an interface SM has requested Tethering
- static final int CMD_TETHER_MODE_REQUESTED = BASE_MASTER + 1;
- // an interface SM has unrequested Tethering
- static final int CMD_TETHER_MODE_UNREQUESTED = BASE_MASTER + 2;
+ // an interface SM has requested Tethering/Local Hotspot
+ static final int EVENT_IFACE_SERVING_STATE_ACTIVE = BASE_MASTER + 1;
+ // an interface SM has unrequested Tethering/Local Hotspot
+ static final int EVENT_IFACE_SERVING_STATE_INACTIVE = BASE_MASTER + 2;
// upstream connection change - do the right thing
static final int CMD_UPSTREAM_CHANGED = BASE_MASTER + 3;
// we don't have a valid upstream conn, check again after a delay
@@ -1023,7 +1085,9 @@
transitionTo(mSetIpForwardingEnabledErrorState);
return false;
}
+ // TODO: Randomize DHCPv4 ranges, especially in hotspot mode.
try {
+ // TODO: Find a more accurate method name (startDHCPv4()?).
mNMService.startTethering(cfg.dhcpRanges);
} catch (Exception e) {
try {
@@ -1325,26 +1389,41 @@
}
}
+ private void handleInterfaceServingStateActive(int mode, TetherInterfaceStateMachine who) {
+ if (mNotifyList.indexOf(who) < 0) {
+ mNotifyList.add(who);
+ mIPv6TetheringCoordinator.addActiveDownstream(who, mode);
+ }
+
+ if (mode == IControlsTethering.STATE_TETHERED) {
+ mForwardedDownstreams.add(who);
+ } else {
+ mForwardedDownstreams.remove(who);
+ }
+ }
+
+ private void handleInterfaceServingStateInactive(TetherInterfaceStateMachine who) {
+ mNotifyList.remove(who);
+ mIPv6TetheringCoordinator.removeActiveDownstream(who);
+ mForwardedDownstreams.remove(who);
+ }
+
class InitialState extends TetherMasterUtilState {
@Override
public boolean processMessage(Message message) {
maybeLogMessage(this, message.what);
boolean retValue = true;
switch (message.what) {
- case CMD_TETHER_MODE_REQUESTED:
+ case EVENT_IFACE_SERVING_STATE_ACTIVE:
TetherInterfaceStateMachine who = (TetherInterfaceStateMachine)message.obj;
if (VDBG) Log.d(TAG, "Tether Mode requested by " + who);
- if (mNotifyList.indexOf(who) < 0) {
- mNotifyList.add(who);
- mIPv6TetheringCoordinator.addActiveDownstream(who);
- }
+ handleInterfaceServingStateActive(message.arg1, who);
transitionTo(mTetherModeAliveState);
break;
- case CMD_TETHER_MODE_UNREQUESTED:
+ case EVENT_IFACE_SERVING_STATE_INACTIVE:
who = (TetherInterfaceStateMachine)message.obj;
if (VDBG) Log.d(TAG, "Tether Mode unrequested by " + who);
- mNotifyList.remove(who);
- mIPv6TetheringCoordinator.removeActiveDownstream(who);
+ handleInterfaceServingStateInactive(who);
break;
default:
retValue = false;
@@ -1356,6 +1435,7 @@
class TetherModeAliveState extends TetherMasterUtilState {
final SimChangeListener simChange = new SimChangeListener(mContext);
+ boolean mUpstreamWanted = false;
boolean mTryCell = true;
@Override
@@ -1366,9 +1446,11 @@
mUpstreamNetworkMonitor.start();
mOffloadController.start();
- // Better try something first pass or crazy tests cases will fail.
- chooseUpstreamType(true);
- mTryCell = false;
+ if (upstreamWanted()) {
+ mUpstreamWanted = true;
+ chooseUpstreamType(true);
+ mTryCell = false;
+ }
}
@Override
@@ -1381,54 +1463,74 @@
handleNewUpstreamNetworkState(null);
}
+ private boolean updateUpstreamWanted() {
+ final boolean previousUpstreamWanted = mUpstreamWanted;
+ mUpstreamWanted = upstreamWanted();
+ return previousUpstreamWanted;
+ }
+
@Override
public boolean processMessage(Message message) {
maybeLogMessage(this, message.what);
boolean retValue = true;
switch (message.what) {
- case CMD_TETHER_MODE_REQUESTED: {
+ case EVENT_IFACE_SERVING_STATE_ACTIVE: {
TetherInterfaceStateMachine who = (TetherInterfaceStateMachine)message.obj;
if (VDBG) Log.d(TAG, "Tether Mode requested by " + who);
- if (mNotifyList.indexOf(who) < 0) {
- mNotifyList.add(who);
- mIPv6TetheringCoordinator.addActiveDownstream(who);
- }
+ handleInterfaceServingStateActive(message.arg1, who);
who.sendMessage(TetherInterfaceStateMachine.CMD_TETHER_CONNECTION_CHANGED,
mCurrentUpstreamIface);
+ // If there has been a change and an upstream is now
+ // desired, kick off the selection process.
+ final boolean previousUpstreamWanted = updateUpstreamWanted();
+ if (!previousUpstreamWanted && mUpstreamWanted) {
+ chooseUpstreamType(true);
+ }
break;
}
- case CMD_TETHER_MODE_UNREQUESTED: {
+ case EVENT_IFACE_SERVING_STATE_INACTIVE: {
TetherInterfaceStateMachine who = (TetherInterfaceStateMachine)message.obj;
if (VDBG) Log.d(TAG, "Tether Mode unrequested by " + who);
- if (mNotifyList.remove(who)) {
- if (DBG) Log.d(TAG, "TetherModeAlive removing notifyee " + who);
- if (mNotifyList.isEmpty()) {
- turnOffMasterTetherSettings(); // transitions appropriately
- } else {
- if (DBG) {
- Log.d(TAG, "TetherModeAlive still has " + mNotifyList.size() +
- " live requests:");
- for (TetherInterfaceStateMachine o : mNotifyList) {
- Log.d(TAG, " " + o);
- }
+ handleInterfaceServingStateInactive(who);
+
+ if (mNotifyList.isEmpty()) {
+ turnOffMasterTetherSettings(); // transitions appropriately
+ } else {
+ if (DBG) {
+ Log.d(TAG, "TetherModeAlive still has " + mNotifyList.size() +
+ " live requests:");
+ for (TetherInterfaceStateMachine o : mNotifyList) {
+ Log.d(TAG, " " + o);
}
}
- } else {
- Log.e(TAG, "TetherModeAliveState UNREQUESTED has unknown who: " + who);
}
- mIPv6TetheringCoordinator.removeActiveDownstream(who);
+ // If there has been a change and an upstream is no
+ // longer desired, release any mobile requests.
+ final boolean previousUpstreamWanted = updateUpstreamWanted();
+ if (previousUpstreamWanted && !mUpstreamWanted) {
+ mUpstreamNetworkMonitor.releaseMobileNetworkRequest();
+ }
break;
}
case CMD_UPSTREAM_CHANGED:
+ updateUpstreamWanted();
+ if (!mUpstreamWanted) break;
+
// Need to try DUN immediately if Wi-Fi goes down.
chooseUpstreamType(true);
mTryCell = false;
break;
case CMD_RETRY_UPSTREAM:
+ updateUpstreamWanted();
+ if (!mUpstreamWanted) break;
+
chooseUpstreamType(mTryCell);
mTryCell = !mTryCell;
break;
case EVENT_UPSTREAM_CALLBACK: {
+ updateUpstreamWanted();
+ if (!mUpstreamWanted) break;
+
final NetworkState ns = (NetworkState) message.obj;
if (ns == null || !pertainsToCurrentUpstream(ns)) {
@@ -1490,7 +1592,7 @@
public boolean processMessage(Message message) {
boolean retValue = true;
switch (message.what) {
- case CMD_TETHER_MODE_REQUESTED:
+ case EVENT_IFACE_SERVING_STATE_ACTIVE:
TetherInterfaceStateMachine who = (TetherInterfaceStateMachine)message.obj;
who.sendMessage(mErrorNotification);
break;
@@ -1604,12 +1706,16 @@
case IControlsTethering.STATE_TETHERED:
pw.print("TetheredState");
break;
+ case IControlsTethering.STATE_LOCAL_HOTSPOT:
+ pw.print("LocalHotspotState");
+ break;
default:
pw.print("UnknownState");
break;
}
pw.println(" - lastError = " + tetherState.lastError);
}
+ pw.println("Upstream wanted: " + upstreamWanted());
pw.decreaseIndent();
}
pw.decreaseIndent();
@@ -1648,15 +1754,21 @@
if (error == ConnectivityManager.TETHER_ERROR_MASTER_ERROR) {
mTetherMasterSM.sendMessage(TetherMasterSM.CMD_CLEAR_ERROR, who);
}
+ int which;
switch (state) {
case IControlsTethering.STATE_UNAVAILABLE:
case IControlsTethering.STATE_AVAILABLE:
- mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_UNREQUESTED, who);
+ which = TetherMasterSM.EVENT_IFACE_SERVING_STATE_INACTIVE;
break;
case IControlsTethering.STATE_TETHERED:
- mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_REQUESTED, who);
+ case IControlsTethering.STATE_LOCAL_HOTSPOT:
+ which = TetherMasterSM.EVENT_IFACE_SERVING_STATE_ACTIVE;
break;
+ default:
+ Log.wtf(TAG, "Unknown interface state: " + state);
+ return;
}
+ mTetherMasterSM.sendMessage(which, state, 0, who);
sendTetherStateChangedBroadcast();
}
diff --git a/services/core/java/com/android/server/connectivity/tethering/IControlsTethering.java b/services/core/java/com/android/server/connectivity/tethering/IControlsTethering.java
index 449b8a8..f3914b7 100644
--- a/services/core/java/com/android/server/connectivity/tethering/IControlsTethering.java
+++ b/services/core/java/com/android/server/connectivity/tethering/IControlsTethering.java
@@ -25,6 +25,7 @@
public final int STATE_UNAVAILABLE = 0;
public final int STATE_AVAILABLE = 1;
public final int STATE_TETHERED = 2;
+ public final int STATE_LOCAL_HOTSPOT = 3;
/**
* Notify that |who| has changed its tethering state. This may be called from any thread.
diff --git a/services/core/java/com/android/server/connectivity/tethering/IPv6TetheringCoordinator.java b/services/core/java/com/android/server/connectivity/tethering/IPv6TetheringCoordinator.java
index 9173feb..5f496ca 100644
--- a/services/core/java/com/android/server/connectivity/tethering/IPv6TetheringCoordinator.java
+++ b/services/core/java/com/android/server/connectivity/tethering/IPv6TetheringCoordinator.java
@@ -24,12 +24,17 @@
import android.net.NetworkCapabilities;
import android.net.NetworkState;
import android.net.RouteInfo;
+import android.net.util.NetworkConstants;
import android.util.Log;
import java.net.Inet6Address;
import java.net.InetAddress;
+import java.net.UnknownHostException;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
import java.util.LinkedList;
+import java.util.Random;
/**
@@ -45,29 +50,65 @@
private static final boolean DBG = false;
private static final boolean VDBG = false;
+ private static class Downstream {
+ public final TetherInterfaceStateMachine tism;
+ public final int mode; // IControlsTethering.STATE_*
+ // Used to append to a ULA /48, constructing a ULA /64 for local use.
+ public final short subnetId;
+
+ Downstream(TetherInterfaceStateMachine tism, int mode, short subnetId) {
+ this.tism = tism;
+ this.mode = mode;
+ this.subnetId = subnetId;
+ }
+ }
+
private final ArrayList<TetherInterfaceStateMachine> mNotifyList;
- private final LinkedList<TetherInterfaceStateMachine> mActiveDownstreams;
+ // NOTE: mActiveDownstreams is a list and not a hash data structure because
+ // we keep active downstreams in arrival order. This is done so /64s can
+ // be parceled out on a "first come, first served" basis and a /64 used by
+ // a downstream that is no longer active can be redistributed to any next
+ // waiting active downstream (again, in arrival order).
+ private final LinkedList<Downstream> mActiveDownstreams;
+ private final byte[] mUniqueLocalPrefix;
+ private short mNextSubnetId;
private NetworkState mUpstreamNetworkState;
public IPv6TetheringCoordinator(ArrayList<TetherInterfaceStateMachine> notifyList) {
mNotifyList = notifyList;
mActiveDownstreams = new LinkedList<>();
+ mUniqueLocalPrefix = generateUniqueLocalPrefix();
+ mNextSubnetId = 0;
}
- public void addActiveDownstream(TetherInterfaceStateMachine downstream) {
- if (mActiveDownstreams.indexOf(downstream) == -1) {
+ public void addActiveDownstream(TetherInterfaceStateMachine downstream, int mode) {
+ if (findDownstream(downstream) == null) {
// Adding a new downstream appends it to the list. Adding a
// downstream a second time without first removing it has no effect.
- mActiveDownstreams.offer(downstream);
+ // We never change the mode of a downstream except by first removing
+ // it and then re-adding it (with its new mode specified);
+ if (mActiveDownstreams.offer(new Downstream(downstream, mode, mNextSubnetId))) {
+ // Make sure subnet IDs are always positive. They are appended
+ // to a ULA /48 to make a ULA /64 for local use.
+ mNextSubnetId = (short) Math.max(0, mNextSubnetId + 1);
+ }
updateIPv6TetheringInterfaces();
}
}
public void removeActiveDownstream(TetherInterfaceStateMachine downstream) {
stopIPv6TetheringOn(downstream);
- if (mActiveDownstreams.remove(downstream)) {
+ if (mActiveDownstreams.remove(findDownstream(downstream))) {
updateIPv6TetheringInterfaces();
}
+
+ // When tethering is stopping we can reset the subnet counter.
+ if (mNotifyList.isEmpty()) {
+ if (!mActiveDownstreams.isEmpty()) {
+ Log.wtf(TAG, "Tethering notify list empty, IPv6 downstreams non-empty.");
+ }
+ mNextSubnetId = 0;
+ }
}
public void updateUpstreamNetworkState(NetworkState ns) {
@@ -123,20 +164,31 @@
}
private LinkProperties getInterfaceIPv6LinkProperties(TetherInterfaceStateMachine sm) {
- if (mUpstreamNetworkState == null) return null;
-
if (sm.interfaceType() == ConnectivityManager.TETHERING_BLUETOOTH) {
// TODO: Figure out IPv6 support on PAN interfaces.
return null;
}
+ final Downstream ds = findDownstream(sm);
+ if (ds == null) return null;
+
+ if (ds.mode == IControlsTethering.STATE_LOCAL_HOTSPOT) {
+ // Build a Unique Locally-assigned Prefix configuration.
+ return getUniqueLocalConfig(mUniqueLocalPrefix, ds.subnetId);
+ }
+
+ // This downstream is in IControlsTethering.STATE_TETHERED mode.
+ if (mUpstreamNetworkState == null || mUpstreamNetworkState.linkProperties == null) {
+ return null;
+ }
+
// NOTE: Here, in future, we would have policies to decide how to divvy
// up the available dedicated prefixes among downstream interfaces.
// At this time we have no such mechanism--we only support tethering
// IPv6 toward the oldest (first requested) active downstream.
- final TetherInterfaceStateMachine currentActive = mActiveDownstreams.peek();
- if (currentActive != null && currentActive == sm) {
+ final Downstream currentActive = mActiveDownstreams.peek();
+ if (currentActive != null && currentActive.tism == sm) {
final LinkProperties lp = getIPv6OnlyLinkProperties(
mUpstreamNetworkState.linkProperties);
if (lp.hasIPv6DefaultRoute() && lp.hasGlobalIPv6Address()) {
@@ -147,6 +199,13 @@
return null;
}
+ Downstream findDownstream(TetherInterfaceStateMachine tism) {
+ for (Downstream ds : mActiveDownstreams) {
+ if (ds.tism == tism) return ds;
+ }
+ return null;
+ }
+
private static boolean canTetherIPv6(NetworkState ns) {
// Broadly speaking:
//
@@ -263,6 +322,44 @@
!ip.isMulticastAddress();
}
+ private static LinkProperties getUniqueLocalConfig(byte[] ulp, short subnetId) {
+ final LinkProperties lp = new LinkProperties();
+
+ final IpPrefix local48 = makeUniqueLocalPrefix(ulp, (short) 0, 48);
+ lp.addRoute(new RouteInfo(local48, null, null));
+
+ final IpPrefix local64 = makeUniqueLocalPrefix(ulp, subnetId, 64);
+ // Because this is a locally-generated ULA, we don't have an upstream
+ // address. But because the downstream IP address management code gets
+ // its prefix from the upstream's IP address, we create a fake one here.
+ lp.addLinkAddress(new LinkAddress(local64.getAddress(), 64));
+
+ lp.setMtu(NetworkConstants.ETHER_MTU);
+ return lp;
+ }
+
+ private static IpPrefix makeUniqueLocalPrefix(byte[] in6addr, short subnetId, int prefixlen) {
+ final byte[] bytes = Arrays.copyOf(in6addr, in6addr.length);
+ bytes[7] = (byte) (subnetId >> 8);
+ bytes[8] = (byte) subnetId;
+ return new IpPrefix(bytes, prefixlen);
+ }
+
+ // Generates a Unique Locally-assigned Prefix:
+ //
+ // https://tools.ietf.org/html/rfc4193#section-3.1
+ //
+ // The result is a /48 that can be used for local-only communications.
+ private static byte[] generateUniqueLocalPrefix() {
+ final byte[] ulp = new byte[6]; // 6 = 48bits / 8bits/byte
+ (new Random()).nextBytes(ulp);
+
+ final byte[] in6addr = Arrays.copyOf(ulp, NetworkConstants.IPV6_ADDR_LEN);
+ in6addr[0] = (byte) 0xfd; // fc00::/7 and L=1
+
+ return in6addr;
+ }
+
private static String toDebugString(NetworkState ns) {
if (ns == null) {
return "NetworkState{null}";
diff --git a/services/core/java/com/android/server/connectivity/tethering/IPv6TetheringInterfaceServices.java b/services/core/java/com/android/server/connectivity/tethering/IPv6TetheringInterfaceServices.java
index 8c6430c..c6a7925 100644
--- a/services/core/java/com/android/server/connectivity/tethering/IPv6TetheringInterfaceServices.java
+++ b/services/core/java/com/android/server/connectivity/tethering/IPv6TetheringInterfaceServices.java
@@ -42,6 +42,7 @@
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Objects;
+import java.util.Random;
/**
@@ -66,10 +67,17 @@
}
public boolean start() {
+ // TODO: Refactor for testability (perhaps passing an android.system.Os
+ // instance and calling getifaddrs() directly).
try {
mNetworkInterface = NetworkInterface.getByName(mIfName);
} catch (SocketException e) {
- Log.e(TAG, "Failed to find NetworkInterface for " + mIfName, e);
+ Log.e(TAG, "Error looking up NetworkInterfaces for " + mIfName, e);
+ stop();
+ return false;
+ }
+ if (mNetworkInterface == null) {
+ Log.e(TAG, "Failed to find NetworkInterface for " + mIfName);
stop();
return false;
}
@@ -267,10 +275,10 @@
return localRoutes;
}
- // Given a prefix like 2001:db8::/64 return 2001:db8::1.
+ // Given a prefix like 2001:db8::/64 return an address like 2001:db8::1.
private static Inet6Address getLocalDnsIpFor(IpPrefix localPrefix) {
final byte[] dnsBytes = localPrefix.getRawAddress();
- dnsBytes[dnsBytes.length - 1] = 0x1;
+ dnsBytes[dnsBytes.length - 1] = getRandomNonZeroByte();
try {
return Inet6Address.getByAddress(null, dnsBytes, 0);
} catch (UnknownHostException e) {
@@ -278,4 +286,11 @@
return null;
}
}
+
+ private static byte getRandomNonZeroByte() {
+ final byte random = (byte) (new Random()).nextInt();
+ // Don't pick the subnet-router anycast address, since that might be
+ // in use on the upstream already.
+ return (random != 0) ? random : 0x1;
+ }
}
diff --git a/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java b/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
index 710ab33..1ffa864 100644
--- a/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
+++ b/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
@@ -78,6 +78,8 @@
public static final int CMD_IPV6_TETHER_UPDATE = BASE_IFACE + 13;
private final State mInitialState;
+ private final State mServingState;
+ private final State mLocalHotspotState;
private final State mTetheredState;
private final State mUnavailableState;
@@ -105,10 +107,14 @@
mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
mInitialState = new InitialState();
- addState(mInitialState);
+ mServingState = new ServingState();
+ mLocalHotspotState = new LocalHotspotState();
mTetheredState = new TetheredState();
- addState(mTetheredState);
mUnavailableState = new UnavailableState();
+ addState(mInitialState);
+ addState(mServingState);
+ addState(mLocalHotspotState, mServingState);
+ addState(mTetheredState, mServingState);
addState(mUnavailableState);
setInitialState(mInitialState);
@@ -172,12 +178,15 @@
}
}
+ private void sendInterfaceState(int newInterfaceState) {
+ mTetherController.notifyInterfaceStateChange(
+ mIfaceName, TetherInterfaceStateMachine.this, newInterfaceState, mLastError);
+ }
+
class InitialState extends State {
@Override
public void enter() {
- mTetherController.notifyInterfaceStateChange(
- mIfaceName, TetherInterfaceStateMachine.this,
- IControlsTethering.STATE_AVAILABLE, mLastError);
+ sendInterfaceState(IControlsTethering.STATE_AVAILABLE);
}
@Override
@@ -187,7 +196,16 @@
switch (message.what) {
case CMD_TETHER_REQUESTED:
mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
- transitionTo(mTetheredState);
+ switch (message.arg1) {
+ case IControlsTethering.STATE_LOCAL_HOTSPOT:
+ transitionTo(mLocalHotspotState);
+ break;
+ case IControlsTethering.STATE_TETHERED:
+ transitionTo(mTetheredState);
+ break;
+ default:
+ Log.e(TAG, "Invalid tethering interface serving state specified.");
+ }
break;
case CMD_INTERFACE_DOWN:
transitionTo(mUnavailableState);
@@ -204,7 +222,7 @@
}
}
- class TetheredState extends State {
+ class ServingState extends State {
@Override
public void enter() {
if (!configureIfaceIp(true)) {
@@ -225,11 +243,6 @@
if (!mIPv6TetherSvc.start()) {
Log.e(TAG, "Failed to start IPv6TetheringInterfaceServices");
}
-
- if (DBG) Log.d(TAG, "Tethered " + mIfaceName);
- mTetherController.notifyInterfaceStateChange(
- mIfaceName, TetherInterfaceStateMachine.this,
- IControlsTethering.STATE_TETHERED, mLastError);
}
@Override
@@ -238,7 +251,6 @@
// of these operations, but it doesn't really change that we have to try them
// all in sequence.
mIPv6TetherSvc.stop();
- cleanupUpstream();
try {
mNMService.untetherInterface(mIfaceName);
@@ -250,6 +262,73 @@
configureIfaceIp(false);
}
+ @Override
+ public boolean processMessage(Message message) {
+ maybeLogMessage(this, message.what);
+ switch (message.what) {
+ case CMD_TETHER_UNREQUESTED:
+ transitionTo(mInitialState);
+ if (DBG) Log.d(TAG, "Untethered (unrequested)" + mIfaceName);
+ break;
+ case CMD_INTERFACE_DOWN:
+ transitionTo(mUnavailableState);
+ if (DBG) Log.d(TAG, "Untethered (ifdown)" + mIfaceName);
+ break;
+ case CMD_IPV6_TETHER_UPDATE:
+ mIPv6TetherSvc.updateUpstreamIPv6LinkProperties(
+ (LinkProperties) message.obj);
+ break;
+ case CMD_IP_FORWARDING_ENABLE_ERROR:
+ case CMD_IP_FORWARDING_DISABLE_ERROR:
+ case CMD_START_TETHERING_ERROR:
+ case CMD_STOP_TETHERING_ERROR:
+ case CMD_SET_DNS_FORWARDERS_ERROR:
+ mLastError = ConnectivityManager.TETHER_ERROR_MASTER_ERROR;
+ transitionTo(mInitialState);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+ }
+
+ class LocalHotspotState extends State {
+ @Override
+ public void enter() {
+ if (DBG) Log.d(TAG, "Local hotspot " + mIfaceName);
+ sendInterfaceState(IControlsTethering.STATE_LOCAL_HOTSPOT);
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ maybeLogMessage(this, message.what);
+ switch (message.what) {
+ case CMD_TETHER_REQUESTED:
+ Log.e(TAG, "CMD_TETHER_REQUESTED while in local hotspot mode.");
+ break;
+ case CMD_TETHER_CONNECTION_CHANGED:
+ // Ignored in local hotspot state.
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+ }
+
+ class TetheredState extends State {
+ @Override
+ public void enter() {
+ if (DBG) Log.d(TAG, "Tethered " + mIfaceName);
+ sendInterfaceState(IControlsTethering.STATE_TETHERED);
+ }
+
+ @Override
+ public void exit() {
+ cleanupUpstream();
+ }
+
private void cleanupUpstream() {
if (mMyUpstreamIfaceName == null) return;
@@ -285,13 +364,8 @@
maybeLogMessage(this, message.what);
boolean retValue = true;
switch (message.what) {
- case CMD_TETHER_UNREQUESTED:
- transitionTo(mInitialState);
- if (DBG) Log.d(TAG, "Untethered (unrequested)" + mIfaceName);
- break;
- case CMD_INTERFACE_DOWN:
- transitionTo(mUnavailableState);
- if (DBG) Log.d(TAG, "Untethered (ifdown)" + mIfaceName);
+ case CMD_TETHER_REQUESTED:
+ Log.e(TAG, "CMD_TETHER_REQUESTED while already tethering.");
break;
case CMD_TETHER_CONNECTION_CHANGED:
String newUpstreamIfaceName = (String)(message.obj);
@@ -317,18 +391,6 @@
}
mMyUpstreamIfaceName = newUpstreamIfaceName;
break;
- case CMD_IPV6_TETHER_UPDATE:
- mIPv6TetherSvc.updateUpstreamIPv6LinkProperties(
- (LinkProperties) message.obj);
- break;
- case CMD_IP_FORWARDING_ENABLE_ERROR:
- case CMD_IP_FORWARDING_DISABLE_ERROR:
- case CMD_START_TETHERING_ERROR:
- case CMD_STOP_TETHERING_ERROR:
- case CMD_SET_DNS_FORWARDERS_ERROR:
- mLastError = ConnectivityManager.TETHER_ERROR_MASTER_ERROR;
- transitionTo(mInitialState);
- break;
default:
retValue = false;
break;
@@ -348,9 +410,7 @@
@Override
public void enter() {
mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
- mTetherController.notifyInterfaceStateChange(
- mIfaceName, TetherInterfaceStateMachine.this,
- IControlsTethering.STATE_UNAVAILABLE, mLastError);
+ sendInterfaceState(IControlsTethering.STATE_UNAVAILABLE);
}
}
}
diff --git a/services/core/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java b/services/core/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java
index 6209929..97a2d5e 100644
--- a/services/core/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java
+++ b/services/core/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java
@@ -308,7 +308,8 @@
// Fetch (and cache) a ConnectivityManager only if and when we need one.
private ConnectivityManager cm() {
if (mCM == null) {
- mCM = mContext.getSystemService(ConnectivityManager.class);
+ // MUST call the String variant to be able to write unittests.
+ mCM = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
}
return mCM;
}
diff --git a/services/net/java/android/net/ip/RouterAdvertisementDaemon.java b/services/net/java/android/net/ip/RouterAdvertisementDaemon.java
index 6802cff..25d3329 100644
--- a/services/net/java/android/net/ip/RouterAdvertisementDaemon.java
+++ b/services/net/java/android/net/ip/RouterAdvertisementDaemon.java
@@ -16,6 +16,8 @@
package android.net.ip;
+import static android.net.util.NetworkConstants.IPV6_MIN_MTU;
+import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH;
import static android.system.OsConstants.*;
import android.net.IpPrefix;
@@ -68,7 +70,6 @@
private static final String TAG = RouterAdvertisementDaemon.class.getSimpleName();
private static final byte ICMPV6_ND_ROUTER_SOLICIT = asByte(133);
private static final byte ICMPV6_ND_ROUTER_ADVERT = asByte(134);
- private static final int IPV6_MIN_MTU = 1280;
private static final int MIN_RA_HEADER_SIZE = 16;
// Summary of various timers and lifetimes.
@@ -542,6 +543,14 @@
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
+ final HashSet<Inet6Address> filteredDnses = new HashSet<>();
+ for (Inet6Address dns : dnses) {
+ if ((new LinkAddress(dns, RFC7421_PREFIX_LENGTH)).isGlobalPreferred()) {
+ filteredDnses.add(dns);
+ }
+ }
+ if (filteredDnses.isEmpty()) return;
+
final byte ND_OPTION_RDNSS = 25;
final byte RDNSS_NUM_8OCTETS = asByte(dnses.size() * 2 + 1);
ra.put(ND_OPTION_RDNSS)
@@ -549,7 +558,7 @@
.putShort(asShort(0))
.putInt(lifetime);
- for (Inet6Address dns : dnses) {
+ for (Inet6Address dns : filteredDnses) {
// NOTE: If the full of list DNS servers doesn't fit in the packet,
// this code will cause a buffer overflow and the RA won't include
// this instance of the option at all.
diff --git a/services/net/java/android/net/util/MultinetworkPolicyTracker.java b/services/net/java/android/net/util/MultinetworkPolicyTracker.java
index ebd131b..424e40d 100644
--- a/services/net/java/android/net/util/MultinetworkPolicyTracker.java
+++ b/services/net/java/android/net/util/MultinetworkPolicyTracker.java
@@ -22,6 +22,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.database.ContentObserver;
+import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
@@ -29,10 +30,14 @@
import android.provider.Settings;
import android.util.Slog;
+import java.util.Arrays;
+import java.util.List;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.R;
import static android.provider.Settings.Global.NETWORK_AVOID_BAD_WIFI;
+import static android.provider.Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE;
/**
* A class to encapsulate management of the "Smart Networking" capability of
@@ -57,12 +62,13 @@
private final Context mContext;
private final Handler mHandler;
private final Runnable mReevaluateRunnable;
- private final Uri mAvoidBadWifiUri;
+ private final List<Uri> mSettingsUris;
private final ContentResolver mResolver;
private final SettingObserver mSettingObserver;
private final BroadcastReceiver mBroadcastReceiver;
private volatile boolean mAvoidBadWifi = true;
+ private volatile int mMeteredMultipathPreference;
public MultinetworkPolicyTracker(Context ctx, Handler handler) {
this(ctx, handler, null);
@@ -72,9 +78,14 @@
mContext = ctx;
mHandler = handler;
mReevaluateRunnable = () -> {
- if (updateAvoidBadWifi() && avoidBadWifiCallback != null) avoidBadWifiCallback.run();
+ if (updateAvoidBadWifi() && avoidBadWifiCallback != null) {
+ avoidBadWifiCallback.run();
+ }
+ updateMeteredMultipathPreference();
};
- mAvoidBadWifiUri = Settings.Global.getUriFor(NETWORK_AVOID_BAD_WIFI);
+ mSettingsUris = Arrays.asList(
+ Settings.Global.getUriFor(NETWORK_AVOID_BAD_WIFI),
+ Settings.Global.getUriFor(NETWORK_METERED_MULTIPATH_PREFERENCE));
mResolver = mContext.getContentResolver();
mSettingObserver = new SettingObserver();
mBroadcastReceiver = new BroadcastReceiver() {
@@ -85,10 +96,13 @@
};
updateAvoidBadWifi();
+ updateMeteredMultipathPreference();
}
public void start() {
- mResolver.registerContentObserver(mAvoidBadWifiUri, false, mSettingObserver);
+ for (Uri uri : mSettingsUris) {
+ mResolver.registerContentObserver(uri, false, mSettingObserver);
+ }
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
@@ -108,6 +122,10 @@
return mAvoidBadWifi;
}
+ public int getMeteredMultipathPreference() {
+ return mMeteredMultipathPreference;
+ }
+
/**
* Whether the device or carrier configuration disables avoiding bad wifi by default.
*/
@@ -138,6 +156,23 @@
return mAvoidBadWifi != prev;
}
+ /**
+ * The default (device and carrier-dependent) value for metered multipath preference.
+ */
+ public int configMeteredMultipathPreference() {
+ return mContext.getResources().getInteger(
+ R.integer.config_networkMeteredMultipathPreference);
+ }
+
+ public void updateMeteredMultipathPreference() {
+ String setting = Settings.Global.getString(mResolver, NETWORK_METERED_MULTIPATH_PREFERENCE);
+ try {
+ mMeteredMultipathPreference = Integer.parseInt(setting);
+ } catch (NumberFormatException e) {
+ mMeteredMultipathPreference = configMeteredMultipathPreference();
+ }
+ }
+
private class SettingObserver extends ContentObserver {
public SettingObserver() {
super(null);
@@ -150,7 +185,9 @@
@Override
public void onChange(boolean selfChange, Uri uri) {
- if (!mAvoidBadWifiUri.equals(uri)) return;
+ if (!mSettingsUris.contains(uri)) {
+ Slog.wtf(TAG, "Unexpected settings observation: " + uri);
+ }
reevaluate();
}
}
diff --git a/services/net/java/android/net/util/NetworkConstants.java b/services/net/java/android/net/util/NetworkConstants.java
index 26f3050..a012e0c 100644
--- a/services/net/java/android/net/util/NetworkConstants.java
+++ b/services/net/java/android/net/util/NetworkConstants.java
@@ -36,6 +36,7 @@
*
* See also:
* - https://tools.ietf.org/html/rfc894
+ * - https://tools.ietf.org/html/rfc2464
* - https://tools.ietf.org/html/rfc7042
* - http://www.iana.org/assignments/ethernet-numbers/ethernet-numbers.xhtml
* - http://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml
@@ -57,6 +58,8 @@
FF, FF, FF, FF, FF, FF
};
+ public static final int ETHER_MTU = 1500;
+
/**
* ARP constants.
*
@@ -97,6 +100,7 @@
public static final int IPV6_SRC_ADDR_OFFSET = 8;
public static final int IPV6_DST_ADDR_OFFSET = 24;
public static final int IPV6_ADDR_LEN = 16;
+ public static final int IPV6_MIN_MTU = 1280;
public static final int RFC7421_PREFIX_LENGTH = 64;
/**
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index 5093a61..94c3dfe 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -755,6 +755,32 @@
}
/**
+ * Returns a list of {@link PhoneAccountHandle}s for self-managed {@link ConnectionService}s.
+ * <p>
+ * Self-Managed {@link ConnectionService}s have a {@link PhoneAccount} with
+ * {@link PhoneAccount#CAPABILITY_SELF_MANAGED}.
+ * <p>
+ * Requires permission {@link android.Manifest.permission#READ_PHONE_STATE}, or that the caller
+ * is the default dialer app.
+ * <p>
+ * A {@link SecurityException} will be thrown if a called is not the default dialer, or lacks
+ * the {@link android.Manifest.permission#READ_PHONE_STATE} permission.
+ *
+ * @return A list of {@code PhoneAccountHandle} objects.
+ */
+ @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
+ public List<PhoneAccountHandle> getSelfManagedPhoneAccounts() {
+ try {
+ if (isServiceConnected()) {
+ return getTelecomService().getSelfManagedPhoneAccounts(mContext.getOpPackageName());
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling ITelecomService#getSelfManagedPhoneAccounts()", e);
+ }
+ return new ArrayList<>();
+ }
+
+ /**
* Returns a list of {@link PhoneAccountHandle}s including those which have not been enabled
* by the user.
*
diff --git a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
index c044742..86f7d7d 100644
--- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
+++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
@@ -59,6 +59,11 @@
boolean includeDisabledAccounts, String callingPackage);
/**
+ * @see TelecomServiceImpl#getSelfManagedPhoneAccounts
+ */
+ List<PhoneAccountHandle> getSelfManagedPhoneAccounts(String callingPackage);
+
+ /**
* @see TelecomManager#getPhoneAccountsSupportingScheme
*/
List<PhoneAccountHandle> getPhoneAccountsSupportingScheme(in String uriScheme,
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index 1cf0d53..2f5c97d 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -632,6 +632,7 @@
private class WrappedMultinetworkPolicyTracker extends MultinetworkPolicyTracker {
public volatile boolean configRestrictsAvoidBadWifi;
+ public volatile int configMeteredMultipathPreference;
public WrappedMultinetworkPolicyTracker(Context c, Handler h, Runnable r) {
super(c, h, r);
@@ -641,6 +642,11 @@
public boolean configRestrictsAvoidBadWifi() {
return configRestrictsAvoidBadWifi;
}
+
+ @Override
+ public int configMeteredMultipathPreference() {
+ return configMeteredMultipathPreference;
+ }
}
private class WrappedConnectivityService extends ConnectivityService {
@@ -2454,6 +2460,26 @@
mCm.unregisterNetworkCallback(defaultCallback);
}
+ @SmallTest
+ public void testMeteredMultipathPreferenceSetting() throws Exception {
+ final ContentResolver cr = mServiceContext.getContentResolver();
+ final WrappedMultinetworkPolicyTracker tracker = mService.getMultinetworkPolicyTracker();
+ final String settingName = Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE;
+
+ for (int config : Arrays.asList(0, 3, 2)) {
+ for (String setting: Arrays.asList(null, "0", "2", "1")) {
+ tracker.configMeteredMultipathPreference = config;
+ Settings.Global.putString(cr, settingName, setting);
+ tracker.reevaluate();
+ mService.waitForIdle();
+
+ final int expected = (setting != null) ? Integer.parseInt(setting) : config;
+ String msg = String.format("config=%d, setting=%s", config, setting);
+ assertEquals(msg, expected, mCm.getMultipathPreference(null));
+ }
+ }
+ }
+
/**
* Validate that a satisfied network request does not trigger onUnavailable() once the
* time-out period expires.
diff --git a/tests/net/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java b/tests/net/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
index 011e505..8c16dbb 100644
--- a/tests/net/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
+++ b/tests/net/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
@@ -58,8 +58,6 @@
String want = joinLines(
"dropped_events: 0",
"events <",
- " time_ms: 1",
- " transport: 0",
" default_network_event <",
" network_id <",
" network_id: 102",
@@ -72,6 +70,8 @@
" transport_types: 2",
" transport_types: 3",
" >",
+ " time_ms: 1",
+ " transport: 0",
">",
"version: 2");
@@ -89,13 +89,14 @@
String want = joinLines(
"dropped_events: 0",
"events <",
- " time_ms: 1",
- " transport: 0",
" dhcp_event <",
" duration_ms: 192",
+ " error_code: 0",
" if_name: \"wlan0\"",
" state_transition: \"SomeState\"",
" >",
+ " time_ms: 1",
+ " transport: 0",
">",
"version: 2");
@@ -112,13 +113,14 @@
String want = joinLines(
"dropped_events: 0",
"events <",
- " time_ms: 1",
- " transport: 0",
" dhcp_event <",
" duration_ms: 0",
- " if_name: \"wlan0\"",
" error_code: 50397184",
+ " if_name: \"wlan0\"",
+ " state_transition: \"\"",
" >",
+ " time_ms: 1",
+ " transport: 0",
">",
"version: 2");
@@ -137,8 +139,6 @@
String want = joinLines(
"dropped_events: 0",
"events <",
- " time_ms: 1",
- " transport: 0",
" dns_lookup_batch <",
" event_types: 1",
" event_types: 1",
@@ -168,6 +168,8 @@
" return_codes: 200",
" return_codes: 178",
" >",
+ " time_ms: 1",
+ " transport: 0",
">",
"version: 2");
@@ -185,13 +187,13 @@
String want = joinLines(
"dropped_events: 0",
"events <",
- " time_ms: 1",
- " transport: 0",
" ip_provisioning_event <",
" event_type: 1",
" if_name: \"wlan0\"",
" latency_ms: 5678",
" >",
+ " time_ms: 1",
+ " transport: 0",
">",
"version: 2");
@@ -208,12 +210,12 @@
String want = joinLines(
"dropped_events: 0",
"events <",
- " time_ms: 1",
- " transport: 0",
" ip_reachability_event <",
" event_type: 512",
" if_name: \"wlan0\"",
" >",
+ " time_ms: 1",
+ " transport: 0",
">",
"version: 2");
@@ -231,8 +233,6 @@
String want = joinLines(
"dropped_events: 0",
"events <",
- " time_ms: 1",
- " transport: 0",
" network_event <",
" event_type: 5",
" latency_ms: 20410",
@@ -240,6 +240,8 @@
" network_id: 100",
" >",
" >",
+ " time_ms: 1",
+ " transport: 0",
">",
"version: 2");
@@ -287,8 +289,6 @@
String want = joinLines(
"dropped_events: 0",
"events <",
- " time_ms: 1",
- " transport: 0",
" apf_program_event <",
" current_ras: 9",
" drop_multicast: true",
@@ -297,6 +297,8 @@
" lifetime: 200",
" program_length: 2048",
" >",
+ " time_ms: 1",
+ " transport: 0",
">",
"version: 2");
@@ -319,8 +321,6 @@
String want = joinLines(
"dropped_events: 0",
"events <",
- " time_ms: 1",
- " transport: 0",
" apf_statistics <",
" dropped_ras: 2",
" duration_ms: 45000",
@@ -331,6 +331,8 @@
" received_ras: 10",
" zero_lifetime_ras: 1",
" >",
+ " time_ms: 1",
+ " transport: 0",
">",
"version: 2");
@@ -351,8 +353,6 @@
String want = joinLines(
"dropped_events: 0",
"events <",
- " time_ms: 1",
- " transport: 0",
" ra_event <",
" dnssl_lifetime: -1",
" prefix_preferred_lifetime: 300",
@@ -361,6 +361,8 @@
" route_info_lifetime: -1",
" router_lifetime: 2000",
" >",
+ " time_ms: 1",
+ " transport: 0",
">",
"version: 2");
diff --git a/tests/net/java/com/android/server/connectivity/IpConnectivityMetricsTest.java b/tests/net/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
index 450653c..9a33cde 100644
--- a/tests/net/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
+++ b/tests/net/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
@@ -158,25 +158,24 @@
String want = joinLines(
"dropped_events: 0",
"events <",
- " time_ms: 100",
- " transport: 0",
" ip_reachability_event <",
" event_type: 512",
" if_name: \"wlan0\"",
" >",
+ " time_ms: 100",
+ " transport: 0",
">",
"events <",
- " time_ms: 200",
- " transport: 0",
" dhcp_event <",
" duration_ms: 192",
+ " error_code: 0",
" if_name: \"wlan0\"",
" state_transition: \"SomeState\"",
" >",
+ " time_ms: 200",
+ " transport: 0",
">",
"events <",
- " time_ms: 300",
- " transport: 0",
" default_network_event <",
" network_id <",
" network_id: 102",
@@ -189,15 +188,17 @@
" transport_types: 2",
" transport_types: 3",
" >",
+ " time_ms: 300",
+ " transport: 0",
">",
"events <",
- " time_ms: 400",
- " transport: 0",
" ip_provisioning_event <",
" event_type: 1",
" if_name: \"wlan0\"",
" latency_ms: 5678",
" >",
+ " time_ms: 400",
+ " transport: 0",
">",
"events <",
" time_ms: 500",
@@ -212,8 +213,6 @@
" >",
">",
"events <",
- " time_ms: 600",
- " transport: 0",
" apf_statistics <",
" dropped_ras: 2",
" duration_ms: 45000",
@@ -224,10 +223,10 @@
" received_ras: 10",
" zero_lifetime_ras: 1",
" >",
+ " time_ms: 600",
+ " transport: 0",
">",
"events <",
- " time_ms: 700",
- " transport: 0",
" ra_event <",
" dnssl_lifetime: -1",
" prefix_preferred_lifetime: 300",
@@ -236,6 +235,8 @@
" route_info_lifetime: -1",
" router_lifetime: 2000",
" >",
+ " time_ms: 700",
+ " transport: 0",
">",
"version: 2");
diff --git a/tests/net/java/com/android/server/connectivity/NetdEventListenerServiceTest.java b/tests/net/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
index 97afa60..6c8babb 100644
--- a/tests/net/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
+++ b/tests/net/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
@@ -212,8 +212,6 @@
IpConnectivityEvent got = events.get(0);
String want = joinLines(
- "time_ms: 0",
- "transport: 0",
"connect_statistics <",
" connect_count: 12",
" errnos_counters <",
@@ -247,7 +245,10 @@
" latencies_ms: 67",
" latencies_ms: 110",
" latencies_ms: 214",
- " latencies_ms: 523");
+ " latencies_ms: 523",
+ ">",
+ "time_ms: 0",
+ "transport: 0");
verifyConnectEvent(want, got);
}
diff --git a/tests/net/java/com/android/server/connectivity/TetheringTest.java b/tests/net/java/com/android/server/connectivity/TetheringTest.java
index a9f68c8..e527d57 100644
--- a/tests/net/java/com/android/server/connectivity/TetheringTest.java
+++ b/tests/net/java/com/android/server/connectivity/TetheringTest.java
@@ -16,22 +16,43 @@
package com.android.server.connectivity;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
import android.content.res.Resources;
+import android.hardware.usb.UsbManager;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
import android.net.INetworkPolicyManager;
import android.net.INetworkStatsService;
+import android.net.InterfaceConfiguration;
+import android.net.NetworkRequest;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+import android.os.Handler;
import android.os.INetworkManagementService;
import android.os.PersistableBundle;
import android.os.test.TestLooper;
+import android.os.UserHandle;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.telephony.CarrierConfigManager;
+import com.android.internal.util.test.BroadcastInterceptingContext;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -44,34 +65,60 @@
private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
@Mock private Context mContext;
+ @Mock private ConnectivityManager mConnectivityManager;
@Mock private INetworkManagementService mNMService;
@Mock private INetworkStatsService mStatsService;
@Mock private INetworkPolicyManager mPolicyManager;
@Mock private MockableSystemProperties mSystemProperties;
@Mock private Resources mResources;
+ @Mock private UsbManager mUsbManager;
+ @Mock private WifiManager mWifiManager;
@Mock private CarrierConfigManager mCarrierConfigManager;
// Like so many Android system APIs, these cannot be mocked because it is marked final.
// We have to use the real versions.
private final PersistableBundle mCarrierConfig = new PersistableBundle();
private final TestLooper mLooper = new TestLooper();
+ private final String mTestIfname = "test_wlan0";
+ private BroadcastInterceptingContext mServiceContext;
private Tethering mTethering;
+ private class MockContext extends BroadcastInterceptingContext {
+ MockContext(Context base) {
+ super(base);
+ }
+
+ @Override
+ public Resources getResources() { return mResources; }
+
+ @Override
+ public Object getSystemService(String name) {
+ if (Context.CONNECTIVITY_SERVICE.equals(name)) return mConnectivityManager;
+ if (Context.WIFI_SERVICE.equals(name)) return mWifiManager;
+ return super.getSystemService(name);
+ }
+ }
+
@Before public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- when(mContext.getResources()).thenReturn(mResources);
when(mResources.getStringArray(com.android.internal.R.array.config_tether_dhcp_range))
.thenReturn(new String[0]);
when(mResources.getStringArray(com.android.internal.R.array.config_tether_usb_regexs))
.thenReturn(new String[0]);
when(mResources.getStringArray(com.android.internal.R.array.config_tether_wifi_regexs))
- .thenReturn(new String[0]);
+ .thenReturn(new String[]{ "test_wlan\\d" });
when(mResources.getStringArray(com.android.internal.R.array.config_tether_bluetooth_regexs))
.thenReturn(new String[0]);
when(mResources.getIntArray(com.android.internal.R.array.config_tether_upstream_types))
.thenReturn(new int[0]);
- mTethering = new Tethering(mContext, mNMService, mStatsService, mPolicyManager,
+ when(mNMService.listInterfaces())
+ .thenReturn(new String[]{ "test_rmnet_data0", mTestIfname });
+ when(mNMService.getInterfaceConfig(anyString()))
+ .thenReturn(new InterfaceConfiguration());
+
+ mServiceContext = new MockContext(mContext);
+ mTethering = new Tethering(mServiceContext, mNMService, mStatsService, mPolicyManager,
mLooper.getLooper(), mSystemProperties);
}
@@ -126,4 +173,144 @@
.thenReturn(new String[] {"malformedApp"});
assertTrue(!mTethering.isTetherProvisioningRequired());
}
+
+ private void sendWifiApStateChanged(int state) {
+ final Intent intent = new Intent(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
+ intent.putExtra(WifiManager.EXTRA_WIFI_AP_STATE, state);
+ mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ }
+
+ @Test
+ public void workingLocalOnlyHotspot() throws Exception {
+ when(mConnectivityManager.isTetheringSupported()).thenReturn(true);
+ when(mWifiManager.setWifiApEnabled(any(WifiConfiguration.class), anyBoolean()))
+ .thenReturn(true);
+
+ // Emulate externally-visible WifiManager effects, causing the
+ // per-interface state machine to start up, and telling us that
+ // hotspot mode is to be started.
+ mTethering.interfaceStatusChanged(mTestIfname, true);
+ sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_ENABLED);
+ mLooper.dispatchAll();
+
+ verify(mNMService, times(1)).listInterfaces();
+ verify(mNMService, times(1)).getInterfaceConfig(mTestIfname);
+ verify(mNMService, times(1))
+ .setInterfaceConfig(eq(mTestIfname), any(InterfaceConfiguration.class));
+ verify(mNMService, times(1)).tetherInterface(mTestIfname);
+ verify(mNMService, times(1)).setIpForwardingEnabled(true);
+ verify(mNMService, times(1)).startTethering(any(String[].class));
+ verifyNoMoreInteractions(mNMService);
+ // UpstreamNetworkMonitor will be started, and will register two callbacks:
+ // a "listen all" and a "track default".
+ verify(mConnectivityManager, times(1)).registerNetworkCallback(
+ any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class));
+ verify(mConnectivityManager, times(1)).registerDefaultNetworkCallback(
+ any(NetworkCallback.class), any(Handler.class));
+ // TODO: Figure out why this isn't exactly once, for sendTetherStateChangedBroadcast().
+ verify(mConnectivityManager, atLeastOnce()).isTetheringSupported();
+ verifyNoMoreInteractions(mConnectivityManager);
+
+ // Emulate externally-visible WifiManager effects, when hotspot mode
+ // is being torn down.
+ sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+ mTethering.interfaceRemoved(mTestIfname);
+ mLooper.dispatchAll();
+
+ verify(mNMService, times(1)).untetherInterface(mTestIfname);
+ // TODO: Why is {g,s}etInterfaceConfig() called more than once?
+ verify(mNMService, atLeastOnce()).getInterfaceConfig(mTestIfname);
+ verify(mNMService, atLeastOnce())
+ .setInterfaceConfig(eq(mTestIfname), any(InterfaceConfiguration.class));
+ verify(mNMService, times(1)).stopTethering();
+ verify(mNMService, times(1)).setIpForwardingEnabled(false);
+ verifyNoMoreInteractions(mNMService);
+ // Asking for the last error after the per-interface state machine
+ // has been reaped yields an unknown interface error.
+ assertEquals(ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE,
+ mTethering.getLastTetherError(mTestIfname));
+ }
+
+ @Test
+ public void workingWifiTethering() throws Exception {
+ when(mConnectivityManager.isTetheringSupported()).thenReturn(true);
+ when(mWifiManager.setWifiApEnabled(any(WifiConfiguration.class), anyBoolean()))
+ .thenReturn(true);
+
+ // Emulate pressing the WiFi tethering button.
+ mTethering.startTethering(ConnectivityManager.TETHERING_WIFI, null, false);
+ mLooper.dispatchAll();
+ verify(mWifiManager, times(1)).setWifiApEnabled(null, true);
+ verifyNoMoreInteractions(mWifiManager);
+ verifyNoMoreInteractions(mConnectivityManager);
+ verifyNoMoreInteractions(mNMService);
+
+ // Emulate externally-visible WifiManager effects, causing the
+ // per-interface state machine to start up, and telling us that
+ // tethering mode is to be started.
+ mTethering.interfaceStatusChanged(mTestIfname, true);
+ sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_ENABLED);
+ mLooper.dispatchAll();
+
+ verify(mNMService, times(1)).listInterfaces();
+ verify(mNMService, times(1)).getInterfaceConfig(mTestIfname);
+ verify(mNMService, times(1))
+ .setInterfaceConfig(eq(mTestIfname), any(InterfaceConfiguration.class));
+ verify(mNMService, times(1)).tetherInterface(mTestIfname);
+ verify(mNMService, times(1)).setIpForwardingEnabled(true);
+ verify(mNMService, times(1)).startTethering(any(String[].class));
+ verifyNoMoreInteractions(mNMService);
+ // UpstreamNetworkMonitor will be started, and will register two callbacks:
+ // a "listen all" and a "track default".
+ verify(mConnectivityManager, times(1)).registerNetworkCallback(
+ any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class));
+ verify(mConnectivityManager, times(1)).registerDefaultNetworkCallback(
+ any(NetworkCallback.class), any(Handler.class));
+ // In tethering mode, in the default configuration, an explicit request
+ // for a mobile network is also made.
+ verify(mConnectivityManager, atLeastOnce()).getNetworkInfo(anyInt());
+ verify(mConnectivityManager, times(1)).requestNetwork(
+ any(NetworkRequest.class), any(NetworkCallback.class), eq(0), anyInt(),
+ any(Handler.class));
+ // TODO: Figure out why this isn't exactly once, for sendTetherStateChangedBroadcast().
+ verify(mConnectivityManager, atLeastOnce()).isTetheringSupported();
+ verifyNoMoreInteractions(mConnectivityManager);
+
+ /////
+ // We do not currently emulate any upstream being found.
+ //
+ // This is why there are no calls to verify mNMService.enableNat() or
+ // mNMService.startInterfaceForwarding().
+ /////
+
+ // Emulate pressing the WiFi tethering button.
+ mTethering.stopTethering(ConnectivityManager.TETHERING_WIFI);
+ mLooper.dispatchAll();
+ verify(mWifiManager, times(1)).setWifiApEnabled(null, false);
+ verifyNoMoreInteractions(mWifiManager);
+ verifyNoMoreInteractions(mConnectivityManager);
+ verifyNoMoreInteractions(mNMService);
+
+ // Emulate externally-visible WifiManager effects, when tethering mode
+ // is being torn down.
+ sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+ mTethering.interfaceRemoved(mTestIfname);
+ mLooper.dispatchAll();
+
+ verify(mNMService, times(1)).untetherInterface(mTestIfname);
+ // TODO: Why is {g,s}etInterfaceConfig() called more than once?
+ verify(mNMService, atLeastOnce()).getInterfaceConfig(mTestIfname);
+ verify(mNMService, atLeastOnce())
+ .setInterfaceConfig(eq(mTestIfname), any(InterfaceConfiguration.class));
+ verify(mNMService, times(1)).stopTethering();
+ verify(mNMService, times(1)).setIpForwardingEnabled(false);
+ verifyNoMoreInteractions(mNMService);
+ // Asking for the last error after the per-interface state machine
+ // has been reaped yields an unknown interface error.
+ assertEquals(ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE,
+ mTethering.getLastTetherError(mTestIfname));
+ }
+
+ // TODO: Test that a request for hotspot mode doesn't interface with an
+ // already operating tethering mode interface.
}
diff --git a/tests/net/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachineTest.java b/tests/net/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachineTest.java
index 32e1b96..caf1a55 100644
--- a/tests/net/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachineTest.java
+++ b/tests/net/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachineTest.java
@@ -32,6 +32,7 @@
import static android.net.ConnectivityManager.TETHERING_USB;
import static android.net.ConnectivityManager.TETHERING_WIFI;
import static com.android.server.connectivity.tethering.IControlsTethering.STATE_AVAILABLE;
+import static com.android.server.connectivity.tethering.IControlsTethering.STATE_LOCAL_HOTSPOT;
import static com.android.server.connectivity.tethering.IControlsTethering.STATE_TETHERED;
import static com.android.server.connectivity.tethering.IControlsTethering.STATE_UNAVAILABLE;
@@ -80,7 +81,7 @@
private void initTetheredStateMachine(int interfaceType, String upstreamIface) throws Exception {
initStateMachine(interfaceType);
- dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
+ dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, STATE_TETHERED);
if (upstreamIface != null) {
dispatchTetherConnectionChanged(upstreamIface);
}
@@ -138,7 +139,7 @@
public void canBeTethered() throws Exception {
initStateMachine(TETHERING_BLUETOOTH);
- dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
+ dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, STATE_TETHERED);
InOrder inOrder = inOrder(mTetherHelper, mNMService);
inOrder.verify(mNMService).tetherInterface(IFACE_NAME);
inOrder.verify(mTetherHelper).notifyInterfaceStateChange(
@@ -162,7 +163,7 @@
public void canBeTetheredAsUsb() throws Exception {
initStateMachine(TETHERING_USB);
- dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
+ dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, STATE_TETHERED);
InOrder inOrder = inOrder(mTetherHelper, mNMService);
inOrder.verify(mNMService).getInterfaceConfig(IFACE_NAME);
inOrder.verify(mNMService).setInterfaceConfig(IFACE_NAME, mInterfaceConfiguration);
@@ -272,7 +273,7 @@
initStateMachine(TETHERING_USB);
doThrow(RemoteException.class).when(mNMService).tetherInterface(IFACE_NAME);
- dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
+ dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, STATE_TETHERED);
InOrder usbTeardownOrder = inOrder(mNMService, mInterfaceConfiguration, mTetherHelper);
usbTeardownOrder.verify(mInterfaceConfiguration).setInterfaceDown();
usbTeardownOrder.verify(mNMService).setInterfaceConfig(
@@ -310,6 +311,17 @@
* Send a command to the state machine under test, and run the event loop to idle.
*
* @param command One of the TetherInterfaceStateMachine.CMD_* constants.
+ * @param obj An additional argument to pass.
+ */
+ private void dispatchCommand(int command, int arg1) {
+ mTestedSm.sendMessage(command, arg1);
+ mLooper.dispatchAll();
+ }
+
+ /**
+ * Send a command to the state machine under test, and run the event loop to idle.
+ *
+ * @param command One of the TetherInterfaceStateMachine.CMD_* constants.
*/
private void dispatchCommand(int command) {
mTestedSm.sendMessage(command);