Zarządzanie wykorzystaniem sieci

Z tej lekcji dowiesz się, jak pisać aplikacje, które mają precyzyjną kontrolę nad wykorzystaniem zasobów sieciowych. Jeśli Twoja aplikacja wykonuje wiele operacji sieciowych, musisz podać ustawienia użytkownika, które pozwolą im kontrolować nawyki użytkownika dotyczące danych. Mogą na przykład określić, jak często aplikacja ma synchronizować dane, czy przesyłać/pobierać tylko przez Wi-Fi, czy używać transmisji danych w roamingu itd. Dzięki takim ustawieniom użytkownicy mogą znacznie rzadziej wyłączyć dostęp aplikacji do danych w tle, gdy zbliżą się do swojego limitu, ponieważ mogą precyzyjnie kontrolować ilość danych zużywanych przez aplikację.

Więcej informacji o wykorzystaniu sieci przez aplikację, w tym o liczbie i typach połączeń sieciowych w danym okresie, znajdziesz w artykułach Aplikacje internetowe i Sprawdzanie ruchu w sieci za pomocą profilu sieci. Ogólne wskazówki dotyczące pisania aplikacji, które minimalizują wpływ pobierania i połączeń sieciowych na żywotność baterii, znajdziesz w artykułach Optymalizowanie czasu pracy na baterii i Przenoszenie danych bez obciążania baterii.

Możesz też zapoznać się z przykładem NetworkConnect.

Sprawdzanie połączenia sieciowego urządzenia

Urządzenie może mieć różne typy połączeń sieciowych. Ta lekcja dotyczy korzystania z sieci Wi-Fi i sieci komórkowej. Pełną listę możliwych typów sieci znajdziesz w sekcji ConnectivityManager.

Wi-Fi jest zwykle szybsze. Poza tym mobilna transmisja danych jest często zmierzona, co może być drogie. Typową strategią w przypadku aplikacji jest pobieranie dużych ilości danych tylko wtedy, gdy dostępna jest sieć Wi-Fi.

Przed wykonaniem operacji sieciowych warto sprawdzić stan połączenia sieciowego. Może to między innymi uniemożliwić aplikacji wykorzystanie niewłaściwego radia. Jeśli połączenie sieciowe jest niedostępne, aplikacja powinna odpowiedzieć płynnie. Do sprawdzania połączenia sieciowego zazwyczaj używane są te klasy:

  • ConnectivityManager: odpowiada na pytania o stan połączenia sieciowego. Informuje też aplikacje o zmianach połączenia sieciowego.
  • NetworkInfo: opisuje stan interfejsu sieciowego danego typu (obecnie Komórka lub Wi-Fi).

Ten fragment kodu testuje połączenia z siecią Wi-Fi i komórkową. Określa ono, czy te interfejsy sieciowe są dostępne (to znaczy, czy można nawiązać połączenie sieciowe) i czy połączenie jest możliwe (tj. czy można nawiązać połączenie sieciowe oraz czy można ustanowić gniazda i przekazywać dane):

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

Pamiętaj, że nie możesz podejmować decyzji o dostępności sieci. Zawsze sprawdzaj parametr isConnected() przed wykonywaniem operacji sieciowych, ponieważ isConnected() obsługuje przypadki niestabilnego połączenia z sieciami komórkowymi, trybu samolotowego i ograniczonych danych w tle.

Poniżej znajdziesz bardziej zwięzły sposób na sprawdzenie dostępności interfejsu sieci. Metoda getActiveNetworkInfo() zwraca instancję NetworkInfo reprezentującą pierwszy połączony interfejs sieci, jaki uda się znaleźć, lub null, jeśli żaden z interfejsów nie jest połączony (co oznacza, że połączenie z internetem jest niedostępne):

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

Aby uzyskać bardziej szczegółowe zapytania, możesz użyć metody NetworkInfo.DetailedState, ale rzadko jest to konieczne.

Zarządzanie wykorzystaniem sieci

Możesz wdrożyć działanie związane z preferencjami, które daje użytkownikom wyraźną kontrolę nad wykorzystaniem zasobów sieciowych w aplikacji. Na przykład:

  • Możesz zezwolić użytkownikom na przesyłanie filmów tylko wtedy, gdy urządzenie jest połączone z siecią Wi-Fi.
  • Dane możesz synchronizować (lub nie) w zależności od określonych kryteriów, takich jak dostępność sieci czy przedział czasu.

