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

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



Якщо ви вважаєте корисним у майбутньому знати рішення, хочете почерпнути щось для себе або у вас просто прокинулося цікавість — ласкаво просимо під кат. Там вас чекає внутрішнє пристрій класів відповідають за конфігурацію 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. Намагаючись максимально задовольнити цим вимогам, на початковому етапі моє запалене свідомість придумав щось таке:



Пояснювальний коментар до картинці вище
  • Клас 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 running applications 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 швидкою як прямі виклики ");
}

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



Більше того, ми могли взагалі знати тільки розташування файлу бібліотеки і не знати нічого про її структуру, 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("НІ"),
/* 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();
}

}


ЗАПУСКАЄМО ТЕСТИ

(прошу вибачення, але це такий момент, що я зважився виділити його заголовком)




Шампанське! Вино! Народні гуляння! Оплески! 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 мережі до якої ми підключені.

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

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

Підбиття підсумків

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

Подальші плани? Так у автора їх цілий список!

  • Зрозуміло, потрібно додати підтримку Marshmallow, як раз з'явився новий телефон під керуванням цієї версії Android (і відчуваю, що робота з новою системою дозволів буде ще завданням).
  • Хочеться так само вивчити питання, що стосується налаштувань для мобільної мережі.
  • Можливо, варто доопрацювати бібліотеку для повноцінного зміни конфігурації Wifi мереж. WifiConfiguration — клас великий і цікавий, і, бути може, скоро в бібліотеці з'явиться інтерфейс IpSettingsChanger, а з ним — і нова стаття.
  • І, зрозуміло, потрібно нормально оформити документацію та інші речі на Bitbucket.
І, зрозуміло, це далеко не все.

Post scriptum і ще трохи коментарів автора» Якщо ви зацікавилися посиланням на бібліотеку, щоб подивитися, що вона являє собою в поточному стані, то за запитом в коментарях я прикреплю посилання на Bitbucket (або залишу в коментах).

» Якщо вас цікавить якісь подробиці — автору можна вільно писати, він завжди радий спілкуванню з розумними людьми.

» Якщо ви раптом усвідомили, що вашому проекту буде корисний функціонал реалізований або близький до цього, і вам захочеться використовувати ці напрацювання — будь ласка.
Дякуємо за інтерес до статті, щиро ваш, «Nickname, який не можна залишити в пісочниці»
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.