Cómo administrar el uso de la red

En esta lección, se describe la forma de escribir aplicaciones que controlen minuciosamente el uso de recursos de red. Si tu aplicación realiza muchas operaciones, debes proporcionar opciones de configuración que permitan a los usuarios controlar los hábitos de datos de tu app, como la frecuencia con la que sincroniza datos, si realiza cargas y descargas solo cuando hay conexión Wi-Fi, si usa datos con el servicio de itinerancia, etc. Con estos controles a su disposición, es mucho menos probable que los usuarios inhabiliten el acceso de tu app a los datos en segundo plano, ya que pueden controlar con precisión la cantidad de datos que usa tu app.

Para obtener más información sobre el uso de la red de tu app, lo que incluye la cantidad y los tipos de conexiones de red durante un período de tiempo, consulta Aplicaciones web y Cómo inspeccionar el tráfico de red con el generador de perfiles de red. Para conocer los lineamientos generales sobre cómo escribir apps que minimicen el impacto de las descargas y las conexiones de red en la duración de la batería, consulta Cómo optimizar la duración de la batería y Cómo transferir datos sin consumir la batería.

También puedes consultar el ejemplo de Network Connect.

Cómo comprobar la conexión de red de un dispositivo

Un dispositivo puede tener varios tipos de conexiones de red. En esta lección, nos centraremos en el uso de una conexión de red Wi-Fi o móvil. Para obtener una lista completa de los tipos de red posibles, consulta ConnectivityManager.

La conexión Wi-Fi suele ser más rápida. Además, los datos móviles suelen ser de uso medido, lo que puede resultar costoso. Una estrategia común para las apps es buscar una gran cantidad de datos solo si hay una red Wi-Fi disponible.

Antes de realizar operaciones de red, te recomendamos verificar el estado de conectividad de la red. De esta manera, puedes evitar, entre otras cosas, que tu app use el radio incorrecto. Si no hay una conexión de red disponible, tu aplicación debería responder de forma correcta. Para verificar la conexión de red, por lo general, se usan las siguientes clases:

  • ConnectivityManager: Responde consultas sobre el estado de conectividad de la red. También notifica a las aplicaciones cuando cambia la conectividad de red.
  • NetworkInfo: Describe el estado de una interfaz de red de un tipo determinado (actualmente, móvil o Wi-Fi).

En este fragmento de código, se prueba la conectividad de red para Wi-Fi y redes móviles. Determina si estas interfaces de red están disponibles (es decir, si es posible establecer conectividad de red) y si el dispositivo está conectado (es decir, si existe una conectividad de red y si es posible establecer sockets y transmitir datos):

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

Ten en cuenta que no debes basar las decisiones en la "disponibilidad" de una red. Siempre debes comprobar la presencia del elemento isConnected() antes de realizar operaciones de red, ya que isConnected() maneja casos como redes móviles inestables, el modo de avión y datos en segundo plano restringidos.

Una forma más concisa de comprobar si una interfaz de red está disponible es la siguiente. El método getActiveNetworkInfo() muestra una instancia de NetworkInfo, que representa la primera interfaz de red conectada que puede encontrar, o null si ninguna de las interfaces está conectada (lo cual significa que no hay una conexión a Internet disponible):

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

Para consultar un estado más detallado, puedes usar NetworkInfo.DetailedState, pero rara vez será necesario.

Cómo administrar el uso de la red

Puedes implementar una actividad de preferencias que brinde a los usuarios un control explícito sobre el uso de los recursos de red de tu app. Por ejemplo:

  • Puedes permitir que los usuarios carguen videos solo cuando el dispositivo esté conectado a una red Wi-Fi.
  • Puedes sincronizar la aplicación (o no) según criterios específicos, como la disponibilidad de la red, el intervalo de tiempo, etc.

Para escribir una app que admita el acceso a la red y administración del uso de la red, tu manifiesto debe tener los permisos correctos y filtros de intents.

  • El extracto de manifiesto que se muestra a continuación incluye los siguientes permisos:
  • Puedes declarar el filtro de intents para la acción ACTION_MANAGE_NETWORK_USAGE (que se incorporó en Android 4.0) a fin de indicar que tu aplicación define una actividad que ofrece opciones para controlar el uso de datos. ACTION_MANAGE_NETWORK_USAGE: Muestra la configuración para administrar el uso de datos de red de una aplicación específica. Cuando tu aplicación tiene una actividad de configuración que permite a los usuarios controlar el uso de la red, debes declarar este filtro de intents para esa actividad. En la aplicación de muestra, la clase SettingsActivity es la que maneja esta acción y muestra una IU de preferencias para permitir que los usuarios decidan cuándo descargar un feed.
    <?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>
    

Cómo implementar una actividad de preferencia

Como puedes ver en el extracto de manifiesto anterior, la actividad de la app de muestra SettingsActivity tiene un filtro de intents para la acción ACTION_MANAGE_NETWORK_USAGE. SettingsActivity es una subclase de PreferenceActivity. Muestra una pantalla de preferencias (aparece en la figura 1) que permite a los usuarios especificar lo siguiente:

  • Si desean mostrar resúmenes para cada entrada de feed XML o simplemente un vínculo para cada entrada.
  • Si quieren descargar el feed XML en caso de que haya alguna conexión de red disponible o solo si la conexión Wi-Fi está disponible.
Panel de preferencias Cómo configurar una preferencia de red

Figura 1: Actividad de preferencias

Aquí se encuentra SettingsActivity. Ten en cuenta que implementa OnSharedPreferenceChangeListener. Cuando un usuario cambia una preferencia, se activa onSharedPreferenceChanged(), lo que establece refreshDisplay en verdadero. Esto hace que se actualice la pantalla cuando el usuario vuelve a la actividad principal:

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

Cómo responder a los cambios de preferencias

Cuando el usuario cambia las preferencias en la pantalla de configuración, suele haber consecuencias en el comportamiento de la app. En este fragmento, la app revisa la configuración de preferencias en onStart(). Si hay una coincidencia entre la configuración y la conexión de red del dispositivo (por ejemplo, si la configuración es "Wi-Fi" y el dispositivo tiene una conexión Wi-Fi), la app descarga el feed y actualiza la pantalla.

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

    }
    

Cómo detectar cambios de conexión

La pieza final del rompecabezas es la subclase BroadcastReceiver, NetworkReceiver. Cuando la conexión de red del dispositivo cambia, NetworkReceiver intercepta la acción CONNECTIVITY_ACTION, determina cuál es el estado de la conexión de red y establece las marcas wifiConnected y mobileConnected en verdadero/falso en consecuencia. El resultado es que la próxima vez que el usuario regrese a la app, esta solo descargará el feed más reciente y actualizará la pantalla si NetworkActivity.refreshDisplay se establece en true.

Configurar un BroadcastReceiver al que se llama innecesariamente puede consumir los recursos del sistema. La aplicación de muestra registra BroadcastReceiver NetworkReceiver en onCreate() y cancela su registro en onDestroy(). Esto es más simple que declarar un elemento <receiver> en el manifiesto. Cuando declaras un elemento <receiver> en el manifiesto, puede activar tu app en cualquier momento, incluso si no la ejecutaste durante semanas. Si registras y anulas el registro de NetworkReceiver en la actividad principal, te aseguras de que no se active la app después de que el usuario salga de ella. Si declaras un elemento <receiver> en el manifiesto y sabes exactamente dónde lo necesitas, puedes usar setComponentEnabledSetting() para habilitarlo o inhabilitarlo según corresponda.

Aquí se encuentra 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();
            }
        }
    }