Особенности программного ProxyChanging’а в Android. Часть 1: от Jelly Bean до Lollipop

в 4:42, , рубрики: android, change proxy settings, java, library, proxy, reflection, wifi, Разработка под android

Как-то раз, для собственного удобства, мне захотелось написать приложение, которое меняет настройки прокси в конфигурации сетей Wifi для Android. Задача, как мне тогда показалось, была на раз плюнуть, однако, на деле, как всегда, возникли непредвиденные сложности.

Особенности программного ProxyChanging'а в Android. Часть 1: от Jelly Bean до Lollipop - 1

Если вы считаете полезным в будущем знать решение, хотите почерпнуть что-то для себя или в вас просто проснулось любопытство — добро пожаловать под кат. Там вас ждет внутреннее устройство классов отвечающих за конфигурацию Wifi в различных версиях Android, небольшая чашечка кода на Java и щепотка Reflection.

Немного общения с Google на тему «change wifi proxy settings in android programmatically» привели, разумеется, на StackOverflow, где присутствовало решение через Reflection. Недолго думая я скопировал код и запустил на своем девайсе.

Результат

image

Разочарование быстро сменилось интересом и азартом, примерно на этом моменте автор решил, что уже потратил слишком много времени, для того, чтобы просто забить на вышеуказанный вопрос, а так же понял, что данная задача требует более глобального подхода. Итак, поехали.

Шаг 1. Изучаем внутреннее устройство библиотеки android.net и отличия в Jelly Bean — Kitkat и Lollipop

Список вещей, которые желательно знать для лучшего понимания происходящего ниже

  • Your English level must be, at least, pre-intermediate.
  • У вас не должно возникать вопросов «Что такое Context?» и аналогичных по сложности, если же он возник на этом моменте — можете почитать developer.android.com или Александра Климова
  • Так же не должны вызывать смущения аннотации @Before, @Test, @After и прочие вещи относящиеся к тестированию. Опять же, ссылка: developer.android.com

Я бы с удовольствием разобрал все это, но сами понимаете — тогда моя статья вырастет в книгу.

Также я хотел бы дать еще несколько общих уточнений:

  • Вы почти не встретите комментариев в моем коде. Я долго размышлял над этим вопросом и очень долго сомневался, но, в конце концов, решил просто дать его девушке, которая вообще не знает java, и, после коротких пояснений, что такое class, void, throws, exception она смогла, прочтя несколько классов, весьма точно сказать, что происходит в них и их методах, потому я почти отказался от них.
  • Если у вас есть комментарии, дополнения, вопросы, замечания (например, по предыдущему пункту) — автор их очень ждет.
  • Статья на данный момент очень обобщена, опять же, если вы хотите увидеть некоторые моменты подробнее, то в зависимости от объема работ я напишу комментарий или, возможно, отдельную статью на заинтересовавшую вас тему.
  • В коде нет импортов, ибо имя пакета содержит в себе ник вашего покорного слуги, а им, судя по правилам, нельзя светить в статье для песочницы.

Еще немного вопросов в Google привели меня на android.googlesource

Настройки прокси (а так же некоторые другие) заключены в экземпляре WifiConfiguration (Ссылка на класс для Kitkat mr2.2) для данной сети. При изучении данного класса был получен ответ на то, почему не работало на моем устройстве решение со StackOverflow. Оказалось, что начиная с пятой версии Android устройство класса WifiConfiguration, а так же пакета android.net претерпели значительные изменения и объекта LinkPropeties, с которым работал вышеуказанный код просто не существует в рамках данного класса. Зато присутствует объект IpConfiguraion с объектом ProxyInfo.

Учитывая что данные версии Android покрывали 80% различных устройств, то задача сводилась к тому, чтобы просто написать нечто такое:

public void changeProxySettings(String host, int port){
	if(Build.VERSION.SDK_INT > 14 && Build.VERSION.SDK_INT < 20){
		changeProxyWithLikProperties(String host, int port);
	}else if(Build.VERSION.SDK_INT > 20 && Build.VERSION.SDK_INT < 23){
		changeProxyWithProxyInfo(String host, int port);
	}else{
		throw new Exception("Sorry, android version not supported")
	}
}

где changeProxyXXX — монструозные методы, на пару страниц. Не самое изящное решение.

Шаг 2. Разрабатываем библиотеку для настройки Wifi proxy в Android

Итак, автор решил не останавливаться на громоздком классе с кучей методов. Свободное время имеется (безвременный отпуск по случаю сокращения финансирования проекта в котором я участвовал), так почему бы не поработать над задачей глобальнее.

Архитектура модуля

Мы имеем различные реализации под разные версии Android, у которых должен быть единый интерфейс для изменения настроек прокси, и работающие с объектом WifiConfiguration. Стараясь максимально удовлетворить данным требованиям, на начальном этапе мое воспаленное сознание придумало нечто такое:

