Transcript Document

Componenti di Android
Docente: Gabriele Lombardi
[email protected]
© 2012 - CEFRIEL
The present original document was produced by CEFRIEL and the Teacher for the benefit and internal use of this
course, and nobody else may claim any right or paternity on it. No right to use the document for any purpose other than
the Intended purpose and no right to distribute, disclose, release, furnish or disseminate it or a part of it in any way or
form to anyone without the prior express written consent of CEFRIEL and the Teacher.
© copyright Cefriel and the Teacher-Milan-Italy-23/06/2008. All rights reserved in accordance with rule of law and
international agreements.
© 2012 - CEFRIEL
Sommario
SLIDE
CONTENUTO
Attività
Cosa sono, ciclo di vita
Intent
Cosa sono, una visione dall’alto, esempi
Permessi
Come definirli, un esempio concreto
ContentProvider
Come accedere a dati esterni
Servizi
Come si creano, un esempio concreto
Broadcast receiver
Ricevere richieste broadcast dalle altre app
© 2012 - CEFRIEL
Attività: ma cosa sono?

Sono componenti Android:
–
–
–
–
–

quindi sono gestite (più di quanto sembri);
vengono create e distrutte dal framework;
possono essere fermate e riavviate;
se serve ram (o altri casi) possono essere distrutte;
l’utente non nota mai nulla (se l’app è scritta bene).
Sono porzioni di interfaccia grafica:
– rappresentano strumenti di interazione con l’utente;
– le app non sono obbligate ad averne solo una;
– normalmente ogni attività è coesa e disaccoppiata:
• low copuling, high coesion (GRASP);
– può offrire i propri servigi anche ad altre app. (?).
© 2012 - CEFRIEL
Una vita piena di imprevisti

Scenario di esempio con 2 Activity:
– sto giocando a sudoku, sono preso e concentrato…
– …arriva una telefonata a cui rispondere:
• deve avviarsi un’attività di gestione della
chiamata con interfaccia (rispondi, chiudi);
• l’attività Sudoku viene messa in pausa:
– chiamate callback relative;
– la RAM è insufficiente per entrambe le attività:
• Android decide di eliminare Sudoku:
– chiamate callback relative;
– l’applicazione Sudoku deve persistere il proprio stato;
– la telefonata può avere atto (con la RAM libera);
– finisce la telefonata, si chiude l’attività di gestione;
• Android avvia Sudoku passandogli le info;
– chiamate callback relative;
– l’applicazione dalle info persistite deve ripristinare il proprio stato;

L’utente è ignaro di tutto e si illude di parallelismo.
© 2012 - CEFRIEL
Ciclo di vita delle Activity

Nasce:
– Creata
– Avviata
– Ripristinato lo stato

Vive:
– L’utente interagisce,
viene mantenuta;

Dorme:
– Pause se tocca a
qualcun altro;
– Stop se manca ram;
– Viene mantenuta
traccia della sua
esistenza;

Muore:
– Viene fermata e
uccisa, eliminate le
tracce di esistenza.
© 2012 - CEFRIEL

Salvare le proprie info
Le Activity vengono distrutte a fronte di:
– altre Activity in foreground che richiedono RAM;
– cambiamenti di configurazione (es orientazione).

FractionCalc è a posto da questo punto di vista?
– No, ha stato interno non mantenuto: isInitial.
Restore stato
per una pausa
Restore per
una ricreazione
© 2012 - CEFRIEL
Rivediamo la calcolatrice sotto nuova luce

Definire una attività:
– la classe FractionCalc estende Activity:
• altre classi estendono Activity per noi:
–
–
–
–
–
AliasActivity  Alias di un’altra attività, la avvia e poi termina.
LouncherActivity  GUI di scelta tra attività per eseguirne una.
ListActivity  Lista di item, con un layout per ogni riga.
ExpandableListActivity  Lista espandibile, come sopra.
TabActivity  Attività basata su Tab (pagine con linguette).
– ascolta degli eventi tra cui onCreate per il setup;
– l’AndroidManifest.xml e strings.xml vengono aggiornati:
<activity android:name="FractionCalc" android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<resources>
<string name="app_name">FractionCalc</string>
</resources>
© 2012 - CEFRIEL
Rivediamo la calcolatrice sotto nuova luce

Dare una faccia nuova all’attività:
–
–
–
–
–
–
–

