Forzar que una petición HTTP se envíe por la WiFi seleccionada

En uno de los proyectos en los que trabajo, nos conectamos a un pequeño dispositivo para leer / escribir datos en el.

La primera vez que se arrranca el dispositivo, este lo hace en un modo “Access Point” que crea una WiFi virtual, desde la que nos podemos conectar al dispositivo directamente para configurarle la red WiFi a la que se tiene que conectar. Este punto de acceso, evidentemente no da salida a Internet.

En Android Marshmallow, cuando el dispositivo está en punto de acceso y, al no ofrecer una conexión que el sistema Android considere de “calidad”, este decide enviar las peticiones siempre por la red 4g/3g. Haciendo imposible la configuración del dispositivo desde esta versión del sistema.

El siguiente código inicializa OkHttpClient para que siempre envie la petición por la primera wifi que tiene configurada (la que tienes seleccionada) en lugar de la que ofrezca más calidad.

Pongo aquí el código por si a alguien le sirviese de utilidad. Básicamente consiste en sobreescribir la clase OkHttpClient añadiéndole un interceptor.

public class DeviceHttpClient extends OkHttpClient {

    private static final String TAG = "DeviceHttpClient";

    public DeviceHttpClient(final Context context) {

        // Configure Timeouts
        setConnectTimeout(Constants.CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);

        try {
            // Configure Retry policy and Prioritize Wifi
            interceptors().add(
            new Interceptor() {
                   @Override
                   public Response intercept(Chain chain) throws IOException {
                       try {
                           boundRequestToWifi(context);
                           Request request = chain.request();
                           return chain.proceed(request);
                       } finally {
                           unBoundRequestToWifi(context);
                       }
                   }
               }
            );
        } catch (Exception e) {
            Log.e(TAG, "Error while initializing http client", e);
        }
    }

    @SuppressWarnings("deprecation")
    private void boundRequestToWifi(Context context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);

            Network[] networks = connectivityManager.getAllNetworks();
            if (networks != null && networks.length > 0) {
                for (Network network : networks) {
                    NetworkInfo networkInfo = connectivityManager.getNetworkInfo(network);

                    if (networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                            connectivityManager.bindProcessToNetwork(network);
                        }
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
                            Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
                            ConnectivityManager.setProcessDefaultNetwork(network);
                        }
                        break;
                    }
                }
            }
        }
    }

    @SuppressWarnings("deprecation")
    private void unBoundRequestToWifi(Context context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                connectivityManager.bindProcessToNetwork(null);
            }
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
                Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
                ConnectivityManager.setProcessDefaultNetwork(null);
            }
        }
    }
}

Evitar warning Nullable en findViewById (Android 23)

Si utilizas appcompat 23.3.0 han añadido la anotación @Nullable al método findViewById en AppCompatActivity y también en el framework base. Lo cual es muy molesto, ya que provoca que el código de las actividades aparezca lleno de warnings.

Mientras llega la solución utilizo esta “chapuzilla”:

public class BaseActivity extends AppCompatActivity {
    @NonNull
    @Override
    public View findViewById(@IdRes int id) {
        //noinspection ConstantConditions
        return super.findViewById(id);
    }
}

Ejecutar código cuando se descarta una SnackBar

En el ejemplo mostramos una snackbar con una acción para reintentar y queremos finalizar una actividad solo en el caso de que el usuario descarte la snackbar (por ejemplo haciendo swipe). Nunca cuando pulse la acción para reintentar.

Snackbar.make(coordinator, message, Snackbar.LENGTH_INDEFINITE)
        .setAction(R.string.retry, new View.OnClickListener() {

            @Override
            public void onClick(View view) {
                // Retry operation
            }
        })
        .setCallback(new Snackbar.Callback() {
            @Override
            public void onDismissed(Snackbar snackbar, int event) {
                super.onDismissed(snackbar, event);
                if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {
                    // Close activity, only if snackbar was dismissed not pressing action button
                    finish();
                }
            }
        })
        .show();

En el método “onDismissed” podemos recuperar el evento generado y comprobar si es uno de los que nos interesa.

JDK 8, Android Studio en Ubuntu

Aprovechando que ha salido Ubuntu 16.04 LTS, he actualizado mi máquina linux. Dejo aquí los pasos para montar el entorno de desarrollo de Android.

Crear directorios de instalación

Como este equipo lo voy a usar yo solo por el momento cambio el propietario a mi usuario para facilitar las cosas (tengo que ver aún como gestionar esto).

    sudo mkdir -p /opt/repository/sdk
    sudo mkdir -p /opt/repository/tools
    sudo chown -R edu: /opt/repository

