miércoles, 28 de agosto de 2013

Mi propio navegador web en Android

En uno de los artículos anteriores hicimos un navegador web con Python y PySide, así que en esta entrada vamos a hacer algo parecido con Android.

Primero lo primero, un nuevo  proyecto
Creamos un nuevo proyecto de Android, éste debe tener una actividad principal sobre la cual trabajaremos, en este caso el nombre de la actividad es Inicio.

Ahora, el diseño
En la carpeta layout abrimos el layout asociado a la actividad principal, en este caso su nombre es activity_inicio.xml

El layout debe tener un EditText para la url de  destino, y un WebView así:

Además de esto es necesario editar el archivo XML del menú, para poner los botones atrás y adelante.

Y final mente el código en Java:
public class Inicio extends Activity {

    ProgressDialog progressBar;
    private int progressBarStatus = 0;
    private Handler progressBarHandler = new Handler();
    EditText txtUrl = null;

    public final String HOME = "http://notoquesmicodigo.blogspot.com/";

    private class MiWebViewClient extends WebViewClient {

        private void iniciaCarga(WebView v) {
            if(progressBar==null){
                progressBar = new ProgressDialog(v.getContext());
            }
            if (progressBar != null && !progressBar.isShowing()) {
                progressBar.setTitle("Espere...");
                progressBar.setMessage("Cargando página...");
                progressBar.setProgressStyle(ProgressDialog.STYLE_SPINNER);
                progressBar.setCancelable(false);
                pbShow();
                progressBarStatus = 0;
                runOnUiThread(new Runnable() {

                    @Override
                    public void run() {
                    progressBarHandler.post(new Runnable() {
                    public void run() {
                        try {
                             progressBar
                              .setProgress(progressBarStatus++);
                        } catch (Exception e) {
                        }
                      }
                     });
                    }
                });
            }
        }

        private void pbCancel() {
            try {
                progressBar.cancel();
            } catch (Exception e) {
                Log.e("ERROR", e.getMessage());
            }
        }

        private void pbShow() {
            try {
                progressBar.show();
            } catch (Exception e) {
                Log.e("ERROR", e.getMessage());
            }
        }

        @Override
        public void onPageStarted(WebView view, String url, Bitmap favicon) {
            iniciaCarga(view);
            if (txtUrl != null) {
                txtUrl.setText(url);
            }
            super.onPageStarted(view, url, favicon);
        }

        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            pbCancel();
        }
    }

    private class MiChromeWebViewClient extends WebChromeClient {

    }

    WebView wv_pagina;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_inicio);
        wv_pagina = (WebView) findViewById(R.id.wv_pagina);
        txtUrl = (EditText) findViewById(R.id.txtUrl);
        wv_pagina.clearHistory();
        wv_pagina.clearCache(true);
        wv_pagina.setWebViewClient(new MiWebViewClient());
        wv_pagina.setWebChromeClient(new MiChromeWebViewClient());
        wv_pagina.getSettings().setJavaScriptEnabled(true);
        runOnUiThread(new Runnable() {

            @Override
            public void run() {
                wv_pagina.loadUrl(HOME);
            }
        });

    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_inicio, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (wv_pagina != null) {
            switch (item.getItemId()) {
            case R.id.menu_back:
                if (wv_pagina.canGoBack()) {
                    wv_pagina.goBack();
                }
                break;
            case R.id.menu_forward:
                if (wv_pagina.canGoForward()) {
                    wv_pagina.goForward();
                }
                break;
            }
            return true;
        } else {
            return super.onOptionsItemSelected(item);
        }
    }

}

Ahora bien, debemos tener en cuenta varias cosas:

  • Para que nuestro navegador no lance otro de los navegadores del celular (como Chrome por ejemplo) debemos crear clases propias que hereden de WebViewClient y WebChromeClient.
  • La mayor parte del código esta en la clase MiWebViewClient para el ProgressDialog que usamos.
  • En la clase MiWebViewClient sobrecargamos los métodos onPageStarted y onPageFinished para poder lanzar el ProgressDialog y cambiar la URL en la caja de texto de la dirección.
  • En el evento onOptionsItemSelected ponemos los eventos de atrás y adelante filtrando los elementos menu_back y menu_forward.