ogni attività si mostra attraverso un albero di view;
questo può essere descritto in XML;
insufflare una vista significa inserirla creandola da XML;
ad ogni View nel layout può essere associato un id:
findViewById ci permette di recuperare le istanze dei widget;
ogni widget implementa Observable per l’ascolto degli eventi;
ogni evento viene gestito nel main thread, quello della UI;
• Nota: se abbiamo lavori lunghi, NON devono
essere eseguiti nel main thread (vedremo dove).
Gestire il salvataggio di stato:
– Bugfixiamo FractionCalc salvandone lo stato:
• in onSaveInstanceState salviamo:
– @Override protected void onSaveInstanceState(Bundle outState) {
–
super.onSaveInstanceState(outState);
–
outState.putBoolean("isInitial", isInitial); }
• in onCreate utiliziamo i dati salvati:
– if (savedInstanceState!=null)
–
isInitial = savedInstanceState.getBoolean("isInitial", isInitial);
© 2012 - CEFRIEL
Rivediamo la calcolatrice sotto nuova luce

Più attività nella nostra applicazione:
–
–
–
–
–
–

aggiungiamo una history delle espressioni;
ogni espressione valutata viene aggiunta alla history;
una attività apposita ci permette di visualizzarle;
un menu ci fa accedere a questa nuova attività;
l’attività può tornare con o senza espressione;
se una espressione è stata scelta va inserita.
Come muoversi tra attività:
– gli Intent rappresentano «gli URL» di Android:
• definiscono cosa interessa raggiungere;
• cosa interessa eseguire (verbo);
• permettono il passaggio di informazioni.
© 2012 - CEFRIEL
Creare l’history durante il normale utilizzo
private LinkedList<String> history = new LinkedList<String>();
// Computing:
String exprStr = new StringBuilder(expr).toString();
Fraction res = Calculator.compute(exprStr);
// Updating the history:
history.add(exprStr);
@Override protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("isInitial", isInitial);
outState.putSerializable("history", history);
}
if (savedInstanceState!=null) {
isInitial = savedInstanceState.getBoolean("isInitial", isInitial);
history = (LinkedList)savedInstanceState.getSerializable("history");
if (history==null) history = new LinkedList<String>();
}
© 2012 - CEFRIEL
Creare un’attività «a lista» per l’history
public class HistoryList extends ListActivity {
@Override
public void onCreate(Bundle icicle) { … }
…}
Creo la classe e
adatto il manifest
<activity android:name="HistoryList“ android:label="History list"></activity>
@Override public void onCreate(Bundle icicle) {
super.onCreate(icicle);
Ottengo l’history
List<String> history = (List<String>)getIntent()
.getSerializableExtra(FractionCalc.HISTORY_EXTRA);
setListAdapter(new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1, history));
Setto l’adattatore
}
@Override protected void onListItemClick(ListView l, View v,
int position, long id) {
setResult(Activity.RESULT_OK,
new Intent().putExtra(FractionCalc.POSITION_EXTRA, position));
finish(); }
© 2012 - CEFRIEL


Creare i menu per la navigazione (da codice)
Vedremo come creare menu da XML;
In caso di creazione da codice:
– vengo avvertito e aggiungo le mie voci:
@Override public boolean onCreateOptionsMenu(Menu menu) {
menu.add(Menu.NONE, MENU_HISTORY, Menu.NONE, "History")
.setIcon(android.R.drawable.ic_menu_gallery);
return super.onCreateOptionsMenu(menu); }

Se una voce viene selezionata…
– …uso un Intent per aprire la lista di history items.
@Override public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_HISTORY:
Intent i = new Intent(this, HistoryList.class);
i.putExtra(HISTORY_EXTRA, history);
startActivityForResult(i, HISTORY_POSITION);
return true;
}
return super.onOptionsItemSelected(item); }
© 2012 - CEFRIEL
Gestire le preferenze utente