Особенности программного ProxyChanging'а в Android. Часть 1: от Jelly Bean до Lollipop - 3

Поясняющий комментарий к картинке выше

  • Класс BaseWifiConfiguration, по сути, хранит объект WifiConfiguration и содержит реализацию взятия конфигурации той сети, которая является текущей, при создании через Context.
  • Интерфейс ProxyChanger, соответственно, гарантирует наличие методов для работы с конфигурацией прокси сети.
  • Нам предстоит работа с Reflection, и желательно вынести основные методы для этого в отдельный класс, так как использоваться они будут часто. Поэтому создаем класс ReflectionHelper.

Классы для разных версий Android наследуются от BaseWifiConfiguration дабы иметь легкий доступ к экземпляру WifiConfiguration интересующей нас сети и облегчить работу с ним, и должны иметь реализации методов объявленных в ProxyChanger.

PS

Я не спорю, что это, быть может, не лучшее архитектурное решение и, если у Вас есть предложения по улучшению или какие-то замечания, — с радостью жду их в комментариях.

Например, меня очень интересует ReflectionHelper, как видите, он объявлен абстрактным, сделано это из соображений того, что он не должен иметь конкретных реализаций и используется только для структуризации и легкого доступа к интересующим нас методам. Я не знаю насколько правилен этот подход, так что если у вас есть комментарий по данному вопросу (или каким-то другим) — я буду премного благодарен его услышать.

Разумеется, это только общий каркас и, спустя несколько итераций рефакторинга, он обрастет новыми подробностями.

Тесты

Итак, мы подумали над архитектурой, настало время написать пару строчек кода. Разумеется, не рабочего, а тестирующего наш проект.

Создаем небольшой класс, который будет отвечать за выбор ProxyChanger'а под конкретный api, класс для работы с вышеозначенным объектом в плане изменения конфигурации прокси и еще один, для взятия информации о настройках текущей сети, достаем пару телефонов и начинаем.

WifiProxyChangerTest.java

@RunWith(AndroidJUnit4.class)
public class WifiProxyChangerTest {

    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    @Rule
    public ActivityTestRule mActivityRule = new ActivityTestRule<>(
            MainActivity.class);

    Context context;


    @Before
    public void prepare() throws Exception {
        context = mActivityRule.getActivity();
        ExceptionsPreparer.prepareExceptions(expectedException, context);
    }

    @Test
    public void testChangeWifiStaticProxySettings() throws Exception {
        String testIp = RandomValuesGenerator.randomIp();
        int testPort = RandomValuesGenerator.randomPort();

        WifiProxyChanger.changeWifiStaticProxySettings(testIp, testPort, context);

        assertEquals(testIp, WifiProxyInfo.getHost(context));
        assertEquals(testPort, WifiProxyInfo.getPort(context));
    }

    @Test
    public void testProxySettingsClear() throws Exception {
        String testIp = RandomValuesGenerator.randomIp();
        int testPort = RandomValuesGenerator.randomPort();

        WifiProxyChanger.changeWifiStaticProxySettings(testIp, testPort, context);
        WifiProxyChanger.clearProxySettings(context);

        assertEquals(ProxySettings.NONE, CurrentProxyChangerGetter
                .chooseProxyChangerForCurrentApi(context)
                .getProxySettings());
    }

    @After
    public void сlearSettings() throws Exception {
        if (NetworkHelper.isWifiConnected(context) && ApiChecker.isSupportedApi())
            WifiProxyChanger.clearProxySettings(context);
    }

}

WifiProxyInfoTest.java

@RunWith(AndroidJUnit4.class)
public class WifiProxyInfoTest {

    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    @Rule
    public ActivityTestRule mActivityRule = new ActivityTestRule<>(
            MainActivity.class);

    Context context;


    @Before
    public void prepareAndPresetProxy() throws Exception {
        context = mActivityRule.getActivity();

        ExceptionsPreparer.prepareExceptions(expectedException, context);

        if (ApiChecker.isSupportedApi()) {
            WifiProxyChanger.clearProxySettings(context);
            WifiProxyChanger.changeWifiStaticProxySettings("localhost", 3030, context);
        }
    }

    @Test
    public void testGetHost() throws Exception {
        assertEquals("localhost", WifiProxyInfo.getHost(context));
    }

    @Test
    public void testGetPort() throws Exception {
        assertEquals(3030, WifiProxyInfo.getPort(context));
    }

    @Test
    public void testGetProxySettings() throws Exception {
        assertEquals(ProxySettings.STATIC, WifiProxyInfo.getProxySettings(context));
    }

    @After
    public void сlearSettings() throws Exception {
        if (NetworkHelper.isWifiConnected(context) && ApiChecker.isSupportedApi())
            WifiProxyChanger.clearProxySettings(context);
    }

}

CurrentProxyChangerGetterTest .java

