blob: 29c1d4cc3ee3f3a040c8b116d31417e4edb096f5 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.car.kitchensink.connectivity;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.graphics.Color;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkRequest;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.fragment.app.Fragment;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.car.kitchensink.R;
import java.net.NetworkInterface;
import java.net.SocketException;
@SuppressLint("SetTextI18n")
public class ConnectivityFragment extends Fragment {
private static final String TAG = ConnectivityFragment.class.getSimpleName();
private final Handler mHandler = new Handler();
private ConnectivityManager mConnectivityManager;
// Sort out current Network objects (NetId -> Network)
private SparseArray<Network> mNetworks = new SparseArray<Network>();
/**
* Create our own network callback object to use with NetworkRequests. Contains a reference to
* a Network so we can be sure to only surface updates on the network we want to see them on.
* We have to do this because there isn't a way to say "give me this SPECIFIC network." There's
* only "give me A network with these capabilities/transports."
*/
public class NetworkByIdCallback extends NetworkCallback {
private final Network mNetwork;
NetworkByIdCallback(Network n) {
mNetwork = n;
}
@Override
public void onAvailable(Network n) {
if (mNetwork.equals(n)) {
showToast("onAvailable(), netId: " + n);
}
}
@Override
public void onLosing(Network n, int maxMsToLive) {
if (mNetwork.equals(n)) {
showToast("onLosing(), netId: " + n);
}
}
@Override
public void onLost(Network n) {
if (mNetwork.equals(n)) {
showToast("onLost(), netId: " + n);
}
}
}
// Map of NetId -> NetworkByIdCallback Objects -- Used to release requested networks
SparseArray<NetworkByIdCallback> mNetworkCallbacks = new SparseArray<NetworkByIdCallback>();
/**
* Implement a swipe-to-refresh list of available networks. NetworkListAdapter takes an array
* of NetworkItems that it cascades to the view. SwipeRefreshLayout wraps the adapter.
*/
public static class NetworkItem {
public int mNetId;
public String mType;
public String mState;
public String mConnected;
public String mAvailable;
public String mRoaming;
public String mInterfaceName;
public String mHwAddress;
public String mIpAddresses;
public String mDnsAddresses;
public String mDomains;
public String mRoutes;
public String mTransports;
public String mCapabilities;
public String mBandwidth;
public boolean mDefault;
public boolean mRequested;
}
private NetworkItem[] mNetworkItems = new NetworkItem[0];
private NetworkListAdapter mNetworksAdapter;
private SwipeRefreshLayout mNetworkListRefresher;
/**
* Builds a NetworkRequest fit to a given network in the hope that we just get updates on that
* one network. This is the best way to get single network updates right now, as the request
* system works only on transport and capability requirements. There aaaare "network
* specifiers" but those only work based on the transport (i.e "eth0" would ask type ETHERNET
* for the correct interface where as "GoogleGuest" might ask type WIFI for the Network on SSID
* "GoogleGuest"). Ends up being paired with the custom callback above to only surface events
* for the specific network in question as well.
*/
private NetworkRequest getRequestForNetwork(Network n) {
NetworkCapabilities nc = mConnectivityManager.getNetworkCapabilities(n);
NetworkRequest.Builder b = new NetworkRequest.Builder();
b.clearCapabilities();
for (int transportType : nc.getTransportTypes()) {
b.addTransportType(transportType);
}
for (int capability : nc.getCapabilities()) {
// Not all capabilities are requestable. According to source, all mutable capabilities
// except trusted are not requestable. Trying to request them results in an error being
// thrown
if (isRequestableCapability(capability)) {
b.addCapability(capability);
}
}
return b.build();
}
private boolean isRequestableCapability(int c) {
if (c == NetworkCapabilities.NET_CAPABILITY_VALIDATED
|| c == NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
|| c == NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
|| c == NetworkCapabilities.NET_CAPABILITY_FOREGROUND
|| c == NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
|| c == NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) {
return false;
}
return true;
}
public void requestNetworkById(int netId) {
if (mNetworkCallbacks.get(netId) != null) {
return;
}
Network network = mNetworks.get(netId);
if (network == null) {
return;
}
NetworkRequest request = getRequestForNetwork(network);
NetworkByIdCallback cb = new NetworkByIdCallback(network);
mNetworkCallbacks.put(netId, cb);
mConnectivityManager.requestNetwork(request, cb);
showToast("Requesting Network " + netId);
}
public void releaseNetworkById(int netId) {
NetworkByIdCallback cb = mNetworkCallbacks.get(netId);
if (cb != null) {
mConnectivityManager.unregisterNetworkCallback(cb);
mNetworkCallbacks.remove(netId);
showToast("Released Network " + netId);
}
}
public void releaseAllNetworks() {
for (NetworkItem n : mNetworkItems) {
releaseNetworkById(n.mNetId);
}
}
public void bindToNetwork(int netId) {
Network network = mNetworks.get(netId);
if (network == null) {
return;
}
Network def = mConnectivityManager.getBoundNetworkForProcess();
if (def != null && def.netId != netId) {
clearBoundNetwork();
}
mConnectivityManager.bindProcessToNetwork(network);
showToast("Set process default network " + netId);
}
public void clearBoundNetwork() {
mConnectivityManager.bindProcessToNetwork(null);
showToast("Clear process default network");
}
public void reportNetworkbyId(int netId) {
Network network = mNetworks.get(netId);
if (network == null) {
return;
}
mConnectivityManager.reportNetworkConnectivity(network, false);
showToast("Reporting Network " + netId);
}
/**
* Maps of NET_CAPABILITY_* and TRANSPORT_* to string representations. A network having these
* capabilities will have the following strings print on their list entry.
*/
private static final SparseArray<String> sTransportNames = new SparseArray<String>();
private static final SparseArray<String> sCapabilityNames = new SparseArray<String>();
static {
sTransportNames.put(NetworkCapabilities.TRANSPORT_LOWPAN, "[LOWPAN]");
sTransportNames.put(NetworkCapabilities.TRANSPORT_WIFI_AWARE, "[WIFI-AWARE]");
sTransportNames.put(NetworkCapabilities.TRANSPORT_VPN, "[VPN]");
sTransportNames.put(NetworkCapabilities.TRANSPORT_ETHERNET, "[ETHERNET]");
sTransportNames.put(NetworkCapabilities.TRANSPORT_BLUETOOTH, "[BLUETOOTH]");
sTransportNames.put(NetworkCapabilities.TRANSPORT_WIFI, "[WIFI]");
sTransportNames.put(NetworkCapabilities.TRANSPORT_CELLULAR, "[CELLULAR]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL, "[CAPTIVE PORTAL]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_CBS, "[CBS]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_DUN, "[DUN]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_EIMS, "[EIMS]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_FOREGROUND, "[FOREGROUND]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_FOTA, "[FOTA]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_IA, "[IA]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_IMS, "[IMS]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_INTERNET, "[INTERNET]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_MMS, "[MMS]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED, "[NOT CONGESTED]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, "[NOT METERED]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED, "[NOT RESTRICTED]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, "[NOT ROAMING]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED, "[NOT SUSPENDED]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_NOT_VPN, "[NOT VPN]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_RCS, "[RCS]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_SUPL, "[SUPL]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_TRUSTED, "[TRUSTED]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_VALIDATED, "[VALIDATED]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_WIFI_P2P, "[WIFI P2P]");
sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_XCAP, "[XCAP]");
}
/**
* Builds a string out of the possible transports that can be applied to a
* NetworkCapabilities object.
*/
private String getTransportString(NetworkCapabilities nCaps) {
String transports = "";
for (int transport : nCaps.getTransportTypes()) {
transports += sTransportNames.get(transport, "");
}
return transports;
}
/**
* Builds a string out of the possible capabilities that can be applied to
* a NetworkCapabilities object.
*/
private String getCapabilitiesString(NetworkCapabilities nCaps) {
String caps = "";
for (int capability : nCaps.getCapabilities()) {
caps += sCapabilityNames.get(capability, "");
}
return caps;
}
// Gets the string representation of a MAC address from a given NetworkInterface object
private String getMacAddress(NetworkInterface ni) {
if (ni == null) {
return "??:??:??:??:??:??";
}
byte[] mac = null;
try {
mac = ni.getHardwareAddress();
} catch (SocketException exception) {
Log.e(TAG, "SocketException -- Failed to get interface MAC address");
return "??:??:??:??:??:??";
}
if (mac == null) {
return "??:??:??:??:??:??";
}
StringBuilder sb = new StringBuilder(18);
for (byte b : mac) {
if (sb.length() > 0) {
sb.append(':');
}
sb.append(String.format("%02x", b));
}
return sb.toString();
}
/**
* Builds a NetworkItem object from a given Network object, aggregating info across Network,
* NetworkCapabilities, NetworkInfo, NetworkInterface, and LinkProperties objects and pass it
* all as a string for the UI to use
*/
private NetworkItem getNetworkItem(Network n) {
// Get default network to assign the button text correctly
// NOTE: activeNetwork != ProcessDefault when you set one, active is tracking the default
// request regardless of your process's default
// Network defNetwork = mConnectivityManager.getActiveNetwork();
Network defNetwork = mConnectivityManager.getBoundNetworkForProcess();
// Used to get network state
NetworkInfo nInfo = mConnectivityManager.getNetworkInfo(n);
// Used to get transport type(s), capabilities
NetworkCapabilities nCaps = mConnectivityManager.getNetworkCapabilities(n);
// Properties of the actual physical link
LinkProperties nLink = mConnectivityManager.getLinkProperties(n);
// Object representing the actual interface
NetworkInterface nIface = null;
try {
nIface = NetworkInterface.getByName(nLink.getInterfaceName());
} catch (SocketException exception) {
Log.e(TAG, "SocketException -- Failed to get interface info");
}
// Pack NetworkItem with all values
NetworkItem ni = new NetworkItem();
// Row key
ni.mNetId = n.netId;
// LinkProperties/NetworkInterface
ni.mInterfaceName = "Interface: " + nLink.getInterfaceName()
+ (nIface != null ? " (" + nIface.getName() + ")" : " ()");
ni.mHwAddress = "HwAddress: " + getMacAddress(nIface);
ni.mIpAddresses = "IP Addresses: " + nLink.getLinkAddresses().toString();
ni.mDnsAddresses = "DNS: " + nLink.getDnsServers().toString();
ni.mDomains = "Domains: " + nLink.getDomains();
ni.mRoutes = "Routes: " + nLink.getRoutes().toString();
// NetworkInfo
ni.mType = "Type: " + nInfo.getTypeName() + " (" + nInfo.getSubtypeName() + ")";
ni.mState = "State: " + nInfo.getState().name() + "/" + nInfo.getDetailedState().name();
ni.mConnected = "Connected: " + (nInfo.isConnected() ? "Connected" : "Disconnected");
ni.mAvailable = "Available: " + (nInfo.isAvailable() ? "Yes" : "No");
ni.mRoaming = "Roaming: " + (nInfo.isRoaming() ? "Yes" : "No");
// NetworkCapabilities
ni.mTransports = "Transports: " + getTransportString(nCaps);
ni.mCapabilities = "Capabilities: " + getCapabilitiesString(nCaps);
ni.mBandwidth = "Bandwidth (Down/Up): " + nCaps.getLinkDownstreamBandwidthKbps()
+ " Kbps/" + nCaps.getLinkUpstreamBandwidthKbps() + " Kbps";
// Other inferred values
ni.mDefault = sameNetworkId(n, defNetwork);
ni.mRequested = (mNetworkCallbacks.get(n.netId) != null);
return ni;
}
// Refresh the networks content and prompt the user that we did it
private void refreshNetworksAndPrompt() {
refreshNetworks();
showToast("Refreshed Networks (" + mNetworkItems.length + ")");
}
/**
* Gets the current set of networks from the connectivity manager and 1) stores the network
* objects 2) builds NetworkItem objects for the view to render and 3) If a network we were
* tracking disappears then it kills its callback.
*/
private void refreshNetworks() {
Log.i(TAG, "refreshNetworks()");
Network[] networks = mConnectivityManager.getAllNetworks();
mNetworkItems = new NetworkItem[networks.length];
mNetworks.clear();
// Add each network to the network info set, turning each field to a string
for (int i = 0; i < networks.length; i++) {
mNetworkItems[i] = getNetworkItem(networks[i]);
mNetworks.put(networks[i].netId, networks[i]);
}
// Check for callbacks that belong to networks that don't exist anymore
for (int i = 0; i < mNetworkCallbacks.size(); i++) {
int key = mNetworkCallbacks.keyAt(i);
if (mNetworks.get(key) == null) {
mNetworkCallbacks.remove(key);
}
}
// Update the view
mNetworksAdapter.refreshNetworks(mNetworkItems);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mConnectivityManager = getActivity().getSystemService(ConnectivityManager.class);
mConnectivityManager.addDefaultNetworkActiveListener(() -> refreshNetworks());
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.connectivity_fragment, container, false);
// Create the ListView of all networks
ListView networksView = view.findViewById(R.id.networks);
mNetworksAdapter = new NetworkListAdapter(getContext(), mNetworkItems, this);
networksView.setAdapter(mNetworksAdapter);
// Find all networks ListView refresher and set the refresh callback
mNetworkListRefresher = (SwipeRefreshLayout) view.findViewById(R.id.refreshNetworksList);
mNetworkListRefresher.setOnRefreshListener(() -> {
refreshNetworksAndPrompt();
mNetworkListRefresher.setRefreshing(false);
});
return view;
}
@Override
public void onResume() {
super.onResume();
refreshNetworks();
}
@Override
public void onPause() {
super.onPause();
releaseAllNetworks();
}
public void showToast(String text) {
Toast toast = Toast.makeText(getContext(), text, Toast.LENGTH_SHORT);
TextView v = (TextView) toast.getView().findViewById(android.R.id.message);
v.setTextColor(Color.WHITE);
toast.show();
}
private static boolean sameNetworkId(Network net1, Network net2) {
return net1 != null && net2 != null && net1.netId == net2.netId;
}
}