Oracle JDK 8

Descargar Oracle JDK 8 de la página de descargas oficial de descargas.

    cp ~/Descargas/jdk-8u91-linux-x64.tar.gz /opt/repository/sdk
    cd /opt/repository/sdk
    tar xvzf jdk-8u91-linux-x64.tar.gz
    ln -s /opt/repository/sdk/jdk-8u91-linux-x64.tar.gz /opt/repository/java

Variables de entorno

Definir las variables de entorno a nivel de sistema. Para ello crearemos un nuevo fichero dev.sh en /etc/profile.d donde almacenaremos nuestra configuración:

    sudo vi /etc/profile.d/dev.sh

El contenido del fichero será:

    export REPOSITORY=/opt/repository
    export JAVA_HOME=$REPOSITORY/sdk/java
    export ANDROID_HOME=$REPOSITORY/sdk/android
    export PATH=$PATH:$JAVA_HOME/bin:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools

Este es un buen lugar para definir las variables de entorno y que estás tengan efecto en todo el sistema.

Una vez hecho esto, reiniciar el sistema para que los cambios surjan efecto.

Android Studio

Descargar Android Studio.

    cp ~/Descargas/android-studio-ide-143.2739321-linux.zip $REPOSITORY/tools
    cd $REPOSITORY/tools
    unzip android-studio-ide-143.2739321-linux.zip

Ejecutaremos Android Studio, desde la línea de comandos:

    $REPOSITORY/tools/android-studio/bin/studio.sh

Durante la primera ejecución nos solicitará en qué ruta queremos ubicar los SDK de android. Seleccionaremos:

    /opt/repository/sdk/android

Instalaremos los SDKs que consideremos y cerraremos AS.

Añadir Android Studio al menú del sistema

Para poder ejecutar Android Studio cómodamente desde el lanzador / menú del sistema, crearemos el fichero:

    vi ~/.local/share/applications/android-studio.desktop

Con el siguiente contenido:

    [Desktop Entry]
    Version=1.0
    Type=Application
    Name=Android Studio
    Comment=IDE Android
    Icon=/opt/repository/tools/android-studio/bin/studio.png
    Exec=/opt/repository/tools/android-studio/bin/studio.sh
    NoDisplay=false
    Categories=Development;
    StartupNotify=false
    Terminal=false

También es posible añadir esta entrada utilizando un frontend gráfico para editar los menús como MenuLibre.

Configuración Checkstyle Google Checks

Instalar plugin “CheckStyle-IDEA”

  • En Android Studio o IntelliJ abrir la opción de menú “File” -> “Settings”.
  • Allí, localizar el apartado “plugins”.
  • Buscar e instalar el plugin “CheckStyle-IDEA”.
  • Reiniciar el IDE cuando lo solicite.

Configurar Google Checks

  • Volver a abrir la vista de “Settings”.
  • Allí, localizar el apartado “Other Settings -> Checkstyle”.
  • Añadir un nuevo fichero de configuración haciendo click en el botón con el símbolo “+” correspondiente.
  • Seleccionar el checkbox correspondiente para activar la configuración.

Captura de pantalla de 2016-04-09 21-00-04

Finalmente, el fichero se puede localizar on-line en las direcciones.

Fichero para la última versión de checkstyle (no recomendable instalar ya que puede ser que tengas una versión anterior)

https://raw.githubusercontent.com/checkstyle/checkstyle/master/src/main/resources/google_checks.xml

Fichero para una versión en concreto de checkstyle (6.17)

https://github.com/checkstyle/checkstyle/blob/checkstyle-6.17/src/main/resources/google_checks.xml

Las diferentes versiones se pueden encontrar marcadas con tags en el repositorio de Checkstyle.

Estilos para las tipografías de Material Design

Para utilizar los estilos de texto definidos en la especificación Material Design existen los siguiente estilos en la librería v7 appcompat:

@style/TextAppearance.AppCompat.Display4
@style/TextAppearance.AppCompat.Display3
@style/TextAppearance.AppCompat.Display2
@style/TextAppearance.AppCompat.Display1
@style/TextAppearance.AppCompat.Display4
@style/TextAppearance.AppCompat.Headline
@style/TextAppearance.AppCompat.Title
@style/TextAppearance.AppCompat.Subhead
@style/TextAppearance.AppCompat.Body2
@style/TextAppearance.AppCompat.Body1
@style/TextAppearance.AppCompat.Caption

Para usarlos especificad la propiedad android:textAppearance:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="@style/TextAppearance.AppCompat.Title" />