Как создать приложение Wi-Fi сканнер для андроид

Сегодня в рубрике “как создать android application” мы с вами напишем небольшое приложение, которое сканирует и выводит на экран список Wi-Fi точек доступа в радиусе вашего устройства с указанием имени сети, типа защиты и уровня мощности сигнала.

Создаем проект в Андроид Студио. Вводим имя проекта. Шаблон для проекта выбираем Basic Activity. Все остальные настройки оставляем по умолчанию.

Шаблон Android Studio Basic Activity
Откроем макет контента главного экрана. Здесь удалим ненужное текстовое поле и вместо него добавим виджет списка ListView. У нас на экране будет список, который будет заполняться информацией о точках доступа Wi-Fi, обнаруженных поблизости.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="info.fandroid.wifiscanner.MainActivity"
    tools:showIn="@layout/activity_main">

    <ListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/listItem">

    </ListView>
</RelativeLayout>

Поскольку стандартный вид списка нам не подходит, создадим макет пункта списка и настроим его под свои нужды.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">


        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceLarge"
            android:text="Имя сети" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceSmall"
            android:text="Тип защиты" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceSmall"
            android:text="Уровень сигнала" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:orientation="vertical">


        <TextView
            android:id="@+id/tvSSID"
            android:layout_gravity="end"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceLarge"
            tools:text="Имя сети" />

        <TextView
            android:id="@+id/tvSecurity"
            android:maxLength="24"
            android:layout_gravity="end"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceSmall"
            tools:text="Тип защиты" />

        <TextView
            android:id="@+id/tvLevel"
            android:layout_gravity="end"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceSmall"
            tools:text="Уровень сигнала" />
    </LinearLayout>

</RelativeLayout>

Сначала создадим файл макета с корневым элементом компоновки RelativeLayout. Добавим линейный компоновщик LinearLayout. Нам понадобятся три текстовых поля – одно с крупным текстом, и два с текстом помельче. Для этого идеально подойдут готовые виджеты, которые находятся на вкладке “Дизайн”. Выберем один Large Text и два Small Text. Изменим текст, который содержат текстовые поля, на заголовки полей.
Идентификаторы этих полей нам не понадобятся – можно удалить. Эти поля будут отображать только заголовки. А для размещения информации о точках доступа создадим еще три текстовых поля – напротив уже созданных. Для этого скопируем весь линейный компоновщик с текстовыми полями и настроим его так, чтобы он отображался с правой стороны экрана. Здесь нам уже понадобятся идентификаторы, допишем их.
Также для всего макета добавим пространство имен tools, с помощью которого сделаем так, что текст полей справа будет отображаться только на предпросмотре.
А еще добавим свойство выравнивания текста по правому краю текстового поля, а также добавим ограничение длины поля “Тип защиты” – оно может вывадить длинные строки, и тест будет наезжать на другие поля.
Макет пункта списка готов, будем использовать его при построении списка.

Теперь откроем макет activity-main.xml и настроим отображение кнопки.

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="info.fandroid.wifiscanner.MainActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/content_main" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        android:src="@drawable/accesspoint" />

</android.support.design.widget.CoordinatorLayout>

Мы будем использовать FAB для запуска сканирования точек доступа. Нужно заменить иконку на кнопке на более подходящую. Для этого я предварительно скачал иконку с сайта materialdesignicons.com. Вы можете скачать ее по ссылке (щелкните правой кнопкой мыши и выберите “Сохранить ссылку как…”). Скопируем иконку в папку drawable и заменим стандартную иконку для плавающей кнопки.

Прежде чем перейти к написанию кода приложения, добавим необходимые разрешения в манифест. Они нужны для получения доступа к списку сетей Wi-Fi, которые видит устройство.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="info.fandroid.wifiscanner">

    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:configChanges="keyboardHidden|orientation|screenSize"
            android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Начнем писать код. В основном пакете создаем класс Element.java. Этот класс будет хранить описание полей пункта списка и будет использоваться адаптером при создании списка.

public class Element {
    private String title;
    private String security;
    private String level;

    public Element(String title, String security, String level) {
        this.title = title;
        this.security = security;
        this.level = level;
    }

    public String getTitle() {
        return title;
    }

    public String getSecurity() {
        return security;
    }

    public String getLevel() {
        return level;
    }
}

Здесь три строковых переменных – по количеству текстовых полей. Также создадим конструктор, инициализирующий все три поля. Комбинация клавиш Alt+Insert используется при этом. И с помощью нее же создадим геттеры для всех полей.

Всю основную реализацию напишем в классе главного экрана – MainActivity.java. Для начала создадим необходимые переменные.

private Element [] nets;
private WifiManager wifiManager;
private List<ScanResult> wifiList;

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

