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

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

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

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

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

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

Wi-Fi обычно быстрее. Кроме того, мобильные данные часто измеряются, что может оказаться дорогостоящим. Общая стратегия для приложений — получать большие данные только в том случае, если доступна сеть Wi-Fi.

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

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

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

Котлин

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")

Ява

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 если ни один из интерфейсов не подключен (это означает, что подключение к Интернету недоступно):

Котлин

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

Ява

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 показывает настройки для управления использованием сетевых данных конкретного приложения. Если в вашем приложении есть действие настроек, которое позволяет пользователям контролировать использование сети, вам следует объявить этот фильтр намерений для этого действия.

В примере приложения это действие обрабатывается классом 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>

Реализуйте действие по предпочтениям

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

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

Панель настроекУстановка предпочтений сети

Рисунок 1. Действие предпочтений.

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

Котлин

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
    }
}

Ява

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), приложение загружает канал и обновляет дисплей.

Котлин

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
    }
...

}

Ява

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 :

Котлин

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()
        }
    }
}

Ява

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();
        }
    }
}