diff --git a/Tethering/Android.bp b/Tethering/Android.bp index cd51f5f90c..7e8721d3cc 100644 --- a/Tethering/Android.bp +++ b/Tethering/Android.bp @@ -22,13 +22,12 @@ java_defaults { ":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 @@ java_defaults { } // 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 @@ android_app { // 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 eb51593e45..1430ed00aa 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"> + + + + + diff --git a/Tethering/AndroidManifest_InProcess.xml b/Tethering/AndroidManifest_InProcess.xml new file mode 100644 index 0000000000..28d405c313 --- /dev/null +++ b/Tethering/AndroidManifest_InProcess.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp index 5b01b1e038..adc5a723a9 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 @@ aidl_interface { 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 0000000000..abb00e819f --- /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 443481e33f..bfe502fbeb 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 0000000000..3d842b3374 --- /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 0000000000..fdf0d16057 --- /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 0000000000..9e8726edf0 --- /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 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. + * + *

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 3eaf48845a..663154a490 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 @@ int register_android_server_connectivity_tethering_OffloadHardwareInterface(JNIE gMethods, NELEM(gMethods)); } +extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { + JNIEnv *env; + if (vm->GetEnv(reinterpret_cast(&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 0000000000..3218c0b387 --- /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 0000000000..e2804abd75 --- /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 mCallback; + private final AtomicInteger mGenerationNumber; + private BroadcastReceiver mReceiver; + + public VersionedBroadcastListener(String tag, Context ctx, Handler handler, + IntentFilter filter, Consumer 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 callback; + // Used to verify this receiver is still current. + public final int generationNumber; + + Receiver(String tag, AtomicInteger atomicGenerationNumber, Consumer 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 6b0f1de7ce..ba5d08dce4 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.Parcel; 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 android.util.SparseIntArray; 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 @@ public class EntitlementManager { private final ArraySet 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 @@ public class EntitlementManager { 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(); mCellularPermitted = new SparseIntArray(); - mSystemProperties = systemProperties; mEntitlementCacheValue = new SparseIntArray(); mTetherMasterSM = tetherMasterSM; mPermissionChangeMessageCode = permissionChangeMessageCode; @@ -287,7 +286,7 @@ public class EntitlementManager { */ @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 @@ public class EntitlementManager { 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 @@ public class EntitlementManager { } /** 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 0000000000..edfe3cad90 --- /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 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 mActiveDownstreams; + private final byte[] mUniqueLocalPrefix; + private short mNextSubnetId; + private NetworkState mUpstreamNetworkState; + + public IPv6TetheringCoordinator(ArrayList 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 01339a4a2c..00a677338b 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 class OffloadHardwareInterface { 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 0000000000..058fb4fe98 --- /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 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 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 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 availableList = new ArrayList<>(); + final ArrayList tetherList = new ArrayList<>(); + final ArrayList localOnlyList = new ArrayList<>(); + final ArrayList erroredList = new ArrayList<>(); + final ArrayList 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 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 list = new ArrayList(); + 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 list = new ArrayList(); + 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 list = new ArrayList(); + 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 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 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) 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 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(""); + } 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 ca9b1681ba..0ab4d634e8 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.ContentResolver; 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 @@ public class TetheringConfiguration { } 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 0000000000..0ba84127d8 --- /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 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 0000000000..0ef3805ff7 --- /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 0000000000..456f2f7f27 --- /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. + * + *

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 363be18a73..5b018df21a 100644 --- a/Tethering/tests/unit/Android.bp +++ b/Tethering/tests/unit/Android.bp @@ -18,7 +18,6 @@ android_test { name: "TetheringTests", certificate: "platform", srcs: [ - ":servicescore-tethering-src", "src/**/*.java", ], test_suites: ["device-tests"], @@ -41,17 +40,3 @@ android_test { "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 0000000000..5a9b6e380e --- /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 5217e26a40..99cf9e90d9 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_NO_ERROR; 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.Bundle; 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.State; 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.Test; 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 @@ public final class EntitlementManagerTest { @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 @@ public final class EntitlementManagerTest { 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 final class EntitlementManagerTest { 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 @@ public final class EntitlementManagerTest { @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 @@ public final class EntitlementManagerTest { 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 @@ public final class EntitlementManagerTest { mSM.quit(); mSM = null; } + mMockingSession.finishMocking(); } private void setupForRequiredProvisioning() { @@ -184,9 +198,6 @@ public final class EntitlementManagerTest { .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 @@ public final class EntitlementManagerTest { } @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 @@ public final class EntitlementManagerTest { 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 @@ public final class EntitlementManagerTest { 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 @@ public final class EntitlementManagerTest { 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 @@ public final class EntitlementManagerTest { 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 @@ public final class EntitlementManagerTest { 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 @@ public final class EntitlementManagerTest { 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 @@ public final class EntitlementManagerTest { 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 0000000000..4f6f61bf95 --- /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 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 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 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 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 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 mActualUpstreams = new ArrayList<>(); + private final ArrayList mTetheringConfigs = + new ArrayList<>(); + private final ArrayList 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 expectedUpstreams = + new ArrayList(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 expectedTetherConfig = + new ArrayList(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. +}