El resultado es éste:


El código esta disponible en GitHub, si tiene alguna duda pueden preguntar en los comentarios.

martes, 27 de agosto de 2013

Consoleando (III)

En artículos pasados he intentado mostrar un poco de las potencia de la consola, pero en realidad la potencia de la consola de los sistemas basados en Unix está en la posibilidad de tener muchísimos pequeños programas que se enlazan de manera mutua para hacer una labor compleja, por éste motivo existen las tuberías o Pipes.

Entre un Tubo...
El uso de tuberías o Pipes permite que el usuario ejecute un proceso y con la información de salida de éste alimente un nuevo proceso, de esta manera se pueden encadenar de manera sucesiva la cantidad de tareas que se desee. El operador encargado de las tuberías es la barra vertical ( | ), ésta es usada en otros lenguajes como el operador de bits OR, para el caso de bash ésta se usa como operador de tubería.

Y, ¿Cómo se usa?
Su uso es sencillo: Basta con separar los procesos con la barra vertical ( | ), lo cual hará que la salida del primer proceso ejecutado sea la entrada del segundo proceso, así:
ps -ef | grep root
En éste ejemplo podemos ver dos instrucciones:
La primera es ps con el argumento -ef, de ésta manera se hace una lista de todos los procesos existentes y muestra muchos de los datos relevantes del proceso, entre estos el comando con el que fue lanzado.
La segunda instrucción es grep con el argumento root, ésta instrucción filtra todas las líneas que contengan la palabra root, de manera que al unir ambas tareas mediante una tubería se mostrarán todos los procesos que contengan la palabra root, es decir, se filtrarán los procesos del usuario root y aquellos que en la línea de comandos tengan ésta palabra.

Un ejemplo más
¿Cómo cerrar un proceso de acuerdo al nombre?, esa es una de las tareas que podrían ser automatizadas. Por ejemplo, si hacemos una aplicación cliente-servidor que internamente usa la instrucción fork (ésta instrucción crea copias del proceso original) podríamos eliminar todas las instancias de la aplicación mediante el uso de las siguientes instrucciones:

  • ps : Lista todos los procesos de la máquina.
  • grep : Filtra las aplicaciones de acuerdo a un patrón especificado.
  • awk : Permite hacer operaciones sobre cadenas, como separarlas por palabras (tokenizarlas) y más.
  • xargs : Pasa los parametros que recibe mediante la tubería a una instrucción especificada.
  • kill : Termina una aplicación.
y las instrucciones se encadenarían así (en una sola línea):

ps -ef | grep NombreProceso | grep -v grep | awk '{print $2}' | xargs kill

La anterior secuencia de comandos y tuberías funciona así:

  1. ps -ef : Lista todos los procesos de la máquina.
  2. grep NombreProceso : Filtra todos los procesos que contengan la subcadena NombreProceso.
  3. grep -v grep : Quita las líneas que contengan la cadena grep (para que no se incluya el proceso del filtro anterior).
  4. awk '{print $2}' : Entrega únicamente el segundo token de cada línea, para éste caso es el número de identificación de cada proceso.
  5. xargs kill : Ejecuta el comando kill sobre cada uno de los números de proceso entregados.
Una de las grandes ventajas de la consola es que mediante el uso de tuberías se pueden automatizar procesos complejos de administración de máquinas y demás.

Espero que les sea de utilidad.

viernes, 2 de agosto de 2013

Muy rápido, muy fácil, reproductor de audio con Python (Ahora con video)

En el articulo pasado hicimos un reproductor de audio, este únicamente reproducía pistas de audio, en esta entrega vamos a hacer que mediante un modulo nuevo y unas cuantas lineas extra sea posible reproducir vídeo.

