Контроль за использованием сети

В этом уроке описывается, как писать приложения, которые имеют детальный контроль над использованием сетевых ресурсов. Если ваше приложение выполняет много сетевых операций, вы должны предоставить пользователям возможность управлять его поведением, например задавать частоту синхронизации данных, разрешать загрузку и скачивание только по Wi-Fi, разрешать или запрещать использование данных в роуминге и т. д. Если у пользователей есть доступ к этим настройкам, они с гораздо меньшей вероятностью отключат доступ вашего приложения к фоновым данным, когда приблизятся к своим лимитам, потому что вместо этого они могут точно контролировать, сколько данных использует ваше приложение.

Чтобы узнать больше об использовании сети вашим приложением, в том числе о количестве и типах сетевых подключений за определенный период времени, прочитайте статьи Веб-приложения и Проверка сетевого трафика с помощью профилировщика сети. Общие рекомендации по созданию приложений, которые минимально влияют на время работы от батареи при скачивании и подключении к сети, можно найти в статьях Оптимизация времени работы от батареи и Передача данных без разрядки батареи.

Вы также можете ознакомиться с примером NetworkConnect.

Как проверить сетевое подключение устройства

Устройство может иметь различные типы сетевых подключений. В этом уроке мы сосредоточимся на использовании Wi-Fi или мобильного сетевого подключения. Полный список возможных типов сетей приведен в ConnectivityManager.

Обычно Wi-Fi работает быстрее. Кроме того, мобильный интернет часто тарифицируется, что может быть дорого. Распространенная стратегия для приложений – скачивать большие объемы данных только при подключении к сети Wi-Fi.

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

  • ConnectivityManager: отвечает на запросы о состоянии сетевого подключения. Он также уведомляет приложения об изменении состояния подключения к сети.
  • NetworkInfo – описывает статус сетевого интерфейса определенного типа (в настоящее время – мобильная сеть или Wi-Fi).

Этот фрагмент кода проверяет подключение к сети Wi-Fi и мобильной сети. Оно определяет, доступны ли эти сетевые интерфейсы (то есть возможно ли подключение к сети) и/или подключены ли они (то есть существует ли подключение к сети и можно ли установить сокеты и передавать данные):

Kotlin

private const val DEBUG_TAG = "NetworkStatusExample"
...
val connMgr = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
var isWifiConn: Boolean = false
var isMobileConn: Boolean = false
connMgr.allNetworks.forEach { network ->
    connMgr.getNetworkInfo(network).apply {
        if (type == ConnectivityManager.TYPE_WIFI) {
            isWifiConn = isWifiConn or isConnected
        }
        if (type == ConnectivityManager.TYPE_MOBILE) {
            isMobileConn = isMobileConn or isConnected
        }
    }
}
Log.d(DEBUG_TAG, "Wifi connected: $isWifiConn")
Log.d(DEBUG_TAG, "Mobile connected: $isMobileConn")

Java

private static final String DEBUG_TAG = "NetworkStatusExample";
...
ConnectivityManager connMgr =
        (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
boolean isWifiConn = false;
boolean isMobileConn = false;
for (Network network : connMgr.getAllNetworks()) {
    NetworkInfo networkInfo = connMgr.getNetworkInfo(network);
    if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
        isWifiConn |= networkInfo.isConnected();
    }
    if (networkInfo.getType() == ConnectivityManager.TYPE_MOBILE) {
        isMobileConn |= networkInfo.isConnected();
    }
}
Log.d(DEBUG_TAG, "Wifi connected: " + isWifiConn);
Log.d(DEBUG_TAG, "Mobile connected: " + isMobileConn);

Обратите внимание, что не следует принимать решения на основе того, доступна ли сеть. Перед выполнением сетевых операций всегда проверяйте isConnected(), поскольку isConnected() обрабатывает такие случаи, как нестабильная мобильная сеть, режим полета и ограниченная передача данных в фоновом режиме.

Более краткий способ проверить, доступен ли сетевой интерфейс, выглядит следующим образом. Метод getActiveNetworkInfo() возвращает экземпляр NetworkInfo, представляющий первый найденный подключенный сетевой интерфейс, или null, если ни один из интерфейсов не подключен (то есть подключение к интернету недоступно):

Kotlin

fun isOnline(): Boolean {
    val connMgr = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val networkInfo: NetworkInfo? = connMgr.activeNetworkInfo
    return networkInfo?.isConnected == true
}

Java

public boolean isOnline() {
    ConnectivityManager connMgr = (ConnectivityManager)
            getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
    return (networkInfo != null && networkInfo.isConnected());
}