L’attività di gestione delle preferenze:
–
–
–
–
esiste già una classe per definirla:
la descrizione delle preferenze viene indicata in XML;
dobbiamo aggiungere però una voce di menu;
gestiamo (in maniera «casereccia») i temi. res/values/arrays.xml
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<ListPreference
res/xml/preferences.xml
android:key="theme"
android:title="UI Theme"
android:summary="Choose the theme for your calc"
android:entries="@array/themes"
android:entryValues="@array/theme_values"
android:dialogTitle="Choose the preferred theme"/>
</PreferenceScreen>
<resources>
<string-array name="themes">
<item>Dark UI</item>
<item>Clean light</item>
<item>Coloured</item>
</string-array>
<string-array name="theme_values">
<item>dark</item>
<item>light</item>
<item>coloured</item>
</string-array>
</resources>
public void onCreate(Bundle savedInstanceState) { …
prefs = PreferenceManager.getDefaultSharedPreferences(this);
prefs.registerOnSharedPreferenceChangeListener(
new SharedPreferences.OnSharedPreferenceChangeListener() {
public void onSharedPreferenceChanged(SharedPreferences prefs,
String key) { if ("theme".equals(key)) applyTheme(); } });
applyTheme(); …
© 2012 - CEFRIEL
case MENU_PREFERENCES:
startActivity(new Intent(this,
Preferences.class));
return true; …
Quali componenti abbiamo assemblato?
Gli Intent sono degli URI
con più informazioni e
capacità: possono
trasportare dati strutturato,
azione, valore di ritorno,
modalità di accesso, …
View
da
XML
FractionCalc
View
da???
Dati
(extra)
Intent
HistoryList
Intent
Menu
Risultati
(extra)
© 2012 - CEFRIEL
Adapter
Attività, task e back/stack

Arrivati all’history possiamo «tornare indietro»:
– la nostra app ha un solo task (potremmo crearne altri);
– ogni task possiede uno stack di (stati di) attività:
• se la nostra app passa in background:
– invocati i metodi relativi ai passaggi di stato  onPause  onStop;
– viene mantenuta traccia dello stato dell’intero back-stack;
• se viene premuto il tasto «back»:
– distrutta l’attività corrente: onPause  onStop  onDestroy;
– viene ripristinata la precedente: onResume
Back-stack del Task
Intent verso
HistoryList
FractionCalc
Back-stack del Task
HistoryList
FractionCalc
© 2012 - CEFRIEL
Back-stack del Task
Pulsante back
oppure finish()
FractionCalc
Gli intent

Rappresentano gli URL in Android:
– hanno un destinatario a cui si vuole accedere:
• definito esplicitamente (classe o nome di componente);
• definito tramite un URI definito nei filtri:
– le app dichiarano di gestire richieste per determinati URI;
– hanno un’azione richiesta (come i verbi HTTP):
• ACTION_MAIN  inizia un’attività come task
• ACTION_CALL  inizia una chiamata …
– definiscono una categoria per il destinatario dell’Intent:
• CATEGORY_LAUNCHER  attività iniziale
• CATEGORY_PREFERENCE  pannello preferenze …
– trasportano dei dati (negli Intent i «dati» sono l’URI):
• ad esempio associato ad ACTION_CALL:
– tel:+39340123456
• avvia una telefonata verso il numero indicato
• il tipo MIME può essere indicato
– trasportano degli extra, ovvero dati serializzabili custom;
– per indicare ad Android come gestire l’intent ci sono alcuni flag.
© 2012 - CEFRIEL
Gli intent: accedere ai contatti

Scopo dell’app di esempio:
– creazione di una lista di chiamate da effettuare in
sequenza scegliendo i destinatari dalla rubrica;
– la sequenza dovrà essere mostrata in una lista
comprendente varie informazioni e dei pulsanti;
– i contatti dovranno venire scelti dal tool della rubrica;
– le chiamate dovranno essere effettuate in ordine.

Attività principale dell’app:
– una ListActivity con la lista di chiamate schedulate;
• vogliamo gestirci noi il layout di riga;
– voce «Add» dal menu per accedere alla rubrica;
– cominciamo con queste funzionalità!
© 2012 - CEFRIEL
Accedere ai contatti

Core features dell’applicazione sono:
1. costruire una lista di contatti da chiamare;
2. effettuare le chiamate in sequenza.

Come effettuare il pick di un contratto?
– Tramite un Intent non rivolto alla nostra app!
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
Chiamo una attività
case R.id.menu_add:
perché voglio qualcosa
startActivityForResult(
new Intent(Intent.ACTION_PICK,
ContactsContract.Contacts.CONTENT_URI),
CONTACT_PICK);
URI dato da una
return true;
costante del framework
}
return super.onOptionsItemSelected(item);
Azione di prelievo dei
}
dati di un contatto
© 2012 - CEFRIEL
Cosa ci regala l’attività rubrica?

Un Intent di risposta… non senza avvisarci!
– gli intent-filter vengono utilizzati per identificare il
destinatario della richiesta (vedremo come);
– il destinatario viene interpellato con il nostro Intent
come richiesta da cui recuperare gli argomenti;
– il destinatario crea un Intent e genera una risposta;
– il nostro metodo onActivityResult viene chiamato.
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// Controllo se devo gestire un contatto:
if (resultCode==RESULT_OK) {
switch (requestCode) {
case CONTACT_PICK:
onContactPick(data);
break;
}
}
}
© 2012 - CEFRIEL

Persistenza: accedere con i cursori!
Cerchiamo il nome/numero del contatto fornito:
– dobbiamo cercare nel database dei contatti;
– eseguiamo una query con un ContentResolver;
– possiamo navigare le «tabelle» in diversi modi:
– (quello che vediamo in questo caso sono dati aggregati).
// Otteniamo un risolutore di contenuti:
ContentResolver cr = getContentResolver();
// Otteniamo un cursore su Phone:
Cursor c = cr.query(Phone.CONTENT_URI, new String[] {Contacts._ID,
Contacts.DISPLAY_NAME, Phone.NUMBER},
Contacts._ID + " = ? AND " + Phone.TYPE + " = ?",
new String[] { String.valueOf(ContentUris.parseId(data.getData())),
String.valueOf(Phone.TYPE_MOBILE)}, null);
if (c.moveToFirst()) {
String name = c.getString(1);
String number = c.getString(2);
…}
c.close(); // Chiudo:
© 2012 - CEFRIEL
Persistenza: accedere con i cursori!