@RunWith(AndroidJUnit4.class)
public class CurrentProxyChangerGetterTest {

    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    @Rule
    public ActivityTestRule mActivityRule = new ActivityTestRule<>(
            MainActivity.class);

    Context context;


    @Before
    public void prepare() throws Exception {
        context = mActivityRule.getActivity();
        ExceptionsPreparer.prepareExceptions(expectedException, context);
    }

    @Test
    public void testChooseProxyChangerForCurrentApi() throws Exception {
        ProxyChanger proxyChanger = CurrentProxyChangerGetter.chooseProxyChangerForCurrentApi(context);
        WifiManager manager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);

        assertEquals(manager.getConnectionInfo().getNetworkId(), proxyChanger.getWifiConfiguration().networkId);

        if (ApiChecker.isJellyBeanOrKitkat()) {
            assertTrue(proxyChanger instanceof WifiConfigurationForApiFrom15To19);
        } else if (ApiChecker.isLolipop()) {
            assertTrue(proxyChanger instanceof WifiConfigurationForApiFrom21To22);
        }
    }

}

ExceptionsPreparer.java

public abstract class ExceptionsPreparer {

    public static void prepareExceptions(ExpectedException expectedException, Context context) throws Exception {
        if (!ApiChecker.isSupportedApi()) {
            expectedException.expect(ApiNotSupportedException.class);
        } else if (!NetworkHelper.isWifiConnected(context)) {
            expectedException.expect(NullWifiConfigurationException.class);
        } else if (!CurrentProxyChangerGetter.chooseProxyChangerForCurrentApi(context).isProxySetted()) {
            expectedException.expect(WifiProxyNotSettedException.class);
        }
    }

}

Комментарий к коду, объясняющий, откуда там куча непонятных вещей про которые я не упомянул.

Я думаю, после прочтения возникли резонные вопросы: Что это за «ProxySettings.STATIC», и за что он отвечает, откуда взялись Exceptions, которые тоже ранее не упоминались, и так далее.
Дело в том, что изначальной версии, к сожалению, у меня не осталось, и в bitbucket наличествуют только уже прошедшие несколько итераций рефакторинга тестовые классы.

Запуск тестов оканчивается провалом, теперь нужно это как-то исправить.

Шаг 3. Реализация

Наконец, продумав что писать и сделав все необходимые приготовления мы можем приступить к реализации нашего проекта.

Часть первая: Подготовительные работы и вспомогательные классы

Для начала — приступим к нашей обещанной щепотке Reflection

Чуть-чуть о Reflection api

Reflection is commonly used by programs which require the ability to examine or modify the runtime behavior of applications running in the Java virtual machine.
Oracle Java Turtorial

Класс, но что конкретно можно с этим сделать?

Небольшой пример

Пусть у нас есть маленькая библиотека, в ней пара классов для взятия веб страницы и ее сохранения. Я хочу подключить ее «на лету» и использовать. Напишем для этого небольшой класс:

public class LibLoader {
    //Я искренне прошу прощения за этот код, писал за 2 минуты для примера.

    URLClassLoader urlClassLoader;
    String page;

    LibLoader(File myJar) throws MalformedURLException {
        urlClassLoader = new URLClassLoader(new URL[]{myJar.toURL()}, this.getClass().getClassLoader());
    }

    public void loadPage(URL url) throws Exception {
        Class classToLoad = Class.forName("com.company.HtmlPageGetter", true, urlClassLoader);
        Method method = classToLoad.getDeclaredMethod("getPageFromURL", URL.class);
        Object instance = classToLoad.newInstance();
        Object result = method.invoke(instance, url);
        page = (String) result;
    }

    public String getCurrentPage() {
        return page;
    }

    public void saveCurrentPage(String name) throws Exception {
        List<String> content = new ArrayList<>();
        content.add(page);
        Class classToLoad = Class.forName("com.company.HtmlPageSaver", true, urlClassLoader);
        Method method = classToLoad.getDeclaredMethod("savePageToFile", String.class, List.class);
        Object instance = classToLoad.newInstance();
        method.invoke(instance, name, content);
    }
}

Теперь используем его:

    public static void main(String[] args) throws Exception {
        File lib = new File("htmlgetandsave.jar");
        LibLoader libLoader = new LibLoader(lib);
        libLoader.loadPage(new URL("https://habrahabr.ru/post/69552/"));
        System.out.println(libLoader.getCurrentPage());
        libLoader.saveCurrentPage("Статья с хабра - Делаем reflection быстрой как прямые вызовы ");
    }

Запускаем и наслаждаемся результатом:

Особенности программного ProxyChanging'а в Android. Часть 1: от Jelly Bean до Lollipop - 4

Более того, мы могли вообще знать только расположение файла библиотеки и не знать ничего о ее структуре, Reflection api позволило бы изучить этот вопрос прямо в рантайме, и использовать ее после этого.

