diff options
35 files changed, 1579 insertions, 138 deletions
diff --git a/api/test-current.txt b/api/test-current.txt index ecc69c0f3d41..6fe730ae08bf 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -3101,6 +3101,7 @@ package android.telecom { method @RequiresPermission("android.permission.READ_PRIVILEGED_PHONE_STATE") public int getCurrentTtyMode(); method @Nullable @RequiresPermission("android.permission.READ_PRIVILEGED_PHONE_STATE") public String getDefaultDialerPackage(@NonNull android.os.UserHandle); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean isInEmergencyCall(); + method @RequiresPermission(anyOf={"android.permission.READ_PRIVILEGED_PHONE_STATE", android.Manifest.permission.READ_PHONE_STATE}) public boolean isRinging(); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setUserSelectedOutgoingPhoneAccount(@Nullable android.telecom.PhoneAccountHandle); field public static final int TTY_MODE_FULL = 1; // 0x1 field public static final int TTY_MODE_HCO = 2; // 0x2 diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto index a2d9c77d202b..331182935a0c 100644 --- a/cmds/statsd/src/atoms.proto +++ b/cmds/statsd/src/atoms.proto @@ -360,6 +360,10 @@ message Atom { BootTimeEventErrorCode boot_time_event_error_code_reported = 242; UserspaceRebootReported userspace_reboot_reported = 243 [(log_from_module) = "framework"]; SnapshotMergeReported snapshot_merge_reported = 255; + NetworkIpProvisioningReported network_ip_provisioning_reported = 290 [(log_from_module) = "network_stack"]; + NetworkDhcpRenewReported network_dhcp_renew_reported = 291 [(log_from_module) = "network_stack"]; + NetworkValidationReported network_validation_reported = 292 [(log_from_module) = "network_stack"]; + NetworkStackQuirkReported network_stack_quirk_reported = 293 [(log_from_module) = "network_stack"]; } // Pulled events will start at field 10000. @@ -5886,6 +5890,172 @@ message NetworkDnsEventReported { } /** + * logs the CapportApiData info + * Logged from: + * packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java + */ +message CapportApiData { + // The TTL of the network connection provided by captive portal + optional int32 remaining_ttl_secs = 1; + + // The limit traffic data of the network connection provided by captive portal + optional int32 remaining_bytes = 2; + + // Is portal url option included in the DHCP packet (Yes, No) + optional bool has_portal_url = 3; + + // Is venue info (e.g. store info, maps, flight status) included (Yes, No) + optional bool has_venue_info = 4; +} + +/** + * logs a network Probe Event + * Logged from: + * packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java + */ +message ProbeEvent { + // The probe type (http or https, or captive portal API...) + optional android.stats.connectivity.ProbeType probe_type = 1; + + // The latency in microseconds of the probe event + optional int32 latency_micros = 2; + + // The result of the probe event + optional android.stats.connectivity.ProbeResult probe_result = 3; + + // The CaptivePortal API info + optional CapportApiData capport_api_data = 4; +} + +/** + * log each ProbeEvent in ProbeEvents + * Logged from: + * packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java + */ +message ProbeEvents { + // Record probe event during the validation + repeated ProbeEvent probe_event = 1; +} + +/** + * The DHCP (Dynamic Host Configuration Protocol) session info + * Logged from: + * packages/modules/NetworkStack/src/android/net/dhcp/DhcpClient.java + */ +message DhcpSession { + // The DHCP Feature(s) enabled in this session + repeated android.stats.connectivity.DhcpFeature used_features = 1; + + // The discover packet (re)transmit count + optional int32 discover_count = 2; + + // The request packet (re)transmit count + optional int32 request_count = 3; + + // The IPv4 address conflict count + // (only be meaningful when duplicate address detection is enabled) + optional int32 conflict_count = 4; + + // The DHCP packet parsing error code in this session + // (defined in android.net.metrics.DhcpErrorEvent) + repeated android.stats.connectivity.DhcpErrorCode error_code = 5; + + // The result of DHCP hostname transliteration + optional android.stats.connectivity.HostnameTransResult ht_result = 6; +} + +/** + * Logs Network IP provisioning event + * Logged from: + * packages/modules/NetworkStack/src/com/android/networkstack/metrics/NetworkIpProvisioningMetrics.java + */ +message NetworkIpProvisioningReported { + // Transport type (WIFI, CELLULAR, BLUETOOTH, ..) + optional android.stats.connectivity.TransportType transport_type = 1; + + // The latency in microseconds of IP Provisioning over IPV4 + optional int32 ipv4_latency_micros = 2; + + // The latency in microseconds of IP Provisioning over IPV6 + optional int32 ipv6_latency_micros = 3; + + // The time duration between provisioning start and end (success or failure) + optional int64 provisioning_duration_micros = 4; + + // The specific disconnect reason for this IP provisioning + optional android.stats.connectivity.DisconnectCode disconnect_code = 5; + + // Log DHCP session info (Only valid for IPv4) + optional DhcpSession dhcp_session = 6 [(log_mode) = MODE_BYTES]; + + // The random number between 0 ~ 999 for sampling + optional int32 random_number = 7; +} + +/** + * Logs Network DHCP Renew event + * Logged from: + * packages/modules/NetworkStack/src/android/net/dhcp/DhcpClient.java + */ +message NetworkDhcpRenewReported { + // Transport type (WIFI, CELLULAR, BLUETOOTH, ..) + optional android.stats.connectivity.TransportType transport_type = 1; + + // The request packet (re)transmit count + optional int32 request_count = 2; + + // The latency in microseconds of DHCP Renew + optional int32 latency_micros = 3; + + // The DHCP error code is defined in android.net.metrics.DhcpErrorEvent + optional android.stats.connectivity.DhcpErrorCode error_code = 4; + + // The result of DHCP renew + optional android.stats.connectivity.DhcpRenewResult renew_result = 5; + + // The random number between 0 ~ 999 for sampling + optional int32 random_number = 6; +} + +/** + * Logs Network Validation event + * Logged from: + * packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java + */ +message NetworkValidationReported { + // Transport type (WIFI, CELLULAR, BLUETOOTH, ..) + optional android.stats.connectivity.TransportType transport_type = 1; + + // Record each probe event + optional ProbeEvents probe_events = 2 [(log_mode) = MODE_BYTES]; + + // The result of the network validation + optional android.stats.connectivity.ValidationResult validation_result = 3; + + // The latency in microseconds of network validation + optional int32 latency_micros = 4; + + // The validation index (the first validation attempt or second, third...) + optional int32 validation_index = 5; + + // The random number between 0 ~ 999 for sampling + optional int32 random_number = 6; +} + +/** + * Logs NetworkStack Quirk event + * Logged from: + * packages/modules/NetworkStack/src/com/android/networkstack/ + */ +message NetworkStackQuirkReported { + // Transport type (WIFI, CELLULAR, BLUETOOTH, ..) + optional android.stats.connectivity.TransportType transport_type = 1; + + // Record each Quirk event + optional android.stats.connectivity.NetworkQuirkEvent event = 2; +} + +/** * Logs when a data stall event occurs. * * Log from: diff --git a/core/java/android/net/RouteInfo.java b/core/java/android/net/RouteInfo.java index e550f85e6b9a..98760761736d 100644 --- a/core/java/android/net/RouteInfo.java +++ b/core/java/android/net/RouteInfo.java @@ -26,7 +26,6 @@ import android.net.util.NetUtils; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; -import android.util.Pair; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -554,15 +553,45 @@ public final class RouteInfo implements Parcelable { } /** - * A helper class that contains the destination and the gateway in a {@code RouteInfo}, - * used by {@link ConnectivityService#updateRoutes} or + * A helper class that contains the destination, the gateway and the interface in a + * {@code RouteInfo}, used by {@link ConnectivityService#updateRoutes} or * {@link LinkProperties#addRoute} to calculate the list to be updated. + * {@code RouteInfo} objects with different interfaces are treated as different routes because + * *usually* on Android different interfaces use different routing tables, and moving a route + * to a new routing table never constitutes an update, but is always a remove and an add. * * @hide */ - public static class RouteKey extends Pair<IpPrefix, InetAddress> { - RouteKey(@NonNull IpPrefix destination, @Nullable InetAddress gateway) { - super(destination, gateway); + public static class RouteKey { + @NonNull private final IpPrefix mDestination; + @Nullable private final InetAddress mGateway; + @Nullable private final String mInterface; + + RouteKey(@NonNull IpPrefix destination, @Nullable InetAddress gateway, + @Nullable String iface) { + mDestination = destination; + mGateway = gateway; + mInterface = iface; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof RouteKey)) { + return false; + } + RouteKey p = (RouteKey) o; + // No need to do anything special for scoped addresses. Inet6Address#equals does not + // consider the scope ID, but the netd route IPCs (e.g., INetd#networkAddRouteParcel) + // and the kernel ignore scoped addresses both in the prefix and in the nexthop and only + // look at RTA_OIF. + return Objects.equals(p.mDestination, mDestination) + && Objects.equals(p.mGateway, mGateway) + && Objects.equals(p.mInterface, mInterface); + } + + @Override + public int hashCode() { + return Objects.hash(mDestination, mGateway, mInterface); } } @@ -574,7 +603,7 @@ public final class RouteInfo implements Parcelable { */ @NonNull public RouteKey getRouteKey() { - return new RouteKey(mDestination, mGateway); + return new RouteKey(mDestination, mGateway, mInterface); } /** diff --git a/core/java/android/telephony/PhoneStateListener.java b/core/java/android/telephony/PhoneStateListener.java index d64077525905..46f4bdf3a305 100644 --- a/core/java/android/telephony/PhoneStateListener.java +++ b/core/java/android/telephony/PhoneStateListener.java @@ -157,8 +157,8 @@ public class PhoneStateListener { * Listen for changes to the device's cell location. Note that * this will result in frequent callbacks to the listener. * {@more} - * Requires Permission: {@link android.Manifest.permission#ACCESS_COARSE_LOCATION - * ACCESS_COARSE_LOCATION} + * Requires Permission: {@link android.Manifest.permission#ACCESS_FINE_LOCATION + * ACCESS_FINE_LOCATION} * <p> * If you need regular location updates but want more control over * the update interval or location precision, you can set up a listener diff --git a/core/proto/android/server/connectivity/Android.bp b/core/proto/android/server/connectivity/Android.bp index 413623963851..50c238b96307 100644 --- a/core/proto/android/server/connectivity/Android.bp +++ b/core/proto/android/server/connectivity/Android.bp @@ -21,4 +21,6 @@ java_library_static { "data_stall_event.proto", ], sdk_version: "system_current", + // this is part of updatable modules(NetworkStack) which targets 29(Q) + min_sdk_version: "29", } diff --git a/core/proto/android/stats/connectivity/Android.bp b/core/proto/android/stats/connectivity/Android.bp index 5d642d3845fe..9cd233e1ba85 100644 --- a/core/proto/android/stats/connectivity/Android.bp +++ b/core/proto/android/stats/connectivity/Android.bp @@ -13,12 +13,12 @@ // limitations under the License. java_library_static { - name: "networkstackprotosnano", + name: "networkstackprotos", proto: { - type: "nano", + type: "lite", }, srcs: [ "network_stack.proto", ], - sdk_version: "system_current", + sdk_version: "system_29", } diff --git a/core/proto/android/stats/connectivity/network_stack.proto b/core/proto/android/stats/connectivity/network_stack.proto index 7d9aa1c6eb23..e9726d7ce195 100644 --- a/core/proto/android/stats/connectivity/network_stack.proto +++ b/core/proto/android/stats/connectivity/network_stack.proto @@ -20,6 +20,160 @@ package android.stats.connectivity; option java_multiple_files = true; option java_outer_classname = "NetworkStackProto"; +enum DhcpRenewResult { + RR_UNKNOWN = 0; + RR_SUCCESS = 1; + RR_ERROR_NAK = 2; + RR_ERROR_IP_MISMATCH = 3; + RR_ERROR_IP_EXPIRE = 4; +} + +enum DisconnectCode { + DC_NONE = 0; + DC_NORMAL_TERMINATION = 1; + DC_PROVISIONING_FAIL = 2; + DC_ERROR_STARTING_IPV4 = 4; + DC_ERROR_STARTING_IPV6 = 5; + DC_ERROR_STARTING_IPREACHABILITYMONITOR = 6; + DC_INVALID_PROVISIONING = 7; + DC_INTERFACE_NOT_FOUND = 8; + DC_PROVISIONING_TIMEOUT = 9; +} + +enum TransportType { + TT_UNKNOWN = 0; + // Indicates this network uses a Cellular transport + TT_CELLULAR = 1; + // Indicates this network uses a Wi-Fi transport + TT_WIFI = 2; + // Indicates this network uses a Bluetooth transport + TT_BLUETOOTH = 3; + // Indicates this network uses an Ethernet transport + TT_ETHERNET = 4; + // Indicates this network uses a Wi-Fi Aware transport + TT_WIFI_AWARE = 5; + // Indicates this network uses a LoWPAN transport + TT_LOWPAN = 6; + // Indicates this network uses a Cellular+VPN transport + TT_CELLULAR_VPN = 7; + // Indicates this network uses a Wi-Fi+VPN transport + TT_WIFI_VPN = 8; + // Indicates this network uses a Bluetooth+VPN transport + TT_BLUETOOTH_VPN = 9; + // Indicates this network uses an Ethernet+VPN transport + TT_ETHERNET_VPN = 10; + // Indicates this network uses a Wi-Fi+Cellular+VPN transport + TT_WIFI_CELLULAR_VPN = 11; + // Indicates this network uses for test only + TT_TEST = 12; +} + +enum DhcpFeature { + DF_UNKNOWN = 0; + // DHCP INIT-REBOOT state + DF_INITREBOOT = 1; + // DHCP rapid commit option + DF_RAPIDCOMMIT = 2; + // Duplicate address detection + DF_DAD = 3; + // Fast initial Link setup + DF_FILS = 4; +} + +enum HostnameTransResult { + HTR_UNKNOWN = 0; + HTR_SUCCESS = 1; + HTR_FAILURE = 2; + HTR_DISABLE = 3; +} + +enum ProbeResult { + PR_UNKNOWN = 0; + PR_SUCCESS = 1; + PR_FAILURE = 2; + PR_PORTAL = 3; + // DNS query for the probe host returned a private IP address + PR_PRIVATE_IP_DNS = 4; +} + +enum ValidationResult { + VR_UNKNOWN = 0; + VR_SUCCESS = 1; + VR_FAILURE = 2; + VR_PORTAL = 3; + VR_PARTIAL = 4; +} + +enum ProbeType { + PT_UNKNOWN = 0; + PT_DNS = 1; + PT_HTTP = 2; + PT_HTTPS = 3; + PT_PAC = 4; + PT_FALLBACK = 5; + PT_PRIVDNS = 6; + PT_CAPPORT_API = 7; +} + +// The Dhcp error code is defined in android.net.metrics.DhcpErrorEvent +enum DhcpErrorCode { + ET_UNKNOWN = 0; + ET_L2_ERROR = 1; + ET_L3_ERROR = 2; + ET_L4_ERROR = 3; + ET_DHCP_ERROR = 4; + ET_MISC_ERROR = 5; + /* Reserve for error type + // ET_L2_ERROR_TYPE = ET_L2_ERROR << 8; + ET_L2_ERROR_TYPE = 256; + // ET_L3_ERROR_TYPE = ET_L3_ERROR << 8; + ET_L3_ERROR_TYPE = 512; + // ET_L4_ERROR_TYPE = ET_L4_ERROR << 8; + ET_L4_ERROR_TYPE = 768; + // ET_DHCP_ERROR_TYPE = ET_DHCP_ERROR << 8; + ET_DHCP_ERROR_TYPE = 1024; + // ET_MISC_ERROR_TYPE = ET_MISC_ERROR << 8; + ET_MISC_ERROR_TYPE = 1280; + */ + // ET_L2_TOO_SHORT = (ET_L2_ERROR_TYPE | 0x1) << 16; + ET_L2_TOO_SHORT = 16842752; + // ET_L2_WRONG_ETH_TYPE = (ET_L2_ERROR_TYPE | 0x2) << 16; + ET_L2_WRONG_ETH_TYPE = 16908288; + // ET_L3_TOO_SHORT = (ET_L3_ERROR_TYPE | 0x1) << 16; + ET_L3_TOO_SHORT = 33619968; + // ET_L3_NOT_IPV4 = (ET_L3_ERROR_TYPE | 0x2) << 16; + ET_L3_NOT_IPV4 = 33685504; + // ET_L3_INVALID_IP = (ET_L3_ERROR_TYPE | 0x3) << 16; + ET_L3_INVALID_IP = 33751040; + // ET_L4_NOT_UDP = (ET_L4_ERROR_TYPE | 0x1) << 16; + ET_L4_NOT_UDP = 50397184; + // ET_L4_WRONG_PORT = (ET_L4_ERROR_TYPE | 0x2) << 16; + ET_L4_WRONG_PORT = 50462720; + // ET_BOOTP_TOO_SHORT = (ET_DHCP_ERROR_TYPE | 0x1) << 16; + ET_BOOTP_TOO_SHORT = 67174400; + // ET_DHCP_BAD_MAGIC_COOKIE = (ET_DHCP_ERROR_TYPE | 0x2) << 16; + ET_DHCP_BAD_MAGIC_COOKIE = 67239936; + // ET_DHCP_INVALID_OPTION_LENGTH = (ET_DHCP_ERROR_TYPE | 0x3) << 16; + ET_DHCP_INVALID_OPTION_LENGTH = 67305472; + // ET_DHCP_NO_MSG_TYPE = (ET_DHCP_ERROR_TYPE | 0x4) << 16; + ET_DHCP_NO_MSG_TYPE = 67371008; + // ET_DHCP_UNKNOWN_MSG_TYPE = (ET_DHCP_ERROR_TYPE | 0x5) << 16; + ET_DHCP_UNKNOWN_MSG_TYPE = 67436544; + // ET_DHCP_NO_COOKIE = (ET_DHCP_ERROR_TYPE | 0x6) << 16; + ET_DHCP_NO_COOKIE = 67502080; + // ET_BUFFER_UNDERFLOW = (ET_MISC_ERROR_TYPE | 0x1) << 16; + ET_BUFFER_UNDERFLOW = 83951616; + // ET_RECEIVE_ERROR = (ET_MISC_ERROR_TYPE | 0x2) << 16; + ET_RECEIVE_ERROR = 84017152; + // ET_PARSING_ERROR = (ET_MISC_ERROR_TYPE | 0x3) << 16; + ET_PARSING_ERROR = 84082688; +} + +enum NetworkQuirkEvent { + QE_UNKNOWN = 0; + QE_IPV6_PROVISIONING_ROUTER_LOST = 1; +} + message NetworkStackEventData { } diff --git a/data/etc/car/com.android.car.bugreport.xml b/data/etc/car/com.android.car.bugreport.xml index 432a838a90e8..4bbf11659214 100644 --- a/data/etc/car/com.android.car.bugreport.xml +++ b/data/etc/car/com.android.car.bugreport.xml @@ -15,7 +15,7 @@ ~ limitations under the License --> <permissions> - <privapp-permissions package="com.android.car.bugreport"> + <privapp-permissions package="com.google.android.car.bugreport"> <permission name="android.permission.DUMP"/> <permission name="android.permission.INTERACT_ACROSS_USERS"/> <permission name="android.permission.READ_LOGS"/> diff --git a/keystore/java/android/security/KeyStore.java b/keystore/java/android/security/KeyStore.java index d35642e362b1..23283fa32b5e 100644 --- a/keystore/java/android/security/KeyStore.java +++ b/keystore/java/android/security/KeyStore.java @@ -485,14 +485,14 @@ public class KeyStore { mBinder.asBinder().linkToDeath(promise, 0); int errorCode = mBinder.addRngEntropy(promise, data, flags); if (errorCode == NO_ERROR) { - return promise.getFuture().get().getErrorCode() == NO_ERROR; + return interruptedPreservingGet(promise.getFuture()).getErrorCode() == NO_ERROR; } else { return false; } } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); return false; - } catch (ExecutionException | InterruptedException e) { + } catch (ExecutionException e) { Log.e(TAG, "AddRngEntropy completed with exception", e); return false; } finally { @@ -550,7 +550,7 @@ public class KeyStore { private int generateKeyInternal(String alias, KeymasterArguments args, byte[] entropy, int uid, int flags, KeyCharacteristics outCharacteristics) - throws RemoteException, ExecutionException, InterruptedException { + throws RemoteException, ExecutionException { KeyCharacteristicsPromise promise = new KeyCharacteristicsPromise(); int error = NO_ERROR; KeyCharacteristicsCallbackResult result = null; @@ -561,7 +561,7 @@ public class KeyStore { Log.e(TAG, "generateKeyInternal failed on request " + error); return error; } - result = promise.getFuture().get(); + result = interruptedPreservingGet(promise.getFuture()); } finally { mBinder.asBinder().unlinkToDeath(promise, 0); } @@ -594,7 +594,7 @@ public class KeyStore { } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); return SYSTEM_ERROR; - } catch (ExecutionException | InterruptedException e) { + } catch (ExecutionException e) { Log.e(TAG, "generateKey completed with exception", e); return SYSTEM_ERROR; } @@ -616,7 +616,7 @@ public class KeyStore { int error = mBinder.getKeyCharacteristics(promise, alias, clientId, appId, uid); if (error != NO_ERROR) return error; - KeyCharacteristicsCallbackResult result = promise.getFuture().get(); + KeyCharacteristicsCallbackResult result = interruptedPreservingGet(promise.getFuture()); error = result.getKeystoreResponse().getErrorCode(); if (error != NO_ERROR) return error; @@ -627,7 +627,7 @@ public class KeyStore { } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); return SYSTEM_ERROR; - } catch (ExecutionException | InterruptedException e) { + } catch (ExecutionException e) { Log.e(TAG, "GetKeyCharacteristics completed with exception", e); return SYSTEM_ERROR; } finally { @@ -642,14 +642,14 @@ public class KeyStore { private int importKeyInternal(String alias, KeymasterArguments args, int format, byte[] keyData, int uid, int flags, KeyCharacteristics outCharacteristics) - throws RemoteException, ExecutionException, InterruptedException { + throws RemoteException, ExecutionException { KeyCharacteristicsPromise promise = new KeyCharacteristicsPromise(); mBinder.asBinder().linkToDeath(promise, 0); try { int error = mBinder.importKey(promise, alias, args, format, keyData, uid, flags); if (error != NO_ERROR) return error; - KeyCharacteristicsCallbackResult result = promise.getFuture().get(); + KeyCharacteristicsCallbackResult result = interruptedPreservingGet(promise.getFuture()); error = result.getKeystoreResponse().getErrorCode(); if (error != NO_ERROR) return error; @@ -677,7 +677,7 @@ public class KeyStore { } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); return SYSTEM_ERROR; - } catch (ExecutionException | InterruptedException e) { + } catch (ExecutionException e) { Log.e(TAG, "ImportKey completed with exception", e); return SYSTEM_ERROR; } @@ -749,7 +749,7 @@ public class KeyStore { String wrappingKeyAlias, byte[] maskingKey, KeymasterArguments args, long rootSid, long fingerprintSid, KeyCharacteristics outCharacteristics) - throws RemoteException, ExecutionException, InterruptedException { + throws RemoteException, ExecutionException { KeyCharacteristicsPromise promise = new KeyCharacteristicsPromise(); mBinder.asBinder().linkToDeath(promise, 0); try { @@ -757,7 +757,7 @@ public class KeyStore { wrappingKeyAlias, maskingKey, args, rootSid, fingerprintSid); if (error != NO_ERROR) return error; - KeyCharacteristicsCallbackResult result = promise.getFuture().get(); + KeyCharacteristicsCallbackResult result = interruptedPreservingGet(promise.getFuture()); error = result.getKeystoreResponse().getErrorCode(); if (error != NO_ERROR) return error; @@ -788,7 +788,7 @@ public class KeyStore { } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); return SYSTEM_ERROR; - } catch (ExecutionException | InterruptedException e) { + } catch (ExecutionException e) { Log.e(TAG, "ImportWrappedKey completed with exception", e); return SYSTEM_ERROR; } @@ -820,14 +820,14 @@ public class KeyStore { appId = appId != null ? appId : new KeymasterBlob(new byte[0]); int error = mBinder.exportKey(promise, alias, format, clientId, appId, uid); if (error == NO_ERROR) { - return promise.getFuture().get(); + return interruptedPreservingGet(promise.getFuture()); } else { return new ExportResult(error); } } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); return null; - } catch (ExecutionException | InterruptedException e) { + } catch (ExecutionException e) { Log.e(TAG, "ExportKey completed with exception", e); return null; } finally { @@ -866,14 +866,14 @@ public class KeyStore { int errorCode = mBinder.begin(promise, getToken(), alias, purpose, pruneable, args, entropy, uid); if (errorCode == NO_ERROR) { - return promise.getFuture().get(); + return interruptedPreservingGet(promise.getFuture()); } else { return new OperationResult(errorCode); } } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); return null; - } catch (ExecutionException | InterruptedException e) { + } catch (ExecutionException e) { Log.e(TAG, "Begin completed with exception", e); return null; } finally { @@ -896,14 +896,14 @@ public class KeyStore { input = input != null ? input : new byte[0]; int errorCode = mBinder.update(promise, token, arguments, input); if (errorCode == NO_ERROR) { - return promise.getFuture().get(); + return interruptedPreservingGet(promise.getFuture()); } else { return new OperationResult(errorCode); } } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); return null; - } catch (ExecutionException | InterruptedException e) { + } catch (ExecutionException e) { Log.e(TAG, "Update completed with exception", e); return null; } finally { @@ -932,14 +932,14 @@ public class KeyStore { signature = signature != null ? signature : new byte[0]; int errorCode = mBinder.finish(promise, token, arguments, input, signature, entropy); if (errorCode == NO_ERROR) { - return promise.getFuture().get(); + return interruptedPreservingGet(promise.getFuture()); } else { return new OperationResult(errorCode); } } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); return null; - } catch (ExecutionException | InterruptedException e) { + } catch (ExecutionException e) { Log.e(TAG, "Finish completed with exception", e); return null; } finally { @@ -974,14 +974,14 @@ public class KeyStore { mBinder.asBinder().linkToDeath(promise, 0); int errorCode = mBinder.abort(promise, token); if (errorCode == NO_ERROR) { - return promise.getFuture().get().getErrorCode(); + return interruptedPreservingGet(promise.getFuture()).getErrorCode(); } else { return errorCode; } } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); return SYSTEM_ERROR; - } catch (ExecutionException | InterruptedException e) { + } catch (ExecutionException e) { Log.e(TAG, "Abort completed with exception", e); return SYSTEM_ERROR; } finally { @@ -1137,7 +1137,7 @@ public class KeyStore { } int error = mBinder.attestKey(promise, alias, params); if (error != NO_ERROR) return error; - KeyAttestationCallbackResult result = promise.getFuture().get(); + KeyAttestationCallbackResult result = interruptedPreservingGet(promise.getFuture()); error = result.getKeystoreResponse().getErrorCode(); if (error == NO_ERROR) { outChain.shallowCopyFrom(result.getCertificateChain()); @@ -1146,7 +1146,7 @@ public class KeyStore { } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); return SYSTEM_ERROR; - } catch (ExecutionException | InterruptedException e) { + } catch (ExecutionException e) { Log.e(TAG, "AttestKey completed with exception", e); return SYSTEM_ERROR; } finally { @@ -1166,7 +1166,7 @@ public class KeyStore { } int error = mBinder.attestDeviceIds(promise, params); if (error != NO_ERROR) return error; - KeyAttestationCallbackResult result = promise.getFuture().get(); + KeyAttestationCallbackResult result = interruptedPreservingGet(promise.getFuture()); error = result.getKeystoreResponse().getErrorCode(); if (error == NO_ERROR) { outChain.shallowCopyFrom(result.getCertificateChain()); @@ -1175,7 +1175,7 @@ public class KeyStore { } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); return SYSTEM_ERROR; - } catch (ExecutionException | InterruptedException e) { + } catch (ExecutionException e) { Log.e(TAG, "AttestDevicdeIds completed with exception", e); return SYSTEM_ERROR; } finally { @@ -1412,4 +1412,20 @@ public class KeyStore { int errorCode) { return getInvalidKeyException(keystoreKeyAlias, uid, getKeyStoreException(errorCode)); } + + private static <R> R interruptedPreservingGet(CompletableFuture<R> future) + throws ExecutionException { + boolean wasInterrupted = false; + while (true) { + try { + R result = future.get(); + if (wasInterrupted) { + Thread.currentThread().interrupt(); + } + return result; + } catch (InterruptedException e) { + wasInterrupted = true; + } + } + } } diff --git a/packages/Tethering/OWNERS b/packages/Tethering/OWNERS new file mode 100644 index 000000000000..5b42d490411e --- /dev/null +++ b/packages/Tethering/OWNERS @@ -0,0 +1,2 @@ +include platform/packages/modules/NetworkStack/:/OWNERS +markchien@google.com diff --git a/packages/Tethering/src/android/net/ip/IpServer.java b/packages/Tethering/src/android/net/ip/IpServer.java index 1671dda4bd57..e24fc2958933 100644 --- a/packages/Tethering/src/android/net/ip/IpServer.java +++ b/packages/Tethering/src/android/net/ip/IpServer.java @@ -615,8 +615,9 @@ public class IpServer extends StateMachine { final Boolean setIfaceUp; if (mInterfaceType == TetheringManager.TETHERING_WIFI - || mInterfaceType == TetheringManager.TETHERING_WIFI_P2P) { - // The WiFi stack has ownership of the interface up/down state. + || mInterfaceType == TetheringManager.TETHERING_WIFI_P2P + || mInterfaceType == TetheringManager.TETHERING_ETHERNET) { + // The WiFi and Ethernet stack has ownership of the interface up/down state. // It is unclear whether the Bluetooth or USB stacks will manage their own // state. setIfaceUp = null; @@ -1321,6 +1322,7 @@ public class IpServer extends StateMachine { class UnavailableState extends State { @Override public void enter() { + mIpNeighborMonitor.stop(); mLastError = TetheringManager.TETHER_ERROR_NO_ERROR; sendInterfaceState(STATE_UNAVAILABLE); } diff --git a/packages/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/packages/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java index fc27b6add052..20f30ea7a460 100644 --- a/packages/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java +++ b/packages/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java @@ -25,6 +25,8 @@ import static android.net.NetworkStats.UID_ALL; import static android.net.NetworkStats.UID_TETHERING; import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED; +import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS; + import android.app.usage.NetworkStatsManager; import android.net.INetd; import android.net.MacAddress; @@ -36,6 +38,7 @@ import android.net.ip.IpServer; import android.net.netstats.provider.NetworkStatsProvider; import android.net.util.SharedLog; import android.net.util.TetheringUtils.ForwardedStats; +import android.os.ConditionVariable; import android.os.Handler; import android.os.RemoteException; import android.os.ServiceSpecificException; @@ -47,11 +50,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; import java.net.Inet6Address; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.Map; import java.util.Objects; /** @@ -65,8 +70,7 @@ import java.util.Objects; */ public class BpfCoordinator { private static final String TAG = BpfCoordinator.class.getSimpleName(); - @VisibleForTesting - static final int DEFAULT_PERFORM_POLL_INTERVAL_MS = 5000; // TODO: Make it customizable. + private static final int DUMP_TIMEOUT_MS = 10_000; @VisibleForTesting enum StatsType { @@ -85,6 +89,13 @@ public class BpfCoordinator { @Nullable private final BpfTetherStatsProvider mStatsProvider; + // True if BPF offload is supported, false otherwise. The BPF offload could be disabled by + // a runtime resource overlay package or device configuration. This flag is only initialized + // in the constructor because it is hard to unwind all existing change once device + // configuration is changed. Especially the forwarding rules. Keep the same setting + // to make it simpler. See also TetheringConfiguration. + private final boolean mIsBpfEnabled; + // Tracks whether BPF tethering is started or not. This is set by tethering before it // starts the first IpServer and is cleared by tethering shortly before the last IpServer // is stopped. Note that rule updates (especially deletions, but sometimes additions as @@ -142,22 +153,34 @@ public class BpfCoordinator { }; @VisibleForTesting - public static class Dependencies { - int getPerformPollInterval() { - // TODO: Consider make this configurable. - return DEFAULT_PERFORM_POLL_INTERVAL_MS; - } + public abstract static class Dependencies { + /** Get handler. */ + @NonNull public abstract Handler getHandler(); + + /** Get netd. */ + @NonNull public abstract INetd getNetd(); + + /** Get network stats manager. */ + @NonNull public abstract NetworkStatsManager getNetworkStatsManager(); + + /** Get shared log. */ + @NonNull public abstract SharedLog getSharedLog(); + + /** Get tethering configuration. */ + @Nullable public abstract TetheringConfiguration getTetherConfig(); } @VisibleForTesting - public BpfCoordinator(@NonNull Handler handler, @NonNull INetd netd, - @NonNull NetworkStatsManager nsm, @NonNull SharedLog log, @NonNull Dependencies deps) { - mHandler = handler; - mNetd = netd; - mLog = log.forSubComponent(TAG); + public BpfCoordinator(@NonNull Dependencies deps) { + mDeps = deps; + mHandler = mDeps.getHandler(); + mNetd = mDeps.getNetd(); + mLog = mDeps.getSharedLog().forSubComponent(TAG); + mIsBpfEnabled = isBpfEnabled(); BpfTetherStatsProvider provider = new BpfTetherStatsProvider(); try { - nsm.registerNetworkStatsProvider(getClass().getSimpleName(), provider); + mDeps.getNetworkStatsManager().registerNetworkStatsProvider( + getClass().getSimpleName(), provider); } catch (RuntimeException e) { // TODO: Perhaps not allow to use BPF offload because the reregistration failure // implied that no data limit could be applies on a metered upstream if any. @@ -165,7 +188,6 @@ public class BpfCoordinator { provider = null; } mStatsProvider = provider; - mDeps = deps; } /** @@ -177,6 +199,11 @@ public class BpfCoordinator { public void startPolling() { if (mPollingStarted) return; + if (!mIsBpfEnabled) { + mLog.i("Offload disabled"); + return; + } + mPollingStarted = true; maybeSchedulePollingStats(); @@ -211,6 +238,8 @@ public class BpfCoordinator { */ public void tetherOffloadRuleAdd( @NonNull final IpServer ipServer, @NonNull final Ipv6ForwardingRule rule) { + if (!mIsBpfEnabled) return; + try { // TODO: Perhaps avoid to add a duplicate rule. mNetd.tetherOffloadRuleAdd(rule.toTetherOffloadRuleParcel()); @@ -250,6 +279,8 @@ public class BpfCoordinator { */ public void tetherOffloadRuleRemove( @NonNull final IpServer ipServer, @NonNull final Ipv6ForwardingRule rule) { + if (!mIsBpfEnabled) return; + try { // TODO: Perhaps avoid to remove a non-existent rule. mNetd.tetherOffloadRuleRemove(rule.toTetherOffloadRuleParcel()); @@ -293,6 +324,8 @@ public class BpfCoordinator { * Note that this can be only called on handler thread. */ public void tetherOffloadRuleClear(@NonNull final IpServer ipServer) { + if (!mIsBpfEnabled) return; + final LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get( ipServer); if (rules == null) return; @@ -308,6 +341,8 @@ public class BpfCoordinator { * Note that this can be only called on handler thread. */ public void tetherOffloadRuleUpdate(@NonNull final IpServer ipServer, int newUpstreamIfindex) { + if (!mIsBpfEnabled) return; + final LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get( ipServer); if (rules == null) return; @@ -330,6 +365,8 @@ public class BpfCoordinator { * Note that this can be only called on handler thread. */ public void addUpstreamNameToLookupTable(int upstreamIfindex, @NonNull String upstreamIface) { + if (!mIsBpfEnabled) return; + if (upstreamIfindex == 0 || TextUtils.isEmpty(upstreamIface)) return; // The same interface index to name mapping may be added by different IpServer objects or @@ -344,6 +381,77 @@ public class BpfCoordinator { } } + /** + * Dump information. + * Block the function until all the data are dumped on the handler thread or timed-out. The + * reason is that dumpsys invokes this function on the thread of caller and the data may only + * be allowed to be accessed on the handler thread. + */ + public void dump(@NonNull IndentingPrintWriter pw) { + final ConditionVariable dumpDone = new ConditionVariable(); + mHandler.post(() -> { + pw.println("mIsBpfEnabled: " + mIsBpfEnabled); + pw.println("Polling " + (mPollingStarted ? "started" : "not started")); + pw.println("Stats provider " + (mStatsProvider != null + ? "registered" : "not registered")); + pw.println("Upstream quota: " + mInterfaceQuotas.toString()); + pw.println("Polling interval: " + getPollingInterval() + " ms"); + + pw.println("Forwarding stats:"); + pw.increaseIndent(); + if (mStats.size() == 0) { + pw.println("<empty>"); + } else { + dumpStats(pw); + } + pw.decreaseIndent(); + + pw.println("Forwarding rules:"); + pw.increaseIndent(); + if (mIpv6ForwardingRules.size() == 0) { + pw.println("<empty>"); + } else { + dumpIpv6ForwardingRules(pw); + } + pw.decreaseIndent(); + + dumpDone.open(); + }); + if (!dumpDone.block(DUMP_TIMEOUT_MS)) { + pw.println("... dump timed-out after " + DUMP_TIMEOUT_MS + "ms"); + } + } + + private void dumpStats(@NonNull IndentingPrintWriter pw) { + for (int i = 0; i < mStats.size(); i++) { + final int upstreamIfindex = mStats.keyAt(i); + final ForwardedStats stats = mStats.get(upstreamIfindex); + pw.println(String.format("%d(%s) - %s", upstreamIfindex, mInterfaceNames.get( + upstreamIfindex), stats.toString())); + } + } + + private void dumpIpv6ForwardingRules(@NonNull IndentingPrintWriter pw) { + for (Map.Entry<IpServer, LinkedHashMap<Inet6Address, Ipv6ForwardingRule>> entry : + mIpv6ForwardingRules.entrySet()) { + IpServer ipServer = entry.getKey(); + // The rule downstream interface index is paired with the interface name from + // IpServer#interfaceName. See #startIPv6, #updateIpv6ForwardingRules in IpServer. + final String downstreamIface = ipServer.interfaceName(); + pw.println("[" + downstreamIface + "]: iif(iface) oif(iface) v6addr srcmac dstmac"); + + pw.increaseIndent(); + LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = entry.getValue(); + for (Ipv6ForwardingRule rule : rules.values()) { + final int upstreamIfindex = rule.upstreamIfindex; + pw.println(String.format("%d(%s) %d(%s) %s %s %s", upstreamIfindex, + mInterfaceNames.get(upstreamIfindex), rule.downstreamIfindex, + downstreamIface, rule.address, rule.srcMac, rule.dstMac)); + } + pw.decreaseIndent(); + } + } + /** IPv6 forwarding rule class. */ public static class Ipv6ForwardingRule { public final int upstreamIfindex; @@ -474,6 +582,11 @@ public class BpfCoordinator { } } + private boolean isBpfEnabled() { + final TetheringConfiguration config = mDeps.getTetherConfig(); + return (config != null) ? config.isBpfOffloadEnabled() : true /* default value */; + } + private int getInterfaceIndexFromRules(@NonNull String ifName) { for (LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules : mIpv6ForwardingRules .values()) { @@ -625,6 +738,17 @@ public class BpfCoordinator { updateQuotaAndStatsFromSnapshot(tetherStatsList); } + @VisibleForTesting + int getPollingInterval() { + // The valid range of interval is DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS..max_long. + // Ignore the config value is less than the minimum polling interval. Note that the + // minimum interval definition is invoked as OffloadController#isPollingStatsNeeded does. + // TODO: Perhaps define a minimum polling interval constant. + final TetheringConfiguration config = mDeps.getTetherConfig(); + final int configInterval = (config != null) ? config.getOffloadPollInterval() : 0; + return Math.max(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS, configInterval); + } + private void maybeSchedulePollingStats() { if (!mPollingStarted) return; @@ -632,6 +756,23 @@ public class BpfCoordinator { mHandler.removeCallbacks(mScheduledPollingTask); } - mHandler.postDelayed(mScheduledPollingTask, mDeps.getPerformPollInterval()); + mHandler.postDelayed(mScheduledPollingTask, getPollingInterval()); + } + + // Return forwarding rule map. This is used for testing only. + // Note that this can be only called on handler thread. + @NonNull + @VisibleForTesting + final HashMap<IpServer, LinkedHashMap<Inet6Address, Ipv6ForwardingRule>> + getForwardingRulesForTesting() { + return mIpv6ForwardingRules; + } + + // Return upstream interface name map. This is used for testing only. + // Note that this can be only called on handler thread. + @NonNull + @VisibleForTesting + final SparseArray<String> getInterfaceNamesForTesting() { + return mInterfaceNames; } } diff --git a/packages/Tethering/src/com/android/networkstack/tethering/Tethering.java b/packages/Tethering/src/com/android/networkstack/tethering/Tethering.java index 8deac537ffef..71ab1767643b 100644 --- a/packages/Tethering/src/com/android/networkstack/tethering/Tethering.java +++ b/packages/Tethering/src/com/android/networkstack/tethering/Tethering.java @@ -62,6 +62,7 @@ import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; import static com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE; +import android.app.usage.NetworkStatsManager; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothPan; import android.bluetooth.BluetoothProfile; @@ -285,8 +286,6 @@ public class Tethering { mUpstreamNetworkMonitor = mDeps.getUpstreamNetworkMonitor(mContext, mTetherMasterSM, mLog, TetherMasterSM.EVENT_UPSTREAM_CALLBACK); mForwardedDownstreams = new LinkedHashSet<>(); - mBpfCoordinator = mDeps.getBpfCoordinator( - mHandler, mNetd, mLog, new BpfCoordinator.Dependencies()); IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_CARRIER_CONFIG_CHANGED); @@ -324,6 +323,36 @@ public class Tethering { // Load tethering configuration. updateConfiguration(); + // Must be initialized after tethering configuration is loaded because BpfCoordinator + // constructor needs to use the configuration. + mBpfCoordinator = mDeps.getBpfCoordinator( + new BpfCoordinator.Dependencies() { + @NonNull + public Handler getHandler() { + return mHandler; + } + + @NonNull + public INetd getNetd() { + return mNetd; + } + + @NonNull + public NetworkStatsManager getNetworkStatsManager() { + return mContext.getSystemService(NetworkStatsManager.class); + } + + @NonNull + public SharedLog getSharedLog() { + return mLog; + } + + @Nullable + public TetheringConfiguration getTetherConfig() { + return mConfig; + } + }); + startStateMachineUpdaters(); } @@ -2217,6 +2246,11 @@ public class Tethering { mOffloadController.dump(pw); pw.decreaseIndent(); + pw.println("BPF offload:"); + pw.increaseIndent(); + mBpfCoordinator.dump(pw); + pw.decreaseIndent(); + pw.println("Private address coordinator:"); pw.increaseIndent(); mPrivateAddressCoordinator.dump(pw); @@ -2351,7 +2385,7 @@ public class Tethering { final TetherState tetherState = new TetherState( new IpServer(iface, mLooper, interfaceType, mLog, mNetd, mBpfCoordinator, makeControlCallback(), mConfig.enableLegacyDhcpServer, - mConfig.enableBpfOffload, mPrivateAddressCoordinator, + mConfig.isBpfOffloadEnabled(), mPrivateAddressCoordinator, mDeps.getIpServerDependencies())); mTetherStates.put(iface, tetherState); tetherState.ipServer.start(); diff --git a/packages/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/packages/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java index 48a600dfe6e1..1e94de12ce11 100644 --- a/packages/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java +++ b/packages/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java @@ -101,8 +101,6 @@ public class TetheringConfiguration { public final String[] legacyDhcpRanges; public final String[] defaultIPv4DNS; public final boolean enableLegacyDhcpServer; - // TODO: Add to TetheringConfigurationParcel if required. - public final boolean enableBpfOffload; public final String[] provisioningApp; public final String provisioningAppNoUi; @@ -111,6 +109,8 @@ public class TetheringConfiguration { public final int activeDataSubId; private final int mOffloadPollInterval; + // TODO: Add to TetheringConfigurationParcel if required. + private final boolean mEnableBpfOffload; public TetheringConfiguration(Context ctx, SharedLog log, int id) { final SharedLog configLog = log.forSubComponent("config"); @@ -137,7 +137,7 @@ public class TetheringConfiguration { legacyDhcpRanges = getLegacyDhcpRanges(res); defaultIPv4DNS = copy(DEFAULT_IPV4_DNS); - enableBpfOffload = getEnableBpfOffload(res); + mEnableBpfOffload = getEnableBpfOffload(res); enableLegacyDhcpServer = getEnableLegacyDhcpServer(res); provisioningApp = getResourceStringArray(res, R.array.config_mobile_hotspot_provision_app); @@ -218,7 +218,7 @@ public class TetheringConfiguration { pw.println(provisioningAppNoUi); pw.print("enableBpfOffload: "); - pw.println(enableBpfOffload); + pw.println(mEnableBpfOffload); pw.print("enableLegacyDhcpServer: "); pw.println(enableLegacyDhcpServer); @@ -240,7 +240,7 @@ public class TetheringConfiguration { toIntArray(preferredUpstreamIfaceTypes))); sj.add(String.format("provisioningApp:%s", makeString(provisioningApp))); sj.add(String.format("provisioningAppNoUi:%s", provisioningAppNoUi)); - sj.add(String.format("enableBpfOffload:%s", enableBpfOffload)); + sj.add(String.format("enableBpfOffload:%s", mEnableBpfOffload)); sj.add(String.format("enableLegacyDhcpServer:%s", enableLegacyDhcpServer)); return String.format("TetheringConfiguration{%s}", sj.toString()); } @@ -279,6 +279,10 @@ public class TetheringConfiguration { return mOffloadPollInterval; } + public boolean isBpfOffloadEnabled() { + return mEnableBpfOffload; + } + private static Collection<Integer> getUpstreamIfaceTypes(Resources res, boolean dunRequired) { final int[] ifaceTypes = res.getIntArray(R.array.config_tether_upstream_types); final ArrayList<Integer> upstreamIfaceTypes = new ArrayList<>(ifaceTypes.length); diff --git a/packages/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/packages/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java index d637c8646b4a..8f4b964323c6 100644 --- a/packages/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java +++ b/packages/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java @@ -44,11 +44,8 @@ public abstract class TetheringDependencies { * Get a reference to the BpfCoordinator to be used by tethering. */ public @NonNull BpfCoordinator getBpfCoordinator( - @NonNull Handler handler, @NonNull INetd netd, @NonNull SharedLog log, @NonNull BpfCoordinator.Dependencies deps) { - final NetworkStatsManager statsManager = - (NetworkStatsManager) getContext().getSystemService(Context.NETWORK_STATS_SERVICE); - return new BpfCoordinator(handler, netd, statsManager, log, deps); + return new BpfCoordinator(deps); } /** diff --git a/packages/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/packages/Tethering/tests/unit/src/android/net/ip/IpServerTest.java index c3bc915a232d..05cf58ab84ff 100644 --- a/packages/Tethering/tests/unit/src/android/net/ip/IpServerTest.java +++ b/packages/Tethering/tests/unit/src/android/net/ip/IpServerTest.java @@ -89,12 +89,14 @@ import android.os.test.TestLooper; import android.text.TextUtils; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.networkstack.tethering.BpfCoordinator; import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule; import com.android.networkstack.tethering.PrivateAddressCoordinator; +import com.android.networkstack.tethering.TetheringConfiguration; import org.junit.Before; import org.junit.Test; @@ -142,6 +144,7 @@ public class IpServerTest { @Mock private IpServer.Dependencies mDependencies; @Mock private PrivateAddressCoordinator mAddressCoordinator; @Mock private NetworkStatsManager mStatsManager; + @Mock private TetheringConfiguration mTetherConfig; @Captor private ArgumentCaptor<DhcpServingParamsParcel> mDhcpParamsCaptor; @@ -225,10 +228,35 @@ public class IpServerTest { MockitoAnnotations.initMocks(this); when(mSharedLog.forSubComponent(anyString())).thenReturn(mSharedLog); when(mAddressCoordinator.requestDownstreamAddress(any())).thenReturn(mTestAddress); - - BpfCoordinator bc = new BpfCoordinator(new Handler(mLooper.getLooper()), mNetd, - mStatsManager, mSharedLog, new BpfCoordinator.Dependencies()); - mBpfCoordinator = spy(bc); + when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(true /* default value */); + + mBpfCoordinator = spy(new BpfCoordinator( + new BpfCoordinator.Dependencies() { + @NonNull + public Handler getHandler() { + return new Handler(mLooper.getLooper()); + } + + @NonNull + public INetd getNetd() { + return mNetd; + } + + @NonNull + public NetworkStatsManager getNetworkStatsManager() { + return mStatsManager; + } + + @NonNull + public SharedLog getSharedLog() { + return mSharedLog; + } + + @Nullable + public TetheringConfiguration getTetherConfig() { + return mTetherConfig; + } + })); } @Test @@ -671,18 +699,21 @@ public class IpServerTest { } } - private TetherOffloadRuleParcel matches( + @NonNull + private static TetherOffloadRuleParcel matches( int upstreamIfindex, InetAddress dst, MacAddress dstMac) { return argThat(new TetherOffloadRuleParcelMatcher(upstreamIfindex, dst, dstMac)); } + @NonNull private static Ipv6ForwardingRule makeForwardingRule( int upstreamIfindex, @NonNull InetAddress dst, @NonNull MacAddress dstMac) { return new Ipv6ForwardingRule(upstreamIfindex, TEST_IFACE_PARAMS.index, (Inet6Address) dst, TEST_IFACE_PARAMS.macAddr, dstMac); } - private TetherStatsParcel buildEmptyTetherStatsParcel(int ifIndex) { + @NonNull + private static TetherStatsParcel buildEmptyTetherStatsParcel(int ifIndex) { TetherStatsParcel parcel = new TetherStatsParcel(); parcel.ifIndex = ifIndex; return parcel; @@ -828,6 +859,7 @@ public class IpServerTest { verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer); verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighA, macA)); verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighB, macB)); + verify(mIpNeighborMonitor).stop(); resetNetdAndBpfCoordinator(); } diff --git a/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java index e2d7aab4e33f..64242ae8255f 100644 --- a/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java +++ b/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java @@ -25,48 +25,76 @@ import static android.net.NetworkStats.UID_ALL; import static android.net.NetworkStats.UID_TETHERING; import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED; -import static com.android.networkstack.tethering.BpfCoordinator - .DEFAULT_PERFORM_POLL_INTERVAL_MS; import static com.android.networkstack.tethering.BpfCoordinator.StatsType; import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_IFACE; import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_UID; - -import static junit.framework.Assert.assertNotNull; - +import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.annotation.NonNull; import android.app.usage.NetworkStatsManager; import android.net.INetd; +import android.net.InetAddresses; +import android.net.MacAddress; import android.net.NetworkStats; +import android.net.TetherOffloadRuleParcel; import android.net.TetherStatsParcel; +import android.net.ip.IpServer; import android.net.util.SharedLog; import android.os.Handler; import android.os.test.TestLooper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule; import com.android.testutils.TestableNetworkStatsProviderCbBinder; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.net.Inet6Address; +import java.net.InetAddress; import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; @RunWith(AndroidJUnit4.class) @SmallTest public class BpfCoordinatorTest { + private static final int DOWNSTREAM_IFINDEX = 10; + private static final MacAddress DOWNSTREAM_MAC = MacAddress.ALL_ZEROS_ADDRESS; + private static final InetAddress NEIGH_A = InetAddresses.parseNumericAddress("2001:db8::1"); + private static final InetAddress NEIGH_B = InetAddresses.parseNumericAddress("2001:db8::2"); + private static final MacAddress MAC_A = MacAddress.fromString("00:00:00:00:00:0a"); + private static final MacAddress MAC_B = MacAddress.fromString("11:22:33:00:00:0b"); + @Mock private NetworkStatsManager mStatsManager; @Mock private INetd mNetd; + @Mock private IpServer mIpServer; + @Mock private TetheringConfiguration mTetherConfig; + // Late init since methods must be called by the thread that created this object. private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb; private BpfCoordinator.BpfTetherStatsProvider mTetherStatsProvider; @@ -75,14 +103,35 @@ public class BpfCoordinatorTest { private final TestLooper mTestLooper = new TestLooper(); private BpfCoordinator.Dependencies mDeps = new BpfCoordinator.Dependencies() { - @Override - int getPerformPollInterval() { - return DEFAULT_PERFORM_POLL_INTERVAL_MS; + @NonNull + public Handler getHandler() { + return new Handler(mTestLooper.getLooper()); + } + + @NonNull + public INetd getNetd() { + return mNetd; + } + + @NonNull + public NetworkStatsManager getNetworkStatsManager() { + return mStatsManager; + } + + @NonNull + public SharedLog getSharedLog() { + return new SharedLog("test"); + } + + @Nullable + public TetheringConfiguration getTetherConfig() { + return mTetherConfig; } }; @Before public void setUp() { MockitoAnnotations.initMocks(this); + when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(true /* default value */); } private void waitForIdle() { @@ -95,9 +144,7 @@ public class BpfCoordinatorTest { @NonNull private BpfCoordinator makeBpfCoordinator() throws Exception { - BpfCoordinator coordinator = new BpfCoordinator( - new Handler(mTestLooper.getLooper()), mNetd, mStatsManager, new SharedLog("test"), - mDeps); + final BpfCoordinator coordinator = new BpfCoordinator(mDeps); final ArgumentCaptor<BpfCoordinator.BpfTetherStatsProvider> tetherStatsProviderCaptor = ArgumentCaptor.forClass(BpfCoordinator.BpfTetherStatsProvider.class); @@ -130,9 +177,11 @@ public class BpfCoordinatorTest { return parcel; } + // Set up specific tether stats list and wait for the stats cache is updated by polling thread + // in the coordinator. Beware of that it is only used for the default polling interval. private void setTetherOffloadStatsList(TetherStatsParcel[] tetherStatsList) throws Exception { when(mNetd.tetherOffloadGetStats()).thenReturn(tetherStatsList); - mTestLooper.moveTimeForward(DEFAULT_PERFORM_POLL_INTERVAL_MS); + mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); waitForIdle(); } @@ -201,7 +250,7 @@ public class BpfCoordinatorTest { clearInvocations(mNetd); // Verify the polling update thread stopped. - mTestLooper.moveTimeForward(DEFAULT_PERFORM_POLL_INTERVAL_MS); + mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); waitForIdle(); verify(mNetd, never()).tetherOffloadGetStats(); } @@ -226,21 +275,333 @@ public class BpfCoordinatorTest { when(mNetd.tetherOffloadGetStats()).thenReturn( new TetherStatsParcel[] {buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0)}); mTetherStatsProvider.onSetAlert(100); - mTestLooper.moveTimeForward(DEFAULT_PERFORM_POLL_INTERVAL_MS); + mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); waitForIdle(); mTetherStatsProviderCb.assertNoCallback(); // Verify that notifyAlertReached fired when quota is reached. when(mNetd.tetherOffloadGetStats()).thenReturn( new TetherStatsParcel[] {buildTestTetherStatsParcel(mobileIfIndex, 50, 0, 50, 0)}); - mTestLooper.moveTimeForward(DEFAULT_PERFORM_POLL_INTERVAL_MS); + mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); waitForIdle(); mTetherStatsProviderCb.expectNotifyAlertReached(); // Verify that set quota with UNLIMITED won't trigger any callback. mTetherStatsProvider.onSetAlert(QUOTA_UNLIMITED); - mTestLooper.moveTimeForward(DEFAULT_PERFORM_POLL_INTERVAL_MS); + mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); waitForIdle(); mTetherStatsProviderCb.assertNoCallback(); } + + // The custom ArgumentMatcher simply comes from IpServerTest. + // TODO: move both of them into a common utility class for reusing the code. + private static class TetherOffloadRuleParcelMatcher implements + ArgumentMatcher<TetherOffloadRuleParcel> { + public final int upstreamIfindex; + public final int downstreamIfindex; + public final Inet6Address address; + public final MacAddress srcMac; + public final MacAddress dstMac; + + TetherOffloadRuleParcelMatcher(@NonNull Ipv6ForwardingRule rule) { + upstreamIfindex = rule.upstreamIfindex; + downstreamIfindex = rule.downstreamIfindex; + address = rule.address; + srcMac = rule.srcMac; + dstMac = rule.dstMac; + } + + public boolean matches(@NonNull TetherOffloadRuleParcel parcel) { + return upstreamIfindex == parcel.inputInterfaceIndex + && (downstreamIfindex == parcel.outputInterfaceIndex) + && Arrays.equals(address.getAddress(), parcel.destination) + && (128 == parcel.prefixLength) + && Arrays.equals(srcMac.toByteArray(), parcel.srcL2Address) + && Arrays.equals(dstMac.toByteArray(), parcel.dstL2Address); + } + + public String toString() { + return String.format("TetherOffloadRuleParcelMatcher(%d, %d, %s, %s, %s", + upstreamIfindex, downstreamIfindex, address.getHostAddress(), srcMac, dstMac); + } + } + + @NonNull + private TetherOffloadRuleParcel matches(@NonNull Ipv6ForwardingRule rule) { + return argThat(new TetherOffloadRuleParcelMatcher(rule)); + } + + @NonNull + private static Ipv6ForwardingRule buildTestForwardingRule( + int upstreamIfindex, @NonNull InetAddress address, @NonNull MacAddress dstMac) { + return new Ipv6ForwardingRule(upstreamIfindex, DOWNSTREAM_IFINDEX, (Inet6Address) address, + DOWNSTREAM_MAC, dstMac); + } + + @Test + public void testSetDataLimit() throws Exception { + setupFunctioningNetdInterface(); + + final BpfCoordinator coordinator = makeBpfCoordinator(); + + final String mobileIface = "rmnet_data0"; + final Integer mobileIfIndex = 100; + coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface); + + // [1] Default limit. + // Set the unlimited quota as default if the service has never applied a data limit for a + // given upstream. Note that the data limit only be applied on an upstream which has rules. + final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A); + final InOrder inOrder = inOrder(mNetd); + coordinator.tetherOffloadRuleAdd(mIpServer, rule); + inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(rule)); + inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, QUOTA_UNLIMITED); + inOrder.verifyNoMoreInteractions(); + + // [2] Specific limit. + // Applying the data limit boundary {min, 1gb, max, infinity} on current upstream. + for (final long quota : new long[] {0, 1048576000, Long.MAX_VALUE, QUOTA_UNLIMITED}) { + mTetherStatsProvider.onSetLimit(mobileIface, quota); + waitForIdle(); + inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, quota); + inOrder.verifyNoMoreInteractions(); + } + + // [3] Invalid limit. + // The valid range of quota is 0..max_int64 or -1 (unlimited). + final long invalidLimit = Long.MIN_VALUE; + try { + mTetherStatsProvider.onSetLimit(mobileIface, invalidLimit); + waitForIdle(); + fail("No exception thrown for invalid limit " + invalidLimit + "."); + } catch (IllegalArgumentException expected) { + assertEquals(expected.getMessage(), "invalid quota value " + invalidLimit); + } + } + + // TODO: Test the case in which the rules are changed from different IpServer objects. + @Test + public void testSetDataLimitOnRuleChange() throws Exception { + setupFunctioningNetdInterface(); + + final BpfCoordinator coordinator = makeBpfCoordinator(); + + final String mobileIface = "rmnet_data0"; + final Integer mobileIfIndex = 100; + coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface); + + // Applying a data limit to the current upstream does not take any immediate action. + // The data limit could be only set on an upstream which has rules. + final long limit = 12345; + final InOrder inOrder = inOrder(mNetd); + mTetherStatsProvider.onSetLimit(mobileIface, limit); + waitForIdle(); + inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong()); + + // Adding the first rule on current upstream immediately sends the quota to netd. + final Ipv6ForwardingRule ruleA = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A); + coordinator.tetherOffloadRuleAdd(mIpServer, ruleA); + inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(ruleA)); + inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, limit); + inOrder.verifyNoMoreInteractions(); + + // Adding the second rule on current upstream does not send the quota to netd. + final Ipv6ForwardingRule ruleB = buildTestForwardingRule(mobileIfIndex, NEIGH_B, MAC_B); + coordinator.tetherOffloadRuleAdd(mIpServer, ruleB); + inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(ruleB)); + inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong()); + + // Removing the second rule on current upstream does not send the quota to netd. + coordinator.tetherOffloadRuleRemove(mIpServer, ruleB); + inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(ruleB)); + inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong()); + + // Removing the last rule on current upstream immediately sends the cleanup stuff to netd. + when(mNetd.tetherOffloadGetAndClearStats(mobileIfIndex)) + .thenReturn(buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0)); + coordinator.tetherOffloadRuleRemove(mIpServer, ruleA); + inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(ruleA)); + inOrder.verify(mNetd).tetherOffloadGetAndClearStats(mobileIfIndex); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testTetherOffloadRuleUpdateAndClear() throws Exception { + setupFunctioningNetdInterface(); + + final BpfCoordinator coordinator = makeBpfCoordinator(); + + final String ethIface = "eth1"; + final String mobileIface = "rmnet_data0"; + final Integer ethIfIndex = 100; + final Integer mobileIfIndex = 101; + coordinator.addUpstreamNameToLookupTable(ethIfIndex, ethIface); + coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface); + + final InOrder inOrder = inOrder(mNetd); + + // Before the rule test, here are the additional actions while the rules are changed. + // - After adding the first rule on a given upstream, the coordinator adds a data limit. + // If the service has never applied the data limit, set an unlimited quota as default. + // - After removing the last rule on a given upstream, the coordinator gets the last stats. + // Then, it clears the stats and the limit entry from BPF maps. + // See tetherOffloadRule{Add, Remove, Clear, Clean}. + + // [1] Adding rules on the upstream Ethernet. + // Note that the default data limit is applied after the first rule is added. + final Ipv6ForwardingRule ethernetRuleA = buildTestForwardingRule( + ethIfIndex, NEIGH_A, MAC_A); + final Ipv6ForwardingRule ethernetRuleB = buildTestForwardingRule( + ethIfIndex, NEIGH_B, MAC_B); + + coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleA); + inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(ethernetRuleA)); + inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(ethIfIndex, QUOTA_UNLIMITED); + + coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleB); + inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(ethernetRuleB)); + + // [2] Update the existing rules from Ethernet to cellular. + final Ipv6ForwardingRule mobileRuleA = buildTestForwardingRule( + mobileIfIndex, NEIGH_A, MAC_A); + final Ipv6ForwardingRule mobileRuleB = buildTestForwardingRule( + mobileIfIndex, NEIGH_B, MAC_B); + when(mNetd.tetherOffloadGetAndClearStats(ethIfIndex)) + .thenReturn(buildTestTetherStatsParcel(ethIfIndex, 10, 20, 30, 40)); + + // Update the existing rules for upstream changes. The rules are removed and re-added one + // by one for updating upstream interface index by #tetherOffloadRuleUpdate. + coordinator.tetherOffloadRuleUpdate(mIpServer, mobileIfIndex); + inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(ethernetRuleA)); + inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(mobileRuleA)); + inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, QUOTA_UNLIMITED); + inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(ethernetRuleB)); + inOrder.verify(mNetd).tetherOffloadGetAndClearStats(ethIfIndex); + inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(mobileRuleB)); + + // [3] Clear all rules for a given IpServer. + when(mNetd.tetherOffloadGetAndClearStats(mobileIfIndex)) + .thenReturn(buildTestTetherStatsParcel(mobileIfIndex, 50, 60, 70, 80)); + coordinator.tetherOffloadRuleClear(mIpServer); + inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(mobileRuleA)); + inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(mobileRuleB)); + inOrder.verify(mNetd).tetherOffloadGetAndClearStats(mobileIfIndex); + + // [4] Force pushing stats update to verify that the last diff of stats is reported on all + // upstreams. + mTetherStatsProvider.pushTetherStats(); + mTetherStatsProviderCb.expectNotifyStatsUpdated( + new NetworkStats(0L, 2) + .addEntry(buildTestEntry(STATS_PER_IFACE, ethIface, 10, 20, 30, 40)) + .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 50, 60, 70, 80)), + new NetworkStats(0L, 2) + .addEntry(buildTestEntry(STATS_PER_UID, ethIface, 10, 20, 30, 40)) + .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 50, 60, 70, 80))); + } + + @Test + public void testTetheringConfigDisable() throws Exception { + setupFunctioningNetdInterface(); + when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(false); + + final BpfCoordinator coordinator = makeBpfCoordinator(); + coordinator.startPolling(); + + // The tether stats polling task should not be scheduled. + mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); + waitForIdle(); + verify(mNetd, never()).tetherOffloadGetStats(); + + // The interface name lookup table can't be added. + final String iface = "rmnet_data0"; + final Integer ifIndex = 100; + coordinator.addUpstreamNameToLookupTable(ifIndex, iface); + assertEquals(0, coordinator.getInterfaceNamesForTesting().size()); + + // The rule can't be added. + final InetAddress neigh = InetAddresses.parseNumericAddress("2001:db8::1"); + final MacAddress mac = MacAddress.fromString("00:00:00:00:00:0a"); + final Ipv6ForwardingRule rule = buildTestForwardingRule(ifIndex, neigh, mac); + coordinator.tetherOffloadRuleAdd(mIpServer, rule); + verify(mNetd, never()).tetherOffloadRuleAdd(any()); + LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = + coordinator.getForwardingRulesForTesting().get(mIpServer); + assertNull(rules); + + // The rule can't be removed. This is not a realistic case because adding rule is not + // allowed. That implies no rule could be removed, cleared or updated. Verify these + // cases just in case. + rules = new LinkedHashMap<Inet6Address, Ipv6ForwardingRule>(); + rules.put(rule.address, rule); + coordinator.getForwardingRulesForTesting().put(mIpServer, rules); + coordinator.tetherOffloadRuleRemove(mIpServer, rule); + verify(mNetd, never()).tetherOffloadRuleRemove(any()); + rules = coordinator.getForwardingRulesForTesting().get(mIpServer); + assertNotNull(rules); + assertEquals(1, rules.size()); + + // The rule can't be cleared. + coordinator.tetherOffloadRuleClear(mIpServer); + verify(mNetd, never()).tetherOffloadRuleRemove(any()); + rules = coordinator.getForwardingRulesForTesting().get(mIpServer); + assertNotNull(rules); + assertEquals(1, rules.size()); + + // The rule can't be updated. + coordinator.tetherOffloadRuleUpdate(mIpServer, rule.upstreamIfindex + 1 /* new */); + verify(mNetd, never()).tetherOffloadRuleRemove(any()); + verify(mNetd, never()).tetherOffloadRuleAdd(any()); + rules = coordinator.getForwardingRulesForTesting().get(mIpServer); + assertNotNull(rules); + assertEquals(1, rules.size()); + } + + @Test + public void testTetheringConfigSetPollingInterval() throws Exception { + setupFunctioningNetdInterface(); + + final BpfCoordinator coordinator = makeBpfCoordinator(); + + // [1] The default polling interval. + coordinator.startPolling(); + assertEquals(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS, coordinator.getPollingInterval()); + coordinator.stopPolling(); + + // [2] Expect the invalid polling interval isn't applied. The valid range of interval is + // DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS..max_long. + for (final int interval + : new int[] {0, 100, DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS - 1}) { + when(mTetherConfig.getOffloadPollInterval()).thenReturn(interval); + coordinator.startPolling(); + assertEquals(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS, coordinator.getPollingInterval()); + coordinator.stopPolling(); + } + + // [3] Set a specific polling interval which is larger than default value. + // Use a large polling interval to avoid flaky test because the time forwarding + // approximation is used to verify the scheduled time of the polling thread. + final int pollingInterval = 100_000; + when(mTetherConfig.getOffloadPollInterval()).thenReturn(pollingInterval); + coordinator.startPolling(); + + // Expect the specific polling interval to be applied. + assertEquals(pollingInterval, coordinator.getPollingInterval()); + + // Start on a new polling time slot. + mTestLooper.moveTimeForward(pollingInterval); + waitForIdle(); + clearInvocations(mNetd); + + // Move time forward to 90% polling interval time. Expect that the polling thread has not + // scheduled yet. + mTestLooper.moveTimeForward((long) (pollingInterval * 0.9)); + waitForIdle(); + verify(mNetd, never()).tetherOffloadGetStats(); + + // Move time forward to the remaining 10% polling interval time. Expect that the polling + // thread has scheduled. + mTestLooper.moveTimeForward((long) (pollingInterval * 0.1)); + waitForIdle(); + verify(mNetd).tetherOffloadGetStats(); + } } diff --git a/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java b/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java index 1999ad786ed4..3f4cfe7afb47 100644 --- a/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java +++ b/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java @@ -292,7 +292,7 @@ public class TetheringConfigurationTest { initializeBpfOffloadConfiguration(true, null /* unset */); final TetheringConfiguration enableByRes = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); - assertTrue(enableByRes.enableBpfOffload); + assertTrue(enableByRes.isBpfOffloadEnabled()); } @Test @@ -301,7 +301,7 @@ public class TetheringConfigurationTest { initializeBpfOffloadConfiguration(res, "true"); final TetheringConfiguration enableByDevConOverride = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); - assertTrue(enableByDevConOverride.enableBpfOffload); + assertTrue(enableByDevConOverride.isBpfOffloadEnabled()); } } @@ -310,7 +310,7 @@ public class TetheringConfigurationTest { initializeBpfOffloadConfiguration(false, null /* unset */); final TetheringConfiguration disableByRes = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); - assertFalse(disableByRes.enableBpfOffload); + assertFalse(disableByRes.isBpfOffloadEnabled()); } @Test @@ -319,7 +319,7 @@ public class TetheringConfigurationTest { initializeBpfOffloadConfiguration(res, "false"); final TetheringConfiguration disableByDevConOverride = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); - assertFalse(disableByDevConOverride.enableBpfOffload); + assertFalse(disableByDevConOverride.isBpfOffloadEnabled()); } } diff --git a/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java index 02dbd4cc1c5f..2b2dfae5c930 100644 --- a/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java +++ b/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java @@ -338,8 +338,8 @@ public class TetheringTest { } @Override - public BpfCoordinator getBpfCoordinator(Handler handler, INetd netd, - SharedLog log, BpfCoordinator.Dependencies deps) { + public BpfCoordinator getBpfCoordinator( + BpfCoordinator.Dependencies deps) { return mBpfCoordinator; } diff --git a/proto/Android.bp b/proto/Android.bp index 65bccbb4aac8..ac1b852abf55 100644 --- a/proto/Android.bp +++ b/proto/Android.bp @@ -26,4 +26,6 @@ java_library_static { }, srcs: ["src/metrics_constants/metrics_constants.proto"], sdk_version: "system_current", + // this is part of updatable modules(CaptivePortalLogin) which targets 29(Q) + min_sdk_version: "29", } diff --git a/services/backup/OWNERS b/services/backup/OWNERS index ec694dfae144..9c21e8fe5e45 100644 --- a/services/backup/OWNERS +++ b/services/backup/OWNERS @@ -6,3 +6,4 @@ ctate@google.com jorlow@google.com nathch@google.com rthakohov@google.com + diff --git a/services/core/java/com/android/server/net/NetworkStatsService.java b/services/core/java/com/android/server/net/NetworkStatsService.java index ba538e1cd826..fd8ac061bbe0 100644 --- a/services/core/java/com/android/server/net/NetworkStatsService.java +++ b/services/core/java/com/android/server/net/NetworkStatsService.java @@ -87,6 +87,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.database.ContentObserver; import android.net.DataUsageRequest; import android.net.INetworkManagementEventObserver; import android.net.INetworkStatsService; @@ -103,6 +104,7 @@ import android.net.NetworkStats.NonMonotonicObserver; import android.net.NetworkStatsHistory; import android.net.NetworkTemplate; import android.net.TrafficStats; +import android.net.Uri; import android.net.netstats.provider.INetworkStatsProvider; import android.net.netstats.provider.INetworkStatsProviderCallback; import android.net.netstats.provider.NetworkStatsProvider; @@ -213,6 +215,9 @@ public class NetworkStatsService extends INetworkStatsService.Stub { private final boolean mUseBpfTrafficStats; + private final ContentObserver mContentObserver; + private final ContentResolver mContentResolver; + @VisibleForTesting public static final String ACTION_NETWORK_STATS_POLL = "com.android.server.action.NETWORK_STATS_POLL"; @@ -437,7 +442,10 @@ public class NetworkStatsService extends INetworkStatsService.Stub { handlerThread.start(); mHandler = new NetworkStatsHandler(handlerThread.getLooper()); mNetworkStatsSubscriptionsMonitor = deps.makeSubscriptionsMonitor(mContext, - new HandlerExecutor(mHandler), this); + mHandler.getLooper(), new HandlerExecutor(mHandler), this); + mContentResolver = mContext.getContentResolver(); + mContentObserver = mDeps.makeContentObserver(mHandler, mSettings, + mNetworkStatsSubscriptionsMonitor); } /** @@ -460,11 +468,31 @@ public class NetworkStatsService extends INetworkStatsService.Stub { */ @NonNull public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor(@NonNull Context context, - @NonNull Executor executor, @NonNull NetworkStatsService service) { + @NonNull Looper looper, @NonNull Executor executor, + @NonNull NetworkStatsService service) { // TODO: Update RatType passively in NSS, instead of querying into the monitor // when forceUpdateIface. - return new NetworkStatsSubscriptionsMonitor(context, executor, (subscriberId, type) -> - service.handleOnCollapsedRatTypeChanged()); + return new NetworkStatsSubscriptionsMonitor(context, looper, executor, + (subscriberId, type) -> service.handleOnCollapsedRatTypeChanged()); + } + + /** + * Create a ContentObserver instance which is used to observe settings changes, + * and dispatch onChange events on handler thread. + */ + public @NonNull ContentObserver makeContentObserver(@NonNull Handler handler, + @NonNull NetworkStatsSettings settings, + @NonNull NetworkStatsSubscriptionsMonitor monitor) { + return new ContentObserver(handler) { + @Override + public void onChange(boolean selfChange, @NonNull Uri uri) { + if (!settings.getCombineSubtypeEnabled()) { + monitor.start(); + } else { + monitor.stop(); + } + } + }; } } @@ -530,11 +558,14 @@ public class NetworkStatsService extends INetworkStatsService.Stub { mAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, currentRealtime, mSettings.getPollInterval(), pollIntent); - // TODO: listen to settings changed to support dynamically enable/disable. - // watch for networkType changes - if (!mSettings.getCombineSubtypeEnabled()) { - mNetworkStatsSubscriptionsMonitor.start(); - } + mContentResolver.registerContentObserver(Settings.Global + .getUriFor(Settings.Global.NETSTATS_COMBINE_SUBTYPE_ENABLED), + false /* notifyForDescendants */, mContentObserver); + + // Post a runnable on handler thread to call onChange(). It's for getting current value of + // NETSTATS_COMBINE_SUBTYPE_ENABLED to decide start or stop monitoring RAT type changes. + mHandler.post(() -> mContentObserver.onChange(false, Settings.Global + .getUriFor(Settings.Global.NETSTATS_COMBINE_SUBTYPE_ENABLED))); registerGlobalAlert(); } @@ -560,6 +591,8 @@ public class NetworkStatsService extends INetworkStatsService.Stub { mNetworkStatsSubscriptionsMonitor.stop(); } + mContentResolver.unregisterContentObserver(mContentObserver); + final long currentTime = mClock.millis(); // persist any pending stats diff --git a/services/core/java/com/android/server/net/NetworkStatsSubscriptionsMonitor.java b/services/core/java/com/android/server/net/NetworkStatsSubscriptionsMonitor.java index 16a63d543626..7711c6a21d20 100644 --- a/services/core/java/com/android/server/net/NetworkStatsSubscriptionsMonitor.java +++ b/services/core/java/com/android/server/net/NetworkStatsSubscriptionsMonitor.java @@ -20,6 +20,7 @@ import static android.net.NetworkTemplate.getCollapsedRatType; import android.annotation.NonNull; import android.content.Context; +import android.os.Looper; import android.telephony.Annotation; import android.telephony.PhoneStateListener; import android.telephony.ServiceState; @@ -28,6 +29,7 @@ import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.CollectionUtils; import java.util.ArrayList; @@ -38,8 +40,6 @@ import java.util.concurrent.Executor; /** * Helper class that watches for events that are triggered per subscription. */ -// TODO (b/152176562): Write tests to verify subscription changes generate corresponding -// register/unregister calls. public class NetworkStatsSubscriptionsMonitor extends SubscriptionManager.OnSubscriptionsChangedListener { @@ -76,9 +76,9 @@ public class NetworkStatsSubscriptionsMonitor extends @NonNull private final Executor mExecutor; - NetworkStatsSubscriptionsMonitor(@NonNull Context context, @NonNull Executor executor, - @NonNull Delegate delegate) { - super(); + NetworkStatsSubscriptionsMonitor(@NonNull Context context, @NonNull Looper looper, + @NonNull Executor executor, @NonNull Delegate delegate) { + super(looper); mSubscriptionManager = (SubscriptionManager) context.getSystemService( Context.TELEPHONY_SUBSCRIPTION_SERVICE); mTeleManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); @@ -207,5 +207,10 @@ public class NetworkStatsSubscriptionsMonitor extends mLastCollapsedRatType = collapsedRatType; mMonitor.mDelegate.onCollapsedRatTypeChanged(mSubscriberId, mLastCollapsedRatType); } + + @VisibleForTesting + public int getSubId() { + return mSubId; + } } } diff --git a/services/core/java/com/android/server/storage/AppFuseBridge.java b/services/core/java/com/android/server/storage/AppFuseBridge.java index 9d6a64701e85..b00540fd2a52 100644 --- a/services/core/java/com/android/server/storage/AppFuseBridge.java +++ b/services/core/java/com/android/server/storage/AppFuseBridge.java @@ -56,6 +56,15 @@ public class AppFuseBridge implements Runnable { public ParcelFileDescriptor addBridge(MountScope mountScope) throws FuseUnavailableMountException, NativeDaemonConnectorException { + /* + ** Dead Lock between Java lock (AppFuseBridge.java) and Native lock (FuseBridgeLoop.cc) + ** + ** (Thread A) Got Java lock (addBrdige) -> Try to get Native lock (native_add_brdige) + ** (Thread B) Got Native lock (FuseBrdigeLoop.start) -> Try to get Java lock (onClosed) + ** + ** Guarantee the lock order (native lock -> java lock) when adding Bridge. + */ + native_lock(); try { synchronized (this) { Preconditions.checkArgument(mScopes.indexOfKey(mountScope.mountId) < 0); @@ -73,6 +82,7 @@ public class AppFuseBridge implements Runnable { return result; } } finally { + native_unlock(); IoUtils.closeQuietly(mountScope); } } @@ -159,4 +169,6 @@ public class AppFuseBridge implements Runnable { private native void native_delete(long loop); private native void native_start_loop(long loop); private native int native_add_bridge(long loop, int mountId, int deviceId); + private native void native_lock(); + private native void native_unlock(); } diff --git a/services/core/jni/com_android_server_storage_AppFuseBridge.cpp b/services/core/jni/com_android_server_storage_AppFuseBridge.cpp index e51963340ae1..20210abd0ea8 100644 --- a/services/core/jni/com_android_server_storage_AppFuseBridge.cpp +++ b/services/core/jni/com_android_server_storage_AppFuseBridge.cpp @@ -123,6 +123,14 @@ jint com_android_server_storage_AppFuseBridge_add_bridge( return proxyFd[1].release(); } +void com_android_server_storage_AppFuseBridge_lock(JNIEnv* env, jobject self) { + fuse::FuseBridgeLoop::Lock(); +} + +void com_android_server_storage_AppFuseBridge_unlock(JNIEnv* env, jobject self) { + fuse::FuseBridgeLoop::Unlock(); +} + const JNINativeMethod methods[] = { { "native_new", @@ -143,6 +151,16 @@ const JNINativeMethod methods[] = { "native_add_bridge", "(JII)I", reinterpret_cast<void*>(com_android_server_storage_AppFuseBridge_add_bridge) + }, + { + "native_lock", + "()V", + reinterpret_cast<void*>(com_android_server_storage_AppFuseBridge_lock) + }, + { + "native_unlock", + "()V", + reinterpret_cast<void*>(com_android_server_storage_AppFuseBridge_unlock) } }; diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java index ce71511c0361..b3bf507d701e 100644 --- a/telecomm/java/android/telecom/TelecomManager.java +++ b/telecomm/java/android/telecom/TelecomManager.java @@ -1630,6 +1630,7 @@ public class TelecomManager { * @hide */ @SystemApi + @TestApi @RequiresPermission(anyOf = { READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 7f45ef4a2eca..c9ee2a1279bc 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -2798,14 +2798,12 @@ public class CarrierConfigManager { /** * A list of 4 customized LTE Reference Signal Signal to Noise Ratio (RSSNR) thresholds. * - * 4 threshold integers must be within the boundaries [-200, 300], and the levels are: - * "NONE: [-200, threshold1)" + * 4 threshold integers must be within the boundaries [-20 dB, 30 dB], and the levels are: + * "NONE: [-20, threshold1)" * "POOR: [threshold1, threshold2)" * "MODERATE: [threshold2, threshold3)" * "GOOD: [threshold3, threshold4)" - * "EXCELLENT: [threshold4, 300]" - * Note: the unit of the values is 10*db; it is derived by multiplying 10 on the original dB - * value reported by modem. + * "EXCELLENT: [threshold4, 30]" * * This key is considered invalid if the format is violated. If the key is invalid or * not configured, a default value set will apply. @@ -4143,10 +4141,10 @@ public class CarrierConfigManager { }); sDefaults.putIntArray(KEY_LTE_RSSNR_THRESHOLDS_INT_ARRAY, new int[] { - -30, /* SIGNAL_STRENGTH_POOR */ - 10, /* SIGNAL_STRENGTH_MODERATE */ - 45, /* SIGNAL_STRENGTH_GOOD */ - 130 /* SIGNAL_STRENGTH_GREAT */ + -3, /* SIGNAL_STRENGTH_POOR */ + 1, /* SIGNAL_STRENGTH_MODERATE */ + 5, /* SIGNAL_STRENGTH_GOOD */ + 13 /* SIGNAL_STRENGTH_GREAT */ }); sDefaults.putIntArray(KEY_WCDMA_RSCP_THRESHOLDS_INT_ARRAY, new int[] { diff --git a/telephony/java/android/telephony/CellSignalStrengthLte.java b/telephony/java/android/telephony/CellSignalStrengthLte.java index 2529387b19b3..c26936e4bdd0 100644 --- a/telephony/java/android/telephony/CellSignalStrengthLte.java +++ b/telephony/java/android/telephony/CellSignalStrengthLte.java @@ -118,7 +118,7 @@ public final class CellSignalStrengthLte extends CellSignalStrength implements P * @param rssi in dBm [-113,-51], UNKNOWN * @param rsrp in dBm [-140,-43], UNKNOWN * @param rsrq in dB [-34, 3], UNKNOWN - * @param rssnr in 10*dB [-200, +300], UNKNOWN + * @param rssnr in dB [-20, +30], UNKNOWN * @param cqi [0, 15], UNKNOWN * @param timingAdvance [0, 1282], UNKNOWN * @@ -131,7 +131,7 @@ public final class CellSignalStrengthLte extends CellSignalStrength implements P mSignalStrength = mRssi; mRsrp = inRangeOrUnavailable(rsrp, -140, -43); mRsrq = inRangeOrUnavailable(rsrq, -34, 3); - mRssnr = inRangeOrUnavailable(rssnr, -200, 300); + mRssnr = inRangeOrUnavailable(rssnr, -20, 30); mCqi = inRangeOrUnavailable(cqi, 0, 15); mTimingAdvance = inRangeOrUnavailable(timingAdvance, 0, 1282); updateLevel(null, null); @@ -143,7 +143,7 @@ public final class CellSignalStrengthLte extends CellSignalStrength implements P this(convertRssiAsuToDBm(lte.signalStrength), lte.rsrp != CellInfo.UNAVAILABLE ? -lte.rsrp : lte.rsrp, lte.rsrq != CellInfo.UNAVAILABLE ? -lte.rsrq : lte.rsrq, - lte.rssnr, lte.cqi, lte.timingAdvance); + convertRssnrUnitFromTenDbToDB(lte.rssnr), lte.cqi, lte.timingAdvance); } /** @hide */ @@ -208,10 +208,10 @@ public final class CellSignalStrengthLte extends CellSignalStrength implements P }; // Lifted from Default carrier configs and max range of RSSNR private static final int[] sRssnrThresholds = new int[] { - -30, /* SIGNAL_STRENGTH_POOR */ - 10, /* SIGNAL_STRENGTH_MODERATE */ - 45, /* SIGNAL_STRENGTH_GOOD */ - 130 /* SIGNAL_STRENGTH_GREAT */ + -3, /* SIGNAL_STRENGTH_POOR */ + 1, /* SIGNAL_STRENGTH_MODERATE */ + 5, /* SIGNAL_STRENGTH_GOOD */ + 13 /* SIGNAL_STRENGTH_GREAT */ }; private static final int sRsrpBoost = 0; @@ -556,6 +556,10 @@ public final class CellSignalStrengthLte extends CellSignalStrength implements P Rlog.w(LOG_TAG, s); } + private static int convertRssnrUnitFromTenDbToDB(int rssnr) { + return rssnr / 10; + } + private static int convertRssiAsuToDBm(int rssiAsu) { if (rssiAsu == SIGNAL_STRENGTH_LTE_RSSI_ASU_UNKNOWN) { return CellInfo.UNAVAILABLE; diff --git a/telephony/java/android/telephony/ims/ImsRcsManager.java b/telephony/java/android/telephony/ims/ImsRcsManager.java index 71bc68e4b4c4..16e54833abdf 100644 --- a/telephony/java/android/telephony/ims/ImsRcsManager.java +++ b/telephony/java/android/telephony/ims/ImsRcsManager.java @@ -56,14 +56,15 @@ public class ImsRcsManager { * Activity Action: Show the opt-in dialog for enabling or disabling RCS contact discovery * using User Capability Exchange (UCE). * <p> - * An application that depends on contact discovery being enabled may send this intent + * An application that depends on RCS contact discovery being enabled must send this intent * using {@link Context#startActivity(Intent)} to ask the user to opt-in for contacts upload for - * capability exchange if it is currently disabled. Whether or not this setting has been enabled - * can be queried using {@link RcsUceAdapter#isUceSettingEnabled()}. + * capability exchange if it is currently disabled. Whether or not RCS contact discovery has + * been enabled by the user can be queried using {@link RcsUceAdapter#isUceSettingEnabled()}. * <p> - * This intent should only be sent if the carrier supports RCS capability exchange, which can be - * queried using the key {@link CarrierConfigManager#KEY_USE_RCS_PRESENCE_BOOL}. Otherwise, the - * setting will not be present. + * This intent will always be handled by the system, however the application should only send + * this Intent if the carrier supports RCS contact discovery, which can be queried using the key + * {@link CarrierConfigManager#KEY_USE_RCS_PRESENCE_BOOL}. Otherwise, the RCS contact discovery + * opt-in dialog will not be shown. * <p> * Input: A mandatory {@link Settings#EXTRA_SUB_ID} extra containing the subscription that the * setting will be be shown for. diff --git a/telephony/java/android/telephony/ims/RcsUceAdapter.java b/telephony/java/android/telephony/ims/RcsUceAdapter.java index 01fc09a1bb26..27456f12302a 100644 --- a/telephony/java/android/telephony/ims/RcsUceAdapter.java +++ b/telephony/java/android/telephony/ims/RcsUceAdapter.java @@ -502,9 +502,10 @@ public class RcsUceAdapter { /** * Change the user’s setting for whether or not UCE is enabled for the associated subscription. * <p> - * If an application Requires UCE, they may launch an Activity using the Intent + * If an application Requires UCE, they will launch an Activity using the Intent * {@link ImsRcsManager#ACTION_SHOW_CAPABILITY_DISCOVERY_OPT_IN}, which will ask the user if - * they wish to enable this feature. + * they wish to enable this feature. This setting should only be enabled after the user has + * opted-in to capability exchange. * <p> * Note: This setting does not affect whether or not the device publishes its service * capabilities if the subscription supports presence publication. diff --git a/tests/net/common/java/android/net/LinkPropertiesTest.java b/tests/net/common/java/android/net/LinkPropertiesTest.java index 0fc9be32f4cf..6eba62e63740 100644 --- a/tests/net/common/java/android/net/LinkPropertiesTest.java +++ b/tests/net/common/java/android/net/LinkPropertiesTest.java @@ -16,6 +16,8 @@ package android.net; +import static android.net.RouteInfo.RTN_THROW; +import static android.net.RouteInfo.RTN_UNICAST; import static android.net.RouteInfo.RTN_UNREACHABLE; import static com.android.testutils.ParcelUtilsKt.assertParcelSane; @@ -1282,4 +1284,20 @@ public class LinkPropertiesTest { assertTrue(lp.hasIpv6UnreachableDefaultRoute()); assertFalse(lp.hasIpv4UnreachableDefaultRoute()); } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testRouteAddWithSameKey() throws Exception { + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("wlan0"); + final IpPrefix v6 = new IpPrefix("64:ff9b::/96"); + lp.addRoute(new RouteInfo(v6, address("fe80::1"), "wlan0", RTN_UNICAST, 1280)); + assertEquals(1, lp.getRoutes().size()); + lp.addRoute(new RouteInfo(v6, address("fe80::1"), "wlan0", RTN_UNICAST, 1500)); + assertEquals(1, lp.getRoutes().size()); + final IpPrefix v4 = new IpPrefix("192.0.2.128/25"); + lp.addRoute(new RouteInfo(v4, address("192.0.2.1"), "wlan0", RTN_UNICAST, 1460)); + assertEquals(2, lp.getRoutes().size()); + lp.addRoute(new RouteInfo(v4, address("192.0.2.1"), "wlan0", RTN_THROW, 1460)); + assertEquals(2, lp.getRoutes().size()); + } } diff --git a/tests/net/common/java/android/net/RouteInfoTest.java b/tests/net/common/java/android/net/RouteInfoTest.java index 8204b494bbb8..60cac0b6b0f5 100644 --- a/tests/net/common/java/android/net/RouteInfoTest.java +++ b/tests/net/common/java/android/net/RouteInfoTest.java @@ -25,6 +25,7 @@ import static com.android.testutils.ParcelUtilsKt.assertParcelingIsLossless; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -56,7 +57,7 @@ public class RouteInfoTest { private static final int INVALID_ROUTE_TYPE = -1; private InetAddress Address(String addr) { - return InetAddress.parseNumericAddress(addr); + return InetAddresses.parseNumericAddress(addr); } private IpPrefix Prefix(String prefix) { @@ -391,4 +392,43 @@ public class RouteInfoTest { r = new RouteInfo(Prefix("0.0.0.0/0"), Address("0.0.0.0"), "wlan0"); assertEquals(0, r.getMtu()); } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testRouteKey() { + RouteInfo.RouteKey k1, k2; + // Only prefix, null gateway and null interface + k1 = new RouteInfo(Prefix("2001:db8::/128"), null).getRouteKey(); + k2 = new RouteInfo(Prefix("2001:db8::/128"), null).getRouteKey(); + assertEquals(k1, k2); + assertEquals(k1.hashCode(), k2.hashCode()); + + // With prefix, gateway and interface. Type and MTU does not affect RouteKey equality + k1 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0", + RTN_UNREACHABLE, 1450).getRouteKey(); + k2 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0", + RouteInfo.RTN_UNICAST, 1400).getRouteKey(); + assertEquals(k1, k2); + assertEquals(k1.hashCode(), k2.hashCode()); + + // Different scope IDs are ignored by the kernel, so we consider them equal here too. + k1 = new RouteInfo(Prefix("2001:db8::/64"), Address("fe80::1%1"), "wlan0").getRouteKey(); + k2 = new RouteInfo(Prefix("2001:db8::/64"), Address("fe80::1%2"), "wlan0").getRouteKey(); + assertEquals(k1, k2); + assertEquals(k1.hashCode(), k2.hashCode()); + + // Different prefix + k1 = new RouteInfo(Prefix("192.0.2.0/24"), null).getRouteKey(); + k2 = new RouteInfo(Prefix("192.0.3.0/24"), null).getRouteKey(); + assertNotEquals(k1, k2); + + // Different gateway + k1 = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::1"), null).getRouteKey(); + k2 = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::2"), null).getRouteKey(); + assertNotEquals(k1, k2); + + // Different interface + k1 = new RouteInfo(Prefix("ff02::1/128"), null, "tun0").getRouteKey(); + k2 = new RouteInfo(Prefix("ff02::1/128"), null, "tun1").getRouteKey(); + assertNotEquals(k1, k2); + } } diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java index ea4982ed7c7d..bc853749e294 100644 --- a/tests/net/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java @@ -100,6 +100,7 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.ArgumentMatchers.startsWith; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.any; @@ -164,6 +165,8 @@ import android.net.LinkAddress; import android.net.LinkProperties; import android.net.MatchAllNetworkSpecifier; import android.net.Network; +import android.net.NetworkAgent; +import android.net.NetworkAgentConfig; import android.net.NetworkCapabilities; import android.net.NetworkFactory; import android.net.NetworkInfo; @@ -6808,6 +6811,30 @@ public class ConnectivityServiceTest { assertEquals(wifiLp, mService.getActiveLinkProperties()); } + @Test + public void testLegacyExtraInfoSentToNetworkMonitor() throws Exception { + class TestNetworkAgent extends NetworkAgent { + TestNetworkAgent(Context context, Looper looper, NetworkAgentConfig config) { + super(context, looper, "MockAgent", new NetworkCapabilities(), + new LinkProperties(), 40 , config, null /* provider */); + } + } + final NetworkAgent naNoExtraInfo = new TestNetworkAgent( + mServiceContext, mCsHandlerThread.getLooper(), new NetworkAgentConfig()); + naNoExtraInfo.register(); + verify(mNetworkStack).makeNetworkMonitor(any(), isNull(String.class), any()); + naNoExtraInfo.unregister(); + + reset(mNetworkStack); + final NetworkAgentConfig config = + new NetworkAgentConfig.Builder().setLegacyExtraInfo("legacyinfo").build(); + final NetworkAgent naExtraInfo = new TestNetworkAgent( + mServiceContext, mCsHandlerThread.getLooper(), config); + naExtraInfo.register(); + verify(mNetworkStack).makeNetworkMonitor(any(), eq("legacyinfo"), any()); + naExtraInfo.unregister(); + } + private void setupLocationPermissions( int targetSdk, boolean locationToggle, String op, String perm) throws Exception { final ApplicationInfo applicationInfo = new ApplicationInfo(); diff --git a/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java index a1bb0d586916..1307a849f1eb 100644 --- a/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java +++ b/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java @@ -41,6 +41,7 @@ import static android.net.NetworkStats.TAG_ALL; import static android.net.NetworkStats.TAG_NONE; import static android.net.NetworkStats.UID_ALL; import static android.net.NetworkStatsHistory.FIELD_ALL; +import static android.net.NetworkTemplate.NETWORK_TYPE_ALL; import static android.net.NetworkTemplate.buildTemplateMobileAll; import static android.net.NetworkTemplate.buildTemplateMobileWithRatType; import static android.net.NetworkTemplate.buildTemplateWifiWildcard; @@ -62,6 +63,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -71,6 +73,7 @@ import android.app.AlarmManager; import android.app.usage.NetworkStatsManager; import android.content.Context; import android.content.Intent; +import android.database.ContentObserver; import android.net.DataUsageRequest; import android.net.INetworkManagementEventObserver; import android.net.INetworkStatsSession; @@ -94,6 +97,7 @@ import android.os.Message; import android.os.Messenger; import android.os.PowerManager; import android.os.SimpleClock; +import android.provider.Settings; import android.telephony.TelephonyManager; import androidx.test.InstrumentationRegistry; @@ -173,6 +177,8 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest { private NetworkStatsService mService; private INetworkStatsSession mSession; private INetworkManagementEventObserver mNetworkObserver; + private ContentObserver mContentObserver; + private Handler mHandler; private final Clock mClock = new SimpleClock(ZoneOffset.UTC) { @Override @@ -212,6 +218,12 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest { mService.systemReady(); // Verify that system ready fetches realtime stats verify(mStatsFactory).readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL); + // Wait for posting onChange() event to handler thread and verify that when system ready, + // start monitoring data usage per RAT type because the settings value is mock as false + // by default in expectSettings(). + waitForIdle(); + verify(mNetworkStatsSubscriptionsMonitor).start(); + reset(mNetworkStatsSubscriptionsMonitor); mSession = mService.openSession(); assertNotNull("openSession() failed", mSession); @@ -233,11 +245,19 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest { @Override public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor( - @NonNull Context context, @NonNull Executor executor, + @NonNull Context context, @NonNull Looper looper, @NonNull Executor executor, @NonNull NetworkStatsService service) { return mNetworkStatsSubscriptionsMonitor; } + + @Override + public ContentObserver makeContentObserver(Handler handler, + NetworkStatsSettings settings, NetworkStatsSubscriptionsMonitor monitor) { + mHandler = handler; + return mContentObserver = super.makeContentObserver(handler, settings, monitor); + } + }; } @@ -1191,6 +1211,99 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest { provider.expectOnSetAlert(MB_IN_BYTES); } + private void setCombineSubtypeEnabled(boolean enable) { + when(mSettings.getCombineSubtypeEnabled()).thenReturn(enable); + mHandler.post(() -> mContentObserver.onChange(false, Settings.Global + .getUriFor(Settings.Global.NETSTATS_COMBINE_SUBTYPE_ENABLED))); + waitForIdle(); + if (enable) { + verify(mNetworkStatsSubscriptionsMonitor).stop(); + } else { + verify(mNetworkStatsSubscriptionsMonitor).start(); + } + } + + @Test + public void testDynamicWatchForNetworkRatTypeChanges() throws Exception { + // Build 3G template, type unknown template to get stats while network type is unknown + // and type all template to get the sum of all network type stats. + final NetworkTemplate template3g = + buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UMTS); + final NetworkTemplate templateUnknown = + buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UNKNOWN); + final NetworkTemplate templateAll = + buildTemplateMobileWithRatType(null, NETWORK_TYPE_ALL); + final NetworkState[] states = new NetworkState[]{buildMobile3gState(IMSI_1)}; + + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + // 3G network comes online. + setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_UMTS); + mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states), + new VpnInfo[0]); + + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 12L, 18L, 14L, 1L, 0L))); + forcePollAndWaitForIdle(); + + // Since CombineSubtypeEnabled is false by default in unit test, the generated traffic + // will be split by RAT type. Verify 3G templates gets stats, while template with unknown + // RAT type gets nothing, and template with NETWORK_TYPE_ALL gets all stats. + assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0); + assertUidTotal(templateUnknown, UID_RED, 0L, 0L, 0L, 0L, 0); + assertUidTotal(templateAll, UID_RED, 12L, 18L, 14L, 1L, 0); + + // Stop monitoring data usage per RAT type changes NetworkStatsService records data + // to {@link TelephonyManager#NETWORK_TYPE_UNKNOWN}. + setCombineSubtypeEnabled(true); + + // Call handleOnCollapsedRatTypeChanged manually to simulate the callback fired + // when stopping monitor, this is needed by NetworkStatsService to trigger updateIfaces. + mService.handleOnCollapsedRatTypeChanged(); + HandlerUtilsKt.waitForIdle(mHandlerThread, WAIT_TIMEOUT); + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + // Append more traffic on existing snapshot. + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 12L + 4L, 18L + 4L, 14L + 3L, 1L + 1L, 0L)) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, + 35L, 29L, 7L, 11L, 1L))); + forcePollAndWaitForIdle(); + + // Verify 3G counters do not increase, while template with unknown RAT type gets new + // traffic and template with NETWORK_TYPE_ALL gets all stats. + assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0); + assertUidTotal(templateUnknown, UID_RED, 4L + 35L, 4L + 29L, 3L + 7L, 1L + 11L, 1); + assertUidTotal(templateAll, UID_RED, 16L + 35L, 22L + 29L, 17L + 7L, 2L + 11L, 1); + + // Start monitoring data usage per RAT type changes and NetworkStatsService records data + // by a granular subtype representative of the actual subtype + setCombineSubtypeEnabled(false); + + mService.handleOnCollapsedRatTypeChanged(); + HandlerUtilsKt.waitForIdle(mHandlerThread, WAIT_TIMEOUT); + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + // Append more traffic on existing snapshot. + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 22L, 26L, 19L, 5L, 0L)) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, + 35L, 29L, 7L, 11L, 1L))); + forcePollAndWaitForIdle(); + + // Verify traffic is split by RAT type, no increase on template with unknown RAT type + // and template with NETWORK_TYPE_ALL gets all stats. + assertUidTotal(template3g, UID_RED, 6L + 12L , 4L + 18L, 2L + 14L, 3L + 1L, 0); + assertUidTotal(templateUnknown, UID_RED, 4L + 35L, 4L + 29L, 3L + 7L, 1L + 11L, 1); + assertUidTotal(templateAll, UID_RED, 22L + 35L, 26L + 29L, 19L + 7L, 5L + 11L, 1); + } + private static File getBaseDir(File statsDir) { File baseDir = new File(statsDir, "netstats"); baseDir.mkdirs(); @@ -1403,6 +1516,10 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest { private void forcePollAndWaitForIdle() { mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL)); + waitForIdle(); + } + + private void waitForIdle() { HandlerUtilsKt.waitForIdle(mHandlerThread, WAIT_TIMEOUT); } diff --git a/tests/net/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java b/tests/net/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java new file mode 100644 index 000000000000..c813269744ef --- /dev/null +++ b/tests/net/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2020 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.net; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.annotation.NonNull; +import android.content.Context; +import android.os.test.TestLooper; +import android.telephony.PhoneStateListener; +import android.telephony.ServiceState; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import com.android.internal.util.CollectionUtils; +import com.android.server.net.NetworkStatsSubscriptionsMonitor.RatTypeListener; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +@RunWith(JUnit4.class) +public final class NetworkStatsSubscriptionsMonitorTest { + private static final int TEST_SUBID1 = 3; + private static final int TEST_SUBID2 = 5; + private static final String TEST_IMSI1 = "466921234567890"; + private static final String TEST_IMSI2 = "466920987654321"; + private static final String TEST_IMSI3 = "466929999999999"; + + @Mock private Context mContext; + @Mock private PhoneStateListener mPhoneStateListener; + @Mock private SubscriptionManager mSubscriptionManager; + @Mock private TelephonyManager mTelephonyManager; + @Mock private NetworkStatsSubscriptionsMonitor.Delegate mDelegate; + private final List<Integer> mTestSubList = new ArrayList<>(); + + private final Executor mExecutor = Executors.newSingleThreadExecutor(); + private NetworkStatsSubscriptionsMonitor mMonitor; + private TestLooper mTestLooper = new TestLooper(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mTelephonyManager); + + when(mContext.getSystemService(eq(Context.TELEPHONY_SUBSCRIPTION_SERVICE))) + .thenReturn(mSubscriptionManager); + when(mContext.getSystemService(eq(Context.TELEPHONY_SERVICE))) + .thenReturn(mTelephonyManager); + + mMonitor = new NetworkStatsSubscriptionsMonitor(mContext, mTestLooper.getLooper(), + mExecutor, mDelegate); + } + + @Test + public void testStartStop() { + // Verify that addOnSubscriptionsChangedListener() is never called before start(). + verify(mSubscriptionManager, never()) + .addOnSubscriptionsChangedListener(mExecutor, mMonitor); + mMonitor.start(); + verify(mSubscriptionManager).addOnSubscriptionsChangedListener(mExecutor, mMonitor); + + // Verify that removeOnSubscriptionsChangedListener() is never called before stop() + verify(mSubscriptionManager, never()).removeOnSubscriptionsChangedListener(mMonitor); + mMonitor.stop(); + verify(mSubscriptionManager).removeOnSubscriptionsChangedListener(mMonitor); + } + + @NonNull + private static int[] convertArrayListToIntArray(@NonNull List<Integer> arrayList) { + final int[] list = new int[arrayList.size()]; + for (int i = 0; i < arrayList.size(); i++) { + list[i] = arrayList.get(i); + } + return list; + } + + private void setRatTypeForSub(List<RatTypeListener> listeners, + int subId, int type) { + final ServiceState serviceState = mock(ServiceState.class); + when(serviceState.getDataNetworkType()).thenReturn(type); + final RatTypeListener match = CollectionUtils + .find(listeners, it -> it.getSubId() == subId); + if (match == null) { + fail("Could not find listener with subId: " + subId); + } + match.onServiceStateChanged(serviceState); + } + + private void addTestSub(int subId, String subscriberId) { + // add SubId to TestSubList. + if (mTestSubList.contains(subId)) fail("The subscriber list already contains this ID"); + + mTestSubList.add(subId); + + final int[] subList = convertArrayListToIntArray(mTestSubList); + when(mSubscriptionManager.getActiveAndHiddenSubscriptionIdList()).thenReturn(subList); + when(mTelephonyManager.getSubscriberId(subId)).thenReturn(subscriberId); + mMonitor.onSubscriptionsChanged(); + } + + private void removeTestSub(int subId) { + // Remove subId from TestSubList. + mTestSubList.removeIf(it -> it == subId); + final int[] subList = convertArrayListToIntArray(mTestSubList); + when(mSubscriptionManager.getActiveAndHiddenSubscriptionIdList()).thenReturn(subList); + mMonitor.onSubscriptionsChanged(); + } + + private void assertRatTypeChangedForSub(String subscriberId, int ratType) { + assertEquals(mMonitor.getRatTypeForSubscriberId(subscriberId), ratType); + final ArgumentCaptor<Integer> typeCaptor = ArgumentCaptor.forClass(Integer.class); + // Verify callback with the subscriberId and the RAT type should be as expected. + // It will fail if get a callback with an unexpected RAT type. + verify(mDelegate).onCollapsedRatTypeChanged(eq(subscriberId), typeCaptor.capture()); + final int type = typeCaptor.getValue(); + assertEquals(ratType, type); + } + + private void assertRatTypeNotChangedForSub(String subscriberId, int ratType) { + assertEquals(mMonitor.getRatTypeForSubscriberId(subscriberId), ratType); + // Should never get callback with any RAT type. + verify(mDelegate, never()).onCollapsedRatTypeChanged(eq(subscriberId), anyInt()); + } + + @Test + public void testSubChangedAndRatTypeChanged() { + final ArgumentCaptor<RatTypeListener> ratTypeListenerCaptor = + ArgumentCaptor.forClass(RatTypeListener.class); + + mMonitor.start(); + // Insert sim1, verify RAT type is NETWORK_TYPE_UNKNOWN, and never get any callback + // before changing RAT type. + addTestSub(TEST_SUBID1, TEST_IMSI1); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + + // Insert sim2. + addTestSub(TEST_SUBID2, TEST_IMSI2); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + verify(mTelephonyManager, times(2)).listen(ratTypeListenerCaptor.capture(), + eq(PhoneStateListener.LISTEN_SERVICE_STATE)); + reset(mDelegate); + + // Set RAT type of sim1 to UMTS. + // Verify RAT type of sim1 after subscription gets onCollapsedRatTypeChanged() callback + // and others remain untouched. + setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1, + TelephonyManager.NETWORK_TYPE_UMTS); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS); + assertRatTypeNotChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_UNKNOWN); + assertRatTypeNotChangedForSub(TEST_IMSI3, TelephonyManager.NETWORK_TYPE_UNKNOWN); + reset(mDelegate); + + // Set RAT type of sim2 to LTE. + // Verify RAT type of sim2 after subscription gets onCollapsedRatTypeChanged() callback + // and others remain untouched. + setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID2, + TelephonyManager.NETWORK_TYPE_LTE); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS); + assertRatTypeChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_LTE); + assertRatTypeNotChangedForSub(TEST_IMSI3, TelephonyManager.NETWORK_TYPE_UNKNOWN); + reset(mDelegate); + + // Remove sim2 and verify that callbacks are fired and RAT type is correct for sim2. + // while the other two remain untouched. + removeTestSub(TEST_SUBID2); + verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_NONE)); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS); + assertRatTypeChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_UNKNOWN); + assertRatTypeNotChangedForSub(TEST_IMSI3, TelephonyManager.NETWORK_TYPE_UNKNOWN); + reset(mDelegate); + + // Set RAT type of sim1 to UNKNOWN. Then stop monitoring subscription changes + // and verify that the listener for sim1 is removed. + setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1, + TelephonyManager.NETWORK_TYPE_UNKNOWN); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + reset(mDelegate); + + mMonitor.stop(); + verify(mTelephonyManager, times(2)).listen(any(), eq(PhoneStateListener.LISTEN_NONE)); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + } +} |