I contatti sono persistenti…
– …e vengono mantenuti quindi in una base dati;
– DBMS SQLite, parzialmente nascosto dalle API;
– per accedere ai dati dobbiamo conoscere lo schema:
Contact
Data
© 2012 - CEFRIEL
_ID
DISPLAY_NAME
CONTACT_ID
NUMBER
Persistenza: accedere con i cursori!

Per accedere ai dati dobbiamo:
– procurarci un punto di accesso al DB dei contatti:
• ContentResolver fornito dal Context;
– eseguire una query fornendo un URI:
• ci viene fornito come risultato nell’Intent;
• identifica un contatto (vediamo come è fatto);
• ci viene restituito un Cursor da scorrere;
– estrarre i dati da Contact (_ID e DISPLAY_NAME);
– solo se è disponibile un numero di telefono:
• eseguire una query su Data:
– accedendo ai dati relativi al contatto di interesse;
– estraendo il numero di telefono (il primo ci va bene).
Intent
ContentResolver
Cursor
Contact
Creiamo e salviamo un
CallSchedule nella lista
Cursor
_ID
DISPLAY_NAME
© 2012 - CEFRIEL
Data
CONTACT_ID
NUMBER
Persistenza: accedere con i cursori!
ContentResolver cr = getContentResolver(); // Da qui accediamo ai dati..
Cursor c = cr.query(data.getData(), null, null, null, null); // ..ottenedo un cursore!
if (c.moveToFirst()) { // Estraiamo le informazioni che ci servono:
String id = c.getString(c.getColumnIndex(ContactsContract.Contacts._ID));
String name = c.getString(c.getColumnIndex(
ContactsContract.Contacts.DISPLAY_NAME)); String number = "-";
if (Integer.parseInt(c.getString(c.getColumnIndex(
ContactsContract.Contacts.HAS_PHONE_NUMBER))) > 0) {
/* Qui otterremo il numero di telefono. */ }
adapter.add(new CallSchedule(name, number)); // Aggiungiamo lo schedule.
} c.close(); // I cursori vanno poi rilasciati.
Cursor pCur = cr.query( // Otteniamo un cursore sui dati del contatto:
ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null,
ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
new String[]{id}, null);
if (pCur.moveToFirst()) { // Scelgo il primo:
number = pCur.getString(pCur.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.NUMBER));
} pCur.close();

Vedremo come persistere i dati della nostra app.
© 2012 - CEFRIEL
Abbiamo chiesto il permesso?

Cenni di sicurezza da parte dell’utente:
– caro utente.. questa app vuole fare questo e quello…
– …per effettuare certe operazioni serve il consenso:
• dell’utente che deve esserne consapevole;
• a tempo di installazione dell’app.
– il manifest dichiara le caratteristiche dell’app:
• versioni supportate del framework;
• supporto per tipi di display;
• feature richieste dall’app per funzionare:
– camera, accelerometro, touch…

• permessi per funzionalità e contenuti.
Nel nostro caso:
– <uses-sdk android:minSdkVersion="10"/>
– <uses-permission android:name=
"android.permission.READ_CONTACTS"/>
© 2012 - CEFRIEL
Una lista completamente custom

Vogliamo mostrare la lista di schedule con:
– nome visualizzato del contatto con sotto il numero;
– tasti per eliminare l’elemento o effettuare la chiamata;

Per farlo dobbiamo costruire una lista che:
– non sia «standard» (ogni riga deve essere un layout);
– permetta di agire attivamente sui dati con pulsanti;
– sia efficiente per poter gestire tante righe:
– alcuni dispositivi hanno ram e capacità di calcolo molto limitate!
ListActivity
CallScheduleAdapter
FastCall
CallScheduleHolder
© 2012 - CEFRIEL
In getView si preoccupa di
costruire la vista di ogni riga
«insuflandola» da XML,
riciclando altre righe quando
possibile, salvando per efficienza
i dati nel tag della vista
Mantiene i dati/componenti per
una riga e ne nasconde i dettagli
di aggiornamento e accesso
(efficienza e disaccoppiamento)
Una lista completamente custom