Qt es un framework que ofrece herramientas muy poderosas, una de estas es el modulo Phonon, el cual nos da la posibilidad de reproducir audio y vídeo, para este fin usaremos el widget VideoWidget, el cual viene incluido dentro del grupo de widgets de la librería Phonon. el VideoWidget será enlazado a un AudioObject  y a un MediaObject, este ultimo sera el encargado de cambiar el archivo fuente e iniciar la reproducción.
Todo lo anterior sera agrupado en una clase para intentar hacer esto un poco más modular y poder reusarlo de ser necesario.
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
from PySide.QtGui import QApplication
from PySide.phonon import Phonon
from PySide import QtCore, QtGui


class player(Phonon.VideoWidget):

    def __init__(self, file_path):
        super(player, self).__init__(parent=None)
        self.setWindowTitle("Video")
        QtGui.QApplication.addLibraryPath(
            os.path.join(os.path.dirname(__file__), 'plugins'))
        media_src = Phonon.MediaSource(file_path)
        self.file_path = file_path
        media_obj = Phonon.MediaObject()
        media_obj.setCurrentSource(media_src)
        Phonon.createPath(media_obj, self)
        self.media_obj = media_obj
        audio_out = Phonon.AudioOutput(Phonon.VideoCategory)
        Phonon.createPath(media_obj, audio_out)
        self.audio_out = audio_out
        self.media_obj.setTickInterval(1000)
        self.media_obj.tick.connect(self.alAvanzar)
        self.media_obj.stateChanged.connect(self.alCambiarEstado)

    def alAvanzar(self, tiempo):
        pass

    def alCambiarEstado(self, new, old):
        pass

    def play(self):
        self.media_obj.play()

    def pause(self):
        self.media_obj.pause()

    def stop(self):
        self.media_obj.stop()

    def state(self):
        return self.media_obj.state()

En el código anterior vemos como una la clase player hereda de la clase Phonon.VideoWidget y tiene como atributos un MediaObject y un AudioOutput, los cuales sirven como control de reproducción y salida de audio respectivamente, y los tres son enlazados en el constructor de la clase en la linea 21 y 24.

Ahora veremos la nueva versión del reproductor, con las mejoras respectivas para implementar nuestro player de vídeo:
from PySide import QtCore, QtGui, phonon, QtUiTools
from PySide.phonon import Phonon

from videopanel import player


class Reproductor(QtGui.QWidget):

    def __init__(self):
        super(Reproductor, self).__init__(parent=None)
        self.setWindowTitle("Python Simple Player")
        self.gridLayout = QtGui.QGridLayout(self)
        loader = QtUiTools.QUiLoader()
        file = QtCore.QFile("repro.ui")
        file.open(QtCore.QFile.ReadOnly)
        self.ui = loader.load(file, self)
        file.close()
        self.gridLayout.addWidget(self.ui, 0, 0, 1, 1)
        # seccion multimedia
        self.audioOutput = Phonon.AudioOutput(Phonon.MusicCategory, self)
        self.mediaObject = Phonon.MediaObject(self)
        Phonon.createPath(self.mediaObject, self.audioOutput)
        self.mediaObject.setTickInterval(1000)
        #
        self.ui.lcdAvance.display("00:00")
        QtCore.QObject.connect(
            self.ui.btnArchivo,
            QtCore.SIGNAL("clicked()"),
            self.openFile)
        QtCore.QObject.connect(
            self.ui.btnPlay,
            QtCore.SIGNAL("clicked()"),
            self.play)
        self.mediaObject.tick.connect(self.alAvanzar)
        self.mediaObject.stateChanged.connect(self.alCambiarEstado)
        #----------------------
        self.player = None

    def alAvanzar(self, tiempo):
        displayTime = QtCore.QTime(
            0, (tiempo / 60000) %
            60, (tiempo / 1000) %
            60)
        self.ui.lcdAvance.display(displayTime.toString('mm:ss'))

    def alCambiarEstado(self, new, old):
        if self.mediaObject.state() == Phonon.State.StoppedState:
            self.ui.btnPlay.setEnabled(True)
        elif self.mediaObject.state() == Phonon.State.PlayingState:
            self.ui.btnPlay.setText("||")  # .setEnabled(False)

    def play(self):
        if self.player:
            if self.player.state() == Phonon.State.PlayingState:
                self.player.pause()
                self.player.show()
                self.ui.btnPlay.setText(">")
            else:
                self.player.show()
                self.player.play()
                self.ui.lblNombre.setText(self.player.file_path)
                self.ui.btnPlay.setText("||")
        else:
            if self.mediaObject.state() == Phonon.State.PlayingState:
                self.mediaObject.pause()
                self.ui.btnPlay.setText(">")
            else:
                self.mediaObject.play()
                #///////////////////////////////////
                meta = self.mediaObject.metaData()
                print(meta)
                meta["TITLE"] = meta[
                    "TITLE"] if "TITLE" in meta else "Indefinido"
                meta["ARTIST"] = meta[
                    "ARTIST"] if "ARTIST" in meta else "SinNombre"
                label = "{TITLE} by {ARTIST}".format(**meta)
                self.ui.lblNombre.setText(label)
                #///////////////////////////////////
                self.ui.btnPlay.setText("||")

    def openFile(self):
        fileName = QtGui.QFileDialog.getOpenFileName(
            self,
            u"Abrir Archivo",
            u".",
            u"Archivos de Audio (*.mp3 *.wav *.ogg);;Archivos de Video (*.mp4 *.mpg *.avi)")
        if fileName[1]:
            self.path = fileName[0]
            self.mediaObject.setCurrentSource(
                Phonon.MediaSource(self.path))
            self.ui.btnPlay.setText(">")
            self.ui.lblNombre.setText("...")
            self.ui.lcdAvance.display("00:00")
            if fileName[0][-3:] in "mp4 mpg avi".split():
                self.player = player(fileName[0])
                self.player.alAvanzar = self.alAvanzar
                self.player.alCambiarEstado = self.alCambiarEstado
            else:
                self.player = None