Однако для нас сейчас важно то, что помимо прочего, благодаря Reflection мы можем получить доступ к приватным полям и методам, а так же помеченным аннотацией hide.

Итак, пишем уже упомянутый выше ReflectionHelper.

ReflectionHelper.java

public abstract class ReflectionHelper {

    /**
     * Used for getting public fields with @hide annotation
     */
    public static Object getField(Object object, String name)
            throws SecurityException, NoSuchFieldException, IllegalArgumentException,
            IllegalAccessException {
        Field field = object.getClass().getField(name);
        return field.get(object);
    }

    /**
     * Used for getting private fields
     */
    public static Object getDeclaredField(Object object, String name)
            throws SecurityException, NoSuchFieldException, IllegalArgumentException,
            IllegalAccessException {
        Field declaredField = object.getClass().getDeclaredField(name);
        declaredField.setAccessible(true);
        return declaredField.get(object);
    }

    /**
     * Used for setting private fields
     */
    public static void setDeclaredField(Object object, String name, Object value)
            throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = object.getClass().getDeclaredField(name);
        declaredField.setAccessible(true);
        declaredField.set(object, value);
    }

    /**
     * Used for setting Enum fields
     */
    public static void setEnumField(Object object, String value, String name)
            throws SecurityException, NoSuchFieldException, IllegalArgumentException,
            IllegalAccessException {
        Field field = object.getClass().getField(name);
        field.set(object, Enum.valueOf((Class<Enum>) field.getType(), value));
    }

    /**
     * Used for simplifying process of invoking private method
     * Automatically detects args types and founds method to get and invoke
     */
    public static Object getMethodAndInvokeIt(Object object, String methodName, Object... args)
            throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Method method = object.getClass().getDeclaredMethod(methodName, parameterTypes(args));
        method.setAccessible(true);
        return method.invoke(object, args);
    }

    private static Class[] parameterTypes(Object... args) {
        ArrayList<Class> classes = new ArrayList<>();
        for (Object arg : args) {
            classes.add(arg.getClass());
        }
        return classes.toArray(new Class[args.length]);
    }

}

Здесь я все же поставил себе памятки, ибо, например, разницу между getField и getDeclaredField и в каких случаях какой использовать — можно легко забыть.

Большая часть работы с Reflection перенесена в отдельный класс, займемся реализацией остальных частей.

Создаем Exceptions на 3 случая:

  • Неподходящая версия Api. Соответствующий класс:
    public class ApiNotSupportedException extends Exception {
    
        public ApiNotSupportedException() {
            super("Api version not supported");
        }
    	
    }
    

  • Попытка создания объекта с не заданной конфигурацией Wifi (Например, пользователь пытается с отключенным wifi изменить параметры прокси текущей сети):
    public class NullWifiConfigurationException extends Exception {
    
        public NullWifiConfigurationException(){
            super("WiFi configuration was null. n" +
                    "If you are trying to change current network settings - check your connection.");
        }
    	
    }
    

  • Не определен объект для настроек прокси в текущем классе WifiConfiguration:
    public class WifiProxyNotSettedException extends IllegalStateException{
    
        public WifiProxyNotSettedException(){
            super("Wifi proxy not setted for current WifiConfiguration");
        }
    	
    }
    

Реализуем класс служащий базовым для подклассов работающих с WifiConfiguration под различными api:

BaseWifiConfiguration.java

public class BaseWifiConfiguration {

    protected WifiConfiguration wifiConfiguration;


    protected BaseWifiConfiguration(WifiConfiguration wifiConfiguration)
            throws NullWifiConfigurationException {
        if (wifiConfiguration == null)
            throw new NullWifiConfigurationException();
        this.wifiConfiguration = wifiConfiguration;
    }

    protected BaseWifiConfiguration(Context context)
            throws NullWifiConfigurationException {
        this(getCurrentWifiConfigurationFromContext(context));
    }

    public WifiConfiguration getWifiConfiguration() {
        return wifiConfiguration;
    }

    private static WifiConfiguration getCurrentWifiConfigurationFromContext(Context context) {
        final WifiManager manager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
        List<WifiConfiguration> wifiConfigurationList = manager.getConfiguredNetworks();
        if (!manager.isWifiEnabled() || wifiConfigurationList == null || wifiConfigurationList.isEmpty())
            return null;
        return findWifiConfigurationByNetworkId(wifiConfigurationList, manager.getConnectionInfo().getNetworkId());

    }

    private static WifiConfiguration findWifiConfigurationByNetworkId(List<WifiConfiguration> wifiConfigurationList, int networkId) {
        for (WifiConfiguration wifiConf : wifiConfigurationList) {
            if (wifiConf.networkId == networkId)
                return wifiConf;
        }
        return null;
    }

}

Объявляем интерфейс ProxyChanger

ProxyChanger.java

public interface ProxyChanger {

    void setProxySettings(ProxySettings proxySettings)
            throws NoSuchFieldException, IllegalAccessException;