Layout XML relativo ad ogni singola riga:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:orientation="vertical">
<TextView android:id="@+id/name"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:textSize="10pt" android:paddingLeft="4pt"
android:text="Gabriele Lombardi"/>
<TextView android:id="@+id/number"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:textSize="6pt" android:paddingLeft="8pt"
android:text="+39 340 123456"/>
</LinearLayout>
<ImageButton android:id="@+id/remove"
android:src="@android:drawable/ic_delete"
android:contentDescription="Delete this schedule"
android:layout_width="20pt" android:layout_height="20pt"
android:layout_weight="0" android:layout_gravity="center"/>
</LinearLayout>
© 2012 - CEFRIEL
Una lista completamente custom
// Lista contenente i dati:
private List<CallSchedule> schedules = new LinkedList<CallSchedule>();
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main); // Insufliamo la nostra lista.
// Usiamo il nostro adattatore:
setListAdapter(adapter = new CallScheduleAdapter());
…
private class CallScheduleAdapter extends ArrayAdapter<CallSchedule> {
public CallScheduleAdapter() { super(FastCall.this, …, schedules); }
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// Verifico se esiste una vista da riciclare:
View row = convertView; CallScheduleHolder holder;
if (row==null) {
row = getLayoutInflater().inflate(R.layout.contact, null); // Ne creo una:
row.setTag(holder = new CallScheduleHolder(row)); } // Attacco l’holder:
else holder = (CallScheduleHolder)row.getTag();
// Ottengo il modello e inserisco i dati usando l'holder:
holder.populateFrom(getItem(position));
return row;
}}
© 2012 - CEFRIEL
Una lista completamente custom
private class CallScheduleHolder {
TextView name, number;
CallSchedule schedule;
CallScheduleHolder(View row) {
name = (TextView)row.findViewById(R.id.name);
number = (TextView)row.findViewById(R.id.number);
// Collego un ascoltatore per ogni tasto:
row.findViewById(R.id.remove).setOnClickListener(
new View.OnClickListener() {
@Override public void onClick(View view) {
adapter.remove(schedule); }
});
}
// Incapsulo la funzionalità di refresh della UI:
void populateFrom(CallSchedule sc) {
name.setText(sc.getName());
number.setText(sc.getNumber());
schedule = sc;
}
}
© 2012 - CEFRIEL
Dati
row view
holder
row view
holder
row view
holder
row view
holder
Aggiungiamo il tasto di chiamata

Widget nel layout e drawable tra le risorse:
<ImageButton android:id="@+id/call" android:src="@drawable/call_contact"
android:contentDescription="Call this schedule"
android:layout_width="20pt" android:layout_height="20pt"
android:layout_weight="0" android:layout_gravity="center"/>

Ascoltatore di eventi per il tasto nell’holder:
row.findViewById(R.id.call).setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View view) {
// Avvio l'attività che risponde all'intent corretto:
startActivity(new Intent(Intent.ACTION_CALL,
Uri.parse("tel:" + schedule.getNumber()))); } });

I permessi per poter effettuare chiamate:
<uses-permission android:name="android.permission.CALL_PHONE"/>

Proviamolo… problemi riscontrati?
– Al termine della chiamata non torna all’attività.
– Ascolteremo lo stato del telefono in un servizio.
© 2012 - CEFRIEL

Rendiamo persistenti i nostri schedule
Molti modi per gestire la persistenza:
– il più corretto consiste nell’utilizzare un database;
– in FastCall utilizzeremo le SharedPreferences:
– si tratta di una mappa persistente di preferenze tipizzate.

Leggiamo le preferenze shared in «onCreate»:
savedSchedules = getSharedPreferences("schedules", MODE_PRIVATE);
for (Map.Entry<String,?> entry: savedSchedules.getAll().entrySet()) {
// Ne aggiungo uno:
adapter.add(new CallSchedule(entry.getKey(), entry.getValue().toString())); }

Aggiungiamo se l’utente lo richiede:
adapter.add(new CallSchedule(name, number));
SharedPreferences.Editor editor = savedSchedules.edit();
editor.putString(name, number);
editor.apply();

Eliminiamo quando lo decide l’utente:
adapter.remove(schedule);
SharedPreferences.Editor editor = savedSchedules.edit();
editor.remove(schedule.getName());
editor.apply();
© 2012 - CEFRIEL
Eliminazione accidentale… preveniamola!

Quando si deve eliminare qualcosa…
– …è sempre meglio chiedere all’utente;
– per farlo serve una dialog…
– …in Android le dialog sono asincrone!
// Lo facciamo in una dialog asincrona:
new AlertDialog.Builder(FastCall.this) // Configuro un creatore di alert:
.setTitle("Confermi la cancellazione?").setCancelable(true)
.setPositiveButton("Si", new DialogInterface.OnClickListener() {
@Override // Solo qui dentro faccio quello che devo:
public void onClick(DialogInterface dialogInterface, int i) {
adapter.remove(schedule);
SharedPreferences.Editor editor =
savedSchedules.edit();
editor.remove(schedule.getName());
editor.apply();
}
}).setNegativeButton("No", null)
.show(); // Mostro la dialog (in maniera asincrona, non subito):
© 2012 - CEFRIEL
Utilizzo di esempio dell’app
Menu
Add
Tasto call
Anna Morra
© 2012 - CEFRIEL
Formaliziamo: i ContentProvider

