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