class AdapterElements extends ArrayAdapter<Object> {
        Activity context;

        public AdapterElements(Activity context) {
            super(context, R.layout.items, nets);
            this.context = context;
        }

        public View getView(int position, View convertView, ViewGroup parent){
            LayoutInflater inflater = context.getLayoutInflater();
            View item = inflater.inflate(R.layout.items, null);

            TextView tvSsid = (TextView) item.findViewById(R.id.tvSSID);
            tvSsid.setText(nets[position].getTitle());

            TextView tvSecurity = (TextView)item.findViewById(R.id.tvSecurity);
            tvSecurity.setText(nets[position].getSecurity());

            TextView tvLevel = (TextView)item.findViewById(R.id.tvLevel);
            String level = nets[position].getLevel();

            try{
                int i = Integer.parseInt(level);
                if (i>-50){
                    tvLevel.setText("Высокий");
                } else if (i<=-50 && i>-80){
                    tvLevel.setText("Средний");
                } else if (i<=-80){
                    tvLevel.setText("Низкий");
                }
            } catch (NumberFormatException e){
                Log.d("TAG", "Неверный формат строки");
            }
            return item;
        }
    }

Здесь для простоты мы создаем внутренний класс, более подробно о создании и настройке своего адаптера смотрите урок 54 на нашем канале, ссылка.
Объявим контекст. Также создадим конструктор, который будет принимать контекст в качестве параметра.
В конструкторе передаем контекст, макет пункта списка и массив элементов списка.
Далее пропишем реализацию заполнения пункта списка данными.
Создаем объект inflater с помощью которого будем заполнять данными макет пункта списка, находим макет по id.
Далее находим по идентификаторам каждое текстовое поле в отдельности и заполняем его данными.
Последнее поле – уровень сигнала – будем заполнять не полученными данными, а по условию. Для этого нам нужно вычислять уровень сигнала. Поскольку Wi-Fi менеджер возвращает строку, будем приводить ее к формату int и настроим условие, в зависимости от значения уровня будем выводить в текстовое поле три слова – высокий, средний и низкий уровень. Вся конструкция будет выполняться в блоке try-catch с отловом исключения NumberFormatException, чтобы в случае ошибки формата приложение не упало, а вывело нам сообщение в консоль.

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

 public void detectWifi(){
        this.wifiManager = (WifiManager)getSystemService(Context.WIFI_SERVICE);
        this.wifiManager.startScan();
        this.wifiList = this.wifiManager.getScanResults();

        Log.d("TAG", wifiList.toString());

        this.nets = new Element[wifiList.size()];

        for (int i = 0; i<wifiList.size(); i++){
            String item = wifiList.get(i).toString();
            String[] vector_item = item.split(",");
            String item_essid = vector_item[0];
            String item_capabilities = vector_item[2];
            String item_level = vector_item[3];
            String ssid = item_essid.split(": ")[1];
            String security = item_capabilities.split(": ")[1];
            String level = item_level.split(":")[1];
            nets[i] = new Element(ssid, security, level);
        }

        AdapterElements adapterElements = new AdapterElements(this);
        ListView netList = (ListView) findViewById(R.id.listItem);
        netList.setAdapter(adapterElements);
    }

Сначала создаем Wi-Fi Manager и запускаем сканирование. Результат сканирования сохраняем в список wifilist.
Далее создаем массив элементов и начинаем разбор данных. Как я уже говорил, Wi-Fi Manager возвращает данные в строковом формате, гда различные праметры расположены по порядку и разделены запятыми. В цикле мы последовательно перебираем каждый параметр, и получаем его значение, которое отделено от имени параметра двоеточием. Нам нужны значения только трех параметров – идентификатор сети, тип защиты и уровень сигнала, заполняем ними каждый элемент массива.
В завершение создаем адаптер, находим макет списка и присваиваем адаптер списку.

Осталось только вызвать метод detectWifi в обработчике нажатия кнопки FAB.

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);

            fab.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {

                    detectWifi();
                    Snackbar.make(view, "Сканирование...", Snackbar.LENGTH_SHORT)
                            .setAction("Action", null).show();
                }
            });

    }

По умолчанию используемый шаблон настроен так, что при нажатии FAB выезжает снекбар с сообщением. Оставим так, но изменим текст сообщения и длительность его отображения.
Давайте запустим приложение на реальном устройстве и проверим, как оно работает.

Приложение работает, список нормально отображается.

device-2016-09-22-093437

Но есть один нюанс – при повороте экрана активити пересоздается, и список исчезает. В манифесте можно настроить активити так, чтобы оно не пересоздавалось при смене ориентации и в некоторых других распространенных случаях. В этом нам поможет атрибут configChanges с такими параметрами:

android:configChanges="keyboardHidden|orientation|screenSize"

Теперь при повороте экрана активити не пересоздается, и список не теряется.

Полный код класса MainActivity:

package ...

import android.app.Activity;
import android.content.Context;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;

import java.util.List;

public class MainActivity extends AppCompatActivity {

    private Element [] nets;
    private WifiManager wifiManager;
    private List<ScanResult> wifiList;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);

            fab.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {

                    detectWifi();
                    Snackbar.make(view, "Сканирование...", Snackbar.LENGTH_SHORT)
                            .setAction("Action", null).show();
                }
            });

    }

    public void detectWifi(){
        this.wifiManager = (WifiManager)getSystemService(Context.WIFI_SERVICE);
        this.wifiManager.startScan();
        this.wifiList = this.wifiManager.getScanResults();

        Log.d("TAG", wifiList.toString());

        this.nets = new Element[wifiList.size()];

        for (int i = 0; i<wifiList.size(); i++){
            String item = wifiList.get(i).toString();
            String[] vector_item = item.split(",");
            String item_essid = vector_item[0];
            String item_capabilities = vector_item[2];
            String item_level = vector_item[3];
            String ssid = item_essid.split(": ")[1];
            String security = item_capabilities.split(": ")[1];
            String level = item_level.split(":")[1];
            nets[i] = new Element(ssid, security, level);
        }

        AdapterElements adapterElements = new AdapterElements(this);
        ListView netList = (ListView) findViewById(R.id.listItem);
        netList.setAdapter(adapterElements);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    class AdapterElements extends ArrayAdapter<Object> {
        Activity context;

        public AdapterElements(Activity context) {
            super(context, R.layout.items, nets);
            this.context = context;
        }

        public View getView(int position, View convertView, ViewGroup parent){
            LayoutInflater inflater = context.getLayoutInflater();
            View item = inflater.inflate(R.layout.items, null);

            TextView tvSsid = (TextView) item.findViewById(R.id.tvSSID);
            tvSsid.setText(nets[position].getTitle());

            TextView tvSecurity = (TextView)item.findViewById(R.id.tvSecurity);
            tvSecurity.setText(nets[position].getSecurity());

            TextView tvLevel = (TextView)item.findViewById(R.id.tvLevel);
            String level = nets[position].getLevel();

            try{
                int i = Integer.parseInt(level);
                if (i>-50){
                    tvLevel.setText("Высокий");
                } else if (i<=-50 && i>-80){
                    tvLevel.setText("Средний");
                } else if (i<=-80){
                    tvLevel.setText("Низкий");
                }
            } catch (NumberFormatException e){
                Log.d("TAG", "Неверный формат строки");
            }
            return item;
        }
    }
}

Файл сборки модуля app:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 24
    buildToolsVersion "24.0.2"

    defaultConfig {
        applicationId "info.fandroid.wifiscanner"
        minSdkVersion 14
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:24.2.1'
    compile 'com.android.support:design:24.2.1'
}

Приложение нужно запускать на устройстве с версиями до Android 6.0. В 6-й версии изменили поход к работе с разрешениями – разрешения приложение запрашивает не в момент установки, а в момент использования функции, требующей наличие разрешения. Поэтому для работы этого приложения на версии Android 6.0 и выше необходимо написать проверку наличия необходимых разрешений и запроса разрешения при его отсутствии. Этим мы займемся на одном из последующих уроков, следите за новостями на нашем канале и сайте fandroid.info.

Коментарі: 5
  1. margadon
    margadon

    у меня заработало когда сменил контекст активити на контекст приложения в detectWifi
    сделал так: this.wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
    кроме того ошибки в String ssid = item_essid.split(“:”)[1]; //лишний пробел в стр 69 и 70

    1. здравствуйте кто может отправить исходный файл проекта на почту очень нужно

  2. vera

    какой должен быть SDK к этому проекту? 23 можно?

    пожалуйста, перечислите ВСЕ что должно быть установлено в SDK.

    я получаю ошибку в Main.activity.java
    design.widget.FloatingActionButton;
    design.widget.Snackbar;
    .v7.app.AppCompatActivity;
    .v7.widget.Toolbar;
    R.

    что у меня не установлено????
    что надо установить чтобы работало?

    1. Виталий Непочатов
      admin (автор)

      В этом проекте использовалась 24 версия – смотрите, файл сборки добавлен в текст урока. И импорты проверьте в главном классе.

  3. Guest

    В 6-й версии изменили ПОХОД к работе с разрешениями — разрешения приложение запрашивает не в момент установки, а в момент использования функции, требующей наличие разрешения.

Додати коментар