    ProxySettings getProxySettings()
            throws NoSuchFieldException, IllegalAccessException;

    void setProxyHostAndPort(String host, int port)
            throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
            InstantiationException, IllegalAccessException, NoSuchFieldException;

    String getProxyHost()
            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException,
            ApiNotSupportedException, NoSuchFieldException;

    int getProxyPort()
            throws ApiNotSupportedException, NoSuchMethodException, IllegalAccessException,
            InvocationTargetException, NoSuchFieldException;

    boolean isProxySetted()
            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException,
            ApiNotSupportedException, NoSuchFieldException;

    WifiConfiguration getWifiConfiguration();

}

Да, списки Exception'ов при использовании Reflection — это нечто.

Вроде все? А, нет, есть еще один маленький подпункт:

ProxySettings.java — что это вообще такое и зачем оно нужно?

Это аналог перечисления, находящегося в классе WifiConfiguration библиотеки Android. Мы создаем его за тем, чтобы облегчить работу с ним и не прописывать каждый раз STATIC, NONE и прочие вручную.

ProxySettings.java

public enum ProxySettings {

    /* No proxy is to be used. Any existing proxy settings
     * should be cleared. */
    NONE("NONE"),
    /* Use statically configured proxy. Configuration can be accessed
     * with httpProxy. */
    STATIC("STATIC"),
    /* no proxy details are assigned, this is used to indicate
     * that any existing proxy settings should be retained */
    UNASSIGNED("UNASSIGNED"),
    /* Use a Pac based proxy.
     */
    PAC("PAC");


    String value = "";


    ProxySettings(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }

}

Часть вторая: Пишем классы реализующие ProxyChanger под конкретные Api

Итак, настало время написать наконец тот самый код, который будет менять наши настройки прокси. Сразу оговорюсь, что есть разнообразные способы, чтобы добраться до них через Reflection: можно вызывать методы класса WifiConfiguration, можно добираться до, собственно, полей, где они находятся и через setDeclaredField менять их напрямую.

Я написал только часть для работы с текущей сетью (ибо это то, в чем нуждался автор), т.е. с созданием экземпляров классов через Context, однако, наша архитектура позволяет добавлением буквально нескольких строк адаптировать данные классы для работы с произвольным объектом WifiConfiguration.

Kitkat и Jelly Bean

Как уже говорилось в шаге 1, в данных версиях Android за хранение настроек Proxy отвечает объект ProxyProperties, хранящийся в LinkProperties, который в свою очередь находится в WifiConfiguration. Да, да, игла в яйце, яйцо в утке, утка в зайце и так далее.

Для того, чтобы изменить настройки прокси создадим новый экземпляр ProxyProperties с нужными нам параметрами, затем заменить данным объектом уже имеющийся и после этого настроить ProxySettings.

За создание экземпляров ProxyProperties будет отвечать отдельный класс:

ProxyPropertiesConstructor.java

public abstract class ProxyPropertiesConstructor {

    public static Object proxyProperties(String host, int port)
            throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
            InstantiationException, IllegalAccessException {
        return proxyProperties(host, port, null);
    }

    public static Object proxyProperties(String host, int port, String exclList)
            throws NoSuchMethodException, ClassNotFoundException, IllegalAccessException,
            InvocationTargetException, InstantiationException {
        return proxyPropertiesConstructor().newInstance(host, port, exclList);
    }

    private static Constructor proxyPropertiesConstructor()
            throws ClassNotFoundException, NoSuchMethodException {
        return Class.forName("android.net.ProxyProperties").getConstructor(String.class, int.class, String.class);
    }

}

Для удобной работы с данным объектом также создадим класс-контейнер, содержащий объект ProxyProperties и предоставляющий доступ к основным полям (и позволяющий удобно создавать его сразу через host и порт):

ProxyPropertiesContainer.java

public class ProxyPropertiesContainer {

    Object proxyProperties;


    ProxyPropertiesContainer(Object proxyProperties) {
        this.proxyProperties = proxyProperties;
    }

    ProxyPropertiesContainer(String host, int port)
            throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
            IllegalAccessException, InvocationTargetException {
        this(host, port, null);
    }

    ProxyPropertiesContainer(String host, int port, String exclList)
            throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
            InstantiationException, IllegalAccessException {
        this(ProxyPropertiesConstructor.proxyProperties(host, port, exclList));
    }

    public String getHost()
            throws NoSuchFieldException, IllegalAccessException {
        return (String) ReflectionHelper.getDeclaredField(proxyProperties, "mHost");
    }

    public int getPort()
            throws NoSuchFieldException, IllegalAccessException {
        return (int) ReflectionHelper.getDeclaredField(proxyProperties, "mPort");
    }

    public String getExclusionList()
            throws NoSuchFieldException, IllegalAccessException {
        return (String) ReflectionHelper.getDeclaredField(proxyProperties, "mExclusionList");
    }