Scopo del gioco… accedere a dati strutturati:
– utilizzando un unico tool standard che li incapsuli;
– nascondendo i dettagli relativi alla sorgente;
– gestendo le problematiche di sicurezza.

Cosa ci permettono di ottenere?
– Accesso trasparente a dati strutturati;
– Inter Process Communication (IPC).

Attori che entrano in gioco:
– ContentProvider:
• classe che si occupa di fornire i contenuti;
• molte implementazioni già disponibili;
• da estendere/implementare solo se si vogliono
fornire contenuti di un nuovo proprio tipo.
– ContentResolver:
• strumento per accedere ai dati;
• associato a un contesto (es.: Activity).
© 2012 - CEFRIEL
Accedere ai dati

Struttura mostrata dei dati (strutturati):
–
–
–
–

come nei DBMS… tabelle con tuple e attributi;
i dati vengono scanditi per mezzo di un cursore;
il cursore è il risultato di una query su una tabella;
la query viene eseguita tramite il ContentResolver.
Struttura di una query:
– clausola FROM indica la tabella di interesse:
• per noi un URI del tipo «content://…»;
– le colonne vengono scelte tramite proiezione:
• elenco di nomi, null per sceglierle tutte;
– una clausola di selezione può essere definita:
• sintassi simile all’SQL con parametri JDBC;
– se viene definita selezione, servono gli argomenti:
• passati per ordine in un array;
– ordinamento definito come ultimo parametro.
Cursor cur = getContentResolver().query(
UserDictionary.Words.CONTENT_URI, // Scelgo le parole a dizionario.
new String[] {UserDictionary.Words._ID, UserDictionary.Words.WORD},
UserDictionary.Words.LOCALE + " = ?", // Verifico il locale.
new String[] {"it_IT"}, // Locale italiano.
null // Non voglio ordinare i risultati in un particolare ordine.
);
© 2012 - CEFRIEL
Struttura dell’URI

I ContentProvider offrono accesso a URI:
– tutti del tipo «content://…»;
– riferiti a una autorità provider indicata dopo:
• content://authority_name/…
– in cui sono indicati i nomi di tabelle:
• content://authority_name/table1/…
– raggruppabili nel percorso (come fossero directory):
• content://authority_name/group1/table1…
– fino alla singola tupla tramite ID numerico:
• content://authority_name/table1/id1
– è ammesso l’utilizzo di wildcard:

• content://authority_name/table1/*
Esempi:
– content://user_dictionary/words
– content://com.android.calendar/time/1335543165348
– content://com.android.contacts/contacts/1
© 2012 - CEFRIEL
Muoversi sui dati

Dato un cursore… possiamo scorrere i dati:
– getColumnNames:  nomi colonne;
– getColumnName/getColumnIndex:
• converte nome di colonna in indice e viceversa;
– move/moveToForst/moveToLast/moveToNext/moveToPosition:
• sposta il cursore e ci dice se ci è riuscito;
– get* (getInt/getString/getDouble…):
• dati di una colonna (dato l’indice);
– getExtra:  meta-dati extra passati al richiedente;
– respond:  meta-dati (in un bundle) comunicati al cursore;
– deactivate/requery/close:

• disattivazione cursore, reinizializzazione, chiusura.
Ascoltiamo gli eventi del cursore:
– (un)registerContentObserver:  ascolto variazioni dei dati;
– (un) registerDataSetObserver:  ascolto azioni sul cursore.
© 2012 - CEFRIEL
Operazioni CRUD

ContentResolver: oltre alla query c’è di più!
– Per completare le operazioni CRUD:
• insert:  inserimento di una nuova tupla;
• update:  aggiornamento di una tupla;
• delete:  cancellazione di una tupla.
– Con le loro varianti:

• bulkInsert:  inserimenti multipli;
• applyBatch:  batch di operazioni.
Oltre alle operazioni CRUD:
–
–
–
–
openInputStream/openOutputStream;
openAssetFileDescriptor/…
(un)registerContentObserver;
requestSynch & co.
© 2012 - CEFRIEL
Classi-contratto, tipi MIME, permessi

Classi-contratto:
– Dobbiamo accedere a contenuti standard di Android?
– Come ricordarci nomi di tabelle e campi?
– Come scrivere codice robusto ai cambiamenti di nome tra
versioni differenti di Android?
– Semplice: usando nomi definiti nelle classi-contratto!
– Esempi: ContactsContract, CalendarContract,
SyncStateContract, VoicemailContract.

