[Tether07] Migrate Tethering into module
Now tethering would be run in dedicated service.
TetheringManager is the interface used to communicate with
TetheringService. The new call flow would be: ConnectivityManager
-> ConnectivityService -> TetheringManager -> TetheringService.
Note: the return value of #tether(), #untether() and #setUsbTethering()
APIs would always be no error. Client can use #getLastTetherError()
or #getTetheredIfaces or listen tether state change to check
status of corresponding interface.
Bug: 136040414
Bug: 144742179
Test: -build, flash, boot
-atest TetheringTests
-atest FrameworksNetTests
Change-Id: I7e78c0e0a3e70f940a749ba2a39ece7c7ec5b9b3
Merged-In: I7e78c0e0a3e70f940a749ba2a39ece7c7ec5b9b3
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index cd51f5f..7e8721d 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -22,13 +22,12 @@
":framework-tethering-shared-srcs",
":net-module-utils-srcs",
":services-tethering-shared-srcs",
- ":servicescore-tethering-src",
],
static_libs: [
"androidx.annotation_annotation",
- "netd_aidl_interface-java",
+ "netd_aidl_interface-unstable-java",
"netlink-client",
- "networkstack-aidl-interfaces-java",
+ "networkstack-aidl-interfaces-unstable-java",
"android.hardware.tetheroffload.control-V1.0-java",
"tethering-client",
],
@@ -96,7 +95,16 @@
}
// Non-updatable tethering running in the system server process for devices not using the module
-// TODO: build in-process tethering APK here.
+android_app {
+ name: "InProcessTethering",
+ defaults: ["TetheringAppDefaults"],
+ static_libs: ["TetheringApiCurrentLib"],
+ certificate: "platform",
+ manifest: "AndroidManifest_InProcess.xml",
+ // InProcessTethering is a replacement for Tethering
+ overrides: ["Tethering"],
+ // TODO: use PlatformNetworkPermissionConfig.
+}
// Updatable tethering packaged as an application
android_app {
@@ -109,36 +117,3 @@
// The permission configuration *must* be included to ensure security of the device
required: ["NetworkPermissionConfig"],
}
-
-// This group will be removed when tethering migration is done.
-filegroup {
- name: "tethering-servicescore-srcs",
- srcs: [
- "src/com/android/server/connectivity/tethering/EntitlementManager.java",
- "src/com/android/server/connectivity/tethering/OffloadController.java",
- "src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java",
- "src/com/android/server/connectivity/tethering/TetheringConfiguration.java",
- "src/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java",
- ],
-}
-
-// This group will be removed when tethering migration is done.
-filegroup {
- name: "tethering-servicesnet-srcs",
- srcs: [
- "src/android/net/dhcp/DhcpServerCallbacks.java",
- "src/android/net/dhcp/DhcpServingParamsParcelExt.java",
- "src/android/net/ip/IpServer.java",
- "src/android/net/ip/RouterAdvertisementDaemon.java",
- "src/android/net/util/InterfaceSet.java",
- "src/android/net/util/PrefixUtils.java",
- ],
-}
-
-// This group would be removed when tethering migration is done.
-filegroup {
- name: "tethering-jni-srcs",
- srcs: [
- "jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp",
- ],
-}
diff --git a/Tethering/AndroidManifest.xml b/Tethering/AndroidManifest.xml
index eb51593..1430ed0 100644
--- a/Tethering/AndroidManifest.xml
+++ b/Tethering/AndroidManifest.xml
@@ -25,5 +25,11 @@
android:process="com.android.networkstack.process"
android:extractNativeLibs="false"
android:persistent="true">
+ <service android:name="com.android.server.connectivity.tethering.TetheringService"
+ android:permission="android.permission.MAINLINE_NETWORK_STACK">
+ <intent-filter>
+ <action android:name="android.net.ITetheringConnector"/>
+ </intent-filter>
+ </service>
</application>
</manifest>
diff --git a/Tethering/AndroidManifest_InProcess.xml b/Tethering/AndroidManifest_InProcess.xml
new file mode 100644
index 0000000..28d405c
--- /dev/null
+++ b/Tethering/AndroidManifest_InProcess.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2019 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.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.tethering.inprocess"
+ android:sharedUserId="android.uid.system"
+ android:process="system">
+ <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
+ <application>
+ <!-- TODO: Using MAINLINE_NETWORK_STACK instead of NETWORK_STACK when tethering run in the
+ same process with networkstack -->
+ <service android:name="com.android.server.connectivity.tethering.TetheringService"
+ android:process="system"
+ android:permission="android.permission.NETWORK_STACK">
+ <intent-filter>
+ <action android:name="android.net.ITetheringConnector.InProcess"/>
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 5b01b1e..adc5a72 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -18,8 +18,12 @@
aidl_interface {
name: "tethering-aidl-interfaces",
local_include_dir: "src",
+ include_dirs: ["frameworks/base/core/java"], // For framework parcelables.
srcs: [
+ "src/android/net/ITetherInternalCallback.aidl",
"src/android/net/ITetheringConnector.aidl",
+ "src/android/net/TetheringConfigurationParcel.aidl",
+ "src/android/net/TetherStatesParcel.aidl",
],
backend: {
ndk: {
@@ -33,8 +37,15 @@
java_library {
name: "tethering-client",
- platform_apis: true,
+ sdk_version: "system_current",
static_libs: [
"tethering-aidl-interfaces-java",
],
}
+
+// This is temporary file group which would be removed after TetheringManager is built
+// into tethering-client. Will be done by aosp/1156906.
+filegroup {
+ name: "tethering-manager",
+ srcs: ["src/android/net/TetheringManager.java"],
+}
diff --git a/Tethering/common/TetheringLib/src/android/net/ITetherInternalCallback.aidl b/Tethering/common/TetheringLib/src/android/net/ITetherInternalCallback.aidl
new file mode 100644
index 0000000..abb00e8
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/ITetherInternalCallback.aidl
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2019 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.net;
+
+import android.net.Network;
+import android.net.TetheringConfigurationParcel;
+import android.net.TetherStatesParcel;
+
+/**
+ * Callback class for receiving tethering changed events
+ * @hide
+ */
+oneway interface ITetherInternalCallback
+{
+ void onUpstreamChanged(in Network network);
+ void onConfigurationChanged(in TetheringConfigurationParcel config);
+ void onTetherStatesChanged(in TetherStatesParcel states);
+ void onCallbackCreated(in Network network, in TetheringConfigurationParcel config,
+ in TetherStatesParcel states);
+}
diff --git a/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl b/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
index 443481e..bfe502f 100644
--- a/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
@@ -15,6 +15,23 @@
*/
package android.net;
+import android.net.ITetherInternalCallback;
+import android.os.ResultReceiver;
+
/** @hide */
oneway interface ITetheringConnector {
+ void tether(String iface);
+
+ void untether(String iface);
+
+ void setUsbTethering(boolean enable);
+
+ void startTethering(int type, in ResultReceiver receiver, boolean showProvisioningUi);
+
+ void stopTethering(int type);
+
+ void requestLatestTetheringEntitlementResult(int type, in ResultReceiver receiver,
+ boolean showEntitlementUi);
+
+ void registerTetherInternalCallback(ITetherInternalCallback callback);
}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl
new file mode 100644
index 0000000..3d842b3
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2019 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.net;
+
+/**
+ * Status details for tethering downstream interfaces.
+ * {@hide}
+ */
+parcelable TetherStatesParcel {
+ String[] availableList;
+ String[] tetheredList;
+ String[] localOnlyList;
+ String[] erroredIfaceList;
+ // List of Last error code corresponding to each errored iface in erroredIfaceList. */
+ // TODO: Improve this as b/143122247.
+ int[] lastErrorList;
+}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringConfigurationParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringConfigurationParcel.aidl
new file mode 100644
index 0000000..fdf0d16
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringConfigurationParcel.aidl
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2019 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.net;
+
+/**
+ * Configuration details for a tethering.
+ * @hide
+ */
+parcelable TetheringConfigurationParcel {
+ int subId;
+ String[] tetherableUsbRegexs;
+ String[] tetherableWifiRegexs;
+ String[] tetherableBluetoothRegexs;
+ boolean isDunRequired;
+ boolean chooseUpstreamAutomatically;
+ int[] preferredUpstreamIfaceTypes;
+ String[] legacyDhcpRanges;
+ String[] defaultIPv4DNS;
+ boolean enableLegacyDhcpServer;
+ String[] provisioningApp;
+ String provisioningAppNoUi;
+ int provisioningCheckPeriod;
+}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
new file mode 100644
index 0000000..9e8726e
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -0,0 +1,507 @@
+/*
+ * Copyright (C) 2019 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.net;
+
+import static android.Manifest.permission.NETWORK_STACK;
+import static android.net.ConnectivityManager.TETHER_ERROR_NO_ERROR;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.util.SharedLog;
+import android.os.ConditionVariable;
+import android.os.IBinder;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.PrintWriter;
+import java.util.StringJoiner;
+
+/**
+ * Service used to communicate with the tethering, which is running in a separate module.
+ * @hide
+ */
+public class TetheringManager {
+ private static final String TAG = TetheringManager.class.getSimpleName();
+
+ private static TetheringManager sInstance;
+
+ @Nullable
+ private ITetheringConnector mConnector;
+ private TetherInternalCallback mCallback;
+ private Network mTetherUpstream;
+ private TetheringConfigurationParcel mTetheringConfiguration;
+ private TetherStatesParcel mTetherStatesParcel;
+
+ private final RemoteCallbackList<ITetheringEventCallback> mTetheringEventCallbacks =
+ new RemoteCallbackList<>();
+ @GuardedBy("mLog")
+ private final SharedLog mLog = new SharedLog(TAG);
+
+ private TetheringManager() { }
+
+ /**
+ * Get the TetheringManager singleton instance.
+ */
+ public static synchronized TetheringManager getInstance() {
+ if (sInstance == null) {
+ sInstance = new TetheringManager();
+ }
+ return sInstance;
+ }
+
+ private class TetheringConnection implements
+ ConnectivityModuleConnector.ModuleServiceCallback {
+ @Override
+ public void onModuleServiceConnected(@NonNull IBinder service) {
+ logi("Tethering service connected");
+ registerTetheringService(service);
+ }
+ }
+
+ private void registerTetheringService(@NonNull IBinder service) {
+ final ITetheringConnector connector = ITetheringConnector.Stub.asInterface(service);
+
+ log("Tethering service registered");
+
+ // Currently TetheringManager instance is only used by ConnectivityService and mConnector
+ // only expect to assign once when system server start and bind tethering service.
+ // STOPSHIP: Change mConnector to final before TetheringManager put into boot classpath.
+ mConnector = connector;
+ mCallback = new TetherInternalCallback();
+ try {
+ mConnector.registerTetherInternalCallback(mCallback);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ private class TetherInternalCallback extends ITetherInternalCallback.Stub {
+ private final ConditionVariable mWaitForCallback = new ConditionVariable(false);
+ private static final int EVENT_CALLBACK_TIMEOUT_MS = 60_000;
+
+ @Override
+ public void onUpstreamChanged(Network network) {
+ mTetherUpstream = network;
+ reportUpstreamChanged(network);
+ }
+
+ @Override
+ public void onConfigurationChanged(TetheringConfigurationParcel config) {
+ mTetheringConfiguration = config;
+ }
+
+ @Override
+ public void onTetherStatesChanged(TetherStatesParcel states) {
+ mTetherStatesParcel = states;
+ }
+
+ @Override
+ public void onCallbackCreated(Network network, TetheringConfigurationParcel config,
+ TetherStatesParcel states) {
+ mTetherUpstream = network;
+ mTetheringConfiguration = config;
+ mTetherStatesParcel = states;
+ mWaitForCallback.open();
+ }
+
+ boolean awaitCallbackCreation() {
+ return mWaitForCallback.block(EVENT_CALLBACK_TIMEOUT_MS);
+ }
+ }
+
+ private void reportUpstreamChanged(Network network) {
+ final int length = mTetheringEventCallbacks.beginBroadcast();
+ try {
+ for (int i = 0; i < length; i++) {
+ try {
+ mTetheringEventCallbacks.getBroadcastItem(i).onUpstreamChanged(network);
+ } catch (RemoteException e) {
+ // Not really very much to do here.
+ }
+ }
+ } finally {
+ mTetheringEventCallbacks.finishBroadcast();
+ }
+ }
+
+ /**
+ * Start the tethering service. Should be called only once on device startup.
+ *
+ * <p>This method will start the tethering service either in the network stack process,
+ * or inside the system server on devices that do not support the tethering module.
+ *
+ * {@hide}
+ */
+ public void start() {
+ // Using MAINLINE_NETWORK_STACK permission after cutting off the dpendency of system server.
+ ConnectivityModuleConnector.getInstance().startModuleService(
+ ITetheringConnector.class.getName(), NETWORK_STACK,
+ new TetheringConnection());
+ log("Tethering service start requested");
+ }
+
+ /**
+ * Attempt to tether the named interface. This will setup a dhcp server
+ * on the interface, forward and NAT IP v4 packets and forward DNS requests
+ * to the best active upstream network interface. Note that if no upstream
+ * IP network interface is available, dhcp will still run and traffic will be
+ * allowed between the tethered devices and this device, though upstream net
+ * access will of course fail until an upstream network interface becomes
+ * active. Note: return value do not have any meaning. It is better to use
+ * #getTetherableIfaces() to ensure corresponding interface is available for
+ * tethering before calling #tether().
+ *
+ * TODO: Deprecate this API. The only usages should be in PanService and Wifi P2P which
+ * need direct access.
+ *
+ * {@hide}
+ */
+ public int tether(@NonNull String iface) {
+ try {
+ mConnector.tether(iface);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ return TETHER_ERROR_NO_ERROR;
+ }
+
+ /**
+ * Stop tethering the named interface.
+ *
+ * {@hide}
+ */
+ public int untether(@NonNull String iface) {
+ try {
+ mConnector.untether(iface);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ return TETHER_ERROR_NO_ERROR;
+ }
+
+ /**
+ * Attempt to both alter the mode of USB and Tethering of USB. WARNING: New client should not
+ * use this API anymore. All clients should use #startTethering or #stopTethering which
+ * encapsulate proper entitlement logic. If the API is used and an entitlement check is needed,
+ * downstream USB tethering will be enabled but will not have any upstream.
+ *
+ * @Deprecated
+ * {@hide}
+ */
+ public int setUsbTethering(boolean enable) {
+ try {
+ mConnector.setUsbTethering(enable);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ return TETHER_ERROR_NO_ERROR;
+ }
+
+ /**
+ * Starts tethering and runs tether provisioning for the given type if needed. If provisioning
+ * fails, stopTethering will be called automatically.
+ *
+ * {@hide}
+ */
+ // TODO: improve the usage of ResultReceiver, b/145096122
+ public void startTethering(int type, @NonNull ResultReceiver receiver,
+ boolean showProvisioningUi) {
+ try {
+ mConnector.startTethering(type, receiver, showProvisioningUi);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Stops tethering for the given type. Also cancels any provisioning rechecks for that type if
+ * applicable.
+ *
+ * {@hide}
+ */
+ public void stopTethering(int type) {
+ try {
+ mConnector.stopTethering(type);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Request the latest value of the tethering entitlement check.
+ *
+ * Note: Allow privileged apps who have TETHER_PRIVILEGED permission to access. If it turns
+ * out some such apps are observed to abuse this API, change to per-UID limits on this API
+ * if it's really needed.
+ */
+ // TODO: improve the usage of ResultReceiver, b/145096122
+ public void requestLatestTetheringEntitlementResult(int type, @NonNull ResultReceiver receiver,
+ boolean showEntitlementUi) {
+ try {
+ mConnector.requestLatestTetheringEntitlementResult(type, receiver, showEntitlementUi);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Register tethering event callback.
+ *
+ * {@hide}
+ */
+ public void registerTetheringEventCallback(@NonNull ITetheringEventCallback callback) {
+ mTetheringEventCallbacks.register(callback);
+ }
+
+ /**
+ * Unregister tethering event callback.
+ *
+ * {@hide}
+ */
+ public void unregisterTetheringEventCallback(@NonNull ITetheringEventCallback callback) {
+ mTetheringEventCallbacks.unregister(callback);
+ }
+
+ /**
+ * Get a more detailed error code after a Tethering or Untethering
+ * request asynchronously failed.
+ *
+ * {@hide}
+ */
+ public int getLastTetherError(@NonNull String iface) {
+ if (!mCallback.awaitCallbackCreation()) {
+ throw new NullPointerException("callback was not ready yet");
+ }
+ if (mTetherStatesParcel == null) return TETHER_ERROR_NO_ERROR;
+
+ int i = 0;
+ for (String errored : mTetherStatesParcel.erroredIfaceList) {
+ if (iface.equals(errored)) return mTetherStatesParcel.lastErrorList[i];
+
+ i++;
+ }
+ return TETHER_ERROR_NO_ERROR;
+ }
+
+ /**
+ * Get the list of regular expressions that define any tetherable
+ * USB network interfaces. If USB tethering is not supported by the
+ * device, this list should be empty.
+ *
+ * {@hide}
+ */
+ public @NonNull String[] getTetherableUsbRegexs() {
+ if (!mCallback.awaitCallbackCreation()) {
+ throw new NullPointerException("callback was not ready yet");
+ }
+ return mTetheringConfiguration.tetherableUsbRegexs;
+ }
+
+ /**
+ * Get the list of regular expressions that define any tetherable
+ * Wifi network interfaces. If Wifi tethering is not supported by the
+ * device, this list should be empty.
+ *
+ * {@hide}
+ */
+ public @NonNull String[] getTetherableWifiRegexs() {
+ if (!mCallback.awaitCallbackCreation()) {
+ throw new NullPointerException("callback was not ready yet");
+ }
+ return mTetheringConfiguration.tetherableWifiRegexs;
+ }
+
+ /**
+ * Get the list of regular expressions that define any tetherable
+ * Bluetooth network interfaces. If Bluetooth tethering is not supported by the
+ * device, this list should be empty.
+ *
+ * {@hide}
+ */
+ public @NonNull String[] getTetherableBluetoothRegexs() {
+ if (!mCallback.awaitCallbackCreation()) {
+ throw new NullPointerException("callback was not ready yet");
+ }
+ return mTetheringConfiguration.tetherableBluetoothRegexs;
+ }
+
+ /**
+ * Get the set of tetherable, available interfaces. This list is limited by
+ * device configuration and current interface existence.
+ *
+ * {@hide}
+ */
+ public @NonNull String[] getTetherableIfaces() {
+ if (!mCallback.awaitCallbackCreation()) {
+ throw new NullPointerException("callback was not ready yet");
+ }
+ if (mTetherStatesParcel == null) return new String[0];
+ return mTetherStatesParcel.availableList;
+ }
+
+ /**
+ * Get the set of tethered interfaces.
+ *
+ * {@hide}
+ */
+ public @NonNull String[] getTetheredIfaces() {
+ if (!mCallback.awaitCallbackCreation()) {
+ throw new NullPointerException("callback was not ready yet");
+ }
+ if (mTetherStatesParcel == null) return new String[0];
+ return mTetherStatesParcel.tetheredList;
+ }
+
+ /**
+ * Get the set of interface names which attempted to tether but
+ * failed.
+ *
+ * {@hide}
+ */
+ public @NonNull String[] getTetheringErroredIfaces() {
+ if (!mCallback.awaitCallbackCreation()) {
+ throw new NullPointerException("callback was not ready yet");
+ }
+ if (mTetherStatesParcel == null) return new String[0];
+ return mTetherStatesParcel.erroredIfaceList;
+ }
+
+ /**
+ * Get the set of tethered dhcp ranges.
+ *
+ * {@hide}
+ */
+ public @NonNull String[] getTetheredDhcpRanges() {
+ if (!mCallback.awaitCallbackCreation()) {
+ throw new NullPointerException("callback was not ready yet");
+ }
+ return mTetheringConfiguration.legacyDhcpRanges;
+ }
+
+ /**
+ * Check if the device allows for tethering.
+ *
+ * {@hide}
+ */
+ public boolean hasTetherableConfiguration() {
+ if (!mCallback.awaitCallbackCreation()) {
+ throw new NullPointerException("callback was not ready yet");
+ }
+ final boolean hasDownstreamConfiguration =
+ (mTetheringConfiguration.tetherableUsbRegexs.length != 0)
+ || (mTetheringConfiguration.tetherableWifiRegexs.length != 0)
+ || (mTetheringConfiguration.tetherableBluetoothRegexs.length != 0);
+ final boolean hasUpstreamConfiguration =
+ (mTetheringConfiguration.preferredUpstreamIfaceTypes.length != 0)
+ || mTetheringConfiguration.chooseUpstreamAutomatically;
+
+ return hasDownstreamConfiguration && hasUpstreamConfiguration;
+ }
+
+ /**
+ * Log a message in the local log.
+ */
+ private void log(@NonNull String message) {
+ synchronized (mLog) {
+ mLog.log(message);
+ }
+ }
+
+ /**
+ * Log a condition that should never happen.
+ */
+ private void logWtf(@NonNull String message, @Nullable Throwable e) {
+ Slog.wtf(TAG, message);
+ synchronized (mLog) {
+ mLog.e(message, e);
+ }
+ }
+
+ /**
+ * Log a ERROR level message in the local and system logs.
+ */
+ private void loge(@NonNull String message, @Nullable Throwable e) {
+ synchronized (mLog) {
+ mLog.e(message, e);
+ }
+ }
+
+ /**
+ * Log a INFO level message in the local and system logs.
+ */
+ private void logi(@NonNull String message) {
+ synchronized (mLog) {
+ mLog.i(message);
+ }
+ }
+
+ /**
+ * Dump TetheringManager logs to the specified {@link PrintWriter}.
+ */
+ public void dump(@NonNull PrintWriter pw) {
+ // dump is thread-safe on SharedLog
+ mLog.dump(null, pw, null);
+
+ pw.print("subId: ");
+ pw.println(mTetheringConfiguration.subId);
+
+ dumpStringArray(pw, "tetherableUsbRegexs",
+ mTetheringConfiguration.tetherableUsbRegexs);
+ dumpStringArray(pw, "tetherableWifiRegexs",
+ mTetheringConfiguration.tetherableWifiRegexs);
+ dumpStringArray(pw, "tetherableBluetoothRegexs",
+ mTetheringConfiguration.tetherableBluetoothRegexs);
+
+ pw.print("isDunRequired: ");
+ pw.println(mTetheringConfiguration.isDunRequired);
+
+ pw.print("chooseUpstreamAutomatically: ");
+ pw.println(mTetheringConfiguration.chooseUpstreamAutomatically);
+
+ dumpStringArray(pw, "legacyDhcpRanges", mTetheringConfiguration.legacyDhcpRanges);
+ dumpStringArray(pw, "defaultIPv4DNS", mTetheringConfiguration.defaultIPv4DNS);
+
+ dumpStringArray(pw, "provisioningApp", mTetheringConfiguration.provisioningApp);
+ pw.print("provisioningAppNoUi: ");
+ pw.println(mTetheringConfiguration.provisioningAppNoUi);
+
+ pw.print("enableLegacyDhcpServer: ");
+ pw.println(mTetheringConfiguration.enableLegacyDhcpServer);
+
+ pw.println();
+ }
+
+ private static void dumpStringArray(@NonNull PrintWriter pw, @NonNull String label,
+ @Nullable String[] values) {
+ pw.print(label);
+ pw.print(": ");
+
+ if (values != null) {
+ final StringJoiner sj = new StringJoiner(", ", "[", "]");
+ for (String value : values) sj.add(value);
+
+ pw.print(sj.toString());
+ } else {
+ pw.print("null");
+ }
+
+ pw.println();
+ }
+}
diff --git a/Tethering/jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp b/Tethering/jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp
index 3eaf488..663154a 100644
--- a/Tethering/jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp
+++ b/Tethering/jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp
@@ -145,4 +145,18 @@
gMethods, NELEM(gMethods));
}
+extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
+ JNIEnv *env;
+ if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+ ALOGE("ERROR: GetEnv failed");
+ return JNI_ERR;
+ }
+
+ if (register_android_server_connectivity_tethering_OffloadHardwareInterface(env) < 0) {
+ return JNI_ERR;
+ }
+
+ return JNI_VERSION_1_6;
+}
+
}; // namespace android
diff --git a/Tethering/src/android/net/util/BaseNetdUnsolicitedEventListener.java b/Tethering/src/android/net/util/BaseNetdUnsolicitedEventListener.java
new file mode 100644
index 0000000..3218c0b
--- /dev/null
+++ b/Tethering/src/android/net/util/BaseNetdUnsolicitedEventListener.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 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.net.util;
+
+import android.net.INetdUnsolicitedEventListener;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Base {@link INetdUnsolicitedEventListener} that provides no-op implementations which can be
+ * overridden.
+ */
+public class BaseNetdUnsolicitedEventListener extends INetdUnsolicitedEventListener.Stub {
+
+ @Override
+ public void onInterfaceClassActivityChanged(boolean isActive, int timerLabel, long timestampNs,
+ int uid) { }
+
+ @Override
+ public void onQuotaLimitReached(@NonNull String alertName, @NonNull String ifName) { }
+
+ @Override
+ public void onInterfaceDnsServerInfo(@NonNull String ifName, long lifetimeS,
+ @NonNull String[] servers) { }
+
+ @Override
+ public void onInterfaceAddressUpdated(@NonNull String addr, String ifName, int flags,
+ int scope) { }
+
+ @Override
+ public void onInterfaceAddressRemoved(@NonNull String addr, @NonNull String ifName, int flags,
+ int scope) { }
+
+ @Override
+ public void onInterfaceAdded(@NonNull String ifName) { }
+
+ @Override
+ public void onInterfaceRemoved(@NonNull String ifName) { }
+
+ @Override
+ public void onInterfaceChanged(@NonNull String ifName, boolean up) { }
+
+ @Override
+ public void onInterfaceLinkStateChanged(@NonNull String ifName, boolean up) { }
+
+ @Override
+ public void onRouteChanged(boolean updated, @NonNull String route, @NonNull String gateway,
+ @NonNull String ifName) { }
+
+ @Override
+ public void onStrictCleartextDetected(int uid, @NonNull String hex) { }
+
+ @Override
+ public int getInterfaceVersion() {
+ return INetdUnsolicitedEventListener.VERSION;
+ }
+}
diff --git a/Tethering/src/android/net/util/VersionedBroadcastListener.java b/Tethering/src/android/net/util/VersionedBroadcastListener.java
new file mode 100644
index 0000000..e2804ab
--- /dev/null
+++ b/Tethering/src/android/net/util/VersionedBroadcastListener.java
@@ -0,0 +1,106 @@
+/*
+ * 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.net.util;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.util.Log;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+
+/**
+ * A utility class that runs the provided callback on the provided handler when
+ * intents matching the provided filter arrive. Intents received by a stale
+ * receiver are safely ignored.
+ *
+ * Calls to startListening() and stopListening() must happen on the same thread.
+ *
+ * @hide
+ */
+public class VersionedBroadcastListener {
+ private static final boolean DBG = false;
+
+ private final String mTag;
+ private final Context mContext;
+ private final Handler mHandler;
+ private final IntentFilter mFilter;
+ private final Consumer<Intent> mCallback;
+ private final AtomicInteger mGenerationNumber;
+ private BroadcastReceiver mReceiver;
+
+ public VersionedBroadcastListener(String tag, Context ctx, Handler handler,
+ IntentFilter filter, Consumer<Intent> callback) {
+ mTag = tag;
+ mContext = ctx;
+ mHandler = handler;
+ mFilter = filter;
+ mCallback = callback;
+ mGenerationNumber = new AtomicInteger(0);
+ }
+
+ /** Start listening to intent broadcast. */
+ public void startListening() {
+ if (DBG) Log.d(mTag, "startListening");
+ if (mReceiver != null) return;
+
+ mReceiver = new Receiver(mTag, mGenerationNumber, mCallback);
+ mContext.registerReceiver(mReceiver, mFilter, null, mHandler);
+ }
+
+ /** Stop listening to intent broadcast. */
+ public void stopListening() {
+ if (DBG) Log.d(mTag, "stopListening");
+ if (mReceiver == null) return;
+
+ mGenerationNumber.incrementAndGet();
+ mContext.unregisterReceiver(mReceiver);
+ mReceiver = null;
+ }
+
+ private static class Receiver extends BroadcastReceiver {
+ public final String tag;
+ public final AtomicInteger atomicGenerationNumber;
+ public final Consumer<Intent> callback;
+ // Used to verify this receiver is still current.
+ public final int generationNumber;
+
+ Receiver(String tag, AtomicInteger atomicGenerationNumber, Consumer<Intent> callback) {
+ this.tag = tag;
+ this.atomicGenerationNumber = atomicGenerationNumber;
+ this.callback = callback;
+ generationNumber = atomicGenerationNumber.incrementAndGet();
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final int currentGenerationNumber = atomicGenerationNumber.get();
+
+ if (DBG) {
+ Log.d(tag, "receiver generationNumber=" + generationNumber
+ + ", current generationNumber=" + currentGenerationNumber);
+ }
+ if (generationNumber != currentGenerationNumber) return;
+
+ callback.accept(intent);
+ }
+ }
+}
diff --git a/Tethering/src/com/android/server/connectivity/tethering/EntitlementManager.java b/Tethering/src/com/android/server/connectivity/tethering/EntitlementManager.java
index 6b0f1de..ba5d08d 100644
--- a/Tethering/src/com/android/server/connectivity/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/server/connectivity/tethering/EntitlementManager.java
@@ -47,6 +47,7 @@
import android.os.PersistableBundle;
import android.os.ResultReceiver;
import android.os.SystemClock;
+import android.os.SystemProperties;
import android.os.UserHandle;
import android.provider.Settings;
import android.telephony.CarrierConfigManager;
@@ -55,7 +56,6 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.StateMachine;
-import com.android.server.connectivity.MockableSystemProperties;
import java.io.PrintWriter;
@@ -94,7 +94,6 @@
private final ArraySet<Integer> mCurrentTethers;
private final Context mContext;
private final int mPermissionChangeMessageCode;
- private final MockableSystemProperties mSystemProperties;
private final SharedLog mLog;
private final SparseIntArray mEntitlementCacheValue;
private final EntitlementHandler mHandler;
@@ -110,12 +109,12 @@
private TetheringConfigurationFetcher mFetcher;
public EntitlementManager(Context ctx, StateMachine tetherMasterSM, SharedLog log,
- int permissionChangeMessageCode, MockableSystemProperties systemProperties) {
+ int permissionChangeMessageCode) {
+
mContext = ctx;
mLog = log.forSubComponent(TAG);
mCurrentTethers = new ArraySet<Integer>();
mCellularPermitted = new SparseIntArray();
- mSystemProperties = systemProperties;
mEntitlementCacheValue = new SparseIntArray();
mTetherMasterSM = tetherMasterSM;
mPermissionChangeMessageCode = permissionChangeMessageCode;
@@ -287,7 +286,7 @@
*/
@VisibleForTesting
protected boolean isTetherProvisioningRequired(final TetheringConfiguration config) {
- if (mSystemProperties.getBoolean(DISABLE_PROVISIONING_SYSPROP_KEY, false)
+ if (SystemProperties.getBoolean(DISABLE_PROVISIONING_SYSPROP_KEY, false)
|| config.provisioningApp.length == 0) {
return false;
}
@@ -526,8 +525,8 @@
handleMaybeRunProvisioning(config);
break;
case EVENT_GET_ENTITLEMENT_VALUE:
- handleGetLatestTetheringEntitlementValue(msg.arg1, (ResultReceiver) msg.obj,
- toBool(msg.arg2));
+ handleRequestLatestTetheringEntitlementValue(msg.arg1,
+ (ResultReceiver) msg.obj, toBool(msg.arg2));
break;
default:
mLog.log("Unknown event: " + msg.what);
@@ -651,15 +650,15 @@
}
/** Get the last value of the tethering entitlement check. */
- public void getLatestTetheringEntitlementResult(int downstream, ResultReceiver receiver,
+ public void requestLatestTetheringEntitlementResult(int downstream, ResultReceiver receiver,
boolean showEntitlementUi) {
mHandler.sendMessage(mHandler.obtainMessage(EVENT_GET_ENTITLEMENT_VALUE,
downstream, encodeBool(showEntitlementUi), receiver));
}
- private void handleGetLatestTetheringEntitlementValue(int downstream, ResultReceiver receiver,
- boolean showEntitlementUi) {
+ private void handleRequestLatestTetheringEntitlementValue(int downstream,
+ ResultReceiver receiver, boolean showEntitlementUi) {
final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
if (!isTetherProvisioningRequired(config)) {
receiver.send(TETHER_ERROR_NO_ERROR, null);
diff --git a/Tethering/src/com/android/server/connectivity/tethering/IPv6TetheringCoordinator.java b/Tethering/src/com/android/server/connectivity/tethering/IPv6TetheringCoordinator.java
new file mode 100644
index 0000000..edfe3ca
--- /dev/null
+++ b/Tethering/src/com/android/server/connectivity/tethering/IPv6TetheringCoordinator.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2016 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.server.connectivity.tethering;
+
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkState;
+import android.net.RouteInfo;
+import android.net.ip.IpServer;
+import android.net.util.NetworkConstants;
+import android.net.util.SharedLog;
+import android.util.Log;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.Random;
+
+
+/**
+ * IPv6 tethering is rather different from IPv4 owing to the absence of NAT.
+ * This coordinator is responsible for evaluating the dedicated prefixes
+ * assigned to the device and deciding how to divvy them up among downstream
+ * interfaces.
+ *
+ * @hide
+ */
+public class IPv6TetheringCoordinator {
+ private static final String TAG = IPv6TetheringCoordinator.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ private static class Downstream {
+ public final IpServer ipServer;
+ public final int mode; // IpServer.STATE_*
+ // Used to append to a ULA /48, constructing a ULA /64 for local use.
+ public final short subnetId;
+
+ Downstream(IpServer ipServer, int mode, short subnetId) {
+ this.ipServer = ipServer;
+ this.mode = mode;
+ this.subnetId = subnetId;
+ }
+ }
+
+ private final ArrayList<IpServer> mNotifyList;
+ private final SharedLog mLog;
+ // 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<IpServer> notifyList, SharedLog log) {
+ mNotifyList = notifyList;
+ mLog = log.forSubComponent(TAG);
+ mActiveDownstreams = new LinkedList<>();
+ mUniqueLocalPrefix = generateUniqueLocalPrefix();
+ mNextSubnetId = 0;
+ }
+
+ /** Add active downstream to ipv6 tethering candidate list. */
+ public void addActiveDownstream(IpServer 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.
+ // 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();
+ }
+ }
+
+ /** Remove downstream from ipv6 tethering candidate list. */
+ public void removeActiveDownstream(IpServer downstream) {
+ stopIPv6TetheringOn(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;
+ }
+ }
+
+ /**
+ * Call when upstream NetworkState may be changed.
+ * If upstream has ipv6 for tethering, update this new NetworkState
+ * to IpServer. Otherwise stop ipv6 tethering on downstream interfaces.
+ */
+ public void updateUpstreamNetworkState(NetworkState ns) {
+ if (VDBG) {
+ Log.d(TAG, "updateUpstreamNetworkState: " + toDebugString(ns));
+ }
+ if (TetheringInterfaceUtils.getIPv6Interface(ns) == null) {
+ stopIPv6TetheringOnAllInterfaces();
+ setUpstreamNetworkState(null);
+ return;
+ }
+
+ if (mUpstreamNetworkState != null
+ && !ns.network.equals(mUpstreamNetworkState.network)) {
+ stopIPv6TetheringOnAllInterfaces();
+ }
+
+ setUpstreamNetworkState(ns);
+ updateIPv6TetheringInterfaces();
+ }
+
+ private void stopIPv6TetheringOnAllInterfaces() {
+ for (IpServer ipServer : mNotifyList) {
+ stopIPv6TetheringOn(ipServer);
+ }
+ }
+
+ private void setUpstreamNetworkState(NetworkState ns) {
+ if (ns == null) {
+ mUpstreamNetworkState = null;
+ } else {
+ // Make a deep copy of the parts we need.
+ mUpstreamNetworkState = new NetworkState(
+ null,
+ new LinkProperties(ns.linkProperties),
+ new NetworkCapabilities(ns.networkCapabilities),
+ new Network(ns.network),
+ null,
+ null);
+ }
+
+ mLog.log("setUpstreamNetworkState: " + toDebugString(mUpstreamNetworkState));
+ }
+
+ private void updateIPv6TetheringInterfaces() {
+ for (IpServer ipServer : mNotifyList) {
+ final LinkProperties lp = getInterfaceIPv6LinkProperties(ipServer);
+ ipServer.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0, lp);
+ break;
+ }
+ }
+
+ private LinkProperties getInterfaceIPv6LinkProperties(IpServer ipServer) {
+ final Downstream ds = findDownstream(ipServer);
+ if (ds == null) return null;
+
+ if (ds.mode == IpServer.STATE_LOCAL_ONLY) {
+ // Build a Unique Locally-assigned Prefix configuration.
+ return getUniqueLocalConfig(mUniqueLocalPrefix, ds.subnetId);
+ }
+
+ // This downstream is in IpServer.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 Downstream currentActive = mActiveDownstreams.peek();
+ if (currentActive != null && currentActive.ipServer == ipServer) {
+ final LinkProperties lp = getIPv6OnlyLinkProperties(
+ mUpstreamNetworkState.linkProperties);
+ if (lp.hasIpv6DefaultRoute() && lp.hasGlobalIpv6Address()) {
+ return lp;
+ }
+ }
+
+ return null;
+ }
+
+ Downstream findDownstream(IpServer ipServer) {
+ for (Downstream ds : mActiveDownstreams) {
+ if (ds.ipServer == ipServer) return ds;
+ }
+ return null;
+ }
+
+ private static LinkProperties getIPv6OnlyLinkProperties(LinkProperties lp) {
+ final LinkProperties v6only = new LinkProperties();
+ if (lp == null) {
+ return v6only;
+ }
+
+ // NOTE: At this time we don't copy over any information about any
+ // stacked links. No current stacked link configuration has IPv6.
+
+ v6only.setInterfaceName(lp.getInterfaceName());
+
+ v6only.setMtu(lp.getMtu());
+
+ for (LinkAddress linkAddr : lp.getLinkAddresses()) {
+ if (linkAddr.isGlobalPreferred() && linkAddr.getPrefixLength() == 64) {
+ v6only.addLinkAddress(linkAddr);
+ }
+ }
+
+ for (RouteInfo routeInfo : lp.getRoutes()) {
+ final IpPrefix destination = routeInfo.getDestination();
+ if ((destination.getAddress() instanceof Inet6Address)
+ && (destination.getPrefixLength() <= 64)) {
+ v6only.addRoute(routeInfo);
+ }
+ }
+
+ for (InetAddress dnsServer : lp.getDnsServers()) {
+ if (isIPv6GlobalAddress(dnsServer)) {
+ // For now we include ULAs.
+ v6only.addDnsServer(dnsServer);
+ }
+ }
+
+ v6only.setDomains(lp.getDomains());
+
+ return v6only;
+ }
+
+ // TODO: Delete this and switch to LinkAddress#isGlobalPreferred once we
+ // announce our own IPv6 address as DNS server.
+ private static boolean isIPv6GlobalAddress(InetAddress ip) {
+ return (ip instanceof Inet6Address)
+ && !ip.isAnyLocalAddress()
+ && !ip.isLoopbackAddress()
+ && !ip.isLinkLocalAddress()
+ && !ip.isSiteLocalAddress()
+ && !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}";
+ }
+ return String.format("NetworkState{%s, %s, %s}",
+ ns.network,
+ ns.networkCapabilities,
+ ns.linkProperties);
+ }
+
+ private static void stopIPv6TetheringOn(IpServer ipServer) {
+ ipServer.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0, null);
+ }
+}
diff --git a/Tethering/src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java b/Tethering/src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java
index 01339a4..00a6773 100644
--- a/Tethering/src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java
+++ b/Tethering/src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java
@@ -107,6 +107,8 @@
public OffloadHardwareInterface(Handler h, SharedLog log) {
mHandler = h;
mLog = log.forSubComponent(TAG);
+
+ System.loadLibrary("tetheroffloadjni");
}
/** Get default value indicating whether offload is supported. */
diff --git a/Tethering/src/com/android/server/connectivity/tethering/Tethering.java b/Tethering/src/com/android/server/connectivity/tethering/Tethering.java
new file mode 100644
index 0000000..058fb4f
--- /dev/null
+++ b/Tethering/src/com/android/server/connectivity/tethering/Tethering.java
@@ -0,0 +1,2029 @@
+/*
+ * Copyright (C) 2010 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.server.connectivity.tethering;
+
+import static android.hardware.usb.UsbManager.USB_CONFIGURED;
+import static android.hardware.usb.UsbManager.USB_CONNECTED;
+import static android.hardware.usb.UsbManager.USB_FUNCTION_RNDIS;
+import static android.net.ConnectivityManager.ACTION_TETHER_STATE_CHANGED;
+import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
+import static android.net.ConnectivityManager.EXTRA_ACTIVE_LOCAL_ONLY;
+import static android.net.ConnectivityManager.EXTRA_ACTIVE_TETHER;
+import static android.net.ConnectivityManager.EXTRA_AVAILABLE_TETHER;
+import static android.net.ConnectivityManager.EXTRA_ERRORED_TETHER;
+import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
+import static android.net.ConnectivityManager.TETHERING_BLUETOOTH;
+import static android.net.ConnectivityManager.TETHERING_INVALID;
+import static android.net.ConnectivityManager.TETHERING_USB;
+import static android.net.ConnectivityManager.TETHERING_WIFI;
+import static android.net.ConnectivityManager.TETHERING_WIFI_P2P;
+import static android.net.ConnectivityManager.TETHER_ERROR_MASTER_ERROR;
+import static android.net.ConnectivityManager.TETHER_ERROR_NO_ERROR;
+import static android.net.ConnectivityManager.TETHER_ERROR_SERVICE_UNAVAIL;
+import static android.net.ConnectivityManager.TETHER_ERROR_UNAVAIL_IFACE;
+import static android.net.ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE;
+import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME;
+import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_MODE;
+import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_STATE;
+import static android.net.wifi.WifiManager.IFACE_IP_MODE_CONFIGURATION_ERROR;
+import static android.net.wifi.WifiManager.IFACE_IP_MODE_LOCAL_ONLY;
+import static android.net.wifi.WifiManager.IFACE_IP_MODE_TETHERED;
+import static android.net.wifi.WifiManager.IFACE_IP_MODE_UNSPECIFIED;
+import static android.net.wifi.WifiManager.WIFI_AP_STATE_DISABLED;
+import static android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothPan;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothProfile.ServiceListener;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.hardware.usb.UsbManager;
+import android.net.INetd;
+import android.net.INetworkPolicyManager;
+import android.net.INetworkStatsService;
+import android.net.ITetherInternalCallback;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkInfo;
+import android.net.NetworkState;
+import android.net.NetworkUtils;
+import android.net.TetherStatesParcel;
+import android.net.TetheringConfigurationParcel;
+import android.net.ip.IpServer;
+import android.net.util.BaseNetdUnsolicitedEventListener;
+import android.net.util.InterfaceSet;
+import android.net.util.PrefixUtils;
+import android.net.util.SharedLog;
+import android.net.util.VersionedBroadcastListener;
+import android.net.wifi.WifiManager;
+import android.net.wifi.p2p.WifiP2pGroup;
+import android.net.wifi.p2p.WifiP2pInfo;
+import android.net.wifi.p2p.WifiP2pManager;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.UserManagerInternal;
+import android.os.UserManagerInternal.UserRestrictionsListener;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.internal.notification.SystemNotificationChannels;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.MessageUtils;
+import com.android.internal.util.Protocol;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+import com.android.server.LocalServices;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ *
+ * This class holds much of the business logic to allow Android devices
+ * to act as IP gateways via USB, BT, and WiFi interfaces.
+ */
+public class Tethering {
+
+ private static final String TAG = Tethering.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ private static final Class[] sMessageClasses = {
+ Tethering.class, TetherMasterSM.class, IpServer.class
+ };
+ private static final SparseArray<String> sMagicDecoderRing =
+ MessageUtils.findMessageNames(sMessageClasses);
+
+ private static class TetherState {
+ public final IpServer ipServer;
+ public int lastState;
+ public int lastError;
+
+ TetherState(IpServer ipServer) {
+ this.ipServer = ipServer;
+ // Assume all state machines start out available and with no errors.
+ lastState = IpServer.STATE_AVAILABLE;
+ lastError = TETHER_ERROR_NO_ERROR;
+ }
+
+ public boolean isCurrentlyServing() {
+ switch (lastState) {
+ case IpServer.STATE_TETHERED:
+ case IpServer.STATE_LOCAL_ONLY:
+ return true;
+ default:
+ return false;
+ }
+ }
+ }
+
+ private final SharedLog mLog = new SharedLog(TAG);
+
+ // used to synchronize public access to members
+ private final Object mPublicSync;
+ private final Context mContext;
+ private final ArrayMap<String, TetherState> mTetherStates;
+ private final BroadcastReceiver mStateReceiver;
+ // Stopship: replace mNMService before production.
+ private final INetworkManagementService mNMService;
+ private final INetworkStatsService mStatsService;
+ private final INetworkPolicyManager mPolicyManager;
+ private final Looper mLooper;
+ private final StateMachine mTetherMasterSM;
+ private final OffloadController mOffloadController;
+ private final UpstreamNetworkMonitor mUpstreamNetworkMonitor;
+ // TODO: Figure out how to merge this and other downstream-tracking objects
+ // into a single coherent structure.
+ private final HashSet<IpServer> mForwardedDownstreams;
+ private final VersionedBroadcastListener mCarrierConfigChange;
+ private final TetheringDependencies mDeps;
+ private final EntitlementManager mEntitlementMgr;
+ private final Handler mHandler;
+ private final PhoneStateListener mPhoneStateListener;
+ private final INetd mNetd;
+ private final NetdCallback mNetdCallback;
+ private int mActiveDataSubId = INVALID_SUBSCRIPTION_ID;
+ // All the usage of mTetherInternalCallback should run in the same thread.
+ private ITetherInternalCallback mTetherInternalCallback = null;
+
+ private volatile TetheringConfiguration mConfig;
+ private InterfaceSet mCurrentUpstreamIfaceSet;
+ private Notification.Builder mTetheredNotificationBuilder;
+ private int mLastNotificationId;
+
+ private boolean mRndisEnabled; // track the RNDIS function enabled state
+ // True iff. WiFi tethering should be started when soft AP is ready.
+ private boolean mWifiTetherRequested;
+ private Network mTetherUpstream;
+ private TetherStatesParcel mTetherStatesParcel;
+
+ public Tethering(TetheringDependencies deps) {
+ mLog.mark("Tethering.constructed");
+ mDeps = deps;
+ mContext = mDeps.getContext();
+ mNMService = mDeps.getINetworkManagementService();
+ mStatsService = mDeps.getINetworkStatsService();
+ mPolicyManager = mDeps.getINetworkPolicyManager();
+ mNetd = mDeps.getINetd(mContext);
+ mLooper = mDeps.getTetheringLooper();
+
+ mPublicSync = new Object();
+
+ mTetherStates = new ArrayMap<>();
+
+ mTetherMasterSM = new TetherMasterSM("TetherMaster", mLooper, deps);
+ mTetherMasterSM.start();
+
+ mHandler = mTetherMasterSM.getHandler();
+ mOffloadController = new OffloadController(mHandler,
+ mDeps.getOffloadHardwareInterface(mHandler, mLog),
+ mContext.getContentResolver(), mNMService,
+ mLog);
+ mUpstreamNetworkMonitor = deps.getUpstreamNetworkMonitor(mContext, mTetherMasterSM, mLog,
+ TetherMasterSM.EVENT_UPSTREAM_CALLBACK);
+ mForwardedDownstreams = new HashSet<>();
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ACTION_CARRIER_CONFIG_CHANGED);
+ // EntitlementManager will send EVENT_UPSTREAM_PERMISSION_CHANGED when cellular upstream
+ // permission is changed according to entitlement check result.
+ mEntitlementMgr = mDeps.getEntitlementManager(mContext, mTetherMasterSM, mLog,
+ TetherMasterSM.EVENT_UPSTREAM_PERMISSION_CHANGED);
+ mEntitlementMgr.setOnUiEntitlementFailedListener((int downstream) -> {
+ mLog.log("OBSERVED UiEnitlementFailed");
+ stopTethering(downstream);
+ });
+ mEntitlementMgr.setTetheringConfigurationFetcher(() -> {
+ return mConfig;
+ });
+
+ mCarrierConfigChange = new VersionedBroadcastListener(
+ "CarrierConfigChangeListener", mContext, mHandler, filter,
+ (Intent ignored) -> {
+ mLog.log("OBSERVED carrier config change");
+ updateConfiguration();
+ mEntitlementMgr.reevaluateSimCardProvisioning(mConfig);
+ });
+
+ mPhoneStateListener = new PhoneStateListener(mLooper) {
+ @Override
+ public void onActiveDataSubscriptionIdChanged(int subId) {
+ mLog.log("OBSERVED active data subscription change, from " + mActiveDataSubId
+ + " to " + subId);
+ if (subId == mActiveDataSubId) return;
+
+ mActiveDataSubId = subId;
+ updateConfiguration();
+ // To avoid launching unexpected provisioning checks, ignore re-provisioning when
+ // no CarrierConfig loaded yet. Assume reevaluateSimCardProvisioning() will be
+ // triggered again when CarrierConfig is loaded.
+ if (mEntitlementMgr.getCarrierConfig(mConfig) != null) {
+ mEntitlementMgr.reevaluateSimCardProvisioning(mConfig);
+ } else {
+ mLog.log("IGNORED reevaluate provisioning due to no carrier config loaded");
+ }
+ }
+ };
+
+ mStateReceiver = new StateReceiver();
+
+ mNetdCallback = new NetdCallback();
+ try {
+ mNetd.registerUnsolicitedEventListener(mNetdCallback);
+ } catch (RemoteException e) {
+ mLog.e("Unable to register netd UnsolicitedEventListener");
+ }
+
+ // Load tethering configuration.
+ updateConfiguration();
+
+ startStateMachineUpdaters(mHandler);
+ startTrackDefaultNetwork();
+ }
+
+ private void startStateMachineUpdaters(Handler handler) {
+ mCarrierConfigChange.startListening();
+ mContext.getSystemService(TelephonyManager.class).listen(
+ mPhoneStateListener, PhoneStateListener.LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE);
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(UsbManager.ACTION_USB_STATE);
+ filter.addAction(CONNECTIVITY_ACTION);
+ filter.addAction(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
+ filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+ filter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
+ mContext.registerReceiver(mStateReceiver, filter, null, handler);
+
+ filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_MEDIA_SHARED);
+ filter.addAction(Intent.ACTION_MEDIA_UNSHARED);
+ filter.addDataScheme("file");
+ mContext.registerReceiver(mStateReceiver, filter, null, handler);
+
+ final UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
+ // This check is useful only for some unit tests; example: ConnectivityServiceTest.
+ if (umi != null) {
+ umi.addUserRestrictionsListener(new TetheringUserRestrictionListener(this));
+ }
+ }
+
+ private WifiManager getWifiManager() {
+ return (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+ }
+
+ // NOTE: This is always invoked on the mLooper thread.
+ private void updateConfiguration() {
+ mConfig = mDeps.generateTetheringConfiguration(mContext, mLog, mActiveDataSubId);
+ mUpstreamNetworkMonitor.updateMobileRequiresDun(mConfig.isDunRequired);
+ reportConfigurationChanged(mConfig.toStableParcelable());
+ }
+
+ private void maybeDunSettingChanged() {
+ final boolean isDunRequired = TetheringConfiguration.checkDunRequired(
+ mContext, mActiveDataSubId);
+ if (isDunRequired == mConfig.isDunRequired) return;
+ updateConfiguration();
+ }
+
+ private class NetdCallback extends BaseNetdUnsolicitedEventListener {
+ @Override
+ public void onInterfaceChanged(String ifName, boolean up) {
+ mHandler.post(() -> interfaceStatusChanged(ifName, up));
+ }
+
+ @Override
+ public void onInterfaceLinkStateChanged(String ifName, boolean up) {
+ mHandler.post(() -> interfaceLinkStateChanged(ifName, up));
+ }
+
+ @Override
+ public void onInterfaceAdded(String ifName) {
+ mHandler.post(() -> interfaceAdded(ifName));
+ }
+
+ @Override
+ public void onInterfaceRemoved(String ifName) {
+ mHandler.post(() -> interfaceRemoved(ifName));
+ }
+ }
+
+ void interfaceStatusChanged(String iface, boolean up) {
+ // Never called directly: only called from interfaceLinkStateChanged.
+ // See NetlinkHandler.cpp: notifyInterfaceChanged.
+ if (VDBG) Log.d(TAG, "interfaceStatusChanged " + iface + ", " + up);
+ synchronized (mPublicSync) {
+ if (up) {
+ maybeTrackNewInterfaceLocked(iface);
+ } else {
+ if (ifaceNameToType(iface) == TETHERING_BLUETOOTH) {
+ stopTrackingInterfaceLocked(iface);
+ } else {
+ // Ignore usb0 down after enabling RNDIS.
+ // We will handle disconnect in interfaceRemoved.
+ // Similarly, ignore interface down for WiFi. We monitor WiFi AP status
+ // through the WifiManager.WIFI_AP_STATE_CHANGED_ACTION intent.
+ if (VDBG) Log.d(TAG, "ignore interface down for " + iface);
+ }
+ }
+ }
+ }
+
+ void interfaceLinkStateChanged(String iface, boolean up) {
+ interfaceStatusChanged(iface, up);
+ }
+
+ private int ifaceNameToType(String iface) {
+ final TetheringConfiguration cfg = mConfig;
+
+ if (cfg.isWifi(iface)) {
+ return TETHERING_WIFI;
+ } else if (cfg.isWifiP2p(iface)) {
+ return TETHERING_WIFI_P2P;
+ } else if (cfg.isUsb(iface)) {
+ return TETHERING_USB;
+ } else if (cfg.isBluetooth(iface)) {
+ return TETHERING_BLUETOOTH;
+ }
+ return TETHERING_INVALID;
+ }
+
+ void interfaceAdded(String iface) {
+ if (VDBG) Log.d(TAG, "interfaceAdded " + iface);
+ synchronized (mPublicSync) {
+ maybeTrackNewInterfaceLocked(iface);
+ }
+ }
+
+
+ void interfaceRemoved(String iface) {
+ if (VDBG) Log.d(TAG, "interfaceRemoved " + iface);
+ synchronized (mPublicSync) {
+ stopTrackingInterfaceLocked(iface);
+ }
+ }
+
+ void startTethering(int type, ResultReceiver receiver, boolean showProvisioningUi) {
+ mEntitlementMgr.startProvisioningIfNeeded(type, showProvisioningUi);
+ enableTetheringInternal(type, true /* enabled */, receiver);
+ }
+
+ void stopTethering(int type) {
+ enableTetheringInternal(type, false /* disabled */, null);
+ mEntitlementMgr.stopProvisioningIfNeeded(type);
+ }
+
+ /**
+ * Enables or disables tethering for the given type. If provisioning is required, it will
+ * schedule provisioning rechecks for the specified interface.
+ */
+ private void enableTetheringInternal(int type, boolean enable, ResultReceiver receiver) {
+ int result;
+ switch (type) {
+ case TETHERING_WIFI:
+ result = setWifiTethering(enable);
+ sendTetherResult(receiver, result);
+ break;
+ case TETHERING_USB:
+ result = setUsbTethering(enable);
+ sendTetherResult(receiver, result);
+ break;
+ case TETHERING_BLUETOOTH:
+ setBluetoothTethering(enable, receiver);
+ break;
+ default:
+ Log.w(TAG, "Invalid tether type.");
+ sendTetherResult(receiver, TETHER_ERROR_UNKNOWN_IFACE);
+ }
+ }
+
+ private void sendTetherResult(ResultReceiver receiver, int result) {
+ if (receiver != null) {
+ receiver.send(result, null);
+ }
+ }
+
+ private int setWifiTethering(final boolean enable) {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ synchronized (mPublicSync) {
+ final WifiManager mgr = getWifiManager();
+ if (mgr == null) {
+ mLog.e("setWifiTethering: failed to get WifiManager!");
+ return TETHER_ERROR_SERVICE_UNAVAIL;
+ }
+ if ((enable && mgr.startSoftAp(null /* use existing wifi config */))
+ || (!enable && mgr.stopSoftAp())) {
+ mWifiTetherRequested = enable;
+ return TETHER_ERROR_NO_ERROR;
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+
+ return TETHER_ERROR_MASTER_ERROR;
+ }
+
+ private void setBluetoothTethering(final boolean enable, final ResultReceiver receiver) {
+ final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ if (adapter == null || !adapter.isEnabled()) {
+ Log.w(TAG, "Tried to enable bluetooth tethering with null or disabled adapter. null: "
+ + (adapter == null));
+ sendTetherResult(receiver, TETHER_ERROR_SERVICE_UNAVAIL);
+ return;
+ }
+
+ adapter.getProfileProxy(mContext, new ServiceListener() {
+ @Override
+ public void onServiceDisconnected(int profile) { }
+
+ @Override
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ // Clear identify is fine because caller already pass tethering permission at
+ // ConnectivityService#startTethering()(or stopTethering) before the control comes
+ // here. Bluetooth will check tethering permission again that there is
+ // Context#getOpPackageName() under BluetoothPan#setBluetoothTethering() to get
+ // caller's package name for permission check.
+ // Calling BluetoothPan#setBluetoothTethering() here means the package name always
+ // be system server. If calling identity is not cleared, that package's uid might
+ // not match calling uid and end up in permission denied.
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ ((BluetoothPan) proxy).setBluetoothTethering(enable);
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ // TODO: Enabling bluetooth tethering can fail asynchronously here.
+ // We should figure out a way to bubble up that failure instead of sending success.
+ final int result = (((BluetoothPan) proxy).isTetheringOn() == enable)
+ ? TETHER_ERROR_NO_ERROR
+ : TETHER_ERROR_MASTER_ERROR;
+ sendTetherResult(receiver, result);
+ adapter.closeProfileProxy(BluetoothProfile.PAN, proxy);
+ }
+ }, BluetoothProfile.PAN);
+ }
+
+ int tether(String iface) {
+ return tether(iface, IpServer.STATE_TETHERED);
+ }
+
+ private int tether(String iface, int requestedState) {
+ if (DBG) Log.d(TAG, "Tethering " + iface);
+ synchronized (mPublicSync) {
+ TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState == null) {
+ Log.e(TAG, "Tried to Tether an unknown iface: " + iface + ", ignoring");
+ return TETHER_ERROR_UNKNOWN_IFACE;
+ }
+ // Ignore the error status of the interface. If the interface is available,
+ // the errors are referring to past tethering attempts anyway.
+ if (tetherState.lastState != IpServer.STATE_AVAILABLE) {
+ Log.e(TAG, "Tried to Tether an unavailable iface: " + iface + ", ignoring");
+ return TETHER_ERROR_UNAVAIL_IFACE;
+ }
+ // 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.ipServer.sendMessage(IpServer.CMD_TETHER_REQUESTED, requestedState);
+ return TETHER_ERROR_NO_ERROR;
+ }
+ }
+
+ int untether(String iface) {
+ if (DBG) Log.d(TAG, "Untethering " + iface);
+ synchronized (mPublicSync) {
+ TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState == null) {
+ Log.e(TAG, "Tried to Untether an unknown iface :" + iface + ", ignoring");
+ return TETHER_ERROR_UNKNOWN_IFACE;
+ }
+ if (!tetherState.isCurrentlyServing()) {
+ Log.e(TAG, "Tried to untether an inactive iface :" + iface + ", ignoring");
+ return TETHER_ERROR_UNAVAIL_IFACE;
+ }
+ tetherState.ipServer.sendMessage(IpServer.CMD_TETHER_UNREQUESTED);
+ return TETHER_ERROR_NO_ERROR;
+ }
+ }
+
+ void untetherAll() {
+ stopTethering(TETHERING_WIFI);
+ stopTethering(TETHERING_WIFI_P2P);
+ stopTethering(TETHERING_USB);
+ stopTethering(TETHERING_BLUETOOTH);
+ }
+
+ int getLastTetherError(String iface) {
+ synchronized (mPublicSync) {
+ TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState == null) {
+ Log.e(TAG, "Tried to getLastTetherError on an unknown iface :" + iface
+ + ", ignoring");
+ return TETHER_ERROR_UNKNOWN_IFACE;
+ }
+ return tetherState.lastError;
+ }
+ }
+
+ // TODO: Figure out how to update for local hotspot mode interfaces.
+ private void sendTetherStateChangedBroadcast() {
+ if (!mDeps.isTetheringSupported()) return;
+
+ final ArrayList<String> availableList = new ArrayList<>();
+ final ArrayList<String> tetherList = new ArrayList<>();
+ final ArrayList<String> localOnlyList = new ArrayList<>();
+ final ArrayList<String> erroredList = new ArrayList<>();
+ final ArrayList<Integer> lastErrorList = new ArrayList<>();
+
+ boolean wifiTethered = false;
+ boolean usbTethered = false;
+ boolean bluetoothTethered = false;
+
+ final TetheringConfiguration cfg = mConfig;
+ final TetherStatesParcel mTetherStatesParcel = new TetherStatesParcel();
+
+ synchronized (mPublicSync) {
+ for (int i = 0; i < mTetherStates.size(); i++) {
+ TetherState tetherState = mTetherStates.valueAt(i);
+ String iface = mTetherStates.keyAt(i);
+ if (tetherState.lastError != TETHER_ERROR_NO_ERROR) {
+ erroredList.add(iface);
+ lastErrorList.add(tetherState.lastError);
+ } else if (tetherState.lastState == IpServer.STATE_AVAILABLE) {
+ availableList.add(iface);
+ } else if (tetherState.lastState == IpServer.STATE_LOCAL_ONLY) {
+ localOnlyList.add(iface);
+ } else if (tetherState.lastState == IpServer.STATE_TETHERED) {
+ if (cfg.isUsb(iface)) {
+ usbTethered = true;
+ } else if (cfg.isWifi(iface)) {
+ wifiTethered = true;
+ } else if (cfg.isBluetooth(iface)) {
+ bluetoothTethered = true;
+ }
+ tetherList.add(iface);
+ }
+ }
+ }
+
+ mTetherStatesParcel.availableList = availableList.toArray(new String[0]);
+ mTetherStatesParcel.tetheredList = tetherList.toArray(new String[0]);
+ mTetherStatesParcel.localOnlyList = localOnlyList.toArray(new String[0]);
+ mTetherStatesParcel.erroredIfaceList = erroredList.toArray(new String[0]);
+ mTetherStatesParcel.lastErrorList = new int[lastErrorList.size()];
+ Iterator<Integer> iterator = lastErrorList.iterator();
+ for (int i = 0; i < lastErrorList.size(); i++) {
+ mTetherStatesParcel.lastErrorList[i] = iterator.next().intValue();
+ }
+ reportTetherStateChanged(mTetherStatesParcel);
+
+ final Intent bcast = new Intent(ACTION_TETHER_STATE_CHANGED);
+ bcast.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
+ | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ bcast.putStringArrayListExtra(EXTRA_AVAILABLE_TETHER, availableList);
+ bcast.putStringArrayListExtra(EXTRA_ACTIVE_LOCAL_ONLY, localOnlyList);
+ bcast.putStringArrayListExtra(EXTRA_ACTIVE_TETHER, tetherList);
+ bcast.putStringArrayListExtra(EXTRA_ERRORED_TETHER, erroredList);
+ mContext.sendStickyBroadcastAsUser(bcast, UserHandle.ALL);
+ if (DBG) {
+ Log.d(TAG, String.format(
+ "sendTetherStateChangedBroadcast %s=[%s] %s=[%s] %s=[%s] %s=[%s]",
+ "avail", TextUtils.join(",", availableList),
+ "local_only", TextUtils.join(",", localOnlyList),
+ "tether", TextUtils.join(",", tetherList),
+ "error", TextUtils.join(",", erroredList)));
+ }
+
+ if (usbTethered) {
+ if (wifiTethered || bluetoothTethered) {
+ showTetheredNotification(SystemMessage.NOTE_TETHER_GENERAL);
+ } else {
+ showTetheredNotification(SystemMessage.NOTE_TETHER_USB);
+ }
+ } else if (wifiTethered) {
+ if (bluetoothTethered) {
+ showTetheredNotification(SystemMessage.NOTE_TETHER_GENERAL);
+ } else {
+ /* We now have a status bar icon for WifiTethering, so drop the notification */
+ clearTetheredNotification();
+ }
+ } else if (bluetoothTethered) {
+ showTetheredNotification(SystemMessage.NOTE_TETHER_BLUETOOTH);
+ } else {
+ clearTetheredNotification();
+ }
+ }
+
+ private void showTetheredNotification(int id) {
+ showTetheredNotification(id, true);
+ }
+
+ @VisibleForTesting
+ protected void showTetheredNotification(int id, boolean tetheringOn) {
+ NotificationManager notificationManager =
+ (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ if (notificationManager == null) {
+ return;
+ }
+ int icon = 0;
+ switch(id) {
+ case SystemMessage.NOTE_TETHER_USB:
+ icon = com.android.internal.R.drawable.stat_sys_tether_usb;
+ break;
+ case SystemMessage.NOTE_TETHER_BLUETOOTH:
+ icon = com.android.internal.R.drawable.stat_sys_tether_bluetooth;
+ break;
+ case SystemMessage.NOTE_TETHER_GENERAL:
+ default:
+ icon = com.android.internal.R.drawable.stat_sys_tether_general;
+ break;
+ }
+
+ if (mLastNotificationId != 0) {
+ if (mLastNotificationId == icon) {
+ return;
+ }
+ notificationManager.cancelAsUser(null, mLastNotificationId,
+ UserHandle.ALL);
+ mLastNotificationId = 0;
+ }
+
+ Intent intent = new Intent();
+ intent.setClassName("com.android.settings", "com.android.settings.TetherSettings");
+ intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
+
+ PendingIntent pi = PendingIntent.getActivityAsUser(mContext, 0, intent, 0,
+ null, UserHandle.CURRENT);
+
+ Resources r = Resources.getSystem();
+ final CharSequence title;
+ final CharSequence message;
+
+ if (tetheringOn) {
+ title = r.getText(com.android.internal.R.string.tethered_notification_title);
+ message = r.getText(com.android.internal.R.string.tethered_notification_message);
+ } else {
+ title = r.getText(com.android.internal.R.string.disable_tether_notification_title);
+ message = r.getText(com.android.internal.R.string.disable_tether_notification_message);
+ }
+
+ if (mTetheredNotificationBuilder == null) {
+ mTetheredNotificationBuilder =
+ new Notification.Builder(mContext, SystemNotificationChannels.NETWORK_STATUS);
+ mTetheredNotificationBuilder.setWhen(0)
+ .setOngoing(true)
+ .setColor(mContext.getColor(
+ com.android.internal.R.color.system_notification_accent_color))
+ .setVisibility(Notification.VISIBILITY_PUBLIC)
+ .setCategory(Notification.CATEGORY_STATUS);
+ }
+ mTetheredNotificationBuilder.setSmallIcon(icon)
+ .setContentTitle(title)
+ .setContentText(message)
+ .setContentIntent(pi);
+ mLastNotificationId = id;
+
+ notificationManager.notifyAsUser(null, mLastNotificationId,
+ mTetheredNotificationBuilder.buildInto(new Notification()), UserHandle.ALL);
+ }
+
+ @VisibleForTesting
+ protected void clearTetheredNotification() {
+ NotificationManager notificationManager =
+ (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ if (notificationManager != null && mLastNotificationId != 0) {
+ notificationManager.cancelAsUser(null, mLastNotificationId,
+ UserHandle.ALL);
+ mLastNotificationId = 0;
+ }
+ }
+
+ private class StateReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context content, Intent intent) {
+ final String action = intent.getAction();
+ if (action == null) return;
+
+ if (action.equals(UsbManager.ACTION_USB_STATE)) {
+ handleUsbAction(intent);
+ } else if (action.equals(CONNECTIVITY_ACTION)) {
+ handleConnectivityAction(intent);
+ } else if (action.equals(WifiManager.WIFI_AP_STATE_CHANGED_ACTION)) {
+ handleWifiApAction(intent);
+ } else if (action.equals(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)) {
+ handleWifiP2pAction(intent);
+ } else if (action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
+ mLog.log("OBSERVED configuration changed");
+ updateConfiguration();
+ }
+ }
+
+ private void handleConnectivityAction(Intent intent) {
+ final NetworkInfo networkInfo =
+ (NetworkInfo) intent.getParcelableExtra(EXTRA_NETWORK_INFO);
+ if (networkInfo == null
+ || networkInfo.getDetailedState() == NetworkInfo.DetailedState.FAILED) {
+ return;
+ }
+
+ if (VDBG) Log.d(TAG, "Tethering got CONNECTIVITY_ACTION: " + networkInfo.toString());
+ mTetherMasterSM.sendMessage(TetherMasterSM.CMD_UPSTREAM_CHANGED);
+ }
+
+ private void handleUsbAction(Intent intent) {
+ final boolean usbConnected = intent.getBooleanExtra(USB_CONNECTED, false);
+ final boolean usbConfigured = intent.getBooleanExtra(USB_CONFIGURED, false);
+ final boolean rndisEnabled = intent.getBooleanExtra(USB_FUNCTION_RNDIS, false);
+
+ mLog.log(String.format("USB bcast connected:%s configured:%s rndis:%s",
+ usbConnected, usbConfigured, rndisEnabled));
+
+ // There are three types of ACTION_USB_STATE:
+ //
+ // - DISCONNECTED (USB_CONNECTED and USB_CONFIGURED are 0)
+ // Meaning: USB connection has ended either because of
+ // software reset or hard unplug.
+ //
+ // - CONNECTED (USB_CONNECTED is 1, USB_CONFIGURED is 0)
+ // Meaning: the first stage of USB protocol handshake has
+ // occurred but it is not complete.
+ //
+ // - CONFIGURED (USB_CONNECTED and USB_CONFIGURED are 1)
+ // Meaning: the USB handshake is completely done and all the
+ // functions are ready to use.
+ //
+ // For more explanation, see b/62552150 .
+ synchronized (Tethering.this.mPublicSync) {
+ if (!usbConnected && mRndisEnabled) {
+ // Turn off tethering if it was enabled and there is a disconnect.
+ tetherMatchingInterfaces(IpServer.STATE_AVAILABLE, TETHERING_USB);
+ mEntitlementMgr.stopProvisioningIfNeeded(TETHERING_USB);
+ } else if (usbConfigured && rndisEnabled) {
+ // Tether if rndis is enabled and usb is configured.
+ tetherMatchingInterfaces(IpServer.STATE_TETHERED, TETHERING_USB);
+ }
+ mRndisEnabled = usbConfigured && rndisEnabled;
+ }
+ }
+
+ private void handleWifiApAction(Intent intent) {
+ final int curState = intent.getIntExtra(EXTRA_WIFI_AP_STATE, WIFI_AP_STATE_DISABLED);
+ final String ifname = intent.getStringExtra(EXTRA_WIFI_AP_INTERFACE_NAME);
+ final int ipmode = intent.getIntExtra(EXTRA_WIFI_AP_MODE, IFACE_IP_MODE_UNSPECIFIED);
+
+ synchronized (Tethering.this.mPublicSync) {
+ switch (curState) {
+ case WifiManager.WIFI_AP_STATE_ENABLING:
+ // We can see this state on the way to both enabled and failure states.
+ break;
+ case WifiManager.WIFI_AP_STATE_ENABLED:
+ enableWifiIpServingLocked(ifname, ipmode);
+ break;
+ case WifiManager.WIFI_AP_STATE_DISABLED:
+ case WifiManager.WIFI_AP_STATE_DISABLING:
+ case WifiManager.WIFI_AP_STATE_FAILED:
+ default:
+ disableWifiIpServingLocked(ifname, curState);
+ break;
+ }
+ }
+ }
+
+ private void handleWifiP2pAction(Intent intent) {
+ if (mConfig.isWifiP2pLegacyTetheringMode()) return;
+
+ final WifiP2pInfo p2pInfo =
+ (WifiP2pInfo) intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO);
+ final WifiP2pGroup group =
+ (WifiP2pGroup) intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP);
+
+ if (VDBG) {
+ Log.d(TAG, "WifiP2pAction: P2pInfo: " + p2pInfo + " Group: " + group);
+ }
+
+ if (p2pInfo == null) return;
+ // When a p2p group is disconnected, p2pInfo would be cleared.
+ // group is still valid for detecting whether this device is group owner.
+ if (group == null || !group.isGroupOwner()
+ || TextUtils.isEmpty(group.getInterface())) return;
+
+ synchronized (Tethering.this.mPublicSync) {
+ // Enter below only if this device is Group Owner with a valid interface.
+ if (p2pInfo.groupFormed) {
+ TetherState tetherState = mTetherStates.get(group.getInterface());
+ if (tetherState == null
+ || (tetherState.lastState != IpServer.STATE_TETHERED
+ && tetherState.lastState != IpServer.STATE_LOCAL_ONLY)) {
+ enableWifiIpServingLocked(group.getInterface(), IFACE_IP_MODE_LOCAL_ONLY);
+ }
+ } else {
+ disableWifiP2pIpServingLocked(group.getInterface());
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ protected static class TetheringUserRestrictionListener implements UserRestrictionsListener {
+ private final Tethering mWrapper;
+
+ public TetheringUserRestrictionListener(Tethering wrapper) {
+ mWrapper = wrapper;
+ }
+
+ public void onUserRestrictionsChanged(int userId,
+ Bundle newRestrictions,
+ Bundle prevRestrictions) {
+ final boolean newlyDisallowed =
+ newRestrictions.getBoolean(UserManager.DISALLOW_CONFIG_TETHERING);
+ final boolean previouslyDisallowed =
+ prevRestrictions.getBoolean(UserManager.DISALLOW_CONFIG_TETHERING);
+ final boolean tetheringDisallowedChanged = (newlyDisallowed != previouslyDisallowed);
+
+ if (!tetheringDisallowedChanged) {
+ return;
+ }
+
+ mWrapper.clearTetheredNotification();
+ final boolean isTetheringActiveOnDevice = (mWrapper.getTetheredIfaces().length != 0);
+
+ if (newlyDisallowed && isTetheringActiveOnDevice) {
+ mWrapper.showTetheredNotification(
+ com.android.internal.R.drawable.stat_sys_tether_general, false);
+ mWrapper.untetherAll();
+ }
+ }
+ }
+
+ private void disableWifiIpServingLockedCommon(int tetheringType, String ifname, int apState) {
+ mLog.log("Canceling WiFi tethering request -"
+ + " type=" + tetheringType
+ + " interface=" + ifname
+ + " state=" + apState);
+
+ if (!TextUtils.isEmpty(ifname)) {
+ final TetherState ts = mTetherStates.get(ifname);
+ if (ts != null) {
+ ts.ipServer.unwanted();
+ return;
+ }
+ }
+
+ for (int i = 0; i < mTetherStates.size(); i++) {
+ final IpServer ipServer = mTetherStates.valueAt(i).ipServer;
+ if (ipServer.interfaceType() == tetheringType) {
+ ipServer.unwanted();
+ return;
+ }
+ }
+
+ mLog.log("Error disabling Wi-Fi IP serving; "
+ + (TextUtils.isEmpty(ifname) ? "no interface name specified"
+ : "specified interface: " + ifname));
+ }
+
+ private void disableWifiIpServingLocked(String ifname, int apState) {
+ // Regardless of whether we requested this transition, the AP has gone
+ // down. Don't try to tether again unless we're requested to do so.
+ // TODO: Remove this altogether, once Wi-Fi reliably gives us an
+ // interface name with every broadcast.
+ mWifiTetherRequested = false;
+
+ disableWifiIpServingLockedCommon(TETHERING_WIFI, ifname, apState);
+ }
+
+ private void disableWifiP2pIpServingLocked(String ifname) {
+ disableWifiIpServingLockedCommon(TETHERING_WIFI_P2P, ifname, /* dummy */ 0);
+ }
+
+ private void enableWifiIpServingLocked(String ifname, int wifiIpMode) {
+ // Map wifiIpMode values to IpServer.Callback serving states, inferring
+ // from mWifiTetherRequested as a final "best guess".
+ final int ipServingMode;
+ switch (wifiIpMode) {
+ case IFACE_IP_MODE_TETHERED:
+ ipServingMode = IpServer.STATE_TETHERED;
+ break;
+ case IFACE_IP_MODE_LOCAL_ONLY:
+ ipServingMode = IpServer.STATE_LOCAL_ONLY;
+ break;
+ default:
+ mLog.e("Cannot enable IP serving in unknown WiFi mode: " + wifiIpMode);
+ return;
+ }
+
+ if (!TextUtils.isEmpty(ifname)) {
+ maybeTrackNewInterfaceLocked(ifname);
+ changeInterfaceState(ifname, ipServingMode);
+ } else {
+ mLog.e(String.format(
+ "Cannot enable IP serving in mode %s on missing interface name",
+ ipServingMode));
+ }
+ }
+
+ // 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 {
+ ifaces = mNMService.listInterfaces();
+ } catch (Exception e) {
+ Log.e(TAG, "Error listing Interfaces", e);
+ return;
+ }
+ String chosenIface = null;
+ if (ifaces != null) {
+ for (String iface : ifaces) {
+ if (ifaceNameToType(iface) == interfaceType) {
+ chosenIface = iface;
+ break;
+ }
+ }
+ }
+ if (chosenIface == null) {
+ Log.e(TAG, "could not find iface of type " + interfaceType);
+ return;
+ }
+
+ changeInterfaceState(chosenIface, requestedState);
+ }
+
+ private void changeInterfaceState(String ifname, int requestedState) {
+ final int result;
+ switch (requestedState) {
+ case IpServer.STATE_UNAVAILABLE:
+ case IpServer.STATE_AVAILABLE:
+ result = untether(ifname);
+ break;
+ case IpServer.STATE_TETHERED:
+ case IpServer.STATE_LOCAL_ONLY:
+ result = tether(ifname, requestedState);
+ break;
+ default:
+ Log.wtf(TAG, "Unknown interface state: " + requestedState);
+ return;
+ }
+ if (result != TETHER_ERROR_NO_ERROR) {
+ Log.e(TAG, "unable start or stop tethering on iface " + ifname);
+ return;
+ }
+ }
+
+ TetheringConfiguration getTetheringConfiguration() {
+ return mConfig;
+ }
+
+ boolean hasTetherableConfiguration() {
+ final TetheringConfiguration cfg = mConfig;
+ final boolean hasDownstreamConfiguration =
+ (cfg.tetherableUsbRegexs.length != 0)
+ || (cfg.tetherableWifiRegexs.length != 0)
+ || (cfg.tetherableBluetoothRegexs.length != 0);
+ final boolean hasUpstreamConfiguration = !cfg.preferredUpstreamIfaceTypes.isEmpty()
+ || cfg.chooseUpstreamAutomatically;
+
+ return hasDownstreamConfiguration && hasUpstreamConfiguration;
+ }
+
+ // TODO - update callers to use getTetheringConfiguration(),
+ // which has only final members.
+ String[] getTetherableUsbRegexs() {
+ return copy(mConfig.tetherableUsbRegexs);
+ }
+
+ String[] getTetherableWifiRegexs() {
+ return copy(mConfig.tetherableWifiRegexs);
+ }
+
+ String[] getTetherableBluetoothRegexs() {
+ return copy(mConfig.tetherableBluetoothRegexs);
+ }
+
+ int setUsbTethering(boolean enable) {
+ if (VDBG) Log.d(TAG, "setUsbTethering(" + enable + ")");
+ UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE);
+ if (usbManager == null) {
+ mLog.e("setUsbTethering: failed to get UsbManager!");
+ return TETHER_ERROR_SERVICE_UNAVAIL;
+ }
+
+ synchronized (mPublicSync) {
+ usbManager.setCurrentFunctions(enable ? UsbManager.FUNCTION_RNDIS
+ : UsbManager.FUNCTION_NONE);
+ }
+ return TETHER_ERROR_NO_ERROR;
+ }
+
+ // TODO review API - figure out how to delete these entirely.
+ String[] getTetheredIfaces() {
+ ArrayList<String> list = new ArrayList<String>();
+ synchronized (mPublicSync) {
+ for (int i = 0; i < mTetherStates.size(); i++) {
+ TetherState tetherState = mTetherStates.valueAt(i);
+ if (tetherState.lastState == IpServer.STATE_TETHERED) {
+ list.add(mTetherStates.keyAt(i));
+ }
+ }
+ }
+ return list.toArray(new String[list.size()]);
+ }
+
+ String[] getTetherableIfaces() {
+ ArrayList<String> list = new ArrayList<String>();
+ synchronized (mPublicSync) {
+ for (int i = 0; i < mTetherStates.size(); i++) {
+ TetherState tetherState = mTetherStates.valueAt(i);
+ if (tetherState.lastState == IpServer.STATE_AVAILABLE) {
+ list.add(mTetherStates.keyAt(i));
+ }
+ }
+ }
+ return list.toArray(new String[list.size()]);
+ }
+
+ String[] getTetheredDhcpRanges() {
+ // TODO: this is only valid for the old DHCP server. Latest search suggests it is only used
+ // by WifiP2pServiceImpl to start dnsmasq: remove/deprecate after migrating callers.
+ return mConfig.legacyDhcpRanges;
+ }
+
+ String[] getErroredIfaces() {
+ ArrayList<String> list = new ArrayList<String>();
+ synchronized (mPublicSync) {
+ for (int i = 0; i < mTetherStates.size(); i++) {
+ TetherState tetherState = mTetherStates.valueAt(i);
+ if (tetherState.lastError != TETHER_ERROR_NO_ERROR) {
+ list.add(mTetherStates.keyAt(i));
+ }
+ }
+ }
+ return list.toArray(new String[list.size()]);
+ }
+
+ private void logMessage(State state, int what) {
+ mLog.log(state.getName() + " got " + sMagicDecoderRing.get(what, Integer.toString(what)));
+ }
+
+ private boolean upstreamWanted() {
+ if (!mForwardedDownstreams.isEmpty()) return true;
+
+ synchronized (mPublicSync) {
+ return mWifiTetherRequested;
+ }
+ }
+
+ // Needed because the canonical source of upstream truth is just the
+ // upstream interface set, |mCurrentUpstreamIfaceSet|.
+ private boolean pertainsToCurrentUpstream(NetworkState ns) {
+ if (ns != null && ns.linkProperties != null && mCurrentUpstreamIfaceSet != null) {
+ for (String ifname : ns.linkProperties.getAllInterfaceNames()) {
+ if (mCurrentUpstreamIfaceSet.ifnames.contains(ifname)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ class TetherMasterSM extends StateMachine {
+ private static final int BASE_MASTER = Protocol.BASE_TETHERING;
+ // 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
+ static final int CMD_RETRY_UPSTREAM = BASE_MASTER + 4;
+ // Events from NetworkCallbacks that we process on the master state
+ // machine thread on behalf of the UpstreamNetworkMonitor.
+ static final int EVENT_UPSTREAM_CALLBACK = BASE_MASTER + 5;
+ // we treated the error and want now to clear it
+ static final int CMD_CLEAR_ERROR = BASE_MASTER + 6;
+ static final int EVENT_IFACE_UPDATE_LINKPROPERTIES = BASE_MASTER + 7;
+ // Events from EntitlementManager to choose upstream again.
+ static final int EVENT_UPSTREAM_PERMISSION_CHANGED = BASE_MASTER + 8;
+
+ private final State mInitialState;
+ private final State mTetherModeAliveState;
+
+ private final State mSetIpForwardingEnabledErrorState;
+ private final State mSetIpForwardingDisabledErrorState;
+ private final State mStartTetheringErrorState;
+ private final State mStopTetheringErrorState;
+ private final State mSetDnsForwardersErrorState;
+
+ // This list is a little subtle. It contains all the interfaces that currently are
+ // requesting tethering, regardless of whether these interfaces are still members of
+ // mTetherStates. This allows us to maintain the following predicates:
+ //
+ // 1) mTetherStates contains the set of all currently existing, tetherable, link state up
+ // interfaces.
+ // 2) mNotifyList contains all state machines that may have outstanding tethering state
+ // that needs to be torn down.
+ //
+ // Because we excise interfaces immediately from mTetherStates, we must maintain mNotifyList
+ // so that the garbage collector does not clean up the state machine before it has a chance
+ // to tear itself down.
+ private final ArrayList<IpServer> mNotifyList;
+ private final IPv6TetheringCoordinator mIPv6TetheringCoordinator;
+ private final OffloadWrapper mOffload;
+
+ private static final int UPSTREAM_SETTLE_TIME_MS = 10000;
+
+ TetherMasterSM(String name, Looper looper, TetheringDependencies deps) {
+ super(name, looper);
+
+ mInitialState = new InitialState();
+ mTetherModeAliveState = new TetherModeAliveState();
+ mSetIpForwardingEnabledErrorState = new SetIpForwardingEnabledErrorState();
+ mSetIpForwardingDisabledErrorState = new SetIpForwardingDisabledErrorState();
+ mStartTetheringErrorState = new StartTetheringErrorState();
+ mStopTetheringErrorState = new StopTetheringErrorState();
+ mSetDnsForwardersErrorState = new SetDnsForwardersErrorState();
+
+ addState(mInitialState);
+ addState(mTetherModeAliveState);
+ addState(mSetIpForwardingEnabledErrorState);
+ addState(mSetIpForwardingDisabledErrorState);
+ addState(mStartTetheringErrorState);
+ addState(mStopTetheringErrorState);
+ addState(mSetDnsForwardersErrorState);
+
+ mNotifyList = new ArrayList<>();
+ mIPv6TetheringCoordinator = deps.getIPv6TetheringCoordinator(mNotifyList, mLog);
+ mOffload = new OffloadWrapper();
+
+ setInitialState(mInitialState);
+ }
+
+ class InitialState extends State {
+ @Override
+ public boolean processMessage(Message message) {
+ logMessage(this, message.what);
+ switch (message.what) {
+ case EVENT_IFACE_SERVING_STATE_ACTIVE: {
+ final IpServer who = (IpServer) message.obj;
+ if (VDBG) Log.d(TAG, "Tether Mode requested by " + who);
+ handleInterfaceServingStateActive(message.arg1, who);
+ transitionTo(mTetherModeAliveState);
+ break;
+ }
+ case EVENT_IFACE_SERVING_STATE_INACTIVE: {
+ final IpServer who = (IpServer) message.obj;
+ if (VDBG) Log.d(TAG, "Tether Mode unrequested by " + who);
+ handleInterfaceServingStateInactive(who);
+ break;
+ }
+ case EVENT_IFACE_UPDATE_LINKPROPERTIES:
+ // Silently ignore these for now.
+ break;
+ default:
+ return NOT_HANDLED;
+ }
+ return HANDLED;
+ }
+ }
+
+ protected boolean turnOnMasterTetherSettings() {
+ final TetheringConfiguration cfg = mConfig;
+ try {
+ mNMService.setIpForwardingEnabled(true);
+ } catch (Exception e) {
+ mLog.e(e);
+ transitionTo(mSetIpForwardingEnabledErrorState);
+ return false;
+ }
+ // TODO: Randomize DHCPv4 ranges, especially in hotspot mode.
+ // Legacy DHCP server is disabled if passed an empty ranges array
+ final String[] dhcpRanges = cfg.enableLegacyDhcpServer
+ ? cfg.legacyDhcpRanges
+ : new String[0];
+ try {
+ // TODO: Find a more accurate method name (startDHCPv4()?).
+ mNMService.startTethering(dhcpRanges);
+ } catch (Exception e) {
+ try {
+ mNMService.stopTethering();
+ mNMService.startTethering(dhcpRanges);
+ } catch (Exception ee) {
+ mLog.e(ee);
+ transitionTo(mStartTetheringErrorState);
+ return false;
+ }
+ }
+ mLog.log("SET master tether settings: ON");
+ return true;
+ }
+
+ protected boolean turnOffMasterTetherSettings() {
+ try {
+ mNMService.stopTethering();
+ } catch (Exception e) {
+ mLog.e(e);
+ transitionTo(mStopTetheringErrorState);
+ return false;
+ }
+ try {
+ mNMService.setIpForwardingEnabled(false);
+ } catch (Exception e) {
+ mLog.e(e);
+ transitionTo(mSetIpForwardingDisabledErrorState);
+ return false;
+ }
+ transitionTo(mInitialState);
+ mLog.log("SET master tether settings: OFF");
+ return true;
+ }
+
+ protected void chooseUpstreamType(boolean tryCell) {
+ // We rebuild configuration on ACTION_CONFIGURATION_CHANGED, but we
+ // do not currently know how to watch for changes in DUN settings.
+ maybeDunSettingChanged();
+
+ final TetheringConfiguration config = mConfig;
+ final NetworkState ns = (config.chooseUpstreamAutomatically)
+ ? mUpstreamNetworkMonitor.getCurrentPreferredUpstream()
+ : mUpstreamNetworkMonitor.selectPreferredUpstreamType(
+ config.preferredUpstreamIfaceTypes);
+ if (ns == null) {
+ if (tryCell) {
+ mUpstreamNetworkMonitor.registerMobileNetworkRequest();
+ // We think mobile should be coming up; don't set a retry.
+ } else {
+ sendMessageDelayed(CMD_RETRY_UPSTREAM, UPSTREAM_SETTLE_TIME_MS);
+ }
+ }
+ setUpstreamNetwork(ns);
+ final Network newUpstream = (ns != null) ? ns.network : null;
+ if (mTetherUpstream != newUpstream) {
+ mTetherUpstream = newUpstream;
+ mUpstreamNetworkMonitor.setCurrentUpstream(mTetherUpstream);
+ reportUpstreamChanged(mTetherUpstream);
+ }
+ }
+
+ protected void setUpstreamNetwork(NetworkState ns) {
+ InterfaceSet ifaces = null;
+ if (ns != null) {
+ // Find the interface with the default IPv4 route. It may be the
+ // interface described by linkProperties, or one of the interfaces
+ // stacked on top of it.
+ mLog.i("Looking for default routes on: " + ns.linkProperties);
+ ifaces = TetheringInterfaceUtils.getTetheringInterfaces(ns);
+ mLog.i("Found upstream interface(s): " + ifaces);
+ }
+
+ if (ifaces != null) {
+ setDnsForwarders(ns.network, ns.linkProperties);
+ }
+ notifyDownstreamsOfNewUpstreamIface(ifaces);
+ if (ns != null && pertainsToCurrentUpstream(ns)) {
+ // If we already have NetworkState for this network update it immediately.
+ handleNewUpstreamNetworkState(ns);
+ } else if (mCurrentUpstreamIfaceSet == null) {
+ // There are no available upstream networks.
+ handleNewUpstreamNetworkState(null);
+ }
+ }
+
+ protected void setDnsForwarders(final Network network, final LinkProperties lp) {
+ // TODO: Set v4 and/or v6 DNS per available connectivity.
+ String[] dnsServers = mConfig.defaultIPv4DNS;
+ final Collection<InetAddress> dnses = lp.getDnsServers();
+ // TODO: Properly support the absence of DNS servers.
+ if (dnses != null && !dnses.isEmpty()) {
+ // TODO: remove this invocation of NetworkUtils.makeStrings().
+ dnsServers = NetworkUtils.makeStrings(dnses);
+ }
+ try {
+ mNMService.setDnsForwarders(network, dnsServers);
+ mLog.log(String.format(
+ "SET DNS forwarders: network=%s dnsServers=%s",
+ network, Arrays.toString(dnsServers)));
+ } catch (Exception e) {
+ // TODO: Investigate how this can fail and what exactly
+ // happens if/when such failures occur.
+ mLog.e("setting DNS forwarders failed, " + e);
+ transitionTo(mSetDnsForwardersErrorState);
+ }
+ }
+
+ protected void notifyDownstreamsOfNewUpstreamIface(InterfaceSet ifaces) {
+ mCurrentUpstreamIfaceSet = ifaces;
+ for (IpServer ipServer : mNotifyList) {
+ ipServer.sendMessage(IpServer.CMD_TETHER_CONNECTION_CHANGED, ifaces);
+ }
+ }
+
+ protected void handleNewUpstreamNetworkState(NetworkState ns) {
+ mIPv6TetheringCoordinator.updateUpstreamNetworkState(ns);
+ mOffload.updateUpstreamNetworkState(ns);
+ }
+
+ private void handleInterfaceServingStateActive(int mode, IpServer who) {
+ if (mNotifyList.indexOf(who) < 0) {
+ mNotifyList.add(who);
+ mIPv6TetheringCoordinator.addActiveDownstream(who, mode);
+ }
+
+ if (mode == IpServer.STATE_TETHERED) {
+ // No need to notify OffloadController just yet as there are no
+ // "offload-able" prefixes to pass along. This will handled
+ // when the TISM informs Tethering of its LinkProperties.
+ mForwardedDownstreams.add(who);
+ } else {
+ mOffload.excludeDownstreamInterface(who.interfaceName());
+ mForwardedDownstreams.remove(who);
+ }
+
+ // If this is a Wi-Fi interface, notify WifiManager of the active serving state.
+ if (who.interfaceType() == TETHERING_WIFI) {
+ final WifiManager mgr = getWifiManager();
+ final String iface = who.interfaceName();
+ switch (mode) {
+ case IpServer.STATE_TETHERED:
+ mgr.updateInterfaceIpState(iface, IFACE_IP_MODE_TETHERED);
+ break;
+ case IpServer.STATE_LOCAL_ONLY:
+ mgr.updateInterfaceIpState(iface, IFACE_IP_MODE_LOCAL_ONLY);
+ break;
+ default:
+ Log.wtf(TAG, "Unknown active serving mode: " + mode);
+ break;
+ }
+ }
+ }
+
+ private void handleInterfaceServingStateInactive(IpServer who) {
+ mNotifyList.remove(who);
+ mIPv6TetheringCoordinator.removeActiveDownstream(who);
+ mOffload.excludeDownstreamInterface(who.interfaceName());
+ mForwardedDownstreams.remove(who);
+
+ // If this is a Wi-Fi interface, tell WifiManager of any errors
+ // or the inactive serving state.
+ if (who.interfaceType() == TETHERING_WIFI) {
+ if (who.lastError() != TETHER_ERROR_NO_ERROR) {
+ getWifiManager().updateInterfaceIpState(
+ who.interfaceName(), IFACE_IP_MODE_CONFIGURATION_ERROR);
+ } else {
+ getWifiManager().updateInterfaceIpState(
+ who.interfaceName(), IFACE_IP_MODE_UNSPECIFIED);
+ }
+ }
+ }
+
+ private void handleUpstreamNetworkMonitorCallback(int arg1, Object o) {
+ if (arg1 == UpstreamNetworkMonitor.NOTIFY_LOCAL_PREFIXES) {
+ mOffload.sendOffloadExemptPrefixes((Set<IpPrefix>) o);
+ return;
+ }
+
+ final NetworkState ns = (NetworkState) o;
+
+ if (ns == null || !pertainsToCurrentUpstream(ns)) {
+ // TODO: In future, this is where upstream evaluation and selection
+ // could be handled for notifications which include sufficient data.
+ // For example, after CONNECTIVITY_ACTION listening is removed, here
+ // is where we could observe a Wi-Fi network becoming available and
+ // passing validation.
+ if (mCurrentUpstreamIfaceSet == null) {
+ // If we have no upstream interface, try to run through upstream
+ // selection again. If, for example, IPv4 connectivity has shown up
+ // after IPv6 (e.g., 464xlat became available) we want the chance to
+ // notice and act accordingly.
+ chooseUpstreamType(false);
+ }
+ return;
+ }
+
+ switch (arg1) {
+ case UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES:
+ handleNewUpstreamNetworkState(ns);
+ break;
+ case UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES:
+ chooseUpstreamType(false);
+ break;
+ case UpstreamNetworkMonitor.EVENT_ON_LOST:
+ // TODO: Re-evaluate possible upstreams. Currently upstream
+ // reevaluation is triggered via received CONNECTIVITY_ACTION
+ // broadcasts that result in being passed a
+ // TetherMasterSM.CMD_UPSTREAM_CHANGED.
+ handleNewUpstreamNetworkState(null);
+ break;
+ default:
+ mLog.e("Unknown arg1 value: " + arg1);
+ break;
+ }
+ }
+
+ class TetherModeAliveState extends State {
+ boolean mUpstreamWanted = false;
+ boolean mTryCell = true;
+
+ @Override
+ public void enter() {
+ // If turning on master tether settings fails, we have already
+ // transitioned to an error state; exit early.
+ if (!turnOnMasterTetherSettings()) {
+ return;
+ }
+
+ mUpstreamNetworkMonitor.startObserveAllNetworks();
+
+ // TODO: De-duplicate with updateUpstreamWanted() below.
+ if (upstreamWanted()) {
+ mUpstreamWanted = true;
+ mOffload.start();
+ chooseUpstreamType(true);
+ mTryCell = false;
+ }
+ }
+
+ @Override
+ public void exit() {
+ mOffload.stop();
+ mUpstreamNetworkMonitor.stop();
+ notifyDownstreamsOfNewUpstreamIface(null);
+ handleNewUpstreamNetworkState(null);
+ if (mTetherUpstream != null) {
+ mTetherUpstream = null;
+ reportUpstreamChanged(null);
+ }
+ }
+
+ private boolean updateUpstreamWanted() {
+ final boolean previousUpstreamWanted = mUpstreamWanted;
+ mUpstreamWanted = upstreamWanted();
+ if (mUpstreamWanted != previousUpstreamWanted) {
+ if (mUpstreamWanted) {
+ mOffload.start();
+ } else {
+ mOffload.stop();
+ }
+ }
+ return previousUpstreamWanted;
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ logMessage(this, message.what);
+ boolean retValue = true;
+ switch (message.what) {
+ case EVENT_IFACE_SERVING_STATE_ACTIVE: {
+ IpServer who = (IpServer) message.obj;
+ if (VDBG) Log.d(TAG, "Tether Mode requested by " + who);
+ handleInterfaceServingStateActive(message.arg1, who);
+ who.sendMessage(IpServer.CMD_TETHER_CONNECTION_CHANGED,
+ mCurrentUpstreamIfaceSet);
+ // 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 EVENT_IFACE_SERVING_STATE_INACTIVE: {
+ IpServer who = (IpServer) message.obj;
+ if (VDBG) Log.d(TAG, "Tether Mode unrequested by " + who);
+ handleInterfaceServingStateInactive(who);
+
+ if (mNotifyList.isEmpty()) {
+ // This transitions us out of TetherModeAliveState,
+ // either to InitialState or an error state.
+ turnOffMasterTetherSettings();
+ break;
+ }
+
+ if (DBG) {
+ Log.d(TAG, "TetherModeAlive still has " + mNotifyList.size()
+ + " live requests:");
+ for (IpServer o : mNotifyList) {
+ Log.d(TAG, " " + o);
+ }
+ }
+ // 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 EVENT_IFACE_UPDATE_LINKPROPERTIES: {
+ final LinkProperties newLp = (LinkProperties) message.obj;
+ if (message.arg1 == IpServer.STATE_TETHERED) {
+ mOffload.updateDownstreamLinkProperties(newLp);
+ } else {
+ mOffload.excludeDownstreamInterface(newLp.getInterfaceName());
+ }
+ break;
+ }
+ case EVENT_UPSTREAM_PERMISSION_CHANGED:
+ 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) {
+ handleUpstreamNetworkMonitorCallback(message.arg1, message.obj);
+ }
+ break;
+ }
+ default:
+ retValue = false;
+ break;
+ }
+ return retValue;
+ }
+ }
+
+ class ErrorState extends State {
+ private int mErrorNotification;
+
+ @Override
+ public boolean processMessage(Message message) {
+ boolean retValue = true;
+ switch (message.what) {
+ case EVENT_IFACE_SERVING_STATE_ACTIVE:
+ IpServer who = (IpServer) message.obj;
+ who.sendMessage(mErrorNotification);
+ break;
+ case CMD_CLEAR_ERROR:
+ mErrorNotification = TETHER_ERROR_NO_ERROR;
+ transitionTo(mInitialState);
+ break;
+ default:
+ retValue = false;
+ }
+ return retValue;
+ }
+
+ void notify(int msgType) {
+ mErrorNotification = msgType;
+ for (IpServer ipServer : mNotifyList) {
+ ipServer.sendMessage(msgType);
+ }
+ }
+
+ }
+
+ class SetIpForwardingEnabledErrorState extends ErrorState {
+ @Override
+ public void enter() {
+ Log.e(TAG, "Error in setIpForwardingEnabled");
+ notify(IpServer.CMD_IP_FORWARDING_ENABLE_ERROR);
+ }
+ }
+
+ class SetIpForwardingDisabledErrorState extends ErrorState {
+ @Override
+ public void enter() {
+ Log.e(TAG, "Error in setIpForwardingDisabled");
+ notify(IpServer.CMD_IP_FORWARDING_DISABLE_ERROR);
+ }
+ }
+
+ class StartTetheringErrorState extends ErrorState {
+ @Override
+ public void enter() {
+ Log.e(TAG, "Error in startTethering");
+ notify(IpServer.CMD_START_TETHERING_ERROR);
+ try {
+ mNMService.setIpForwardingEnabled(false);
+ } catch (Exception e) { }
+ }
+ }
+
+ class StopTetheringErrorState extends ErrorState {
+ @Override
+ public void enter() {
+ Log.e(TAG, "Error in stopTethering");
+ notify(IpServer.CMD_STOP_TETHERING_ERROR);
+ try {
+ mNMService.setIpForwardingEnabled(false);
+ } catch (Exception e) { }
+ }
+ }
+
+ class SetDnsForwardersErrorState extends ErrorState {
+ @Override
+ public void enter() {
+ Log.e(TAG, "Error in setDnsForwarders");
+ notify(IpServer.CMD_SET_DNS_FORWARDERS_ERROR);
+ try {
+ mNMService.stopTethering();
+ } catch (Exception e) { }
+ try {
+ mNMService.setIpForwardingEnabled(false);
+ } catch (Exception e) { }
+ }
+ }
+
+ // A wrapper class to handle multiple situations where several calls to
+ // the OffloadController need to happen together.
+ //
+ // TODO: This suggests that the interface between OffloadController and
+ // Tethering is in need of improvement. Refactor these calls into the
+ // OffloadController implementation.
+ class OffloadWrapper {
+ public void start() {
+ mOffloadController.start();
+ sendOffloadExemptPrefixes();
+ }
+
+ public void stop() {
+ mOffloadController.stop();
+ }
+
+ public void updateUpstreamNetworkState(NetworkState ns) {
+ mOffloadController.setUpstreamLinkProperties(
+ (ns != null) ? ns.linkProperties : null);
+ }
+
+ public void updateDownstreamLinkProperties(LinkProperties newLp) {
+ // Update the list of offload-exempt prefixes before adding
+ // new prefixes on downstream interfaces to the offload HAL.
+ sendOffloadExemptPrefixes();
+ mOffloadController.notifyDownstreamLinkProperties(newLp);
+ }
+
+ public void excludeDownstreamInterface(String ifname) {
+ // This and other interfaces may be in local-only hotspot mode;
+ // resend all local prefixes to the OffloadController.
+ sendOffloadExemptPrefixes();
+ mOffloadController.removeDownstreamInterface(ifname);
+ }
+
+ public void sendOffloadExemptPrefixes() {
+ sendOffloadExemptPrefixes(mUpstreamNetworkMonitor.getLocalPrefixes());
+ }
+
+ public void sendOffloadExemptPrefixes(final Set<IpPrefix> localPrefixes) {
+ // Add in well-known minimum set.
+ PrefixUtils.addNonForwardablePrefixes(localPrefixes);
+ // Add tragically hardcoded prefixes.
+ localPrefixes.add(PrefixUtils.DEFAULT_WIFI_P2P_PREFIX);
+
+ // Maybe add prefixes or addresses for downstreams, depending on
+ // the IP serving mode of each.
+ for (IpServer ipServer : mNotifyList) {
+ final LinkProperties lp = ipServer.linkProperties();
+
+ switch (ipServer.servingMode()) {
+ case IpServer.STATE_UNAVAILABLE:
+ case IpServer.STATE_AVAILABLE:
+ // No usable LinkProperties in these states.
+ continue;
+ case IpServer.STATE_TETHERED:
+ // Only add IPv4 /32 and IPv6 /128 prefixes. The
+ // directly-connected prefixes will be sent as
+ // downstream "offload-able" prefixes.
+ for (LinkAddress addr : lp.getAllLinkAddresses()) {
+ final InetAddress ip = addr.getAddress();
+ if (ip.isLinkLocalAddress()) continue;
+ localPrefixes.add(PrefixUtils.ipAddressAsPrefix(ip));
+ }
+ break;
+ case IpServer.STATE_LOCAL_ONLY:
+ // Add prefixes covering all local IPs.
+ localPrefixes.addAll(PrefixUtils.localPrefixesFrom(lp));
+ break;
+ }
+ }
+
+ mOffloadController.setLocalPrefixes(localPrefixes);
+ }
+ }
+ }
+
+ private void startTrackDefaultNetwork() {
+ mUpstreamNetworkMonitor.startTrackDefaultNetwork(mDeps.getDefaultNetworkRequest(),
+ mEntitlementMgr);
+ }
+
+ /** Get the latest value of the tethering entitlement check. */
+ void requestLatestTetheringEntitlementResult(int type, ResultReceiver receiver,
+ boolean showEntitlementUi) {
+ if (receiver != null) {
+ mEntitlementMgr.requestLatestTetheringEntitlementResult(type, receiver,
+ showEntitlementUi);
+ }
+ }
+
+ /** Register tethering event callback */
+ void registerTetherInternalCallback(ITetherInternalCallback callback) {
+ mHandler.post(() -> {
+ mTetherInternalCallback = callback;
+ try {
+ mTetherInternalCallback.onCallbackCreated(mTetherUpstream,
+ mConfig.toStableParcelable(), mTetherStatesParcel);
+ } catch (RemoteException e) {
+ // Not really very much to do here.
+ }
+ });
+ }
+
+ private void reportUpstreamChanged(Network network) {
+ // Don't need to synchronized mTetherInternalCallback because all the usage of this variable
+ // should run at the same thread.
+ if (mTetherInternalCallback == null) return;
+
+ try {
+ mTetherInternalCallback.onUpstreamChanged(network);
+ } catch (RemoteException e) {
+ // Not really very much to do here.
+ }
+ }
+
+ private void reportConfigurationChanged(TetheringConfigurationParcel config) {
+ if (mTetherInternalCallback == null) return;
+
+ try {
+ mTetherInternalCallback.onConfigurationChanged(config);
+ } catch (RemoteException e) {
+ // Not really very much to do here.
+ }
+ }
+
+ private void reportTetherStateChanged(TetherStatesParcel states) {
+ if (mTetherInternalCallback == null) return;
+
+ try {
+ mTetherInternalCallback.onTetherStatesChanged(states);
+ } catch (RemoteException e) {
+ // Not really very much to do here.
+ }
+ }
+
+ void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ // Binder.java closes the resource for us.
+ @SuppressWarnings("resource")
+ final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
+ if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+
+ pw.println("Tethering:");
+ pw.increaseIndent();
+
+ pw.println("Configuration:");
+ pw.increaseIndent();
+ final TetheringConfiguration cfg = mConfig;
+ cfg.dump(pw);
+ pw.decreaseIndent();
+
+ pw.println("Entitlement:");
+ pw.increaseIndent();
+ mEntitlementMgr.dump(pw);
+ pw.decreaseIndent();
+
+ synchronized (mPublicSync) {
+ pw.println("Tether state:");
+ pw.increaseIndent();
+ for (int i = 0; i < mTetherStates.size(); i++) {
+ final String iface = mTetherStates.keyAt(i);
+ final TetherState tetherState = mTetherStates.valueAt(i);
+ pw.print(iface + " - ");
+
+ switch (tetherState.lastState) {
+ case IpServer.STATE_UNAVAILABLE:
+ pw.print("UnavailableState");
+ break;
+ case IpServer.STATE_AVAILABLE:
+ pw.print("AvailableState");
+ break;
+ case IpServer.STATE_TETHERED:
+ pw.print("TetheredState");
+ break;
+ case IpServer.STATE_LOCAL_ONLY:
+ pw.print("LocalHotspotState");
+ break;
+ default:
+ pw.print("UnknownState");
+ break;
+ }
+ pw.println(" - lastError = " + tetherState.lastError);
+ }
+ pw.println("Upstream wanted: " + upstreamWanted());
+ pw.println("Current upstream interface(s): " + mCurrentUpstreamIfaceSet);
+ pw.decreaseIndent();
+ }
+
+ pw.println("Hardware offload:");
+ pw.increaseIndent();
+ mOffloadController.dump(pw);
+ pw.decreaseIndent();
+
+ pw.println("Log:");
+ pw.increaseIndent();
+ if (argsContain(args, "--short")) {
+ pw.println("<log removed for brevity>");
+ } else {
+ mLog.dump(fd, pw, args);
+ }
+ pw.decreaseIndent();
+
+ pw.decreaseIndent();
+ }
+
+ private static boolean argsContain(String[] args, String target) {
+ for (String arg : args) {
+ if (target.equals(arg)) return true;
+ }
+ return false;
+ }
+
+ private IpServer.Callback makeControlCallback() {
+ return new IpServer.Callback() {
+ @Override
+ public void updateInterfaceState(IpServer who, int state, int lastError) {
+ notifyInterfaceStateChange(who, state, lastError);
+ }
+
+ @Override
+ public void updateLinkProperties(IpServer who, LinkProperties newLp) {
+ notifyLinkPropertiesChanged(who, newLp);
+ }
+ };
+ }
+
+ // TODO: Move into TetherMasterSM.
+ private void notifyInterfaceStateChange(IpServer who, int state, int error) {
+ final String iface = who.interfaceName();
+ synchronized (mPublicSync) {
+ final TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState != null && tetherState.ipServer.equals(who)) {
+ tetherState.lastState = state;
+ tetherState.lastError = error;
+ } else {
+ if (DBG) Log.d(TAG, "got notification from stale iface " + iface);
+ }
+ }
+
+ mLog.log(String.format("OBSERVED iface=%s state=%s error=%s", iface, state, error));
+
+ try {
+ // Notify that we're tethering (or not) this interface.
+ // This is how data saver for instance knows if the user explicitly
+ // turned on tethering (thus keeping us from being in data saver mode).
+ mPolicyManager.onTetheringChanged(iface, state == IpServer.STATE_TETHERED);
+ } catch (RemoteException e) {
+ // Not really very much we can do here.
+ }
+
+ // If TetherMasterSM is in ErrorState, TetherMasterSM stays there.
+ // Thus we give a chance for TetherMasterSM to recover to InitialState
+ // by sending CMD_CLEAR_ERROR
+ if (error == TETHER_ERROR_MASTER_ERROR) {
+ mTetherMasterSM.sendMessage(TetherMasterSM.CMD_CLEAR_ERROR, who);
+ }
+ int which;
+ switch (state) {
+ case IpServer.STATE_UNAVAILABLE:
+ case IpServer.STATE_AVAILABLE:
+ which = TetherMasterSM.EVENT_IFACE_SERVING_STATE_INACTIVE;
+ break;
+ case IpServer.STATE_TETHERED:
+ case IpServer.STATE_LOCAL_ONLY:
+ which = TetherMasterSM.EVENT_IFACE_SERVING_STATE_ACTIVE;
+ break;
+ default:
+ Log.wtf(TAG, "Unknown interface state: " + state);
+ return;
+ }
+ mTetherMasterSM.sendMessage(which, state, 0, who);
+ sendTetherStateChangedBroadcast();
+ }
+
+ private void notifyLinkPropertiesChanged(IpServer who, LinkProperties newLp) {
+ final String iface = who.interfaceName();
+ final int state;
+ synchronized (mPublicSync) {
+ final TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState != null && tetherState.ipServer.equals(who)) {
+ state = tetherState.lastState;
+ } else {
+ mLog.log("got notification from stale iface " + iface);
+ return;
+ }
+ }
+
+ mLog.log(String.format(
+ "OBSERVED LinkProperties update iface=%s state=%s lp=%s",
+ iface, IpServer.getStateString(state), newLp));
+ final int which = TetherMasterSM.EVENT_IFACE_UPDATE_LINKPROPERTIES;
+ mTetherMasterSM.sendMessage(which, state, 0, newLp);
+ }
+
+ private void maybeTrackNewInterfaceLocked(final String iface) {
+ // If we don't care about this type of interface, ignore.
+ final int interfaceType = ifaceNameToType(iface);
+ if (interfaceType == TETHERING_INVALID) {
+ mLog.log(iface + " is not a tetherable iface, ignoring");
+ return;
+ }
+ maybeTrackNewInterfaceLocked(iface, interfaceType);
+ }
+
+ private void maybeTrackNewInterfaceLocked(final String iface, int interfaceType) {
+ // If we have already started a TISM for this interface, skip.
+ if (mTetherStates.containsKey(iface)) {
+ mLog.log("active iface (" + iface + ") reported as added, ignoring");
+ return;
+ }
+
+ mLog.log("adding TetheringInterfaceStateMachine for: " + iface);
+ final TetherState tetherState = new TetherState(
+ new IpServer(iface, mLooper, interfaceType, mLog, mNMService, mStatsService,
+ makeControlCallback(), mConfig.enableLegacyDhcpServer,
+ mDeps.getIpServerDependencies()));
+ mTetherStates.put(iface, tetherState);
+ tetherState.ipServer.start();
+ }
+
+ private void stopTrackingInterfaceLocked(final String iface) {
+ final TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState == null) {
+ mLog.log("attempting to remove unknown iface (" + iface + "), ignoring");
+ return;
+ }
+ tetherState.ipServer.stop();
+ mLog.log("removing TetheringInterfaceStateMachine for: " + iface);
+ mTetherStates.remove(iface);
+ }
+
+ private static String[] copy(String[] strarray) {
+ return Arrays.copyOf(strarray, strarray.length);
+ }
+}
diff --git a/Tethering/src/com/android/server/connectivity/tethering/TetheringConfiguration.java b/Tethering/src/com/android/server/connectivity/tethering/TetheringConfiguration.java
index ca9b168..0ab4d63 100644
--- a/Tethering/src/com/android/server/connectivity/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/server/connectivity/tethering/TetheringConfiguration.java
@@ -38,6 +38,7 @@
import android.content.Context;
import android.content.res.Resources;
import android.net.ConnectivityManager;
+import android.net.TetheringConfigurationParcel;
import android.net.util.SharedLog;
import android.provider.Settings;
import android.telephony.SubscriptionManager;
@@ -384,4 +385,32 @@
}
return false;
}
+
+ /**
+ * Convert this TetheringConfiguration to a TetheringConfigurationParcel.
+ */
+ public TetheringConfigurationParcel toStableParcelable() {
+ final TetheringConfigurationParcel parcel = new TetheringConfigurationParcel();
+ parcel.subId = subId;
+ parcel.tetherableUsbRegexs = tetherableUsbRegexs;
+ parcel.tetherableWifiRegexs = tetherableWifiRegexs;
+ parcel.tetherableBluetoothRegexs = tetherableBluetoothRegexs;
+ parcel.isDunRequired = isDunRequired;
+ parcel.chooseUpstreamAutomatically = chooseUpstreamAutomatically;
+
+ int[] preferredTypes = new int[preferredUpstreamIfaceTypes.size()];
+ int index = 0;
+ for (Integer type : preferredUpstreamIfaceTypes) {
+ preferredTypes[index++] = type;
+ }
+ parcel.preferredUpstreamIfaceTypes = preferredTypes;
+
+ parcel.legacyDhcpRanges = legacyDhcpRanges;
+ parcel.defaultIPv4DNS = defaultIPv4DNS;
+ parcel.enableLegacyDhcpServer = enableLegacyDhcpServer;
+ parcel.provisioningApp = provisioningApp;
+ parcel.provisioningAppNoUi = provisioningAppNoUi;
+ parcel.provisioningCheckPeriod = provisioningCheckPeriod;
+ return parcel;
+ }
}
diff --git a/Tethering/src/com/android/server/connectivity/tethering/TetheringDependencies.java b/Tethering/src/com/android/server/connectivity/tethering/TetheringDependencies.java
new file mode 100644
index 0000000..0ba8412
--- /dev/null
+++ b/Tethering/src/com/android/server/connectivity/tethering/TetheringDependencies.java
@@ -0,0 +1,151 @@
+/*
+ * 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 com.android.server.connectivity.tethering;
+
+import android.content.Context;
+import android.net.INetd;
+import android.net.INetworkPolicyManager;
+import android.net.INetworkStatsService;
+import android.net.NetworkRequest;
+import android.net.ip.IpServer;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.ServiceManager;
+
+import com.android.internal.util.StateMachine;
+
+import java.util.ArrayList;
+
+
+/**
+ * Capture tethering dependencies, for injection.
+ *
+ * @hide
+ */
+public class TetheringDependencies {
+ /**
+ * Get a reference to the offload hardware interface to be used by tethering.
+ */
+ public OffloadHardwareInterface getOffloadHardwareInterface(Handler h, SharedLog log) {
+ return new OffloadHardwareInterface(h, log);
+ }
+
+ /**
+ * Get a reference to the UpstreamNetworkMonitor to be used by tethering.
+ */
+ public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx, StateMachine target,
+ SharedLog log, int what) {
+ return new UpstreamNetworkMonitor(ctx, target, log, what);
+ }
+
+ /**
+ * Get a reference to the IPv6TetheringCoordinator to be used by tethering.
+ */
+ public IPv6TetheringCoordinator getIPv6TetheringCoordinator(
+ ArrayList<IpServer> notifyList, SharedLog log) {
+ return new IPv6TetheringCoordinator(notifyList, log);
+ }
+
+ /**
+ * Get dependencies to be used by IpServer.
+ */
+ public IpServer.Dependencies getIpServerDependencies() {
+ return new IpServer.Dependencies();
+ }
+
+ /**
+ * Indicates whether tethering is supported on the device.
+ */
+ public boolean isTetheringSupported() {
+ return true;
+ }
+
+ /**
+ * Get the NetworkRequest that should be fulfilled by the default network.
+ */
+ public NetworkRequest getDefaultNetworkRequest() {
+ return null;
+ }
+
+ /**
+ * Get a reference to the EntitlementManager to be used by tethering.
+ */
+ public EntitlementManager getEntitlementManager(Context ctx, StateMachine target,
+ SharedLog log, int what) {
+ return new EntitlementManager(ctx, target, log, what);
+ }
+
+ /**
+ * Generate a new TetheringConfiguration according to input sub Id.
+ */
+ public TetheringConfiguration generateTetheringConfiguration(Context ctx, SharedLog log,
+ int subId) {
+ return new TetheringConfiguration(ctx, log, subId);
+ }
+
+ /**
+ * Get a reference to INetworkManagementService to registerTetheringStatsProvider from
+ * OffloadController. Note: This should be removed soon by Usage refactor work in R
+ * development cycle.
+ */
+ public INetworkManagementService getINetworkManagementService() {
+ return INetworkManagementService.Stub.asInterface(
+ ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE));
+ }
+
+ /**
+ * Get a reference to INetworkStatsService to force update tethering usage.
+ * Note: This should be removed in R development cycle.
+ */
+ public INetworkStatsService getINetworkStatsService() {
+ return INetworkStatsService.Stub.asInterface(
+ ServiceManager.getService(Context.NETWORK_STATS_SERVICE));
+ }
+
+ /**
+ * Get a reference to INetworkPolicyManager to be used by tethering.
+ */
+ public INetworkPolicyManager getINetworkPolicyManager() {
+ return INetworkPolicyManager.Stub.asInterface(
+ ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
+ }
+
+ /**
+ * Get a reference to INetd to be used by tethering.
+ */
+ public INetd getINetd(Context context) {
+ return INetd.Stub.asInterface(
+ (IBinder) context.getSystemService(Context.NETD_SERVICE));
+ }
+
+ /**
+ * Get tethering thread looper.
+ */
+ public Looper getTetheringLooper() {
+ return null;
+ }
+
+ /**
+ * Get Context of TetheringSerice.
+ */
+ public Context getContext() {
+ return null;
+ }
+}
diff --git a/Tethering/src/com/android/server/connectivity/tethering/TetheringInterfaceUtils.java b/Tethering/src/com/android/server/connectivity/tethering/TetheringInterfaceUtils.java
new file mode 100644
index 0000000..0ef3805
--- /dev/null
+++ b/Tethering/src/com/android/server/connectivity/tethering/TetheringInterfaceUtils.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.tethering;
+
+import android.annotation.Nullable;
+import android.net.LinkProperties;
+import android.net.NetworkCapabilities;
+import android.net.NetworkState;
+import android.net.RouteInfo;
+import android.net.util.InterfaceSet;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+
+/**
+ * @hide
+ */
+public final class TetheringInterfaceUtils {
+ /**
+ * Get upstream interfaces for tethering based on default routes for IPv4/IPv6.
+ * @return null if there is no usable interface, or a set of at least one interface otherwise.
+ */
+ public static @Nullable InterfaceSet getTetheringInterfaces(NetworkState ns) {
+ if (ns == null) {
+ return null;
+ }
+
+ final LinkProperties lp = ns.linkProperties;
+ final String if4 = getInterfaceForDestination(lp, Inet4Address.ANY);
+ final String if6 = getIPv6Interface(ns);
+
+ return (if4 == null && if6 == null) ? null : new InterfaceSet(if4, if6);
+ }
+
+ /**
+ * Get the upstream interface for IPv6 tethering.
+ * @return null if there is no usable interface, or the interface name otherwise.
+ */
+ public static @Nullable String getIPv6Interface(NetworkState ns) {
+ // Broadly speaking:
+ //
+ // [1] does the upstream have an IPv6 default route?
+ //
+ // and
+ //
+ // [2] does the upstream have one or more global IPv6 /64s
+ // dedicated to this device?
+ //
+ // In lieu of Prefix Delegation and other evaluation of whether a
+ // prefix may or may not be dedicated to this device, for now just
+ // check whether the upstream is TRANSPORT_CELLULAR. This works
+ // because "[t]he 3GPP network allocates each default bearer a unique
+ // /64 prefix", per RFC 6459, Section 5.2.
+ final boolean canTether =
+ (ns != null) && (ns.network != null)
+ && (ns.linkProperties != null) && (ns.networkCapabilities != null)
+ // At least one upstream DNS server:
+ && ns.linkProperties.hasIpv6DnsServer()
+ // Minimal amount of IPv6 provisioning:
+ && ns.linkProperties.hasGlobalIpv6Address()
+ // Temporary approximation of "dedicated prefix":
+ && ns.networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR);
+
+ return canTether
+ ? getInterfaceForDestination(ns.linkProperties, Inet6Address.ANY)
+ : null;
+ }
+
+ private static String getInterfaceForDestination(LinkProperties lp, InetAddress dst) {
+ final RouteInfo ri = (lp != null)
+ ? RouteInfo.selectBestRoute(lp.getAllRoutes(), dst)
+ : null;
+ return (ri != null) ? ri.getInterface() : null;
+ }
+}
diff --git a/Tethering/src/com/android/server/connectivity/tethering/TetheringService.java b/Tethering/src/com/android/server/connectivity/tethering/TetheringService.java
new file mode 100644
index 0000000..456f2f7
--- /dev/null
+++ b/Tethering/src/com/android/server/connectivity/tethering/TetheringService.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2019 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.server.connectivity.tethering;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.ITetherInternalCallback;
+import android.net.ITetheringConnector;
+import android.net.NetworkRequest;
+import android.net.util.SharedLog;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.os.SystemProperties;
+import android.provider.Settings;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * Android service used to manage tethering.
+ *
+ * <p>The service returns a binder for the system server to communicate with the tethering.
+ */
+public class TetheringService extends Service {
+ private static final String TAG = TetheringService.class.getSimpleName();
+
+ private final SharedLog mLog = new SharedLog(TAG);
+ private TetheringConnector mConnector;
+ private Context mContext;
+ private TetheringDependencies mDeps;
+ private Tethering mTethering;
+
+ @Override
+ public void onCreate() {
+ mLog.mark("onCreate");
+ mDeps = getTetheringDependencies();
+ mContext = mDeps.getContext();
+ mTethering = makeTethering(mDeps);
+ }
+
+ /**
+ * Make a reference to Tethering object.
+ */
+ @VisibleForTesting
+ public Tethering makeTethering(TetheringDependencies deps) {
+ return new Tethering(deps);
+ }
+
+ /**
+ * Create a binder connector for the system server to communicate with the tethering.
+ */
+ private synchronized IBinder makeConnector() {
+ if (mConnector == null) {
+ mConnector = new TetheringConnector(mTethering);
+ }
+ return mConnector;
+ }
+
+ @NonNull
+ @Override
+ public IBinder onBind(Intent intent) {
+ mLog.mark("onBind");
+ return makeConnector();
+ }
+
+ private static class TetheringConnector extends ITetheringConnector.Stub {
+ private final Tethering mService;
+
+ TetheringConnector(Tethering tether) {
+ mService = tether;
+ }
+
+ @Override
+ public void tether(String iface) {
+ mService.tether(iface);
+ }
+
+ @Override
+ public void untether(String iface) {
+ mService.untether(iface);
+ }
+
+ @Override
+ public void setUsbTethering(boolean enable) {
+ mService.setUsbTethering(enable);
+ }
+
+ @Override
+ public void startTethering(int type, ResultReceiver receiver, boolean showProvisioningUi) {
+ mService.startTethering(type, receiver, showProvisioningUi);
+ }
+
+ @Override
+ public void stopTethering(int type) {
+ mService.stopTethering(type);
+ }
+
+ @Override
+ public void requestLatestTetheringEntitlementResult(int type, ResultReceiver receiver,
+ boolean showEntitlementUi) {
+ mService.requestLatestTetheringEntitlementResult(type, receiver, showEntitlementUi);
+ }
+
+ @Override
+ public void registerTetherInternalCallback(ITetherInternalCallback callback) {
+ mService.registerTetherInternalCallback(callback);
+ }
+ }
+
+ @Override
+ protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer,
+ @Nullable String[] args) {
+ mTethering.dump(fd, writer, args);
+ }
+
+ /**
+ * An injection method for testing.
+ */
+ @VisibleForTesting
+ public TetheringDependencies getTetheringDependencies() {
+ if (mDeps == null) {
+ mDeps = new TetheringDependencies() {
+ @Override
+ public NetworkRequest getDefaultNetworkRequest() {
+ ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+ return cm.getDefaultRequest();
+ }
+
+ @Override
+ public Looper getTetheringLooper() {
+ final HandlerThread tetherThread = new HandlerThread("android.tethering");
+ tetherThread.start();
+ return tetherThread.getLooper();
+ }
+
+ @Override
+ public boolean isTetheringSupported() {
+ int defaultVal =
+ SystemProperties.get("ro.tether.denied").equals("true") ? 0 : 1;
+ boolean tetherSupported = Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.TETHER_SUPPORTED, defaultVal) != 0;
+ return tetherSupported;
+ }
+
+ @Override
+ public Context getContext() {
+ return TetheringService.this;
+ }
+ };
+ }
+
+ return mDeps;
+ }
+}
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index 363be18..5b018df 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -18,7 +18,6 @@
name: "TetheringTests",
certificate: "platform",
srcs: [
- ":servicescore-tethering-src",
"src/**/*.java",
],
test_suites: ["device-tests"],
@@ -41,17 +40,3 @@
"libstaticjvmtiagent",
],
}
-
-// This group would be removed when tethering migration is done.
-filegroup {
- name: "tethering-tests-src",
- srcs: [
- "src/com/android/server/connectivity/tethering/EntitlementManagerTest.java",
- "src/com/android/server/connectivity/tethering/OffloadControllerTest.java",
- "src/com/android/server/connectivity/tethering/TetheringConfigurationTest.java",
- "src/com/android/server/connectivity/tethering/UpstreamNetworkMonitorTest.java",
- "src/android/net/dhcp/DhcpServingParamsParcelExtTest.java",
- "src/android/net/ip/IpServerTest.java",
- "src/android/net/util/InterfaceSetTest.java",
- ],
-}
diff --git a/Tethering/tests/unit/src/android/net/util/VersionedBroadcastListenerTest.java b/Tethering/tests/unit/src/android/net/util/VersionedBroadcastListenerTest.java
new file mode 100644
index 0000000..5a9b6e3
--- /dev/null
+++ b/Tethering/tests/unit/src/android/net/util/VersionedBroadcastListenerTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.net.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.reset;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.test.BroadcastInterceptingContext;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VersionedBroadcastListenerTest {
+ private static final String TAG = VersionedBroadcastListenerTest.class.getSimpleName();
+ private static final String ACTION_TEST = "action.test.happy.broadcasts";
+
+ @Mock private Context mContext;
+ private BroadcastInterceptingContext mServiceContext;
+ private Handler mHandler;
+ private VersionedBroadcastListener mListener;
+ private int mCallbackCount;
+
+ private void doCallback() {
+ mCallbackCount++;
+ }
+
+ private class MockContext extends BroadcastInterceptingContext {
+ MockContext(Context base) {
+ super(base);
+ }
+ }
+
+ @BeforeClass
+ public static void setUpBeforeClass() throws Exception {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+ }
+
+ @Before public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ reset(mContext);
+ mServiceContext = new MockContext(mContext);
+ mHandler = new Handler(Looper.myLooper());
+ mCallbackCount = 0;
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(ACTION_TEST);
+ mListener = new VersionedBroadcastListener(
+ TAG, mServiceContext, mHandler, filter, (Intent intent) -> doCallback());
+ }
+
+ @After public void tearDown() throws Exception {
+ if (mListener != null) {
+ mListener.stopListening();
+ mListener = null;
+ }
+ }
+
+ private void sendBroadcast() {
+ final Intent intent = new Intent(ACTION_TEST);
+ mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ }
+
+ @Test
+ public void testBasicListening() {
+ assertEquals(0, mCallbackCount);
+ mListener.startListening();
+ for (int i = 0; i < 5; i++) {
+ sendBroadcast();
+ assertEquals(i + 1, mCallbackCount);
+ }
+ mListener.stopListening();
+ }
+
+ @Test
+ public void testBroadcastsBeforeStartAreIgnored() {
+ assertEquals(0, mCallbackCount);
+ for (int i = 0; i < 5; i++) {
+ sendBroadcast();
+ assertEquals(0, mCallbackCount);
+ }
+
+ mListener.startListening();
+ sendBroadcast();
+ assertEquals(1, mCallbackCount);
+ }
+
+ @Test
+ public void testBroadcastsAfterStopAreIgnored() {
+ mListener.startListening();
+ sendBroadcast();
+ assertEquals(1, mCallbackCount);
+ mListener.stopListening();
+
+ for (int i = 0; i < 5; i++) {
+ sendBroadcast();
+ assertEquals(1, mCallbackCount);
+ }
+ }
+}
diff --git a/Tethering/tests/unit/src/com/android/server/connectivity/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/EntitlementManagerTest.java
index 5217e26..99cf9e9 100644
--- a/Tethering/tests/unit/src/com/android/server/connectivity/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/EntitlementManagerTest.java
@@ -24,6 +24,9 @@
import static android.net.ConnectivityManager.TETHER_ERROR_PROVISION_FAILED;
import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -44,6 +47,7 @@
import android.os.Message;
import android.os.PersistableBundle;
import android.os.ResultReceiver;
+import android.os.SystemProperties;
import android.os.test.TestLooper;
import android.provider.Settings;
import android.telephony.CarrierConfigManager;
@@ -57,7 +61,6 @@
import com.android.internal.util.StateMachine;
import com.android.internal.util.test.BroadcastInterceptingContext;
import com.android.internal.util.test.FakeSettingsProvider;
-import com.android.server.connectivity.MockableSystemProperties;
import org.junit.After;
import org.junit.Before;
@@ -65,6 +68,8 @@
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
@@ -80,7 +85,6 @@
@Mock private CarrierConfigManager mCarrierConfigManager;
@Mock private Context mContext;
- @Mock private MockableSystemProperties mSystemProperties;
@Mock private Resources mResources;
@Mock private SharedLog mLog;
@Mock private EntitlementManager.OnUiEntitlementFailedListener mEntitlementFailedListener;
@@ -95,6 +99,7 @@
private TestStateMachine mSM;
private WrappedEntitlementManager mEnMgr;
private TetheringConfiguration mConfig;
+ private MockitoSession mMockingSession;
private class MockContext extends BroadcastInterceptingContext {
MockContext(Context base) {
@@ -118,8 +123,8 @@
public int silentProvisionCount = 0;
public WrappedEntitlementManager(Context ctx, StateMachine target,
- SharedLog log, int what, MockableSystemProperties systemProperties) {
- super(ctx, target, log, what, systemProperties);
+ SharedLog log, int what) {
+ super(ctx, target, log, what);
}
public void reset() {
@@ -144,6 +149,15 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
+ mMockingSession = mockitoSession()
+ .initMocks(this)
+ .spyStatic(SystemProperties.class)
+ .strictness(Strictness.WARN)
+ .startMocking();
+ // Don't disable tethering provisioning unless requested.
+ doReturn(false).when(
+ () -> SystemProperties.getBoolean(
+ eq(EntitlementManager.DISABLE_PROVISIONING_SYSPROP_KEY), anyBoolean()));
when(mResources.getStringArray(R.array.config_tether_dhcp_range))
.thenReturn(new String[0]);
@@ -161,8 +175,7 @@
mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
mMockContext = new MockContext(mContext);
mSM = new TestStateMachine();
- mEnMgr = new WrappedEntitlementManager(mMockContext, mSM, mLog, EVENT_EM_UPDATE,
- mSystemProperties);
+ mEnMgr = new WrappedEntitlementManager(mMockContext, mSM, mLog, EVENT_EM_UPDATE);
mEnMgr.setOnUiEntitlementFailedListener(mEntitlementFailedListener);
mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
mEnMgr.setTetheringConfigurationFetcher(() -> {
@@ -176,6 +189,7 @@
mSM.quit();
mSM = null;
}
+ mMockingSession.finishMocking();
}
private void setupForRequiredProvisioning() {
@@ -184,9 +198,6 @@
.thenReturn(PROVISIONING_APP_NAME);
when(mResources.getString(R.string.config_mobile_hotspot_provision_app_no_ui))
.thenReturn(PROVISIONING_NO_UI_APP_NAME);
- // Don't disable tethering provisioning unless requested.
- when(mSystemProperties.getBoolean(eq(EntitlementManager.DISABLE_PROVISIONING_SYSPROP_KEY),
- anyBoolean())).thenReturn(false);
// Act like the CarrierConfigManager is present and ready unless told otherwise.
when(mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE))
.thenReturn(mCarrierConfigManager);
@@ -244,7 +255,7 @@
}
@Test
- public void testGetLastEntitlementCacheValue() throws Exception {
+ public void testRequestLastEntitlementCacheValue() throws Exception {
final CountDownLatch mCallbacklatch = new CountDownLatch(1);
// 1. Entitlement check is not required.
mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
@@ -255,7 +266,7 @@
mCallbacklatch.countDown();
}
};
- mEnMgr.getLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
+ mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
mLooper.dispatchAll();
callbackTimeoutHelper(mCallbacklatch);
assertEquals(0, mEnMgr.uiProvisionCount);
@@ -270,7 +281,7 @@
mCallbacklatch.countDown();
}
};
- mEnMgr.getLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
+ mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
mLooper.dispatchAll();
callbackTimeoutHelper(mCallbacklatch);
assertEquals(0, mEnMgr.uiProvisionCount);
@@ -284,7 +295,7 @@
mCallbacklatch.countDown();
}
};
- mEnMgr.getLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
+ mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
mLooper.dispatchAll();
callbackTimeoutHelper(mCallbacklatch);
assertEquals(1, mEnMgr.uiProvisionCount);
@@ -298,7 +309,7 @@
mCallbacklatch.countDown();
}
};
- mEnMgr.getLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
+ mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
mLooper.dispatchAll();
callbackTimeoutHelper(mCallbacklatch);
assertEquals(0, mEnMgr.uiProvisionCount);
@@ -312,7 +323,7 @@
mCallbacklatch.countDown();
}
};
- mEnMgr.getLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
+ mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
mLooper.dispatchAll();
callbackTimeoutHelper(mCallbacklatch);
assertEquals(1, mEnMgr.uiProvisionCount);
@@ -326,7 +337,7 @@
mCallbacklatch.countDown();
}
};
- mEnMgr.getLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
+ mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
mLooper.dispatchAll();
callbackTimeoutHelper(mCallbacklatch);
assertEquals(0, mEnMgr.uiProvisionCount);
@@ -339,7 +350,7 @@
mCallbacklatch.countDown();
}
};
- mEnMgr.getLatestTetheringEntitlementResult(TETHERING_USB, receiver, false);
+ mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_USB, receiver, false);
mLooper.dispatchAll();
callbackTimeoutHelper(mCallbacklatch);
assertEquals(0, mEnMgr.uiProvisionCount);
diff --git a/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java
new file mode 100644
index 0000000..4f6f61b
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java
@@ -0,0 +1,1319 @@
+/*
+ * Copyright (C) 2016 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.server.connectivity.tethering;
+
+import static android.hardware.usb.UsbManager.USB_CONFIGURED;
+import static android.hardware.usb.UsbManager.USB_CONNECTED;
+import static android.hardware.usb.UsbManager.USB_FUNCTION_RNDIS;
+import static android.net.ConnectivityManager.ACTION_TETHER_STATE_CHANGED;
+import static android.net.ConnectivityManager.EXTRA_ACTIVE_LOCAL_ONLY;
+import static android.net.ConnectivityManager.EXTRA_ACTIVE_TETHER;
+import static android.net.ConnectivityManager.EXTRA_AVAILABLE_TETHER;
+import static android.net.ConnectivityManager.TETHERING_USB;
+import static android.net.ConnectivityManager.TETHERING_WIFI;
+import static android.net.ConnectivityManager.TETHER_ERROR_NO_ERROR;
+import static android.net.ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
+import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
+import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME;
+import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_MODE;
+import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_STATE;
+import static android.net.wifi.WifiManager.IFACE_IP_MODE_LOCAL_ONLY;
+import static android.net.wifi.WifiManager.IFACE_IP_MODE_TETHERED;
+import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLED;
+import static android.provider.Settings.Global.TETHER_ENABLE_LEGACY_DHCP_SERVER;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+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.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.res.Resources;
+import android.hardware.usb.UsbManager;
+import android.net.INetd;
+import android.net.INetworkPolicyManager;
+import android.net.INetworkStatsService;
+import android.net.ITetherInternalCallback;
+import android.net.InterfaceConfiguration;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.MacAddress;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkState;
+import android.net.NetworkUtils;
+import android.net.RouteInfo;
+import android.net.TetherStatesParcel;
+import android.net.TetheringConfigurationParcel;
+import android.net.dhcp.DhcpServerCallbacks;
+import android.net.dhcp.DhcpServingParamsParcel;
+import android.net.dhcp.IDhcpServer;
+import android.net.ip.IpServer;
+import android.net.ip.RouterAdvertisementDaemon;
+import android.net.util.InterfaceParams;
+import android.net.util.NetworkConstants;
+import android.net.util.SharedLog;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+import android.net.wifi.p2p.WifiP2pGroup;
+import android.net.wifi.p2p.WifiP2pInfo;
+import android.net.wifi.p2p.WifiP2pManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.test.TestLooper;
+import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.test.mock.MockContentResolver;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.StateMachine;
+import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.internal.util.test.FakeSettingsProvider;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Vector;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class TetheringTest {
+ private static final int IFINDEX_OFFSET = 100;
+
+ private static final String TEST_MOBILE_IFNAME = "test_rmnet_data0";
+ private static final String TEST_XLAT_MOBILE_IFNAME = "v4-test_rmnet_data0";
+ private static final String TEST_USB_IFNAME = "test_rndis0";
+ private static final String TEST_WLAN_IFNAME = "test_wlan0";
+ private static final String TEST_P2P_IFNAME = "test_p2p-p2p0-0";
+
+ private static final int DHCPSERVER_START_TIMEOUT_MS = 1000;
+
+ @Mock private ApplicationInfo mApplicationInfo;
+ @Mock private Context mContext;
+ @Mock private INetworkManagementService mNMService;
+ @Mock private INetworkStatsService mStatsService;
+ @Mock private INetworkPolicyManager mPolicyManager;
+ @Mock private OffloadHardwareInterface mOffloadHardwareInterface;
+ @Mock private Resources mResources;
+ @Mock private TelephonyManager mTelephonyManager;
+ @Mock private UsbManager mUsbManager;
+ @Mock private WifiManager mWifiManager;
+ @Mock private CarrierConfigManager mCarrierConfigManager;
+ @Mock private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
+ @Mock private IPv6TetheringCoordinator mIPv6TetheringCoordinator;
+ @Mock private RouterAdvertisementDaemon mRouterAdvertisementDaemon;
+ @Mock private IDhcpServer mDhcpServer;
+ @Mock private INetd mNetd;
+
+ private final MockIpServerDependencies mIpServerDependencies =
+ spy(new MockIpServerDependencies());
+ private final MockTetheringDependencies mTetheringDependencies =
+ new MockTetheringDependencies();
+
+ // 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 Vector<Intent> mIntents;
+ private BroadcastInterceptingContext mServiceContext;
+ private MockContentResolver mContentResolver;
+ private BroadcastReceiver mBroadcastReceiver;
+ private Tethering mTethering;
+ private PhoneStateListener mPhoneStateListener;
+
+ private class TestContext extends BroadcastInterceptingContext {
+ TestContext(Context base) {
+ super(base);
+ }
+
+ @Override
+ public ApplicationInfo getApplicationInfo() {
+ return mApplicationInfo;
+ }
+
+ @Override
+ public ContentResolver getContentResolver() {
+ return mContentResolver;
+ }
+
+ @Override
+ public String getPackageName() {
+ return "TetheringTest";
+ }
+
+ @Override
+ public Resources getResources() {
+ return mResources;
+ }
+
+ @Override
+ public Object getSystemService(String name) {
+ if (Context.WIFI_SERVICE.equals(name)) return mWifiManager;
+ if (Context.USB_SERVICE.equals(name)) return mUsbManager;
+ if (Context.TELEPHONY_SERVICE.equals(name)) return mTelephonyManager;
+ return super.getSystemService(name);
+ }
+
+ @Override
+ public String getSystemServiceName(Class<?> serviceClass) {
+ if (TelephonyManager.class.equals(serviceClass)) return Context.TELEPHONY_SERVICE;
+ return super.getSystemServiceName(serviceClass);
+ }
+ }
+
+ public class MockIpServerDependencies extends IpServer.Dependencies {
+ @Override
+ public RouterAdvertisementDaemon getRouterAdvertisementDaemon(
+ InterfaceParams ifParams) {
+ return mRouterAdvertisementDaemon;
+ }
+
+ @Override
+ public InterfaceParams getInterfaceParams(String ifName) {
+ assertTrue("Non-mocked interface " + ifName,
+ ifName.equals(TEST_USB_IFNAME)
+ || ifName.equals(TEST_WLAN_IFNAME)
+ || ifName.equals(TEST_MOBILE_IFNAME)
+ || ifName.equals(TEST_P2P_IFNAME));
+ final String[] ifaces = new String[] {
+ TEST_USB_IFNAME, TEST_WLAN_IFNAME, TEST_MOBILE_IFNAME, TEST_P2P_IFNAME};
+ return new InterfaceParams(ifName, ArrayUtils.indexOf(ifaces, ifName) + IFINDEX_OFFSET,
+ MacAddress.ALL_ZEROS_ADDRESS);
+ }
+
+ @Override
+ public INetd getNetdService() {
+ return mNetd;
+ }
+
+ @Override
+ public void makeDhcpServer(String ifName, DhcpServingParamsParcel params,
+ DhcpServerCallbacks cb) {
+ new Thread(() -> {
+ try {
+ cb.onDhcpServerCreated(STATUS_SUCCESS, mDhcpServer);
+ } catch (RemoteException e) {
+ fail(e.getMessage());
+ }
+ }).run();
+ }
+ }
+
+ private class MockTetheringConfiguration extends TetheringConfiguration {
+ MockTetheringConfiguration(Context ctx, SharedLog log, int id) {
+ super(ctx, log, id);
+ }
+
+ @Override
+ protected Resources getResourcesForSubIdWrapper(Context ctx, int subId) {
+ return mResources;
+ }
+ }
+
+ public class MockTetheringDependencies extends TetheringDependencies {
+ StateMachine mUpstreamNetworkMonitorMasterSM;
+ ArrayList<IpServer> mIpv6CoordinatorNotifyList;
+ int mIsTetheringSupportedCalls;
+
+ public void reset() {
+ mUpstreamNetworkMonitorMasterSM = null;
+ mIpv6CoordinatorNotifyList = null;
+ mIsTetheringSupportedCalls = 0;
+ }
+
+ @Override
+ public OffloadHardwareInterface getOffloadHardwareInterface(Handler h, SharedLog log) {
+ return mOffloadHardwareInterface;
+ }
+
+ @Override
+ public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx,
+ StateMachine target, SharedLog log, int what) {
+ mUpstreamNetworkMonitorMasterSM = target;
+ return mUpstreamNetworkMonitor;
+ }
+
+ @Override
+ public IPv6TetheringCoordinator getIPv6TetheringCoordinator(
+ ArrayList<IpServer> notifyList, SharedLog log) {
+ mIpv6CoordinatorNotifyList = notifyList;
+ return mIPv6TetheringCoordinator;
+ }
+
+ @Override
+ public IpServer.Dependencies getIpServerDependencies() {
+ return mIpServerDependencies;
+ }
+
+ @Override
+ public boolean isTetheringSupported() {
+ mIsTetheringSupportedCalls++;
+ return true;
+ }
+
+ @Override
+ public TetheringConfiguration generateTetheringConfiguration(Context ctx, SharedLog log,
+ int subId) {
+ return new MockTetheringConfiguration(ctx, log, subId);
+ }
+
+ @Override
+ public INetworkManagementService getINetworkManagementService() {
+ return mNMService;
+ }
+
+ @Override
+ public INetworkStatsService getINetworkStatsService() {
+ return mStatsService;
+ }
+
+ @Override
+ public INetworkPolicyManager getINetworkPolicyManager() {
+ return mPolicyManager;
+ }
+
+ @Override
+ public INetd getINetd(Context context) {
+ return mNetd;
+ }
+
+ @Override
+ public Looper getTetheringLooper() {
+ return mLooper.getLooper();
+ }
+
+ @Override
+ public Context getContext() {
+ return mServiceContext;
+ }
+ }
+
+ private static NetworkState buildMobileUpstreamState(boolean withIPv4, boolean withIPv6,
+ boolean with464xlat) {
+ final NetworkInfo info = new NetworkInfo(TYPE_MOBILE, 0, null, null);
+ info.setDetailedState(NetworkInfo.DetailedState.CONNECTED, null, null);
+ final LinkProperties prop = new LinkProperties();
+ prop.setInterfaceName(TEST_MOBILE_IFNAME);
+
+ if (withIPv4) {
+ prop.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0),
+ NetworkUtils.numericToInetAddress("10.0.0.1"), TEST_MOBILE_IFNAME));
+ }
+
+ if (withIPv6) {
+ prop.addDnsServer(NetworkUtils.numericToInetAddress("2001:db8::2"));
+ prop.addLinkAddress(
+ new LinkAddress(NetworkUtils.numericToInetAddress("2001:db8::"),
+ NetworkConstants.RFC7421_PREFIX_LENGTH));
+ prop.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0),
+ NetworkUtils.numericToInetAddress("2001:db8::1"), TEST_MOBILE_IFNAME));
+ }
+
+ if (with464xlat) {
+ final LinkProperties stackedLink = new LinkProperties();
+ stackedLink.setInterfaceName(TEST_XLAT_MOBILE_IFNAME);
+ stackedLink.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0),
+ NetworkUtils.numericToInetAddress("192.0.0.1"), TEST_XLAT_MOBILE_IFNAME));
+
+ prop.addStackedLink(stackedLink);
+ }
+
+
+ final NetworkCapabilities capabilities = new NetworkCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
+ return new NetworkState(info, prop, capabilities, new Network(100), null, "netid");
+ }
+
+ private static NetworkState buildMobileIPv4UpstreamState() {
+ return buildMobileUpstreamState(true, false, false);
+ }
+
+ private static NetworkState buildMobileIPv6UpstreamState() {
+ return buildMobileUpstreamState(false, true, false);
+ }
+
+ private static NetworkState buildMobileDualStackUpstreamState() {
+ return buildMobileUpstreamState(true, true, false);
+ }
+
+ private static NetworkState buildMobile464xlatUpstreamState() {
+ return buildMobileUpstreamState(false, true, true);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ 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[] { "test_rndis\\d" });
+ when(mResources.getStringArray(com.android.internal.R.array.config_tether_wifi_regexs))
+ .thenReturn(new String[]{ "test_wlan\\d" });
+ when(mResources.getStringArray(com.android.internal.R.array.config_tether_wifi_p2p_regexs))
+ .thenReturn(new String[]{ "test_p2p-p2p\\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]);
+ when(mResources.getBoolean(com.android.internal.R.bool.config_tether_upstream_automatic))
+ .thenReturn(false);
+ when(mNMService.listInterfaces())
+ .thenReturn(new String[] {
+ TEST_MOBILE_IFNAME, TEST_WLAN_IFNAME, TEST_USB_IFNAME, TEST_P2P_IFNAME});
+ when(mNMService.getInterfaceConfig(anyString()))
+ .thenReturn(new InterfaceConfiguration());
+ when(mRouterAdvertisementDaemon.start())
+ .thenReturn(true);
+
+ mServiceContext = new TestContext(mContext);
+ mContentResolver = new MockContentResolver(mServiceContext);
+ mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+ Settings.Global.putInt(mContentResolver, TETHER_ENABLE_LEGACY_DHCP_SERVER, 0);
+ mIntents = new Vector<>();
+ mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mIntents.addElement(intent);
+ }
+ };
+ mServiceContext.registerReceiver(mBroadcastReceiver,
+ new IntentFilter(ACTION_TETHER_STATE_CHANGED));
+ mTethering = makeTethering();
+ verify(mNMService).registerTetheringStatsProvider(any(), anyString());
+ verify(mNetd).registerUnsolicitedEventListener(any());
+ final ArgumentCaptor<PhoneStateListener> phoneListenerCaptor =
+ ArgumentCaptor.forClass(PhoneStateListener.class);
+ verify(mTelephonyManager).listen(phoneListenerCaptor.capture(),
+ eq(PhoneStateListener.LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE));
+ mPhoneStateListener = phoneListenerCaptor.getValue();
+ }
+
+ private Tethering makeTethering() {
+ mTetheringDependencies.reset();
+ return new Tethering(mTetheringDependencies);
+ }
+
+ @After
+ public void tearDown() {
+ mServiceContext.unregisterReceiver(mBroadcastReceiver);
+ }
+
+ private void sendWifiApStateChanged(int state) {
+ final Intent intent = new Intent(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
+ intent.putExtra(EXTRA_WIFI_AP_STATE, state);
+ mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ }
+
+ private void sendWifiApStateChanged(int state, String ifname, int ipmode) {
+ final Intent intent = new Intent(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
+ intent.putExtra(EXTRA_WIFI_AP_STATE, state);
+ intent.putExtra(EXTRA_WIFI_AP_INTERFACE_NAME, ifname);
+ intent.putExtra(EXTRA_WIFI_AP_MODE, ipmode);
+ mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ }
+
+ private static final String[] P2P_RECEIVER_PERMISSIONS_FOR_BROADCAST = {
+ android.Manifest.permission.ACCESS_FINE_LOCATION,
+ android.Manifest.permission.ACCESS_WIFI_STATE
+ };
+
+ private void sendWifiP2pConnectionChanged(
+ boolean isGroupFormed, boolean isGroupOwner, String ifname) {
+ WifiP2pInfo p2pInfo = new WifiP2pInfo();
+ p2pInfo.groupFormed = isGroupFormed;
+ p2pInfo.isGroupOwner = isGroupOwner;
+
+ NetworkInfo networkInfo = new NetworkInfo(TYPE_WIFI_P2P, 0, null, null);
+
+ WifiP2pGroup group = new WifiP2pGroup();
+ group.setIsGroupOwner(isGroupOwner);
+ group.setInterface(ifname);
+
+ final Intent intent = new Intent(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
+ intent.putExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO, p2pInfo);
+ intent.putExtra(WifiP2pManager.EXTRA_NETWORK_INFO, networkInfo);
+ intent.putExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP, group);
+ mServiceContext.sendBroadcastAsUserMultiplePermissions(intent, UserHandle.ALL,
+ P2P_RECEIVER_PERMISSIONS_FOR_BROADCAST);
+ }
+
+ private void sendUsbBroadcast(boolean connected, boolean configured, boolean rndisFunction) {
+ final Intent intent = new Intent(UsbManager.ACTION_USB_STATE);
+ intent.putExtra(USB_CONNECTED, connected);
+ intent.putExtra(USB_CONFIGURED, configured);
+ intent.putExtra(USB_FUNCTION_RNDIS, rndisFunction);
+ mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ }
+
+ private void sendConfigurationChanged() {
+ final Intent intent = new Intent(Intent.ACTION_CONFIGURATION_CHANGED);
+ mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ }
+
+ private void verifyInterfaceServingModeStarted(String ifname) throws Exception {
+ verify(mNMService, times(1)).getInterfaceConfig(ifname);
+ verify(mNMService, times(1))
+ .setInterfaceConfig(eq(ifname), any(InterfaceConfiguration.class));
+ verify(mNMService, times(1)).tetherInterface(ifname);
+ }
+
+ private void verifyTetheringBroadcast(String ifname, String whichExtra) {
+ // Verify that ifname is in the whichExtra array of the tether state changed broadcast.
+ final Intent bcast = mIntents.get(0);
+ assertEquals(ACTION_TETHER_STATE_CHANGED, bcast.getAction());
+ final ArrayList<String> ifnames = bcast.getStringArrayListExtra(whichExtra);
+ assertTrue(ifnames.contains(ifname));
+ mIntents.remove(bcast);
+ }
+
+ public void failingLocalOnlyHotspotLegacyApBroadcast(
+ boolean emulateInterfaceStatusChanged) throws Exception {
+ // Emulate externally-visible WifiManager effects, causing the
+ // per-interface state machine to start up, and telling us that
+ // hotspot mode is to be started.
+ if (emulateInterfaceStatusChanged) {
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+ }
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED);
+ mLooper.dispatchAll();
+
+ // If, and only if, Tethering received an interface status changed then
+ // it creates a IpServer and sends out a broadcast indicating that the
+ // interface is "available".
+ if (emulateInterfaceStatusChanged) {
+ assertEquals(1, mTetheringDependencies.mIsTetheringSupportedCalls);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ }
+ verifyNoMoreInteractions(mNMService);
+ verifyNoMoreInteractions(mWifiManager);
+ }
+
+ private void prepareUsbTethering(NetworkState upstreamState) {
+ when(mUpstreamNetworkMonitor.getCurrentPreferredUpstream()).thenReturn(upstreamState);
+ when(mUpstreamNetworkMonitor.selectPreferredUpstreamType(any()))
+ .thenReturn(upstreamState);
+
+ // Emulate pressing the USB tethering button in Settings UI.
+ mTethering.startTethering(TETHERING_USB, null, false);
+ mLooper.dispatchAll();
+ verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
+
+ mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true);
+ }
+
+ @Test
+ public void testUsbConfiguredBroadcastStartsTethering() throws Exception {
+ NetworkState upstreamState = buildMobileIPv4UpstreamState();
+ prepareUsbTethering(upstreamState);
+
+ // This should produce no activity of any kind.
+ verifyNoMoreInteractions(mNMService);
+
+ // Pretend we then receive USB configured broadcast.
+ sendUsbBroadcast(true, true, true);
+ mLooper.dispatchAll();
+ // Now we should see the start of tethering mechanics (in this case:
+ // tetherMatchingInterfaces() which starts by fetching all interfaces).
+ verify(mNMService, times(1)).listInterfaces();
+
+ // UpstreamNetworkMonitor should receive selected upstream
+ verify(mUpstreamNetworkMonitor, times(1)).selectPreferredUpstreamType(any());
+ verify(mUpstreamNetworkMonitor, times(1)).setCurrentUpstream(upstreamState.network);
+ }
+
+ @Test
+ public void failingLocalOnlyHotspotLegacyApBroadcastWithIfaceStatusChanged() throws Exception {
+ failingLocalOnlyHotspotLegacyApBroadcast(true);
+ }
+
+ @Test
+ public void failingLocalOnlyHotspotLegacyApBroadcastSansIfaceStatusChanged() throws Exception {
+ failingLocalOnlyHotspotLegacyApBroadcast(false);
+ }
+
+ public void workingLocalOnlyHotspotEnrichedApBroadcast(
+ boolean emulateInterfaceStatusChanged) throws Exception {
+ // Emulate externally-visible WifiManager effects, causing the
+ // per-interface state machine to start up, and telling us that
+ // hotspot mode is to be started.
+ if (emulateInterfaceStatusChanged) {
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+ }
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_LOCAL_ONLY);
+ mLooper.dispatchAll();
+
+ verifyInterfaceServingModeStarted(TEST_WLAN_IFNAME);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+ verify(mNMService, times(1)).setIpForwardingEnabled(true);
+ verify(mNMService, times(1)).startTethering(any(String[].class));
+ verifyNoMoreInteractions(mNMService);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_LOCAL_ONLY);
+ verifyNoMoreInteractions(mWifiManager);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
+ verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks();
+ // This will be called twice, one is on entering IpServer.STATE_AVAILABLE,
+ // and another one is on IpServer.STATE_TETHERED/IpServer.STATE_LOCAL_ONLY.
+ assertEquals(2, mTetheringDependencies.mIsTetheringSupportedCalls);
+
+ // Emulate externally-visible WifiManager effects, when hotspot mode
+ // is being torn down.
+ sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+ mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
+ mLooper.dispatchAll();
+
+ verify(mNMService, times(1)).untetherInterface(TEST_WLAN_IFNAME);
+ // {g,s}etInterfaceConfig() called twice for enabling and disabling IPv4.
+ verify(mNMService, times(2)).getInterfaceConfig(TEST_WLAN_IFNAME);
+ verify(mNMService, times(2))
+ .setInterfaceConfig(eq(TEST_WLAN_IFNAME), any(InterfaceConfiguration.class));
+ verify(mNMService, times(1)).stopTethering();
+ verify(mNMService, times(1)).setIpForwardingEnabled(false);
+ verify(mWifiManager, times(3)).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ verifyNoMoreInteractions(mNMService);
+ verifyNoMoreInteractions(mWifiManager);
+ // Asking for the last error after the per-interface state machine
+ // has been reaped yields an unknown interface error.
+ assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastTetherError(TEST_WLAN_IFNAME));
+ }
+
+ /**
+ * Send CMD_IPV6_TETHER_UPDATE to IpServers as would be done by IPv6TetheringCoordinator.
+ */
+ private void sendIPv6TetherUpdates(NetworkState upstreamState) {
+ // IPv6TetheringCoordinator must have been notified of downstream
+ verify(mIPv6TetheringCoordinator, times(1)).addActiveDownstream(
+ argThat(sm -> sm.linkProperties().getInterfaceName().equals(TEST_USB_IFNAME)),
+ eq(IpServer.STATE_TETHERED));
+
+ for (IpServer ipSrv : mTetheringDependencies.mIpv6CoordinatorNotifyList) {
+ NetworkState ipv6OnlyState = buildMobileUpstreamState(false, true, false);
+ ipSrv.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0,
+ upstreamState.linkProperties.isIpv6Provisioned()
+ ? ipv6OnlyState.linkProperties
+ : null);
+ }
+ mLooper.dispatchAll();
+ }
+
+ private void runUsbTethering(NetworkState upstreamState) {
+ prepareUsbTethering(upstreamState);
+ sendUsbBroadcast(true, true, true);
+ mLooper.dispatchAll();
+ }
+
+ @Test
+ public void workingMobileUsbTethering_IPv4() throws Exception {
+ NetworkState upstreamState = buildMobileIPv4UpstreamState();
+ runUsbTethering(upstreamState);
+
+ verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+
+ sendIPv6TetherUpdates(upstreamState);
+ verify(mRouterAdvertisementDaemon, never()).buildNewRa(any(), notNull());
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+ }
+
+ @Test
+ public void workingMobileUsbTethering_IPv4LegacyDhcp() {
+ Settings.Global.putInt(mContentResolver, TETHER_ENABLE_LEGACY_DHCP_SERVER, 1);
+ sendConfigurationChanged();
+ final NetworkState upstreamState = buildMobileIPv4UpstreamState();
+ runUsbTethering(upstreamState);
+ sendIPv6TetherUpdates(upstreamState);
+
+ verify(mIpServerDependencies, never()).makeDhcpServer(any(), any(), any());
+ }
+
+ @Test
+ public void workingMobileUsbTethering_IPv6() throws Exception {
+ NetworkState upstreamState = buildMobileIPv6UpstreamState();
+ runUsbTethering(upstreamState);
+
+ verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+
+ sendIPv6TetherUpdates(upstreamState);
+ verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull());
+ verify(mNetd, times(1)).tetherApplyDnsInterfaces();
+ }
+
+ @Test
+ public void workingMobileUsbTethering_DualStack() throws Exception {
+ NetworkState upstreamState = buildMobileDualStackUpstreamState();
+ runUsbTethering(upstreamState);
+
+ verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mRouterAdvertisementDaemon, times(1)).start();
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+
+ sendIPv6TetherUpdates(upstreamState);
+ verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull());
+ verify(mNetd, times(1)).tetherApplyDnsInterfaces();
+ }
+
+ @Test
+ public void workingMobileUsbTethering_MultipleUpstreams() throws Exception {
+ NetworkState upstreamState = buildMobile464xlatUpstreamState();
+ runUsbTethering(upstreamState);
+
+ verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME);
+ verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+ verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME,
+ TEST_XLAT_MOBILE_IFNAME);
+
+ sendIPv6TetherUpdates(upstreamState);
+ verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull());
+ verify(mNetd, times(1)).tetherApplyDnsInterfaces();
+ }
+
+ @Test
+ public void workingMobileUsbTethering_v6Then464xlat() throws Exception {
+ // Setup IPv6
+ NetworkState upstreamState = buildMobileIPv6UpstreamState();
+ runUsbTethering(upstreamState);
+
+ verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+ verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+
+ // Then 464xlat comes up
+ upstreamState = buildMobile464xlatUpstreamState();
+ when(mUpstreamNetworkMonitor.selectPreferredUpstreamType(any()))
+ .thenReturn(upstreamState);
+
+ // Upstream LinkProperties changed: UpstreamNetworkMonitor sends EVENT_ON_LINKPROPERTIES.
+ mTetheringDependencies.mUpstreamNetworkMonitorMasterSM.sendMessage(
+ Tethering.TetherMasterSM.EVENT_UPSTREAM_CALLBACK,
+ UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES,
+ 0,
+ upstreamState);
+ mLooper.dispatchAll();
+
+ // Forwarding is added for 464xlat
+ verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME);
+ verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME,
+ TEST_XLAT_MOBILE_IFNAME);
+ // Forwarding was not re-added for v6 (still times(1))
+ verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+ verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+ // DHCP not restarted on downstream (still times(1))
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+ }
+
+ @Test
+ public void configTetherUpstreamAutomaticIgnoresConfigTetherUpstreamTypes() throws Exception {
+ when(mResources.getBoolean(com.android.internal.R.bool.config_tether_upstream_automatic))
+ .thenReturn(true);
+ sendConfigurationChanged();
+
+ // Setup IPv6
+ final NetworkState upstreamState = buildMobileIPv6UpstreamState();
+ runUsbTethering(upstreamState);
+
+ // UpstreamNetworkMonitor should choose upstream automatically
+ // (in this specific case: choose the default network).
+ verify(mUpstreamNetworkMonitor, times(1)).getCurrentPreferredUpstream();
+ verify(mUpstreamNetworkMonitor, never()).selectPreferredUpstreamType(any());
+
+ verify(mUpstreamNetworkMonitor, times(1)).setCurrentUpstream(upstreamState.network);
+ }
+
+ @Test
+ public void workingLocalOnlyHotspotEnrichedApBroadcastWithIfaceChanged() throws Exception {
+ workingLocalOnlyHotspotEnrichedApBroadcast(true);
+ }
+
+ @Test
+ public void workingLocalOnlyHotspotEnrichedApBroadcastSansIfaceChanged() throws Exception {
+ workingLocalOnlyHotspotEnrichedApBroadcast(false);
+ }
+
+ // TODO: Test with and without interfaceStatusChanged().
+ @Test
+ public void failingWifiTetheringLegacyApBroadcast() throws Exception {
+ when(mWifiManager.startSoftAp(any(WifiConfiguration.class))).thenReturn(true);
+
+ // Emulate pressing the WiFi tethering button.
+ mTethering.startTethering(TETHERING_WIFI, null, false);
+ mLooper.dispatchAll();
+ verify(mWifiManager, times(1)).startSoftAp(null);
+ verifyNoMoreInteractions(mWifiManager);
+ 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(TEST_WLAN_IFNAME, true);
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED);
+ mLooper.dispatchAll();
+
+ assertEquals(1, mTetheringDependencies.mIsTetheringSupportedCalls);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ verifyNoMoreInteractions(mNMService);
+ verifyNoMoreInteractions(mWifiManager);
+ }
+
+ // TODO: Test with and without interfaceStatusChanged().
+ @Test
+ public void workingWifiTetheringEnrichedApBroadcast() throws Exception {
+ when(mWifiManager.startSoftAp(any(WifiConfiguration.class))).thenReturn(true);
+
+ // Emulate pressing the WiFi tethering button.
+ mTethering.startTethering(TETHERING_WIFI, null, false);
+ mLooper.dispatchAll();
+ verify(mWifiManager, times(1)).startSoftAp(null);
+ verifyNoMoreInteractions(mWifiManager);
+ 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(TEST_WLAN_IFNAME, true);
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ mLooper.dispatchAll();
+
+ verifyInterfaceServingModeStarted(TEST_WLAN_IFNAME);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+ verify(mNMService, times(1)).setIpForwardingEnabled(true);
+ verify(mNMService, times(1)).startTethering(any(String[].class));
+ verifyNoMoreInteractions(mNMService);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_TETHERED);
+ verifyNoMoreInteractions(mWifiManager);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_ACTIVE_TETHER);
+ verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks();
+ // In tethering mode, in the default configuration, an explicit request
+ // for a mobile network is also made.
+ verify(mUpstreamNetworkMonitor, times(1)).registerMobileNetworkRequest();
+ // This will be called twice, one is on entering IpServer.STATE_AVAILABLE,
+ // and another one is on IpServer.STATE_TETHERED/IpServer.STATE_LOCAL_ONLY.
+ assertEquals(2, mTetheringDependencies.mIsTetheringSupportedCalls);
+
+ /////
+ // 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(TETHERING_WIFI);
+ mLooper.dispatchAll();
+ verify(mWifiManager, times(1)).stopSoftAp();
+ verifyNoMoreInteractions(mWifiManager);
+ verifyNoMoreInteractions(mNMService);
+
+ // Emulate externally-visible WifiManager effects, when tethering mode
+ // is being torn down.
+ sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+ mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
+ mLooper.dispatchAll();
+
+ verify(mNMService, times(1)).untetherInterface(TEST_WLAN_IFNAME);
+ // {g,s}etInterfaceConfig() called twice for enabling and disabling IPv4.
+ verify(mNMService, atLeastOnce()).getInterfaceConfig(TEST_WLAN_IFNAME);
+ verify(mNMService, atLeastOnce())
+ .setInterfaceConfig(eq(TEST_WLAN_IFNAME), any(InterfaceConfiguration.class));
+ verify(mNMService, times(1)).stopTethering();
+ verify(mNMService, times(1)).setIpForwardingEnabled(false);
+ verify(mWifiManager, times(3)).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ verifyNoMoreInteractions(mNMService);
+ verifyNoMoreInteractions(mWifiManager);
+ // Asking for the last error after the per-interface state machine
+ // has been reaped yields an unknown interface error.
+ assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastTetherError(TEST_WLAN_IFNAME));
+ }
+
+ // TODO: Test with and without interfaceStatusChanged().
+ @Test
+ public void failureEnablingIpForwarding() throws Exception {
+ when(mWifiManager.startSoftAp(any(WifiConfiguration.class))).thenReturn(true);
+ doThrow(new RemoteException()).when(mNMService).setIpForwardingEnabled(true);
+
+ // Emulate pressing the WiFi tethering button.
+ mTethering.startTethering(TETHERING_WIFI, null, false);
+ mLooper.dispatchAll();
+ verify(mWifiManager, times(1)).startSoftAp(null);
+ verifyNoMoreInteractions(mWifiManager);
+ 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(TEST_WLAN_IFNAME, true);
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ mLooper.dispatchAll();
+
+ // We verify get/set called thrice here: twice for setup (on NMService) and once during
+ // teardown (on Netd) because all events happen over the course of the single
+ // dispatchAll() above. Note that once the IpServer IPv4 address config
+ // code is refactored the two calls during shutdown will revert to one.
+ verify(mNMService, times(2)).getInterfaceConfig(TEST_WLAN_IFNAME);
+ verify(mNMService, times(2))
+ .setInterfaceConfig(eq(TEST_WLAN_IFNAME), any(InterfaceConfiguration.class));
+ verify(mNetd, times(1)).interfaceSetCfg(argThat(p -> TEST_WLAN_IFNAME.equals(p.ifName)));
+ verify(mNMService, times(1)).tetherInterface(TEST_WLAN_IFNAME);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_TETHERED);
+ // There are 3 state change event:
+ // AVAILABLE -> STATE_TETHERED -> STATE_AVAILABLE.
+ assertEquals(3, mTetheringDependencies.mIsTetheringSupportedCalls);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+ // This is called, but will throw.
+ verify(mNMService, times(1)).setIpForwardingEnabled(true);
+ // This never gets called because of the exception thrown above.
+ verify(mNMService, times(0)).startTethering(any(String[].class));
+ // When the master state machine transitions to an error state it tells
+ // downstream interfaces, which causes us to tell Wi-Fi about the error
+ // so it can take down AP mode.
+ verify(mNMService, times(1)).untetherInterface(TEST_WLAN_IFNAME);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_CONFIGURATION_ERROR);
+
+ verifyNoMoreInteractions(mWifiManager);
+ verifyNoMoreInteractions(mNMService);
+ }
+
+ private void userRestrictionsListenerBehaviour(
+ boolean currentDisallow, boolean nextDisallow, String[] activeTetheringIfacesList,
+ int expectedInteractionsWithShowNotification) throws Exception {
+ final int userId = 0;
+ final Bundle currRestrictions = new Bundle();
+ final Bundle newRestrictions = new Bundle();
+ Tethering tethering = mock(Tethering.class);
+ Tethering.TetheringUserRestrictionListener turl =
+ new Tethering.TetheringUserRestrictionListener(tethering);
+
+ currRestrictions.putBoolean(UserManager.DISALLOW_CONFIG_TETHERING, currentDisallow);
+ newRestrictions.putBoolean(UserManager.DISALLOW_CONFIG_TETHERING, nextDisallow);
+ when(tethering.getTetheredIfaces()).thenReturn(activeTetheringIfacesList);
+
+ turl.onUserRestrictionsChanged(userId, newRestrictions, currRestrictions);
+
+ verify(tethering, times(expectedInteractionsWithShowNotification))
+ .showTetheredNotification(anyInt(), eq(false));
+
+ verify(tethering, times(expectedInteractionsWithShowNotification)).untetherAll();
+ }
+
+ @Test
+ public void testDisallowTetheringWhenNoTetheringInterfaceIsActive() throws Exception {
+ final String[] emptyActiveIfacesList = new String[]{};
+ final boolean currDisallow = false;
+ final boolean nextDisallow = true;
+ final int expectedInteractionsWithShowNotification = 0;
+
+ userRestrictionsListenerBehaviour(currDisallow, nextDisallow, emptyActiveIfacesList,
+ expectedInteractionsWithShowNotification);
+ }
+
+ @Test
+ public void testDisallowTetheringWhenAtLeastOneTetheringInterfaceIsActive() throws Exception {
+ final String[] nonEmptyActiveIfacesList = new String[]{TEST_WLAN_IFNAME};
+ final boolean currDisallow = false;
+ final boolean nextDisallow = true;
+ final int expectedInteractionsWithShowNotification = 1;
+
+ userRestrictionsListenerBehaviour(currDisallow, nextDisallow, nonEmptyActiveIfacesList,
+ expectedInteractionsWithShowNotification);
+ }
+
+ @Test
+ public void testAllowTetheringWhenNoTetheringInterfaceIsActive() throws Exception {
+ final String[] nonEmptyActiveIfacesList = new String[]{};
+ final boolean currDisallow = true;
+ final boolean nextDisallow = false;
+ final int expectedInteractionsWithShowNotification = 0;
+
+ userRestrictionsListenerBehaviour(currDisallow, nextDisallow, nonEmptyActiveIfacesList,
+ expectedInteractionsWithShowNotification);
+ }
+
+ @Test
+ public void testAllowTetheringWhenAtLeastOneTetheringInterfaceIsActive() throws Exception {
+ final String[] nonEmptyActiveIfacesList = new String[]{TEST_WLAN_IFNAME};
+ final boolean currDisallow = true;
+ final boolean nextDisallow = false;
+ final int expectedInteractionsWithShowNotification = 0;
+
+ userRestrictionsListenerBehaviour(currDisallow, nextDisallow, nonEmptyActiveIfacesList,
+ expectedInteractionsWithShowNotification);
+ }
+
+ @Test
+ public void testDisallowTetheringUnchanged() throws Exception {
+ final String[] nonEmptyActiveIfacesList = new String[]{TEST_WLAN_IFNAME};
+ final int expectedInteractionsWithShowNotification = 0;
+ boolean currDisallow = true;
+ boolean nextDisallow = true;
+
+ userRestrictionsListenerBehaviour(currDisallow, nextDisallow, nonEmptyActiveIfacesList,
+ expectedInteractionsWithShowNotification);
+
+ currDisallow = false;
+ nextDisallow = false;
+
+ userRestrictionsListenerBehaviour(currDisallow, nextDisallow, nonEmptyActiveIfacesList,
+ expectedInteractionsWithShowNotification);
+ }
+
+ private class TestTetherInternalCallback extends ITetherInternalCallback.Stub {
+ private final ArrayList<Network> mActualUpstreams = new ArrayList<>();
+ private final ArrayList<TetheringConfigurationParcel> mTetheringConfigs =
+ new ArrayList<>();
+ private final ArrayList<TetherStatesParcel> mTetherStates = new ArrayList<>();
+
+ // This function will remove the recorded callbacks, so it must be called once for
+ // each callback. If this is called after multiple callback, the order matters.
+ // onCallbackCreated counts as the first call to expectUpstreamChanged with
+ // @see onCallbackCreated.
+ public void expectUpstreamChanged(Network... networks) {
+ if (networks == null) {
+ assertNoUpstreamChangeCallback();
+ return;
+ }
+
+ final ArrayList<Network> expectedUpstreams =
+ new ArrayList<Network>(Arrays.asList(networks));
+ for (Network upstream : expectedUpstreams) {
+ // throws OOB if no expectations
+ assertEquals(mActualUpstreams.remove(0), upstream);
+ }
+ assertNoUpstreamChangeCallback();
+ }
+
+ // This function will remove the recorded callbacks, so it must be called once
+ // for each callback. If this is called after multiple callback, the order matters.
+ // onCallbackCreated counts as the first call to onConfigurationChanged with
+ // @see onCallbackCreated.
+ public void expectConfigurationChanged(TetheringConfigurationParcel... tetherConfigs) {
+ final ArrayList<TetheringConfigurationParcel> expectedTetherConfig =
+ new ArrayList<TetheringConfigurationParcel>(Arrays.asList(tetherConfigs));
+ for (TetheringConfigurationParcel config : expectedTetherConfig) {
+ // throws OOB if no expectations
+ final TetheringConfigurationParcel actualConfig = mTetheringConfigs.remove(0);
+ assertTetherConfigParcelEqual(actualConfig, config);
+ }
+ assertNoConfigChangeCallback();
+ }
+
+ public TetherStatesParcel pollTetherStatesChanged() {
+ assertStateChangeCallback();
+ return mTetherStates.remove(0);
+ }
+
+ @Override
+ public void onUpstreamChanged(Network network) {
+ mActualUpstreams.add(network);
+ }
+
+ @Override
+ public void onConfigurationChanged(TetheringConfigurationParcel config) {
+ mTetheringConfigs.add(config);
+ }
+
+ @Override
+ public void onTetherStatesChanged(TetherStatesParcel states) {
+ mTetherStates.add(states);
+ }
+
+ @Override
+ public void onCallbackCreated(Network network, TetheringConfigurationParcel config,
+ TetherStatesParcel states) {
+ mActualUpstreams.add(network);
+ mTetheringConfigs.add(config);
+ mTetherStates.add(states);
+ }
+
+ public void assertNoUpstreamChangeCallback() {
+ assertTrue(mActualUpstreams.isEmpty());
+ }
+
+ public void assertNoConfigChangeCallback() {
+ assertTrue(mTetheringConfigs.isEmpty());
+ }
+
+ public void assertStateChangeCallback() {
+ assertFalse(mTetherStates.isEmpty());
+ }
+
+ private void assertTetherConfigParcelEqual(@NonNull TetheringConfigurationParcel actual,
+ @NonNull TetheringConfigurationParcel expect) {
+ assertEquals(actual.subId, expect.subId);
+ assertArrayEquals(actual.tetherableUsbRegexs, expect.tetherableUsbRegexs);
+ assertArrayEquals(actual.tetherableWifiRegexs, expect.tetherableWifiRegexs);
+ assertArrayEquals(actual.tetherableBluetoothRegexs, expect.tetherableBluetoothRegexs);
+ assertEquals(actual.isDunRequired, expect.isDunRequired);
+ assertEquals(actual.chooseUpstreamAutomatically, expect.chooseUpstreamAutomatically);
+ assertArrayEquals(actual.preferredUpstreamIfaceTypes,
+ expect.preferredUpstreamIfaceTypes);
+ assertArrayEquals(actual.legacyDhcpRanges, expect.legacyDhcpRanges);
+ assertArrayEquals(actual.defaultIPv4DNS, expect.defaultIPv4DNS);
+ assertEquals(actual.enableLegacyDhcpServer, expect.enableLegacyDhcpServer);
+ assertArrayEquals(actual.provisioningApp, expect.provisioningApp);
+ assertEquals(actual.provisioningAppNoUi, expect.provisioningAppNoUi);
+ assertEquals(actual.provisioningCheckPeriod, expect.provisioningCheckPeriod);
+ }
+ }
+
+ @Test
+ public void testRegisterTetherInternalCallback() throws Exception {
+ TestTetherInternalCallback callback = new TestTetherInternalCallback();
+
+ // 1. Register one callback before running any tethering.
+ mTethering.registerTetherInternalCallback(callback);
+ mLooper.dispatchAll();
+ callback.expectUpstreamChanged(new Network[] {null});
+ callback.expectConfigurationChanged(
+ mTethering.getTetheringConfiguration().toStableParcelable());
+ TetherStatesParcel tetherState = callback.pollTetherStatesChanged();
+ assertEquals(tetherState, null);
+ // 2. Enable wifi tethering
+ NetworkState upstreamState = buildMobileDualStackUpstreamState();
+ when(mUpstreamNetworkMonitor.getCurrentPreferredUpstream()).thenReturn(upstreamState);
+ when(mUpstreamNetworkMonitor.selectPreferredUpstreamType(any()))
+ .thenReturn(upstreamState);
+ when(mWifiManager.startSoftAp(any(WifiConfiguration.class))).thenReturn(true);
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+ mLooper.dispatchAll();
+ tetherState = callback.pollTetherStatesChanged();
+ assertArrayEquals(tetherState.availableList, new String[] {TEST_WLAN_IFNAME});
+
+ mTethering.startTethering(TETHERING_WIFI, null, false);
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ mLooper.dispatchAll();
+ tetherState = callback.pollTetherStatesChanged();
+ assertArrayEquals(tetherState.tetheredList, new String[] {TEST_WLAN_IFNAME});
+ callback.expectUpstreamChanged(upstreamState.network);
+
+ // 3. Disable wifi tethering.
+ mTethering.stopTethering(TETHERING_WIFI);
+ sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+ mLooper.dispatchAll();
+ tetherState = callback.pollTetherStatesChanged();
+ assertArrayEquals(tetherState.availableList, new String[] {TEST_WLAN_IFNAME});
+ mLooper.dispatchAll();
+ callback.expectUpstreamChanged(new Network[] {null});
+ }
+
+ @Test
+ public void testMultiSimAware() throws Exception {
+ final TetheringConfiguration initailConfig = mTethering.getTetheringConfiguration();
+ assertEquals(INVALID_SUBSCRIPTION_ID, initailConfig.subId);
+
+ final int fakeSubId = 1234;
+ mPhoneStateListener.onActiveDataSubscriptionIdChanged(fakeSubId);
+ final TetheringConfiguration newConfig = mTethering.getTetheringConfiguration();
+ assertEquals(fakeSubId, newConfig.subId);
+ }
+
+ private void workingWifiP2pGroupOwner(
+ boolean emulateInterfaceStatusChanged) throws Exception {
+ if (emulateInterfaceStatusChanged) {
+ mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
+ }
+ sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME);
+ mLooper.dispatchAll();
+
+ verifyInterfaceServingModeStarted(TEST_P2P_IFNAME);
+ verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_AVAILABLE_TETHER);
+ verify(mNMService, times(1)).setIpForwardingEnabled(true);
+ verify(mNMService, times(1)).startTethering(any(String[].class));
+ verifyNoMoreInteractions(mNMService);
+ verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
+ verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks();
+ // This will be called twice, one is on entering IpServer.STATE_AVAILABLE,
+ // and another one is on IpServer.STATE_TETHERED/IpServer.STATE_LOCAL_ONLY.
+ assertEquals(2, mTetheringDependencies.mIsTetheringSupportedCalls);
+
+ assertEquals(TETHER_ERROR_NO_ERROR, mTethering.getLastTetherError(TEST_P2P_IFNAME));
+
+ // Emulate externally-visible WifiP2pManager effects, when wifi p2p group
+ // is being removed.
+ sendWifiP2pConnectionChanged(false, true, TEST_P2P_IFNAME);
+ mTethering.interfaceRemoved(TEST_P2P_IFNAME);
+ mLooper.dispatchAll();
+
+ verify(mNMService, times(1)).untetherInterface(TEST_P2P_IFNAME);
+ // {g,s}etInterfaceConfig() called twice for enabling and disabling IPv4.
+ verify(mNMService, times(2)).getInterfaceConfig(TEST_P2P_IFNAME);
+ verify(mNMService, times(2))
+ .setInterfaceConfig(eq(TEST_P2P_IFNAME), any(InterfaceConfiguration.class));
+ verify(mNMService, times(1)).stopTethering();
+ verify(mNMService, times(1)).setIpForwardingEnabled(false);
+ verify(mUpstreamNetworkMonitor, never()).getCurrentPreferredUpstream();
+ verify(mUpstreamNetworkMonitor, never()).selectPreferredUpstreamType(any());
+ verifyNoMoreInteractions(mNMService);
+ // Asking for the last error after the per-interface state machine
+ // has been reaped yields an unknown interface error.
+ assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastTetherError(TEST_P2P_IFNAME));
+ }
+
+ private void workingWifiP2pGroupClient(
+ boolean emulateInterfaceStatusChanged) throws Exception {
+ if (emulateInterfaceStatusChanged) {
+ mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
+ }
+ sendWifiP2pConnectionChanged(true, false, TEST_P2P_IFNAME);
+ mLooper.dispatchAll();
+
+ verify(mNMService, never()).getInterfaceConfig(TEST_P2P_IFNAME);
+ verify(mNMService, never())
+ .setInterfaceConfig(eq(TEST_P2P_IFNAME), any(InterfaceConfiguration.class));
+ verify(mNMService, never()).tetherInterface(TEST_P2P_IFNAME);
+ verify(mNMService, never()).setIpForwardingEnabled(true);
+ verify(mNMService, never()).startTethering(any(String[].class));
+
+ // Emulate externally-visible WifiP2pManager effects, when wifi p2p group
+ // is being removed.
+ sendWifiP2pConnectionChanged(false, false, TEST_P2P_IFNAME);
+ mTethering.interfaceRemoved(TEST_P2P_IFNAME);
+ mLooper.dispatchAll();
+
+ verify(mNMService, never()).untetherInterface(TEST_P2P_IFNAME);
+ verify(mNMService, never()).getInterfaceConfig(TEST_P2P_IFNAME);
+ verify(mNMService, never())
+ .setInterfaceConfig(eq(TEST_P2P_IFNAME), any(InterfaceConfiguration.class));
+ verify(mNMService, never()).stopTethering();
+ verify(mNMService, never()).setIpForwardingEnabled(false);
+ verifyNoMoreInteractions(mNMService);
+ // Asking for the last error after the per-interface state machine
+ // has been reaped yields an unknown interface error.
+ assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastTetherError(TEST_P2P_IFNAME));
+ }
+
+ @Test
+ public void workingWifiP2pGroupOwnerWithIfaceChanged() throws Exception {
+ workingWifiP2pGroupOwner(true);
+ }
+
+ @Test
+ public void workingWifiP2pGroupOwnerSansIfaceChanged() throws Exception {
+ workingWifiP2pGroupOwner(false);
+ }
+
+ private void workingWifiP2pGroupOwnerLegacyMode(
+ boolean emulateInterfaceStatusChanged) throws Exception {
+ // change to legacy mode and update tethering information by chaning SIM
+ when(mResources.getStringArray(com.android.internal.R.array.config_tether_wifi_p2p_regexs))
+ .thenReturn(new String[]{});
+ final int fakeSubId = 1234;
+ mPhoneStateListener.onActiveDataSubscriptionIdChanged(fakeSubId);
+
+ if (emulateInterfaceStatusChanged) {
+ mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
+ }
+ sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME);
+ mLooper.dispatchAll();
+
+ verify(mNMService, never()).getInterfaceConfig(TEST_P2P_IFNAME);
+ verify(mNMService, never())
+ .setInterfaceConfig(eq(TEST_P2P_IFNAME), any(InterfaceConfiguration.class));
+ verify(mNMService, never()).tetherInterface(TEST_P2P_IFNAME);
+ verify(mNMService, never()).setIpForwardingEnabled(true);
+ verify(mNMService, never()).startTethering(any(String[].class));
+ assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastTetherError(TEST_P2P_IFNAME));
+ }
+ @Test
+ public void workingWifiP2pGroupOwnerLegacyModeWithIfaceChanged() throws Exception {
+ workingWifiP2pGroupOwnerLegacyMode(true);
+ }
+
+ @Test
+ public void workingWifiP2pGroupOwnerLegacyModeSansIfaceChanged() throws Exception {
+ workingWifiP2pGroupOwnerLegacyMode(false);
+ }
+
+ @Test
+ public void workingWifiP2pGroupClientWithIfaceChanged() throws Exception {
+ workingWifiP2pGroupClient(true);
+ }
+
+ @Test
+ public void workingWifiP2pGroupClientSansIfaceChanged() throws Exception {
+ workingWifiP2pGroupClient(false);
+ }
+
+ // TODO: Test that a request for hotspot mode doesn't interfere with an
+ // already operating tethering mode interface.
+}