if __name__ == "__main__":
    import sys
    app = QtGui.QApplication(sys.argv)
    rep = Reproductor()
    rep.show()
    sys.exit(app.exec_())

Ahora lo primero que vemos es una modificación en el constructor, en el cual creamos una variable de clase llamada player que es nula (linea 38). Luego vemos en el método play una condición en la cual si el player es diferente de nulo llama al objeto player para que este inicie la reproducción o la detenga. Finalmente en el método openFile vemos que se verifica la extensión del archivo a reproducir, si es un archivo de vídeo la variable de clase player deja de ser nula y se convierte en una instancia de la clase player que ya habíamos hecho antes, de lo contrario sigue siendo nula.

Espero que les guste y que les sirva, dejo el código en GitHub, si tienen sugerencias o demás comenten.

jueves, 1 de agosto de 2013

Entendiendo Javascript (VI)


Hasta el momento todo lo abarcado en los artículos ha permitido entender un poco mejor muchos de los pormenores de este interesante pero complejo lenguaje, pero muy poco se ha hecho por entender cómo modificar lo que el usuario utiliza, qué es en esencia lo que más necesita un usuario JavaScript, por esto en esta entrega vamos a empezar con la exploración de el objeto donde se almacena el contenido de la página, el objeto document.

Peleando con el documento
Ya habíamos hablado acerca de que en el momento en el que el navegador carga una página web toma todo el contenido HTML y lo convierte en objetos, estos objetos en realidad son nodos "manipulables", que a su vez contienen otros nodos. A esto se le llama document. Es muy importante resaltar que todo lo que hay en el document es un nodo, a continuación vamos a ver un ejemplo de esto.
<html>
    <head>
        <title>El Documento...</title>
    </head>
    <body>
        <h1>El Documento</h1>
        <div>Este es un ejemplo...</div>
    </body>
</html>

Si hiciéramos un esquema a manera de árbol de cómo se ve este HTML tendríamos:
De esta manera vemos como el nodo html es el padre inmediato de head y body, head es el padre de title y title tiene una propiedad llamada text, cuyo valor es "El documento...". body es el padre de h1 y div, los cuales a su vez tienen cada uno como propiedad text "El Documento" y "Ejemplo de documento..." respectivamente.

Muy bonito pero...
¿Para qué nos sirve entender esto?, la idea de entender esto es poder desplazarnos por el árbol sin depender únicamente de los identificadores, las etiquetas o los nombres.

