sabato 27 luglio 2013

Android: backup e restore di un database sqlite

Per l'applicazione Mie Ricette mi sono trovato a risolvere un problema: dare la possibilità all'utente di eseguire un backup del database delle ricette.
Il database usato nello sviluppo Android è Sqlite, quindi è essenzialmente un file che può essere copiato in una posizione "sicura" come la scheda SD, o comunque un percorso accessibile collegando il telefono al computer.
Nello script quindi sono presenti due funzioni, backupDatabase e restoreDatabase. Il primo l'ho trovato in qualche blog (perdonatemi, non mi ricordo quale). Entrambi utilizzano i FileChannel, ma mentre il cuore del funzionamento del primo si ha nella funzione nativa FileChannel.transferFrom, nel secondo specularmente si ha in FileChannel.transferTo.

package net.stefanobianchini.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import net.stefanobianchini.ricette.R;

import android.app.Activity;
import android.content.res.Resources;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;

public class backupdb {

 public static void backupDatabase(String dbPath, String nomeFileBackup,
   Activity act, Resources res) {

  try {
    File currentDB = new File(dbPath);// path del db su telefono
    File backupDB = new File(sd, nomeFileBackup);// file di destinazione
    @SuppressWarnings("resource")
    FileChannel src = new FileInputStream(currentDB)
       .getChannel();// apriamo un filechannel sul db e sul
           // file di destinazione
    @SuppressWarnings("resource")
    FileChannel dst = new FileOutputStream(backupDB)
       .getChannel();
    dst.transferFrom(src, 0, src.size());// trasferiamo il contenuto
    src.close();
     dst.close();
     Toast.makeText(
       act.getBaseContext(),

         res.getString(R.string.msg_db_location) + "\n " +
         backupDB.getAbsolutePath(),
       Toast.LENGTH_LONG).show();

  } catch (IOException e) {
   Toast.makeText(act.getBaseContext(), e.toString(),
     Toast.LENGTH_LONG).show();
  }
 }

 @SuppressWarnings("resource")
 private static void copyFile(File src, File dst) throws IOException {
  FileChannel inChannel = new FileInputStream(src).getChannel();
  FileChannel outChannel = new FileOutputStream(dst).getChannel();
  try {
   inChannel.transferTo(0, inChannel.size(), outChannel);
  } finally {
   if (inChannel != null)
    inChannel.close();
   if (outChannel != null)
    outChannel.close();
  }
 }

 public static void restoreDatabase(String dbPath, String percorsoFileBackup,
   Activity act, Resources res) {

  try {
   File currentDB = new File(dbPath);// path del db su telefono
   File backupDB = new File(percorsoFileBackup);// file di backup sorgente

   copyFile(backupDB, currentDB);

   Toast.makeText(
     act.getBaseContext(),
     String.format(res.getString(R.string.restore_done),
       backupDB.toString()), Toast.LENGTH_LONG).show();

  } catch (IOException e) {
   Toast.makeText(act.getBaseContext(), e.toString(),
     Toast.LENGTH_LONG).show();
  }
 }

}
Ora siamo pronti per eseguire il backup. Da notare che prima di eseguire il backup viene chiuso per sicurezza il db mediante il SQLiteOpenHelper (nel mio caso chiamato db_help);
//Backup del DB sqlite
db_help.close();
backupdb.backupDatabase(db_help.getReadableDatabase().getPath(), "backup_mie_ricette.db", HomeActivity.this, res);

venerdì 26 luglio 2013

C# Reflection, ovvero come richiamare librerie DLL a runtime


Per lavoro ultimamente mi son trovato a sviluppare un programma per la vendita al dettaglio di cosmesi. Le tecnologie scelte sono state C# (serviva un qualcosa solo su sistema Windows a finestre) e MySql (varie postazioni, sistema distribuito).
La sfida si è presentata quando ho dovuto progettare un sistema dinamico per gestire i registratori di cassa. Questo perché i registratori di cassa sono principalmente di due tipi, "Sweda" e "Ditron" con connessioni diverse e soprattutto comandi diversi. Lo Sweda ha una seriale rs232 mentre il Ditron è sempre collegato in seriale ma si appoggia su un programmino demone che fa il lavoro di comunicazione leggendo un determinato file (invio.txt) in una determinata cartella.
Per riassumere: per uno il C# deve aprire una connessione seriale, per l'altro creare un file. Entrambi hanno una sintassi completamente diversa.
Come fare per ottenere una progettazione "dinamica" che renda facile una successiva espansione, ad esempio l'inserimento di un altro tipo di registratore di cassa con un suo interfacciamento?

La soluzione intrapresa si basa sulla "Reflection", ossia sulla capacità del C# di caricare una DLL esterna a runtime (e gli oggetti e metodi in essa contenuti, quindi senza che sia per forza inclusa nel progetto), tenendo così separati il progetto della vendita al dettaglio dai singoli progetti C# delle librerie di interfacciamento ai registratori di cassa.
In questo modo, lo sviluppo delle suddette librerie dei registratori di cassa può essere anche lasciato ad un altra persona (o addirittura ditta).
La soluzione è quindi scalabile e si basa sul creare librerie con un nome differente (es. sweda.dll e ditron.dll) e comportamento differente ma con all'interno un oggetto chiamato RegistratoreFiscale, le cui funzioni pubbliche sono identiche in entrambe le librerie.
Vediamo un esempio di scheletro di libreria:

using System;
using System.Collections.Generic;
using System.Text;
using System.IO.Ports;
using System.Windows.Forms;

namespace Sweda
{
    public static class RegistratoreFiscale
    {
        public static SerialPort serialPort = null;
        public static void inizializza(string opzioni_di_inizializzazione) {}
        public static void aggiungiVoceAcquisto(double prezzo, string reparto, string descrizione, double prezzo_originale) {}
        public static void scegliOperatore(string operatore) {}
        public static void chiusuraScontrino(double totale) {}
     }
}
A questo punto la libreria Ditron sarà esattamente identica (o altre librerie in futuro che gestiranno altri registratori), quello che cambia è ovviamente l'implementazione dei metodi pubblici sopracitati.

Quello che si deve predisporre ora nel progetto principale (quello di vendita al dettaglio) è un oggetto "standard" che riceverà le chiamate ai metodi e si preoccuperà di richiamare questi metodi sulla libreria opportuna (sweda per i pc che hanno uno sweda collegato, ditron per i pc che hanno un ditron collegato, ecc.). Fondamentale la direttiva using System.Reflection per usare la caratteristica chiave di questa soluzione progettuale:

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;

namespace CosmeticSeller.oggetti
{
    class RegistratoreFiscale
    {
        private Type oggettoRegistratoreRuntime;

        public RegistratoreFiscale(string tipo_registratore)
        {
            Assembly SampleAssembly;
            SampleAssembly = Assembly.LoadFrom(tipo_registratore + ".dll");
            AppDomain.CurrentDomain.Load(SampleAssembly.GetName());
            foreach (Type t in SampleAssembly.GetTypes())
            {
                if (t.Name.Equals("RegistratoreFiscale"))
                {
                    oggettoRegistratoreRuntime = t;
                }
            }
        }

        public void aggiungiVoceAcquisto(double prezzo, string reparto, string descrizione, double prezzo_originale)
        {
            MethodInfo Method = oggettoRegistratoreRuntime.GetMethod("aggiungiVoceAcquisto");

            List>object< listaParametri = new List>object<();
            listaParametri.Add(prezzo);
            listaParametri.Add(reparto);
            listaParametri.Add(descrizione);
            listaParametri.Add(prezzo_originale);
            Method.Invoke(this, listaParametri.ToArray());
        }

        public void inizializza(string opzioni_di_inizializzazione)
        {
            MethodInfo Method = oggettoRegistratoreRuntime.GetMethod("inizializza");
            List<object> listaParametri = new List<object>();
            listaParametri.Add(opzioni_di_inizializzazione);
            Method.Invoke(this, listaParametri.ToArray());
        }

        public void chiusuraScontrino(double totale)
        {
            MethodInfo Method = oggettoRegistratoreRuntime.GetMethod("chiusuraScontrino");

            List<object> listaParametri = new List<object>();
            listaParametri.Add(totale);
            Method.Invoke(this, listaParametri.ToArray());
        }

        public void scegliOperatore(string operatore)
        {
            MethodInfo Method = oggettoRegistratoreRuntime.GetMethod("scegliOperatore");

            List<object> listaParametri = new List<object>();
            listaParametri.Add(operatore);
            Method.Invoke(this, listaParametri.ToArray());
        }
    }
}
Volendo descrivere questa classe in due parole, il suo compito è essere un wrapper tra il programma di vendita  e la libreria da utilizzare. Il cuore del funzionamento è nel costruttore, dove viene caricata dinamicamente la dll a partire dal nome passato come parametro. Nella libreria dll viene quindi cercato l'oggetto RegistratoreFiscale (implementato in tutte le librerie) e caricato in memoria come oggetto Type (oggettoRegistratoreRuntime).
L'oggetto deve essere di tipo Type poiché il compilatore non è in grado di capire che oggetto sia (sappiamo però quali metodi abbiamo implementato e possiamo richiamarli).
Nelle varie funzioni infatti, le quali mappano le implementazioni nelle varie librerie, viene eseguita l'invocazione del metodo desiderato a runtime (Method.Invoke). Durante l'invocazione è possibile passare anche i parametri al metodo desiderato (sotto forma di array di object).

A questo punto è possibile dal programma principale evocare il wrapper a seconda del registratore fiscale utilizzato (questa informazione nel mio caso è salvata su una tabella "impostazioni" su DB).
...
RegistratoreFiscale reg = new RegistratoreFiscale("Sweda");
reg.inizializza("COM1");
for( ... ) { //Ciclo tutti gli acquisti
    reg.aggiungiVoceAcquisto(prezzoComplessivo, reparto, descrizione, totaleriga);
}
reg.chiusuraScontrino(importo_totale);
...