Чтобы получить более подробные сведения о состоянии, можно использовать NetworkInfo.DetailedState, но это требуется редко.

Контроль за использованием сети

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

  • Например, вы можете разрешить пользователям загружать видео, только когда устройство подключено к сети Wi-Fi.
  • Синхронизация может выполняться или не выполняться в зависимости от определенных критериев, таких как доступность сети, интервал времени и т. д.

Чтобы написать приложение, поддерживающее доступ к сети и управление ее использованием, в манифесте должны быть указаны правильные разрешения и фильтры интентов.

  • Фрагмент манифеста, приведенный ниже, содержит следующие разрешения:
  • Вы можете объявить фильтр интентов для действия ACTION_MANAGE_NETWORK_USAGE, чтобы указать, что в вашем приложении определено действие, которое предлагает варианты управления использованием данных. ACTION_MANAGE_NETWORK_USAGE показывает настройки для управления использованием данных сети определенным приложением. Если в вашем приложении есть Activity настроек, позволяющая пользователям управлять использованием сети, вы должны объявить этот фильтр интентов для этой Activity.

В примере приложения это действие обрабатывается классом SettingsActivity, который отображает интерфейс настроек, позволяющий пользователям решать, когда скачивать фид.

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

    <uses-sdk android:minSdkVersion="4"
           android:targetSdkVersion="14" />

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <application
        ...>
        ...
        <activity android:label="SettingsActivity" android:name=".SettingsActivity">
             <intent-filter>
                <action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
                <category android:name="android.intent.category.DEFAULT" />
          </intent-filter>
        </activity>
    </application>
</manifest>

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

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

Ниже приведен пример файла манифеста, в котором используется функция для каждого процесса.

<processes>
    <process />
    <deny-permission android:name="android.permission.INTERNET" />
    <process android:process=":withoutnet1" />
    <process android:process="com.android.cts.useprocess.withnet1">
        <allow-permission android:name="android.permission.INTERNET" />
    </process>
    <allow-permission android:name="android.permission.INTERNET" />
    <process android:process=":withoutnet2">
        <deny-permission android:name="android.permission.INTERNET" />
    </process>
    <process android:process="com.android.cts.useprocess.withnet2" />
</processes>

Как реализовать PreferenceActivity

Как видно из фрагмента манифеста, приведенного выше, у действия SettingsActivity в примере приложения есть фильтр интентов для действия ACTION_MANAGE_NETWORK_USAGE. SettingsActivity – это подкласс PreferenceActivity. На экране настроек (рис. 1) пользователи могут указать:

  • Показывать ли краткое описание для каждой записи XML-фида или только ссылку на нее.
  • Скачивать XML-фид при любом подключении к сети или только при подключении к Wi-Fi.

Панель настроек Как задать настройки сети

Рисунок 1. Действия с настройками.

Результаты поиска по запросу SettingsActivity Обратите внимание, что он реализует OnSharedPreferenceChangeListener. Когда пользователь меняет настройки, активируется onSharedPreferenceChanged(), что задает для refreshDisplay значение true. Это приведет к обновлению экрана, когда пользователь вернется к основному действию:

Kotlin

class SettingsActivity : PreferenceActivity(), OnSharedPreferenceChangeListener {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Loads the XML preferences file
        addPreferencesFromResource(R.xml.preferences)
    }

    override fun onResume() {
        super.onResume()

        // Registers a listener whenever a key changes
        preferenceScreen?.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)
    }

    override fun onPause() {
        super.onPause()

        // Unregisters the listener set in onResume().
        // It's best practice to unregister listeners when your app isn't using them to cut down on
        // unnecessary system overhead. You do this in onPause().
        preferenceScreen?.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this)
    }

    // When the user changes the preferences selection,
    // onSharedPreferenceChanged() restarts the main activity as a new
    // task. Sets the refreshDisplay flag to "true" to indicate that
    // the main activity should update its display.
    // The main activity queries the PreferenceManager to get the latest settings.

    override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
        // Sets refreshDisplay to true so that when the user returns to the main
        // activity, the display refreshes to reflect the new settings.
        NetworkActivity.refreshDisplay = true
    }
}

Java

public class SettingsActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Loads the XML preferences file
        addPreferencesFromResource(R.xml.preferences);
    }

    @Override
    protected void onResume() {
        super.onResume();

        // Registers a listener whenever a key changes
        getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
    }

    @Override
    protected void onPause() {
        super.onPause();

       // Unregisters the listener set in onResume().
       // It's best practice to unregister listeners when your app isn't using them to cut down on
       // unnecessary system overhead. You do this in onPause().
       getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
    }

    // When the user changes the preferences selection,
    // onSharedPreferenceChanged() restarts the main activity as a new
    // task. Sets the refreshDisplay flag to "true" to indicate that
    // the main activity should update its display.
    // The main activity queries the PreferenceManager to get the latest settings.

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        // Sets refreshDisplay to true so that when the user returns to the main
        // activity, the display refreshes to reflect the new settings.
        NetworkActivity.refreshDisplay = true;
    }
}

