diff --git a/app/build.gradle b/app/build.gradle index 06d213947..85e072328 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -181,7 +181,6 @@ dependencies { implementation 'org.bouncycastle:bcprov-jdk15on:1.65' fullImplementation 'org.bouncycastle:bcpkix-jdk15on:1.65' - fullImplementation 'cc.mvdan.accesspoint:library:0.2.0' fullImplementation 'org.jmdns:jmdns:3.5.5' fullImplementation 'org.nanohttpd:nanohttpd:2.3.1' diff --git a/app/src/full/java/cc/mvdan/accesspoint/WifiApControl.java b/app/src/full/java/cc/mvdan/accesspoint/WifiApControl.java new file mode 100644 index 000000000..9b5020d6e --- /dev/null +++ b/app/src/full/java/cc/mvdan/accesspoint/WifiApControl.java @@ -0,0 +1,451 @@ +/** + * Copyright 2015 Daniel Martí + * + * 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 cc.mvdan.accesspoint; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiManager; +import android.os.Build; +import android.provider.Settings; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.regex.Pattern; + +/** + * WifiApControl provides control over Wi-Fi APs using the singleton pattern. + * Even though isSupported should be reliable, the underlying hidden APIs that + * are obtained via reflection to provide the main features may not work as + * expected. + *

+ * TODO Note that this project is **abandoned** since its method doesn't work on Android + * 7.1 or later. Have a look at these newer alternatives that have been tested to + * work on Android 8.0: + * + * @see shinilms/direct-net-share + * @see geekywoman/direct-net-share + * @see aegis1980/WifiHotSpot + */ +final public class WifiApControl { + + private static final String TAG = "WifiApControl"; + + private static Method getWifiApConfigurationMethod; + private static Method getWifiApStateMethod; + private static Method isWifiApEnabledMethod; + private static Method setWifiApEnabledMethod; + + static { + for (Method method : WifiManager.class.getDeclaredMethods()) { + switch (method.getName()) { + case "getWifiApConfiguration": + getWifiApConfigurationMethod = method; + break; + case "getWifiApState": + getWifiApStateMethod = method; + break; + case "isWifiApEnabled": + isWifiApEnabledMethod = method; + break; + case "setWifiApEnabled": + setWifiApEnabledMethod = method; + break; + } + } + } + + public static final int WIFI_AP_STATE_DISABLING = 10; + public static final int WIFI_AP_STATE_DISABLED = 11; + public static final int WIFI_AP_STATE_ENABLING = 12; + public static final int WIFI_AP_STATE_ENABLED = 13; + public static final int WIFI_AP_STATE_FAILED = 14; + + public static final int STATE_DISABLING = WIFI_AP_STATE_DISABLING; + public static final int STATE_DISABLED = WIFI_AP_STATE_DISABLED; + public static final int STATE_ENABLING = WIFI_AP_STATE_ENABLING; + public static final int STATE_ENABLED = WIFI_AP_STATE_ENABLED; + public static final int STATE_FAILED = WIFI_AP_STATE_FAILED; + + private static boolean isSoftwareSupported() { + return (getWifiApStateMethod != null + && isWifiApEnabledMethod != null + && setWifiApEnabledMethod != null + && getWifiApConfigurationMethod != null); + } + + private static boolean isHardwareSupported() { + // TODO: implement via native code + return true; + } + + // isSupported reports whether Wi-Fi APs are supported by this device. + public static boolean isSupported() { + return isSoftwareSupported() && isHardwareSupported(); + } + + private static final String FALLBACK_DEVICE = "wlan0"; + + private final WifiManager wm; + private final String deviceName; + + private static WifiApControl instance = null; + + private WifiApControl(Context context) { + wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + deviceName = getDeviceName(wm); + } + + // getInstance is a standard singleton instance getter, constructing + // the actual class when first called. + public static WifiApControl getInstance(Context context) { + if (instance == null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.System.canWrite(context)) { + Log.e(TAG, "6.0 or later, but haven't been granted WRITE_SETTINGS!"); + return null; + } + instance = new WifiApControl(context); + } + return instance; + } + + @TargetApi(Build.VERSION_CODES.GINGERBREAD) + private static String getDeviceName(WifiManager wifiManager) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) { + Log.w(TAG, "Older device - falling back to the default device name: " + FALLBACK_DEVICE); + return FALLBACK_DEVICE; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Log.w(TAG, "6.0 or later, unaccessible MAC - falling back to the default device name: " + FALLBACK_DEVICE); + return FALLBACK_DEVICE; + } + + @SuppressLint("HardwareIds") + String macString = wifiManager.getConnectionInfo().getMacAddress(); + if (macString == null) { + Log.w(TAG, "MAC Address not found - Wi-Fi disabled? Falling back to the default device name: " + FALLBACK_DEVICE); + return FALLBACK_DEVICE; + } + byte[] macBytes = macAddressToByteArray(macString); + + try { + Enumeration ifaces = NetworkInterface.getNetworkInterfaces(); + while (ifaces.hasMoreElements()) { + NetworkInterface iface = ifaces.nextElement(); + + byte[] hardwareAddress = iface.getHardwareAddress(); + if (hardwareAddress != null && Arrays.equals(macBytes, hardwareAddress)) { + return iface.getName(); + } + } + } catch (IOException e) { + Log.e(TAG, "", e); + } + + Log.w(TAG, "None found - falling back to the default device name: " + FALLBACK_DEVICE); + return FALLBACK_DEVICE; + } + + private static byte[] macAddressToByteArray(String macString) { + String[] mac = macString.split("[:\\s-]"); + byte[] macAddress = new byte[6]; + for (int i = 0; i < mac.length; i++) { + macAddress[i] = Integer.decode("0x" + mac[i]).byteValue(); + } + return macAddress; + } + + private static Object invokeQuietly(Method method, Object receiver, Object... args) { + try { + return method.invoke(receiver, args); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + Log.e(TAG, "", e); + } + return null; + } + + // isWifiApEnabled returns whether the Wi-Fi AP is currently enabled. + // If an error occured invoking the method via reflection, false is + // returned. + public boolean isWifiApEnabled() { + Object result = invokeQuietly(isWifiApEnabledMethod, wm); + if (result == null) { + return false; + } + return (Boolean) result; + } + + // isEnabled is a commodity function alias for isWifiApEnabled. + public boolean isEnabled() { + return isWifiApEnabled(); + } + + // newStateNumber adapts the state constants to the current values in + // the SDK. They were changed on 4.0 to have higher integer values. + public static int newStateNumber(int state) { + if (state < 10) { + return state + 10; + } + return state; + } + + // getWifiApState returns the current Wi-Fi AP state. + // If an error occured invoking the method via reflection, -1 is + // returned. + public int getWifiApState() { + Object result = invokeQuietly(getWifiApStateMethod, wm); + if (result == null) { + return -1; + } + return newStateNumber((Integer) result); + } + + // getState is a commodity function alias for getWifiApState. + public int getState() { + return getWifiApState(); + } + + // getWifiApConfiguration returns the current Wi-Fi AP configuration. + // If an error occured invoking the method via reflection, null is + // returned. + public WifiConfiguration getWifiApConfiguration() { + Object result = invokeQuietly(getWifiApConfigurationMethod, wm); + if (result == null) { + return null; + } + return (WifiConfiguration) result; + } + + // getConfiguration is a commodity function alias for + // getWifiApConfiguration. + public WifiConfiguration getConfiguration() { + return getWifiApConfiguration(); + } + + // setWifiApEnabled starts a Wi-Fi AP with the specified + // configuration. If one is already running, start using the new + // configuration. You should call WifiManager.setWifiEnabled(false) + // yourself before calling this method. + // If an error occured invoking the method via reflection, false is + // returned. + public boolean setWifiApEnabled(WifiConfiguration config, boolean enabled) { + Object result = invokeQuietly(setWifiApEnabledMethod, wm, config, enabled); + if (result == null) { + return false; + } + return (Boolean) result; + } + + // setEnabled is a commodity function alias for setWifiApEnabled. + public boolean setEnabled(WifiConfiguration config, boolean enabled) { + return setWifiApEnabled(config, enabled); + } + + // enable starts the currently configured Wi-Fi AP. + public boolean enable() { + return setEnabled(getConfiguration(), true); + } + + // disable stops any currently running Wi-Fi AP. + public boolean disable() { + return setEnabled(null, false); + } + + // getInet6Address returns the IPv6 address that the device has in its + // own Wi-Fi AP local network. Will return null if no Wi-Fi AP is + // currently enabled. + public Inet6Address getInet6Address() { + if (!isEnabled()) { + return null; + } + return getInetAddress(Inet6Address.class); + } + + // getInet4Address returns the IPv4 address that the device has in its + // own Wi-Fi AP local network. Will return null if no Wi-Fi AP is + // currently enabled. + public Inet4Address getInet4Address() { + if (!isEnabled()) { + return null; + } + return getInetAddress(Inet4Address.class); + } + + + private T getInetAddress(Class addressType) { + try { + Enumeration ifaces = NetworkInterface.getNetworkInterfaces(); + while (ifaces.hasMoreElements()) { + NetworkInterface iface = ifaces.nextElement(); + + if (!iface.getName().equals(deviceName)) { + continue; + } + + Enumeration addrs = iface.getInetAddresses(); + while (addrs.hasMoreElements()) { + InetAddress addr = addrs.nextElement(); + + if (addressType.isInstance(addr)) { + return addressType.cast(addr); + } + } + } + } catch (IOException e) { + Log.e(TAG, "", e); + } + return null; + } + + // Client describes a Wi-Fi AP device connected to the network. + public static class Client { + + // ipAddr is the raw string of the IP Address client + public String ipAddr; + + // hwAddr is the raw string of the MAC of the client + public String hwAddr; + + public Client(String ipAddr, String hwAddr) { + this.ipAddr = ipAddr; + this.hwAddr = hwAddr; + } + } + + // getClients returns a list of all clients connected to the network. + // Since the information is pulled from ARP, which is cached for up to + // five minutes, this method may yield clients that disconnected + // minutes ago. + public List getClients() { + if (!isEnabled()) { + return null; + } + List result = new ArrayList<>(); + + // Basic sanity checks + Pattern macPattern = Pattern.compile("..:..:..:..:..:.."); + + BufferedReader br = null; + try { + br = new BufferedReader(new FileReader("/proc/net/arp")); + String line; + while ((line = br.readLine()) != null) { + String[] parts = line.split(" +"); + if (parts.length < 6) { + continue; + } + + String ipAddr = parts[0]; + String hwAddr = parts[3]; + String device = parts[5]; + + if (!device.equals(deviceName)) { + continue; + } + + if (!macPattern.matcher(parts[3]).find()) { + continue; + } + + result.add(new Client(ipAddr, hwAddr)); + } + } catch (IOException e) { + Log.e(TAG, "", e); + } finally { + try { + if (br != null) { + br.close(); + } + } catch (IOException e) { + Log.e(TAG, "", e); + } + } + + return result; + } + + // ReachableClientListener is an interface to collect the results + // provided by getReachableClients via callbacks. + public interface ReachableClientListener { + + // onReachableClient is called each time a reachable client is + // found. + void onReachableClient(Client c); + + // onComplete is called when we are done looking for reachable + // clients + void onComplete(); + } + + // getReachableClients fetches the clients connected to the network + // much like getClients, but only those which are reachable. Since + // checking for reachability requires network I/O, the reachable + // clients are returned via callbacks. All the clients are returned + // like in getClients so that the callback returns a subset of the + // same objects. + public List getReachableClients(final int timeout, + final ReachableClientListener listener) { + List clients = getClients(); + if (clients == null) { + return null; + } + final CountDownLatch latch = new CountDownLatch(clients.size()); + ExecutorService es = Executors.newCachedThreadPool(); + for (final Client c : clients) { + es.submit(new Runnable() { + public void run() { + try { + InetAddress ip = InetAddress.getByName(c.ipAddr); + if (ip.isReachable(timeout)) { + listener.onReachableClient(c); + } + } catch (IOException e) { + Log.e(TAG, "", e); + } + latch.countDown(); + } + }); + } + new Thread() { + public void run() { + try { + latch.await(); + } catch (InterruptedException e) { + Log.e(TAG, "", e); + } + listener.onComplete(); + } + }.start(); + return clients; + } +} diff --git a/build.gradle b/build.gradle index a54a08ac4..950b25bb4 100644 --- a/build.gradle +++ b/build.gradle @@ -11,10 +11,5 @@ allprojects { repositories { mavenCentral() maven { url 'https://maven.google.com/' } - jcenter() { - content { - includeModule("cc.mvdan.accesspoint", "library") - } - } } } diff --git a/config/errorprone.gradle b/config/errorprone.gradle index d028ea9cd..c3134a44c 100644 --- a/config/errorprone.gradle +++ b/config/errorprone.gradle @@ -15,6 +15,7 @@ apply plugin: "net.ltgt.errorprone" tasks.withType(JavaCompile) { options.compilerArgs += [ '-XepAllDisabledChecksAsWarnings', + '-XepExcludedPaths:.*/cc/mvdan/accesspoint/.*', '-Xep:CatchFail:OFF', '-Xep:ClassCanBeStatic:OFF', '-Xep:DateFormatConstant:OFF', diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 1888572cb..f4a076480 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -828,14 +828,6 @@ - - - - - - - -