Esta bien, y ¿Cómo me desplazo?
Esa es la parte divertida, porque para desplazarnos el objeto document nos ofrece unas herramientas, a continuación vamos a ver algunas de estas:
Orden Descripción
document.getElementById(valor) Retorna el elemento que en el atributo id tenga el valor especificado.
document.getElementsByName(nombre) Retorna una lista de todos los elementos que tengan el nombre especificado.
document.getElementsByTagName(tag) Retorna una lista de todos los elementos hechos con con el tag especificado

Y Para Terminar...
Vamos a hacer un ejemplo de una función que busca el título de nuestra página y lo cambia, otra que cambia el contenido de un div y su color de fondo y cada una va a ser asociada a un botón. Después del código esta el ejemplo.

<html>
    <head>
        <title>El Documento...</title>
        <script>
             function cambiarTituloPor(nuevoTitulo){
                document.getElementsByTagName("title")[0].innerHTML=nuevoTitulo;
             }
             function cambiarContenidoPor(nuevoContenido){
                var elemento = document.getElementById("modificable");
                elemento.innerHTML=nuevoContenido;
                var estilo = document.createAttribute("style");
                estilo.value = "background-color:#1662A8;";
                elemento.setAttributeNode(estilo);
             }
        </script>
    </head>
    <body>
        <h1>El Documento</h1>
        <div id="modificable">Este es un ejemplo...</div>
        <button onclick="cambiarTituloPor(prompt('Nuevo Título','Hola Mundo!!!'))">Cambiar Título</button>
        <button onclick="cambiarContenidoPor(prompt('Nuevo Contenido','Estoy aprendiendo JavaScript...'))">Cambiar Div</button>
    </body>
</html>

Este es un ejemplo...

martes, 30 de julio de 2013

Muy rápido, muy fácil, reproductor de audio con Python


En la entrada anterior pudimos ver cómo con muy poco código fue posible obtener un simple pero poderoso navegador web con el lenguaje de programación Python, PySide (Los bindings de Qt, uno de los  frameworks gráficos del lenguaje C++) y poco más de 70 líneas de código. En esta entrega veremos cómo usando el diseñador de interfaces y algo de código podemos hacer un muy simple reproductor de audio, el cual será extendido en próximas entregas.
Primero que todo es necesario decir que Qt ofrece una herramienta para diseñar interfaces de usuario. Esta se llama QtDesigner y viene incluida en el instalador de la libreria PySide de windows (para los entornos linux hay que instalarlo a mano). Esta herramienta es muy útil, puesto que con ella podemos hacer la interfaz y luego exportarla al lenguaje que vayamos a usar para realizar la aplicación (en la actualidad Qt tiene bindings para varios lenguajes, entre ellos Java, Ruby, Python y Php, entre otros), además de proveer un sistema de enlace entre objetos y métodos mediante eventos (en Qt se habla de slots y signals), lo cual facilita en cierta manera la tarea del programador. Esta herramienta es una herramienta WYSIWYG, la cual nos permite arrastrar, soltar y acomodar hasta obtener la interfaz como se desea. A continuación la imagen de la interfaz gráfica del reproductor:


La interfaz gráfica contiene unos cuantos QLabel para mostrar información, un QLCDNumber para mostrar cuánto tiempo ha transcurrido, un par de botones y unos espaciadores para conservar las proporciones al cambiar el tamaño de la ventana. A pesar de que sería posible "compilar" la interfaz a el lenguaje nativo con herramientas como el pyside-uic no lo vamos a hacer.

Ahora vamos a ver el código Python:

#!/usr/bin/env python

from PySide import QtCore, QtGui, phonon, QtUiTools
from PySide.phonon import Phonon