Tipi MIME:
– Per ogni URI, i provider forniscono il timo MIME:
• identificabile con ContentResolver.getType;
– con i tipi MIME si indentificano tipo e formato dei dati.

Permessi:
– L’utente deve sempre poter decidere se permettere a
un’applicazione di accedere alle risorse del telefono!
– Permessi temporanei su un URI:
• l’app che restituisce l’URI può settare:
– FLAG_GRANT_READ_URI_PERMISSION
– FLAG_GRANT_WRITE_URI_PERMISSION
© 2012 - CEFRIEL
Modalità di accesso ai dati

Tramite Intent:
– non abbiamo permessi, non accediamo direttamente;
– possiamo richiedere l’accesso a un’altra app.

Tramite content provider/resolver:
– come abbiamo fatto per i contatti.

Tramite accesso batch:
– sempre utilizzando un provider/resolver;
– creando delle ContentProviderOperation:
• utilizzando la nested class Builder;
• impostando li dati e direttive:
– ContentProviderOperation.newInsert(uri)
.withValues(values)
• con valori definiti in un oggetto ContentValues:
– ContentValues values = new ContentValues();
– values.put("NAME", "Gabriele"); …
– Fornendo un array di operazioni al resolver:
• getContentResolver().applyBatch(ops);
© 2012 - CEFRIEL
Tornando alla nostra app…

Notato la scomodità???
– Effettuando una chiamata si esce dall’app;
– eliminiamo gli schedule di chiamate già effettuate;
– l’utente potrebbe aver trovato occupato, libero, voler
chiamare nuovamente… in generale meglio chiedere;
– vogliamo chiedere se eliminare lo schedule al rientro;
– vogliamo farlo SOLO se l’utente è riuscito a parlare.

Come gestiamo il problema:
– aggiungiamo un ascoltatore dello stato del telefono;
– se si passa da idle a chiamata e poi viceversa:

• fine telefonata, mostriamo una AltertDialog.
Cosa ci serve?
– Un PhoneStateListener custom (nostro);
– il servizio di telefonia a cui registrarsi;
– i permessi giusti.
© 2012 - CEFRIEL
Un ascoltatore «al telefono»
// Registriamo un ascoltatore di chiamate:
onCreate
TelephonyManager telMgr =
(TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);
telMgr.listen(phoneListener, PhoneStateListener.LISTEN_CALL_STATE);
private PhoneStateListener phoneListener = new PhoneStateListener() {
AtomicBoolean isCalling = new AtomicBoolean(false);
public void onCallStateChanged(int state, String incomingNumber) {
if (state == TelephonyManager.CALL_STATE_OFFHOOK) {
isCalling.set(true); // Chiamata iniziata.. ricordiamolo:
}
if (state == TelephonyManager.CALL_STATE_IDLE) {
// Fine chiamata?
if (isCalling.compareAndSet(true, false)) { // Mostro una dialog:
new AlertDialog.Builder(FastCall.this)
.setTitle("Vuoi eliminare lo schedule?").setCancelable(true)
.setPositiveButton("Si", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialogInterface, int i) {
removeSchedule(schedule); } })
.setNegativeButton("No", null).show(); } } } };
manifest
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
© 2012 - CEFRIEL
Problemi di questo approccio

Non posso s-registrare l’ascoltatore:
– per farlo è sufficiente ascoltare l’evento NONE;
– se voglio smettere di ascoltare devo farlo in onStop;
– se smetto di ascoltare… non vengono notificati gli
eventi di chiamata proprio quando servono;

Notifiche non volute arrivano:
– registrazioni multiple ascoltano lo stesso evento;
– se la chiamata è avviata da altre app...?

Notifiche possono arrivare ad attività non attiva:
– impossibile mostrare la dialog.

Qualcosa dovrebbe rimanere in ascolto di eventi
telefonici lavorando in background… un servizio.
© 2012 - CEFRIEL
Servizi: cosa sono?

Le nostre app fino ad ora:
– hanno aderito all’automa a stati finiti delle Activity;
– non operavano negli stati di pause e stop;
– FastCall non può ascoltare il servizio di telefonia:
non in maniera corretta per lo meno,

Cosa ci manca?
– La possibilità di eseguire task in background,
indipendentemente dal ciclo di vita dell’Activity.
– I servizi di Android assomigliano a quelli di Windows:
• sempre attivi in background;
• mai fissi in esecuzione:
– altrimenti il dispositivo eseguirebbe solo quel task.

Esempi:
– servizio di download e notifica delle mail;
– aggiornamento dello stato di Facebook o Twitter.
© 2012 - CEFRIEL
Tipologie di servizi

Servizi locali:
– sono strettamente legati all’applicazione che li ha creati e
comunicano solo con quella, mai con altre;
– sono definiti come sottoclassi di Service.

