Cómo administrar el uso de red

En esta lección, se describe la forma de desarrollar aplicaciones que controlen minuciosamente el uso de recursos de red. Si tu aplicación realiza muchas operaciones de red, debes proporcionar una configuración que permita a los usuarios controlar los hábitos de datos de tu app, como la frecuencia con la que sincroniza los datos, si realiza cargas/descargas solo cuando hay una conexión Wi-Fi, si utiliza los datos mientras está en roaming, 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 cuando se acercan a sus límites, ya que pueden controlar precisamente la cantidad de datos que usa tu app.

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

También puedes consultar el ejemplo de Conexión de red.

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

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

Por lo general, la conexión Wi-Fi es más rápida. Además, los datos móviles suelen medirse según el uso, lo que puede resultar costoso. Una estrategia común para las apps es descargar grandes cantidades de datos solo si hay una red Wi-Fi disponible.

Antes de realizar operaciones de red, se recomienda comprobar el estado de la conectividad. Entre otros aspectos, esto podría evitar que tu app utilice inadvertidamente la radio equivocada. Si una conexión de red no está disponible, tu app debería responder de la mejor forma posible. Para verificar la conexión de red, normalmente se utilizan las siguientes clases:

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

Este fragmento de código prueba la conectividad de la red para Wi-Fi y datos móviles. Determina si estas interfaces de red están disponibles (es decir, si existe conectividad de red) o conectadas (es decir, si existe conectividad de red y si es posible establecer sockets y transferir 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 tus decisiones en si una red está "disponible". Siempre debes verificar isConnected() antes de realizar operaciones de red, ya que isConnected() se encarga de aspectos como redes móviles inestables, el modo de avión y datos restringidos en segundo plano.

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 que significa que una conexión a Internet no está 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 con mayor profundidad, puedes utilizar NetworkInfo.DetailedState, pero casi nunca es necesario.

Cómo administrar el uso de red

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

  • Los usuarios solo pueden cargar videos cuando el dispositivo está conectado a una red Wi-Fi.
  • La sincronización depende de criterios específicos, como la disponibilidad de la red, el intervalo de tiempo, etc.

Para diseñar una app que admita el acceso a la red y administre el uso de esta, su manifiesto debe incluir los permisos y filtros de intent correctos.

  • El manifiesto extraído a continuación incluye los siguientes permisos:
  • Puedes declarar el filtro de intent de la acción ACTION_MANAGE_NETWORK_USAGE (introducido en Android 4.0) para indicar que tu app define una actividad que ofrece opciones para controlar el uso de datos. ACTION_MANAGE_NETWORK_USAGE muestra la configuración para gestionar el uso de datos de red de una aplicación específica. Cuando tu app tiene una actividad de configuración que permite a los usuarios controlar el uso de la red, debes declarar este filtro de intent para esa actividad. En la aplicación de ejemplo, esta acción es manejada por SettingsActivity, que muestra una IU de preferencias para que los usuarios puedan decidir 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 del manifiesto anterior, el SettingsActivity de la actividad de ejemplo tiene un filtro de intent para la acción ACTION_MANAGE_NETWORK_USAGE. SettingsActivity es una subclase de PreferenceActivity. Muestra una pantalla de preferencias (en la figura 1) que permite a los usuarios especificar lo siguiente:

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

Figura 1. Actividad de preferencia.

Aquí está SettingsActivity. Ten en cuenta que implementa OnSharedPreferenceChangeListener. Cuando un usuario cambia una preferencia, se activa onSharedPreferenceChanged(), que establece refreshDisplay como verdadero. Esto hace que la pantalla se actualice 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, esto normalmente tiene 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 de BroadcastReceiver, NetworkReceiver. Cuando cambia la conexión de red del dispositivo, NetworkReceiver intercepta la acción CONNECTIVITY_ACTION, determina el estado de la conexión de red y establece los indicadores wifiConnected y mobileConnected en verdadero o falso. Como resultado, la próxima vez que el usuario vuelva a la aplicación, esta solo descargará el último feed y actualizará la pantalla si NetworkActivity.refreshDisplay está configurado como true.

La configuración de un BroadcastReceiver al que se invoca innecesariamente puede suponer una pérdida de recursos del sistema. La aplicación de ejemplo registra el NetworkReceiver de BroadcastReceiver en onCreate(), y cancela el registro en onDestroy(). Esto es más sencillo que declarar un <receiver> en el manifiesto. Si declaras un <receiver> en el manifiesto, podrías activar tu aplicación en cualquier momento, incluso si no se la ejecutó en semanas. Al registrar NetworkReceiver y cancelar su registro en la actividad principal, te aseguras de que la aplicación no se active después de que el usuario la abandone. Si declaras un <receiver> en el manifiesto y sabes exactamente dónde lo necesitas, puedes utilizar setComponentEnabledSetting() para habilitarlo e inhabilitarlo según corresponda.

Aquí está 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();
        }
    }
}