Как реагировать на изменения в настройках

Когда пользователь меняет настройки, это обычно влияет на поведение приложения. В этом фрагменте приложение проверяет настройки предпочтений в onStart(). Если настройка соответствует сетевому подключению устройства (например, выбрано значение "Wi-Fi" и устройство подключено по Wi-Fi), приложение скачивает фид и обновляет экран.

Kotlin

class NetworkActivity : Activity() {

    // The BroadcastReceiver that tracks network connectivity changes.
    private lateinit var receiver: NetworkReceiver

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Registers BroadcastReceiver to track network connection changes.
        val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
        receiver = NetworkReceiver()
        this.registerReceiver(receiver, filter)
    }

    public override fun onDestroy() {
        super.onDestroy()
        // Unregisters BroadcastReceiver when app is destroyed.
        this.unregisterReceiver(receiver)
    }

    // Refreshes the display if the network connection and the
    // pref settings allow it.

    public override fun onStart() {
        super.onStart()

        // Gets the user's network preference settings
        val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)

        // Retrieves a string value for the preferences. The second parameter
        // is the default value to use if a preference value is not found.
        sPref = sharedPrefs.getString("listPref", "Wi-Fi")

        updateConnectedFlags()

        if (refreshDisplay) {
            loadPage()
        }
    }

    // Checks the network connection and sets the wifiConnected and mobileConnected
    // variables accordingly.
    fun updateConnectedFlags() {
        val connMgr = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

        val activeInfo: NetworkInfo? = connMgr.activeNetworkInfo
        if (activeInfo?.isConnected == true) {
            wifiConnected = activeInfo.type == ConnectivityManager.TYPE_WIFI
            mobileConnected = activeInfo.type == ConnectivityManager.TYPE_MOBILE
        } else {
            wifiConnected = false
            mobileConnected = false
        }
    }

    // Uses AsyncTask subclass to download the XML feed from stackoverflow.com.
    fun loadPage() {
        if (sPref == ANY && (wifiConnected || mobileConnected) || sPref == WIFI && wifiConnected) {
            // AsyncTask subclass
            DownloadXmlTask().execute(URL)
        } else {
            showErrorPage()
        }
    }

    companion object {

        const val WIFI = "Wi-Fi"
        const val ANY = "Any"
        const val SO_URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort;=newest"

        // Whether there is a Wi-Fi connection.
        private var wifiConnected = false
        // Whether there is a mobile connection.
        private var mobileConnected = false
        // Whether the display should be refreshed.
        var refreshDisplay = true

        // The user's current network preference setting.
        var sPref: String? = null
    }
...

}

Java

public class NetworkActivity extends Activity {
    public static final String WIFI = "Wi-Fi";
    public static final String ANY = "Any";
    private static final String URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort;=newest";

    // Whether there is a Wi-Fi connection.
    private static boolean wifiConnected = false;
    // Whether there is a mobile connection.
    private static boolean mobileConnected = false;
    // Whether the display should be refreshed.
    public static boolean refreshDisplay = true;

    // The user's current network preference setting.
    public static String sPref = null;

    // The BroadcastReceiver that tracks network connectivity changes.
    private NetworkReceiver receiver = new NetworkReceiver();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Registers BroadcastReceiver to track network connection changes.
        IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
        receiver = new NetworkReceiver();
        this.registerReceiver(receiver, filter);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // Unregisters BroadcastReceiver when app is destroyed.
        if (receiver != null) {
            this.unregisterReceiver(receiver);
        }
    }

    // Refreshes the display if the network connection and the
    // pref settings allow it.

    @Override
    public void onStart () {
        super.onStart();

        // Gets the user's network preference settings
        SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);

        // Retrieves a string value for the preferences. The second parameter
        // is the default value to use if a preference value is not found.
        sPref = sharedPrefs.getString("listPref", "Wi-Fi");

        updateConnectedFlags();

        if(refreshDisplay){
            loadPage();
        }
    }

    // Checks the network connection and sets the wifiConnected and mobileConnected
    // variables accordingly.
    public void updateConnectedFlags() {
        ConnectivityManager connMgr = (ConnectivityManager)
                getSystemService(Context.CONNECTIVITY_SERVICE);

        NetworkInfo activeInfo = connMgr.getActiveNetworkInfo();
        if (activeInfo != null && activeInfo.isConnected()) {
            wifiConnected = activeInfo.getType() == ConnectivityManager.TYPE_WIFI;
            mobileConnected = activeInfo.getType() == ConnectivityManager.TYPE_MOBILE;
        } else {
            wifiConnected = false;
            mobileConnected = false;
        }
    }

    // Uses AsyncTask subclass to download the XML feed from stackoverflow.com.
    public void loadPage() {
        if (((sPref.equals(ANY)) && (wifiConnected || mobileConnected))
                || ((sPref.equals(WIFI)) && (wifiConnected))) {
            // AsyncTask subclass
            new DownloadXmlTask().execute(URL);
        } else {
            showErrorPage();
        }
    }