    public Object getProxyProperties() {
        return proxyProperties;
    }

}

Теперь пишем реализацию собственно класса:

WifiConfigurationForApiFrom15To19.java

public class WifiConfigurationForApiFrom15To19 extends BaseWifiConfiguration implements ProxyChanger {

    private ProxyPropertiesContainer proxyPropertiesContainer;


    public WifiConfigurationForApiFrom15To19(Context context)
            throws NoSuchFieldException, IllegalAccessException, NullWifiConfigurationException {
        super(context);
        this.proxyPropertiesContainer = new ProxyPropertiesContainer(getCurrentProxyProperties());
    }

    public static WifiConfigurationForApiFrom15To19 createFromCurrentContext(Context context)
            throws NoSuchFieldException, IllegalAccessException, NullWifiConfigurationException {
        return new WifiConfigurationForApiFrom15To19(context);
    }

    @Override
    public void setProxySettings(ProxySettings proxySettings)
            throws NoSuchFieldException, IllegalAccessException {
        ReflectionHelper.setEnumField(wifiConfiguration, proxySettings.getValue(), "proxySettings");
    }

    @Override
    public ProxySettings getProxySettings()
            throws NoSuchFieldException, IllegalAccessException {
        return ProxySettings.valueOf(String.valueOf(ReflectionHelper.getDeclaredField(wifiConfiguration, "proxySettings")));
    }

    @Override
    public void setProxyHostAndPort(String host, int port)
            throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
            InstantiationException, IllegalAccessException, NoSuchFieldException {
        proxyPropertiesContainer = new ProxyPropertiesContainer(host, port);
        ReflectionHelper.getMethodAndInvokeIt(
                getLinkProperties(),
                "setHttpProxy",
                proxyPropertiesContainer.getProxyProperties());
    }

    @Override
    public String getProxyHost()
            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException,
            ApiNotSupportedException, NoSuchFieldException {
        if (proxyPropertiesContainer == null)
            throw new WifiProxyNotSettedException();
        return proxyPropertiesContainer.getHost();
    }

    @Override
    public int getProxyPort()
            throws ApiNotSupportedException, NoSuchMethodException, IllegalAccessException,
            InvocationTargetException, NoSuchFieldException {
        if (proxyPropertiesContainer == null)
            throw new WifiProxyNotSettedException();
        return proxyPropertiesContainer.getPort();
    }

    @Override
    public boolean isProxySetted()
            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException,
            ApiNotSupportedException, NoSuchFieldException {
        return !(proxyPropertiesContainer == null);
    }

    private LinkProperties getLinkProperties()
            throws NoSuchFieldException, IllegalAccessException {
        return (LinkProperties) ReflectionHelper.getField(wifiConfiguration, "linkProperties");
    }

    private Object getCurrentProxyProperties()
            throws NoSuchFieldException, IllegalAccessException {
        return ReflectionHelper.getDeclaredField(getLinkProperties(), "mHttpProxy");
    }

}

C этой версией закончили, остался:

Lollipop

Опять же, апеллируя к шагу 1, можно сделать вывод, что настройки прокси в данной версии Api находятся в классе ProxyInfo, содержащемся в IpConfiguration, который в свою очередь имеет своим местом дислокации наш WifiConfiguration. ProxySettings — тоже переехал, теперь он в вышеупомянутом IpConfiguration.

Напишем класс, делающий новые экземпляры ProxyInfo по заданным параметрам.

ProxyInfoConstructor.java

public abstract class ProxyInfoConstructor {

    public static ProxyInfo proxyInfo(String host, int port)
            throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException,
            InvocationTargetException, InstantiationException {
        return proxyInfo(host, port, null);
    }

    public static ProxyInfo proxyInfo(String host, int port, String exclude)
            throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException,
            InvocationTargetException, InstantiationException {
        Object newProxyInfo = proxyInfoConstructor().newInstance(host, port, exclude);
        return (ProxyInfo) newProxyInfo;

    }

    private static Constructor proxyInfoConstructor()
            throws ClassNotFoundException, NoSuchMethodException {
        return Class.forName("android.net.ProxyInfo").getConstructor(String.class, int.class, String.class);
    }

}

Как видите, здесь мы уже возвращаем не Object'ы, а именно экземпляры ProxyInfo, более того, далее будет видно, что у этого класса есть еще и методы getHost и getPort. В предыдущем случае мы этого сделать не могли, класс ProxyProperties был спрятан, именно поэтому мы писали для него «оболочку».

И, собственно, код для еще одной реализации:

WifiConfigurationForApiFrom21To22.java

public class WifiConfigurationForApiFrom21To22 extends BaseWifiConfiguration implements ProxyChanger {

    public WifiConfigurationForApiFrom21To22(Context context)
            throws NullWifiConfigurationException {
        super(context);
    }