class Reproductor(QtGui.QWidget):

    def __init__(self):
        super(Reproductor, self).__init__(parent=None)
        self.setWindowTitle("Python Simple Player")
        self.gridLayout = QtGui.QGridLayout(self)
        loader = QtUiTools.QUiLoader()
        file = QtCore.QFile("repro.ui")
        file.open(QtCore.QFile.ReadOnly)
        self.ui = loader.load(file, self)
        file.close()
        self.gridLayout.addWidget(self.ui, 0, 0, 1, 1)
        # seccion multimedia
        self.audioOutput = Phonon.AudioOutput(Phonon.MusicCategory, self)
        self.mediaObject = Phonon.MediaObject(self)
        Phonon.createPath(self.mediaObject, self.audioOutput)
        self.mediaObject.setTickInterval(1000)
        #
        self.ui.lcdAvance.display("00:00")
        QtCore.QObject.connect(
            self.ui.btnArchivo,
            QtCore.SIGNAL("clicked()"),
            self.openFile)
        QtCore.QObject.connect(
            self.ui.btnPlay,
            QtCore.SIGNAL("clicked()"),
            self.play)
        self.mediaObject.tick.connect(self.alAvanzar)
        self.mediaObject.stateChanged.connect(self.alCambiarEstado)

    def alAvanzar(self, tiempo):
        displayTime = QtCore.QTime(
            0, (tiempo / 60000) %
            60, (tiempo / 1000) %
            60)
        self.ui.lcdAvance.display(displayTime.toString('mm:ss'))

    def alCambiarEstado(self, new, old):
        if self.mediaObject.state() == Phonon.State.StoppedState:
            self.ui.btnPlay.setEnabled(True)
        elif self.mediaObject.state() == Phonon.State.PlayingState:
            self.ui.btnPlay.setText("||")  # .setEnabled(False)

    def play(self):
        if self.mediaObject.state() == Phonon.State.PlayingState:
            self.mediaObject.pause()
            self.ui.btnPlay.setText(">")
        else:
            self.mediaObject.play()
            #///////////////////////////////////
            meta = self.mediaObject.metaData()
            meta["TITLE"] = meta["TITLE"] if "TITLE" in meta else "Indefinido"
            meta["ARTIST"] = meta[
                "ARTIST"] if "ARTIST" in meta else "SinNombre"
            label = "{TITLE} by {ARTIST}".format(**meta)
            self.ui.lblNombre.setText(label)
            #///////////////////////////////////
            self.ui.btnPlay.setText("||")

    def openFile(self):
        fileName = QtGui.QFileDialog.getOpenFileName(
            self,
            u"Abrir Archivo",
            u"/",
            u"Archivos de Audio (*.mp3 *.wav *.ogg)")
        if fileName[1]:
            self.path = fileName[0]
            self.mediaObject.setCurrentSource(
                Phonon.MediaSource(self.path))
            self.ui.btnPlay.setText(">")
            self.ui.lblNombre.setText("...")
            self.ui.lcdAvance.display("00:00")

if __name__ == "__main__":
    import sys
    app = QtGui.QApplication(sys.argv)
    rep = Reproductor()
    rep.show()
    sys.exit(app.exec_())

En el código anterior podemos ver cómo mediante el uso del modulo QtUiTools es posible usar la clase QUiLoader para cargar en tiempo de ejecución el archivo de interfaz gráfica (repro.ui), y obtenemos un objeto QWidget, el cual contiene toda la jerarquía de objetos que habíamos hecho en el diseñador de interfaces, los cuales quedan listos para ser usados a gusto. Después de esto acoplamos el QWidget obtenido mediante un gridLayout. Seguidamente creamos las instancias de la salida de audio (audioOutput) y del reproductor (mediaObject), los cuales son enlazados mediante la instrucción:
Phonon.createPath(self.mediaObject, self.audioOutput)
Esta instrucción enlaza el reproductor con la salida de audio. Después de esto se establece cada cuánto se va a lanzar una señal de actualización (es para que pueda medirse el tiempo transcurrido y hacer cosas de acuerdo a este) mediante el tickInterval, el cual se establece cada 1000 milisegundos (un segundo) y se conectan objetos como botones a los eventos correspondientes para que antes de finalizar el constructor se enlacen las señales del reproductor a los métodos encargados de modificar el avance y cambiar la etiqueta del botón de reproducción.
Los métodos siguientes se encargan de cambiar el valor de las etiquetas, de iniciar o detener la reproducción o cambiar el título de la pista.

 El código correspondiente esta en GitHub para quienes estén interesados.