...

}

Обнаружение изменений подключения

Последний фрагмент головоломки – подкласс BroadcastReceiver, NetworkReceiver. Когда сетевое подключение устройства меняется, NetworkReceiver перехватывает действие CONNECTIVITY_ACTION, определяет статус сетевого подключения и устанавливает для флагов wifiConnected и mobileConnected значение true или false. В результате при следующем запуске приложение скачает только последнюю версию фида и обновит экран, если для параметра NetworkActivity.refreshDisplay задано значение true.

Если BroadcastReceiver вызывается без необходимости, это может привести к чрезмерному расходу системных ресурсов. Пример приложения регистрирует BroadcastReceiver NetworkReceiver в onCreate(), и отменяет регистрацию в onDestroy(). Это проще, чем объявлять <receiver> в манифесте. Если вы объявите <receiver> в манифесте, он сможет активировать приложение в любое время, даже если вы не запускали его несколько недель. Регистрируя и отменяя регистрацию NetworkReceiver в основном действии, вы гарантируете, что приложение не будет активировано после того, как пользователь выйдет из него. Если вы объявляете <receiver> в манифесте и точно знаете, где он вам нужен, вы можете использовать setComponentEnabledSetting(), чтобы включать и отключать его по мере необходимости.

Вот NetworkReceiver:

Kotlin

class NetworkReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        val conn = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val networkInfo: NetworkInfo? = conn.activeNetworkInfo

        // Checks the user prefs and the network connection. Based on the result, decides whether
        // to refresh the display or keep the current display.
        // If the userpref is Wi-Fi only, checks to see if the device has a Wi-Fi connection.
        if (WIFI == sPref && networkInfo?.type == ConnectivityManager.TYPE_WIFI) {
            // If device has its Wi-Fi connection, sets refreshDisplay
            // to true. This causes the display to be refreshed when the user
            // returns to the app.
            refreshDisplay = true
            Toast.makeText(context, R.string.wifi_connected, Toast.LENGTH_SHORT).show()

            // If the setting is ANY network and there is a network connection
            // (which by process of elimination would be mobile), sets refreshDisplay to true.
        } else if (ANY == sPref && networkInfo != null) {
            refreshDisplay = true

            // Otherwise, the app can't download content--either because there is no network
            // connection (mobile or Wi-Fi), or because the pref setting is WIFI, and there
            // is no Wi-Fi connection.
            // Sets refreshDisplay to false.
        } else {
            refreshDisplay = false
            Toast.makeText(context, R.string.lost_connection, Toast.LENGTH_SHORT).show()
        }
    }
}

Java

public class NetworkReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        ConnectivityManager conn =  (ConnectivityManager)
            context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = conn.getActiveNetworkInfo();

        // Checks the user prefs and the network connection. Based on the result, decides whether
        // to refresh the display or keep the current display.
        // If the userpref is Wi-Fi only, checks to see if the device has a Wi-Fi connection.
        if (WIFI.equals(sPref) && networkInfo != null
            && networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
            // If device has its Wi-Fi connection, sets refreshDisplay
            // to true. This causes the display to be refreshed when the user
            // returns to the app.
            refreshDisplay = true;
            Toast.makeText(context, R.string.wifi_connected, Toast.LENGTH_SHORT).show();

        // If the setting is ANY network and there is a network connection
        // (which by process of elimination would be mobile), sets refreshDisplay to true.
        } else if (ANY.equals(sPref) && networkInfo != null) {
            refreshDisplay = true;

        // Otherwise, the app can't download content--either because there is no network
        // connection (mobile or Wi-Fi), or because the pref setting is WIFI, and there
        // is no Wi-Fi connection.
        // Sets refreshDisplay to false.
        } else {
            refreshDisplay = false;
            Toast.makeText(context, R.string.lost_connection, Toast.LENGTH_SHORT).show();
        }
    }
}