    public static WifiConfigurationForApiFrom21To22 createFromCurrentContext(Context context)
            throws NullWifiConfigurationException {
        return new WifiConfigurationForApiFrom21To22(context);
    }

    @Override
    public ProxySettings getProxySettings()
            throws NoSuchFieldException, IllegalAccessException {
        return ProxySettings.valueOf(String.valueOf(ReflectionHelper.getDeclaredField(getIpConfigurationObject(), "proxySettings")));
    }

    @Override
    public void setProxySettings(ProxySettings proxySettings)
            throws NoSuchFieldException, IllegalAccessException {
        ReflectionHelper.setEnumField(getIpConfigurationObject(), proxySettings.getValue(), "proxySettings");
    }

    @Override
    public void setProxyHostAndPort(String host, int port)
            throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
            InstantiationException, IllegalAccessException, NoSuchFieldException {
        setProxyInfo(ProxyInfoConstructor.proxyInfo(host, port));
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    public String getProxyHost()
            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException,
            ApiNotSupportedException {
        ProxyInfo info = getProxyInfo();
        if (info == null)
            throw new WifiProxyNotSettedException();
        return info.getHost();
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    public int getProxyPort()
            throws ApiNotSupportedException, NoSuchMethodException, IllegalAccessException,
            InvocationTargetException {
        ProxyInfo info = getProxyInfo();
        if (info == null)
            throw new WifiProxyNotSettedException();
        return info.getPort();
    }

    @Override
    public boolean isProxySetted()
            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException,
            ApiNotSupportedException, NoSuchFieldException {
        return !(getProxyInfo() == null);
    }

    private Object getIpConfigurationObject()
            throws NoSuchFieldException, IllegalAccessException {
        return ReflectionHelper.getDeclaredField(wifiConfiguration, "mIpConfiguration");
    }

    private ProxyInfo getProxyInfo()
            throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        return (ProxyInfo) ReflectionHelper.getMethodAndInvokeIt(wifiConfiguration, "getHttpProxy");
    }

    private void setProxyInfo(ProxyInfo proxyInfo)
            throws NoSuchMethodException, InvocationTargetException, IllegalAccessException,
            NoSuchFieldException {
        ReflectionHelper.getMethodAndInvokeIt(wifiConfiguration, "setHttpProxy", proxyInfo);
    }

}

С основной реализацией на этом все. До финиша осталось совсем чуть-чуть.

Шаг 4. Предстартовая подготовка

Реализуем классы, упоминавшиеся ранее в тестах (замечание: мы реализуем настройку прокси по IP и порту, соответственно тип ProxySettings STATIC.)

CurrentProxyChangerGetter.java

public abstract class CurrentProxyChangerGetter {

    public static ProxyChanger chooseProxyChangerForCurrentApi(Context context)
            throws ApiNotSupportedException, NoSuchFieldException, IllegalAccessException,
            NullWifiConfigurationException {
        if (ApiChecker.isJellyBeanOrKitkat()) {
            return WifiConfigurationForApiFrom15To19.createFromCurrentContext(context);
        } else if (ApiChecker.isLolipop()) {
            return WifiConfigurationForApiFrom21To22.createFromCurrentContext(context);
        } else {
            throw new ApiNotSupportedException();
        }
    }

}

WifiProxyChanger.java

public abstract class WifiProxyChanger {

    public static void changeWifiStaticProxySettings(String host, int port, Context context)
            throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
            InstantiationException, IllegalAccessException, NoSuchFieldException,
            ApiNotSupportedException, NullWifiConfigurationException {
        updateWifiWithNewConfiguration(
                getCurrentWifiConfiguretionWithUpdatedSettings(host, port, ProxySettings.STATIC, context),
                context);
    }

    public static void clearProxySettings(Context context)
            throws IllegalAccessException, ApiNotSupportedException, NoSuchFieldException,
            NullWifiConfigurationException, ClassNotFoundException, NoSuchMethodException,
            InstantiationException, InvocationTargetException {
        updateWifiWithNewConfiguration(
                getCurrentWifiConfiguretionWithUpdatedSettings("", 0, ProxySettings.NONE, context),
                context);
    }

    private static WifiConfiguration getCurrentWifiConfiguretionWithUpdatedSettings(String host, int port, ProxySettings proxySettings, Context context)
            throws ApiNotSupportedException, IllegalAccessException, NullWifiConfigurationException,
            NoSuchFieldException, ClassNotFoundException, NoSuchMethodException,
            InstantiationException, InvocationTargetException {
        ProxyChanger proxyChanger = CurrentProxyChangerGetter.chooseProxyChangerForCurrentApi(context);
        proxyChanger.setProxyHostAndPort(host, port);
        proxyChanger.setProxySettings(proxySettings);
        return proxyChanger.getWifiConfiguration();
    }


