Android Persistenza dei dati Corso di programmazione di sistemi mobile 1 Gestione dei file La maggior parte delle applicazioni Android ha bisogno di salvare i dati, anche solo per salvare lo stato dell'applicazione in modo che i dati dell'utente non vadano persi. I principali tipi di memorizzazione presenti sono: •Database SQLite: possibilità di salvare i dati in un database • SharedPreferences: per salvare una coppia chiave-valore su un file xml generato dal sistema • Internal storage: spazio interno che risiede in una parte del filesystem a cui solo la nostra l’applicazione può accedere. • External storage: spazio esterno spesso si ci riferisci a SD card esterne o alla porzione di disco accessibile da tutte le applicazioni. Corso di programmazione di sistemi mobile 2 Internal Storage Ogni applicazione dispone di un’area protetta ed esclusiva all’interno della quale può effettuare una qualsiasi operazione senza arrecare inconvenienti al sistema o alle altre applicazioni installate. La classe Activity dispone dei metodi utili per accedere alla porzione di file system assegnata e ottenere il riferimento ad uno stream in lettura o scrittura: public FileInputStream openFileInput(String name); Se il file non è presente all’interno della memoria dell’applicazione viene lanciata un eccezzione. public FileOutputStream openFileOutput(String name, int mode); Restituisce un oggetto che può essere manipolato come un qualsiasi output stream di Java. Il parametro mode indica il tipo di scrittura del file. (si possono usare combinazioni tramite l’operatore binario OR) Context.MODE_PRIVATE Rende il file privato e accessibile solo alla nostra applicazione Context.MODE_APPEND Se il file esiste invece di sovrascriverlo gli accoda i nuovi byte che saranno scritti nello stream Context.MODE_WORLD_READABLE Rende il file accessibile in sola lettura dalle altre applicazioni installate nel sistema Context.MODE_WORLD_WRITEABLE Rende il file accessibile in sola scrittura dalle altre applicazioni installate nel sistema. Corso di programmazione di sistemi mobile 3 External Storage I dispositivi Android dispongono di un secondo spazio di memoria, definito "External storage". Solitamente è una porzione di disco oppure una scheda di memoria che può all’occorrenza essere rimossa e sostituita. Quando si vuole scrivere sulla memoria esterna bisogna accertarsi che essa sia disponibile, il metodo per verificare lo stato è contenuto staticamente nella classe android.os.Environment: public static String getExternalStorageState(); Environment.MEDIA_MOUNTED È possibile scrivere e leggere sulla memoria esterna Environment.MEDIA_MOUNTED_READ_ONLY È possibile solo leggere la memoria esterna Environment.MEDIA_UNMOUNTED Environment.MEDIA_UNMOUNTABLE Environment.MEDIA_BAD_REMOVAL Environment.MEDIA_CHECKING Environment.MEDIA_NOFS Environment.MEDIA_REMOVED Environment.MEDIA_SHARED Uno qualsiasi di questi valori indica che la memoria esterna non è accessibile e non si possono eseguire operazioni di I/O Corso di programmazione di sistemi mobile 4 Una volta accertati che sia possibile accedere alla memoria esterna, è possibile recupere il percorso sempre attraverso il metodo statico di Environment: public static File getExternalStorageDirectory(); Il file restituito è la radice della memoria esterna, è anche possibile recuperare una delle cartelle pubbliche attraverso il metodo: public static File getExternalStoragePublicDirectory(String type); Environment.DIRECTORY_ALARMS Directory in cui collocare tutti i file audio da utilizzare come allarmi Environment.DIRECTORY_DCIM Directory per le foto e i video Environment.DIRECTORY_DOCUMENTS Directory in cui inserire documenti che sono stati creati dall'utente Environment.DIRECTORY_DOWNLOADS Directory in cui collocare i file che sono stati scaricati dall'utente Environment.DIRECTORY_MOVIES Directory in cui inserire film che sono disponibili all'utente Environment.DIRECTORY_MUSIC Directory in cui collocare i file audio da utilizzare come musica Environment.DIRECTORY_NOTIFICATIONS Directory in cui collocare i file audio per le notifiche Environment. DIRECTORY_PICTURES Directory in cui inserire immagini come screenshot Environment.DIRECTORY_PODCASTS Directory in cui collocare tutti i file audio podcast Environment.DIRECTORY_RINGTONES Directory in cui collocare tutti i file audio per la suoneria Corso di programmazione di sistemi mobile 5 A differenza di quanto avviene per la memoria interna Android non mette a disposizione metodi che ritornino direttamente Stream di dati. Sarà compito dello sviluppatore creare un oggetto File e utilizzare i metodi java per effettuare operazioni di I/O String text = "Questo fiore è molto petaloso"; if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS); File doc = new File(dir, "Petaloso.txt"); FileOutputStream fos = null; try { byte[] data = text.getBytes(); fos = new FileOutputStream(doc); fos.write(data); fos.flush(); } catch (Exception e) { Log.e("FileOutputStream", "Errore durante la scrittura del file", e); } finally { if (fos != null) try { fos.close(); } catch (Exception e) { } } } else { Toast.makeText(this, "Impossibile accedere alla sdcard!", Toast.LENGTH_LONG).show(); } Corso di programmazione di sistemi mobile 6 Shared Preference Le SharedPreferences sono nate dalla necessità di avere un sistema standard di salvataggio delle informazioni all’interno di Android (es impostazioni dell’app). Esse ci permettono di salvare dei singoli dati identificati da una chiave in maniera rapida, semplice e persistente. Il metodo: public SharedPreferences getSharedPreferences(String name, int mode) ci permette di recuperare un oggetto SharedPreference associato al nome passato in ingresso. Mode può assumere i valori: MODE_PRIVATE, MODE_WORLD_READABLE e MODE_WORLD_WRITEABLE Per recuperare un valore salvato è possibile invocare i metodi presenti all’interno della classe SharedPreferences, i più comuni sono: getBoolean(String key, boolean defValue) getFloat(String key, float defValue) getInt(String key, int defValue) getLong(String key, long defValue) getString(String key, String defValue) Corso di programmazione di sistemi mobile 7 Per poter salvare dei dati è necessario recuperare l’oggetto SharedPreferences e invocare il metodo edit che restituisce un oggetto di tipo SharedPreferences.Editor. Come per la SharedPreferences all’interno dell’oggetto Editor sono disponibili i metodi per salvare i dati: putBoolean(String key, boolean value) putFloat(String key, float value) putInt(String key, int value) putLong(String key, long value) putString(String key, String value) Una volta terminato l’inserimento dei dati per confermare il salvataggio è necessario invocare il metodo apply() o commit(). La principale differenza tra i due metodi è che commit() esegue il salvataggio dei dati in thread separato e non restituisce il risultato dell’operazione. Corso di programmazione di sistemi mobile 8 Preference Activity Android mette a disposizione un framework completo per la gestione delle preference, si tratta dell’insieme delle classi appartenenti al package android.preference. I componenti di gestione delle preferenze saranno contenute in una particolare specializzazione della classe Activity: PreferenceActivity il cui layout dovrà essere contenuto in un documento che avrà come root: <PreferenceScreen>. Si dovrà utilizzare inoltre la classe PreferenceFragment poiché PrefenceActivity ha dei metodi deprecati. public class MyPreferenceFragment extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Caricamento delle preferenze dal file XML addPreferencesFromResource(R.xml.setting); } } Corso di programmazione di sistemi mobile 9 Un esempio di file setting.xml <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <PreferenceCategory android:title="@string/inline_preferences"> <CheckBoxPreference android:key="checkbox_preference" android:title="@string/title_checkbox_preference" android:summary="@string/summary_checkbox_preference" /> </PreferenceCategory> </PreferenceScreen> PreferenceScreen rappresenta la radice della gerarchia delle preferenze, le preferenze possono essere inoltre raggruppate in tag PreferenceCategory. Questa implementazione popola in automatico le preferenze una volta create e ne mantiene la persistenza, per recuperare le modifiche effettuate dall’utente bisogna utilizzare il metodo: SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); boolean check = preferences.getBoolean("checkbox_preference", false); Corso di programmazione di sistemi mobile 10 Le classi base per le preferenze e gli elementi del file XML a disposizione sono: Corso di programmazione di sistemi mobile 11 Serializable Nel passaggio di dati complessi tra servizi, come ad esempio il passaggio di dati fra activity, nasce la necessità di serializzare i dati. Un oggetto si dice serializzato se è trasformabile in un array di byte e può essere ricostruito al suo stato originale. La serializzazione prevede la sola trasformazione dello stato dell’oggetto e non della sua struttura, cioè della relativa classe. Quindi il bytecode relativo alla classe dovrà essere disponibile in entrambi i processi che si scambiano questo tipo di oggetti. Per serializzare un oggetto basta implementare l’interfaccia java.io.Serializable, inoltre tutti i membri devono essere di tipo serializzabile. public class Person implements Serializable { private int id; private About about; } Corso di programmazione di sistemi mobile 12 Parcelable Android mette a disposizione un’altra struttura per serializzare i dati dal nome Parcelable. È stato creata questa alternativa perché il meccanismo adottato per comprimere un oggetto Serializable è più lento e se complesso può portare l’applicazione ad errori di tipo ARN. A differenza di quanto avviene per una classe che implementa l’interfaccia Serializable l’utilizzo dell’interfaccia Parcelable richiede più lavora da parte dello sviluppatore. L'interfaccia Parcelable descrive una modalità di registrazione di un oggetto con tutti i suoi dati primitivi o un qualsiasi oggetto che a sua volta implementa Parcelable. Il metodo da implementare è: public void writeToParcel(Parcel parcel, int flags) Parcel parcel è l’oggetto dove andremo a trasferire i dati della nostra classe int flags altre informazioni su come deve essere scritto l’oggetto public int describeContents() questo metodo in genere ritorna zero, esso viene utilizzato in alcuni casi particolari. Corso di programmazione di sistemi mobile 13 Sarà inoltre necessario definire un metodo statico chiamato CREATOR, che è un oggetto che implementa l'interfaccia Parcelable.Creator, tale oggetto serve a ricostruire la classe che implementa Parcelable. public class Person implements Parcelable { public static final Creator<Person> CREATOR = new Creator<Person>() { @Override public Person createFromParcel(Parcel in) { return new Person(in); } @Override public Person[] newArray(int size) { return new Person[size]; } }; private int id; protected Person(Parcel in) { id = in.readInt(); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(id); } } Corso di programmazione di sistemi mobile 14 Database All’interno del framework Android sono presenti le API per utilizzare i Database, in particolare Android si avvale del database SQLite. SQLite è un leggerissimo database engine transazionale che occupa poco spazio in memoria e sul disco pertanto è la tecnologia perfetta in un ambiente mobile, dove le risorse sono limitate e dunque è importante ottimizzarne l’utilizzo. SQLite supporta i principali tipi di dato (Integer, Real, Text) fatta eccezione per i booleani che vengono memorizzati come interi 0, 1. All’interno di un applicazione mobile si possono avere infiniti database, essi vengono memorizzati all’interno dello spazio di memoria riservato all’applicazione nella sottodirectory chiamata databases, il cui percorso assoluto è: /data/data/packagename/databases dove "packagename" è il nome del package del corrispondete alla nostra applicazione. Corso di programmazione di sistemi mobile 15 Per includere e gestire un database in una app Android è necessario creare: 1) La struttura del database tramite uno script SQL. 2) Una classe java che estende SQLiteOpenHelper per gestire la creazione e i vari aggiornamenti del DB, inoltre la classe deve poter recuperare un riferimento all’oggetto SQLiteDatabase per accedere ai dati. 3) Una classe per l’interazione con il database sfruttando il riferimento alla classe SQLiteOpenHelper e contenente metodi per eseguire le operazioni sui dati. Per creare la tabella sarà necessaria la creazione di una stringa contenente il comando , ad esempio: private static final String CREA_TABELLA_PERSONE = "CREATE TABLE " + TABELLA_PERSONE + " (" + ID + " INTEGER PRIMARY KEY," + PERSONE_NOME + " TEXT," + PERSONE_COGNOME + " TEXT," + PERSONE_FOTO + " TEXT)"; È utile creare anche delle stringhe per i comandi di update o di altre modifiche: private static final String SQL_DELETE_ENTRIES ="DROP TABLE IF EXISTS " + "Persone" ; Corso di programmazione di sistemi mobile 16 SQLiteOpenHelper public class PersonDbHelper extends SQLiteOpenHelper { public static final String DATABASE_NAME = "PersonDb.db"; private static final int DATABASE_VERSION = 1; private private private private public public public public public static static static static static static static static static final final final final final final final final final String String String String String String String String String TEXT_TYPE = " TEXT"; REAL_TYPE = " REAL"; INTEGER_TYPE = " INTEGER"; COMMA_SEP = ","; TABLE_NAME = "person"; _ID = "_id"; NOME = "nome"; COGNOME = "cognome"; FOTO = "foto"; private static final String TABLE_PERSON_CREATE = "CREATE TABLE " + TABLE_NAME + " (" + _ID + " INTEGER PRIMARY KEY," + NOME + TEXT_TYPE + COMMA_SEP + COGNOME + TEXT_TYPE + COMMA_SEP + FOTO + TEXT_TYPE + " )"; Corso di programmazione di sistemi mobile 17 public PersonDbHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(TABLE_PERSON_CREATE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { switch (newVersion) { case 2: break; } } } Il metodo onCreate viene invocato una sola volta per la creazione del DB e riceve come input un oggetto di tipo SQLiteDatabase il quale fornisce i metodi necessari ad effettuare le principali operazioni su un database. Si può ottenere un’istanza di tale classe anche utilizzando altri due metodi presenti nella classe helper: getReadableDatabase() e getWritableDatabase(). Tali metodi permettono l’accesso rispettivamente in lettura ed in scrittura al DB. Alla fine di ogni utilizzo dell’istanza ottenuta è necessario rilasciare il database invocando il metodo close() . In alternativa a questo metodo si può usare releaseReference(). Corso di programmazione di sistemi mobile 18 ExecSQL Nell’onCreate del database si può notare che il comando SQL è inviato tramite l’invocazione del metodo execSQL. Tale metodo consente l’esecuzione di qualsiasi comando SQL che non sia di tipo query (quindi che non preveda la restituzione di informazioni da parte del DB) e sono disponibili due overload. Il primo richiede semplicemente una stringa SQL da eseguire, l’altro richiede di passare dei parametri per l’esecuzione dei cosiddetti prepared statements: db.execSQL("INSERT INTO nomi VALUES(?)", new Object[]{"Andrea"}); La piattaforma ci offre comunque supporto nativo per le operazioni più comuni come delete, insert, update e query generiche. Corso di programmazione di sistemi mobile 19 Principali Operazioni sul DB • Query: si ottiene un oggetto Cursor che può essere usato per spostarsi tra le righe del risultato con i metodi moveToNext, moveToFirst, moveToLast. Per ogni colonna si possono prelevare i valori tramite i metodi getString, getLong … tali metodi necessitano dell’indice della colonna desiderata. Esistono due metodi per effettuare le query: 1. query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) 2. rawQuery(String sql, String[] selectionArgs) • Delete: utilizzato per eliminare uno o più record data la tabella è necessario specificare la condizione e gli eventuali parametri per effettuare il bind ( concetto simile alla clausola WHERE). Ritorna il numero di record eliminati o errore (-1). • Insert: per inserire un nuovo record, come valore di ritorno restituisce l’ID della riga inserita o -1 in caso di errore. • Update: per modificare dei record inseriti oppure delle righe. Corso di programmazione di sistemi mobile 20 Query long itemId; SQLiteDatabase db = getWritableDatabase(); Cursor c = db.query( TABLE_NAME , // La tabella da interrogare projection, // Le colonne che si vogliono ottenere (String[]) selection, // le colonne della clausola WHERE selectionArgs, // I valori per la clausola WHERE null, // null indica che non si vuole raggruppare le righe null, // nessun filtro per gruppi di righe sortOrder // modalità di ordinamento (String) ); c.moveToFirst(); itemId = c.getLong(cursor.getColumnIndexOrThrow(_ID)); db.close(); Corso di programmazione di sistemi mobile 21 RawQuery public List<Persona> getPersone() { SQLiteDatabase db = getReadableDatabase(); Cursor cursor = db.rawQuery("SELECT * FROM " + TABLE_NAME, null); if (cursor == null || !cursor.moveToFirst()) return null; List<Persona> persone = new ArrayList<Persona>(cursor.getCount()); for (int i = 0; i < cursor.getCount(); i++) { String nome = cursor.getString(cursor.getColumnIndex(NOME)); String cognome = cursor.getString(cursor.getColumnIndex(COGNOME)); String foto = cursor.getString(cursor.getColumnIndex(FOTO)); long id = cursor.getLong(cursor.getColumnIndex(_ID)); Persona persona = new Persona(); persona.setCognome(cognome); persona.setNome(nome); persona.setFoto(foto); persona.setId(id); persone.add(persona); cursor.moveToNext(); } db.close(); return persone; } Corso di programmazione di sistemi mobile 22 Inserimento Per inserire un oggetto all’interno del database si utilizza il ContentValues che ci consente di inserire i dati tramite l’assegnazione della rispettiva colonna del database. public int addNuovaPersona(Persona persona) { SQLiteDatabase db = getWritableDatabase(); ContentValues values = new ContentValues(); values.put(NOME, persona.getNome()); values.put(COGNOME, persona.getCognome()); values.put(FOTO, persona.getFoto()); long result = db.insert(TABLE_NAME, null, values); db.close(); return result; } Corso di programmazione di sistemi mobile 23 Eliminazione Il valore di ritorno indica il numero di record eliminati o -1 in caso di errore. public boolean deletePersona(long id) { SQLiteDatabase db = getWritableDatabase(); int result = db.delete(TABLE_NAME, ID + "=?", new String[] { Long.toString(id) }); db.close(); return result>0; } Corso di programmazione di sistemi mobile 24 Aggiornamento public Persona updatePersona(Persona persona) { SQLiteDatabase db = getWritableDatabase(); ContentValues values = new ContentValues(); values.put(NOME, persona.getNome()); values.put(COGNOME, persona.getCognome()); values.put(FOTO, persona.getFoto()); int result = db.update(TABLE_NAME, values, ID + "=?", new String[]{ Long.toString(persona.getId()) }); db.close(); return result>0; } Corso di programmazione di sistemi mobile 25