Servizi remoti:
– sono accessibili a più applicazioni (oltre alla creante);
– sono definiti come sottoclassi di Service;
– sono descritti ai client tramite un’interfaccia Android:

• Android Interface Definition Language (AIDL).
Tutto formalmente dichiarato… nel manifest:
– come per le Activity, anche i Service devono essere
dichiarati nel manifest, con eventuali opzioni a corredo.

Per ora iniziamo con i servizi locali:
– Nel nostro esempio un servizio monitorerà la telefonia.
© 2012 - CEFRIEL
Servizi locali e remoti: cicli di vita

Servizi locali:
– meccanismo per eseguire
operazioni in background;
– altri meccanismi che
vedremo sono:
• thread avviati da noi
in una attività;
• estensione della
classe AsyncTask;
• utilizzo di un Handler
di un thread;
– il servizio viene avviato con
startService.

Servizi remoti:
– permette la comunicazione
tra processi (IPC)
– stessa classe da estendere,
ciclo di vita diverso;
– avviato implicitamente, ci si
connette con bindService.
© 2012 - CEFRIEL
Servizi… un esempio di utilizzo pratico

Vogliamo ascoltare chiamate uscenti/entranti:
– usiamo un servizio locale (avviato con startService);
– ci inseriamo l’ascoltatore di telefonia;
– ascoltiamo anche gli eventi di chiamate in uscita:
• utilizzeremo un BroadcastReceiver;
– aggiorneremo l’elenco chiedendo all’utente.
FastCall
CallNotificationService
PhoneStateListener
© 2012 - CEFRIEL
BroadcastReceiver
Il servizio
public class CallNotificationService extends Service {
private static CallNotificationService service; // Gestisco un riferimento all’istanza:
public static CallNotificationService getInstance() { return service; }
private TelephonyManager telMgr;
private Set<String> calledNumbers = new HashSet<String>();
@Override public void onCreate() {
super.onCreate();
telMgr = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);
telMgr.listen(phoneListener, PhoneStateListener.LISTEN_CALL_STATE);
// Registro un ricevitore per le chiamate uscenti:
registerReceiver(outgoingCallReceiver,
new IntentFilter("android.intent.action.NEW_OUTGOING_CALL"));
service = this;
}
@Override public void onDestroy() {
telMgr.listen(phoneListener, PhoneStateListener.LISTEN_NONE);
super.onDestroy();
}
@Override public IBinder onBind(Intent intent) { return null; }
…}
© 2012 - CEFRIEL
Ascoltare le chiamate entranti/uscenti
private PhoneStateListener phoneListener = new PhoneStateListener() {
AtomicBoolean isCalling = new AtomicBoolean(false);
@Override
public void onCallStateChanged(int state, String incomingNumber) {
if (state == TelephonyManager.CALL_STATE_OFFHOOK) {
isCalling.set(true); // Chiamata iniziata.. ricordiamolo:
}
if (state == TelephonyManager.CALL_STATE_IDLE) {
if (isCalling.compareAndSet(true, false) &&
incomingNumber!=null && !incomingNumber.isEmpty()) {
// Aggiungo il numero all'insieme:
addNumber(incomingNumber);
} } } };
private BroadcastReceiver outgoingCallReceiver = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) {
String outgoingNumber = // Ottengo il numero:
intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
// Lo aggiungo:
if (outgoingNumber!=null && !outgoingNumber.isEmpty()) {
addNumber(outgoingNumber); } } };
© 2012 - CEFRIEL
Nella nostra attività…
@Override protected void onStart() { super.onStart();
// Avvio il servizio (se non è già avviato):
startService(new Intent(this, CallNotificationService.class));
}
@Override protected void onResume() { super.onResume();
updateFromService(); // Aggiorno dal servizio:
}
protected void updateFromService() {
CallNotificationService service = CallNotificationService.getInstance();
if (service==null) return;
Set<String> nums = service.getNumbers();
final List<CallSchedule> toBeRemoved = new LinkedList<CallSchedule>();
for (CallSchedule schedule: schedules) {
for (String num2: nums) {
if (PhoneNumberUtils.compare(schedule.getNumber(),num2)) {
toBeRemoved.add(schedule);
}}}
// Chiedo ed elimino se serve…
}
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
© 2012 - CEFRIEL
Da qui?

Abbiamo solamente iniziato:
– con la rassegna delle componenti;
– con la rassegna dei pattern applicati negli esempi;
– con gli strumenti per la costruzione di UI.

Giochiamoci un po’:
– «Learning by doing», Richard Feynman;
– costruiamo delle app di esempio che sfruttino servizi.

Nelle prossimo puntate:
– ampliamo il set di strumenti per la gestione della UI;
– impariamo ad interagire con le altre app;
– molto altro ancora!
© 2012 - CEFRIEL