    private static void updateWifiWithNewConfiguration(WifiConfiguration wifiConfiguration, Context context) {
        WifiManager currentWifiManager = NetworkHelper.getWifiManager(context);
        currentWifiManager.updateNetwork(wifiConfiguration);
        currentWifiManager.saveConfiguration();
        currentWifiManager.reconnect();
    }

}

WifiProxyInfo.java

public abstract class WifiProxyInfo {

    public static String getHost(Context context)
            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException,
            ApiNotSupportedException, NoSuchFieldException, NullWifiConfigurationException {
        return CurrentProxyChangerGetter.chooseProxyChangerForCurrentApi(context).getProxyHost();
    }

    public static int getPort(Context context)
            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException,
            ApiNotSupportedException, NoSuchFieldException, NullWifiConfigurationException {
        return CurrentProxyChangerGetter.chooseProxyChangerForCurrentApi(context).getProxyPort();
    }

    public static ProxySettings getProxySettings(Context context)
            throws ApiNotSupportedException, IllegalAccessException, NoSuchFieldException,
            NullWifiConfigurationException {
        return CurrentProxyChangerGetter.chooseProxyChangerForCurrentApi(context).getProxySettings();
    }

}

Реализуем вспомогательный класс для проверки версии API:

ApiChecker.java

public abstract class ApiChecker {

    public static boolean isJellyBeanOrKitkat() {
        return Build.VERSION.SDK_INT > 14 && Build.VERSION.SDK_INT < 20;
    }

    public static boolean isLolipop() {
        return Build.VERSION.SDK_INT > 20 && Build.VERSION.SDK_INT < 23;
    }

    public static boolean isSupportedApi() {
        return isJellyBeanOrKitkat() || isLolipop();
    }

}

ЗАПУСКАЕМ ТЕСТЫ

(прошу прощения, но это такой момент, что я решился выделить его заголовком)

Особенности программного ProxyChanging'а в Android. Часть 1: от Jelly Bean до Lollipop - 5

Шампанское! Вино! Народные гулянья! Аплодисменты! Queen — We are the champions в качестве музыкального сопровождения!

Небольшой комментарий

Разумеется, автор запускал тесты до этого момента несколько десятков раз и созерцал ошибки в огромных количествах. А иногда ему приходилось править и криво написанные тестовые сценарии, в которых была допущена какая-нибудь пустяковая ошибка, типа пропущенного "!". Но, так как к моменту написания статьи код уже был по 10 раз отрефакторен, переотрефакторен и выотрефакторен, то почему бы не позволить себе немного приукрасить действительность.

Шаг 5. Наслаждаемся плодами нашей деятельности

Подключаем библиотеку к приложению созданному по умолчанию и проверяем результат:

MainActivity.java

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        changeProxySettings("myhost.com", 12345);
    }

    void changeProxySettings(String host, int port) {
        try {
            WifiProxyChanger.changeWifiStaticProxySettings(host, port, this);
        } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | InvocationTargetException | NoSuchFieldException | IllegalAccessException | NullWifiConfigurationException | ApiNotSupportedException e) {
            e.printStackTrace();
        }
    }

}

Запускаем. На экране приложения мы ничего интересного не увидим, потому сразу идем в настройки wifi сети к которой мы подключены.

Результат. Картинка великовата.

Особенности программного ProxyChanging'а в Android. Часть 1: от Jelly Bean до Lollipop - 6

Результат достигнут.

Подведение итогов

На данный момент мы получили рабочую, легко расширяемую библиотеку, которую уже можно использовать для изменения настроек прокси wifi сетей в указанных выше версиях Android.

Дальнейшие планы? Да у автора их целый список!

  • Разумеется, нужно добавить поддержку Marshmallow, как раз появился новый телефон под управлением данной версии Android (и чувствую, что работа с новой системой разрешений будет той еще задачей).
  • Хочется так же изучить вопрос, касающийся настроек для мобильной сети.
  • Возможно, стоит доработать библиотеку для полноценного изменения конфигурации Wifi сетей. WifiConfiguration — класс большой и интересный, и, быть может, скоро в библиотеке появится интерфейс IpSettingsChanger, а с ним — и новая статья.
  • И, разумеется, нужно нормально оформить readme и прочие вещи на Bitbucket.

И, разумеется это далеко не все.

Post scriptum и еще немного комментариев автора

» Если вы заинтересовались ссылкой на библиотеку, дабы посмотреть, что она являет собой в текущем состоянии, то по запросу в комментариях я прикреплю ссылку на Bitbucket (либо оставлю в комментах).

» Если вас интересует какие либо подробности — автору можно свободно писать, он всегда рад общению с умными людьми.

» Если вы внезапно осознали, что вашему проекту будет полезен функционал реализованный или близкий к данному, и вам захочется использовать сии наработки — пожалуйста.

Спасибо за интерес к статье, искренне ваш, «Nickname, который нельзя оставить в песочнице»

Автор: LonelyDeveloper97

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js