Aby można było napisać aplikację, która obsługuje dostęp do sieci i zarządza jej wykorzystaniem, plik manifestu musi mieć odpowiednie uprawnienia i filtry intencji.

  • Plik manifestu wyodrębniony w dalszej części tej sekcji obejmuje te uprawnienia:
  • Możesz zadeklarować filtr intencji dla działania ACTION_MANAGE_NETWORK_USAGE, aby wskazać, że aplikacja definiuje działanie, które udostępnia opcje kontrolowania użycia danych. ACTION_MANAGE_NETWORK_USAGE pokazuje ustawienia zarządzania siecią danych przez określoną aplikację. Jeśli w aplikacji występuje działanie związane z ustawieniami, które umożliwia użytkownikom kontrolowanie wykorzystania sieci, zadeklaruj ten filtr intencji dla tej aktywności.

W przykładowej aplikacji działanie to jest obsługiwane przez klasę SettingsActivity, która wyświetla interfejs ustawień, aby użytkownicy mogli zdecydować, kiedy pobrać plik danych.

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

Aplikacje, które mają dostęp do poufnych danych użytkownika i są kierowane na Androida 11 lub nowszego, mogą przyznawać dostęp do sieci poszczególnym procesom. Jasno określając, które procesy mogą mieć dostęp do sieci, izolujesz cały kod, który nie musi przesyłać danych.

Chociaż nie chronimy aplikacji przed przypadkowym przesyłaniem danych, to jednak pozwala zmniejszyć ryzyko wystąpienia błędów w aplikacji powodujących wyciek danych.

Poniżej znajdziesz przykład pliku manifestu, który korzysta z funkcji poszczególnych procesów:

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

Wdrażanie działania opartego na preferencjach

Jak widać we wcześniejszym fragmencie pliku manifestu w tym temacie, aktywność SettingsActivity w przykładowej aplikacji zawiera filtr intencji dla działania ACTION_MANAGE_NETWORK_USAGE. SettingsActivity jest podklasą grupy PreferenceActivity. Wyświetla on ekran ustawień (jak widać na Rysunku 1), na którym użytkownicy mogą określić:

  • Określa, czy mają być wyświetlane podsumowania dla każdego wpisu w kanale XML, czy tylko dla linku dla każdego wpisu.
  • Określa, czy plik XML ma zostać pobrany, gdy dostępne jest połączenie sieciowe, czy tylko wtedy, gdy dostępna jest sieć Wi-Fi.

Panel preferencji Ustawianie preferencji dotyczących sieci

Rysunek 1. Aktywność związana z ustawieniami.

Oto SettingsActivity. Pamiętaj, że implementuje ona OnSharedPreferenceChangeListener. Gdy użytkownik zmieni ustawienie, uruchamia się tag onSharedPreferenceChanged(), który ustawia refreshDisplay na wartość prawda. Spowoduje to odświeżenie ekranu, gdy użytkownik wróci do głównej aktywności:

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

Reagowanie na zmiany preferencji

Gdy użytkownik zmieni swoje preferencje na ekranie ustawień, zwykle ma to związek z działaniem aplikacji. Ten fragment kodu sprawdza ustawienia preferencji w onStart(). jeśli ustawienie jest zgodne z połączeniem sieciowym urządzenia (np. jeśli ustawienie to "Wi-Fi", a urządzenie ma połączenie z Wi-Fi), aplikacja pobiera plik danych i odświeża wyświetlacz.

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

}

Wykrywanie zmian połączeń

Ostatnim elementem łamigłówki jest podklasa BroadcastReceiver (NetworkReceiver). Gdy połączenie sieciowe urządzenia się zmieni, NetworkReceiver przechwytuje działanie CONNECTIVITY_ACTION, określa stan połączenia sieciowego i odpowiednio ustawia flagi wifiConnected oraz mobileConnected na prawda/fałsz. W efekcie przy następnym powrocie użytkownika do aplikacji aplikacja pobierze tylko najnowszy kanał i zaktualizuje wyświetlacz, jeśli NetworkActivity.refreshDisplay ma wartość true.

Niepotrzebnie skonfigurowane BroadcastReceiver może spowodować zużycie zasobów systemowych. Przykładowa aplikacja rejestruje BroadcastReceiver NetworkReceiver w onCreate() i wyrejestrowana jest w onDestroy(). To łatwiejsze niż zadeklarowanie <receiver> w pliku manifestu. Jeśli zadeklarujesz w pliku manifestu funkcję <receiver>, może ona ją wybudzić w dowolnym momencie, nawet jeśli nie była używana od tygodni. Rejestrując i wyrejestrowując usługę NetworkReceiver w głównej aktywności, masz pewność, że aplikacja nie zostanie wybudzona po opuszczeniu aplikacji przez użytkownika. Jeśli zadeklarujesz w pliku manifestu właściwość <receiver> i będziesz dokładnie wiedzieć, gdzie jest potrzebna, możesz ją włączyć i wyłączyć za pomocą setComponentEnabledSetting().

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