Docente: Gabriele Lombardi lombardi@dsi.unimi.it Componenti di Android Docente: Gabriele Lombardi lombardi@dsi.unimi.it © 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 Attività Intent Permessi ContentProvider Servizi 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
Sono componenti Android: 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: 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; 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); List<String> history = (List<String>)getIntent() .getSerializableExtra(FractionCalc.HISTORY_EXTRA); setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, history)); } Ottengo l’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. <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> </resources> res/values/arrays.xml <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <ListPreference 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> res/xml/preferences.xml 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(); … case MENU_PREFERENCES: startActivity(new Intent(this, Preferences.class)); return true; … © 2012 - CEFRIEL
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 View da??? Dati (extra) FractionCalc HistoryList Intent Intent Risultati (extra) Adapter Menu © 2012 - CEFRIEL
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 FractionCalc Back-stack del Task FractionCalc HistoryList Back-stack del Task FractionCalc Intent verso HistoryList Pulsante back oppure finish() © 2012 - CEFRIEL
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
Core features dell’applicazione sono: Accedere ai contatti Core features dell’applicazione sono: costruire una lista di contatti da chiamare; 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()) { case R.id.menu_add: startActivityForResult( new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI), CONTACT_PICK); return true; } return super.onOptionsItemSelected(item); Chiamo una attività perché voglio qualcosa URI dato da una costante del framework 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 _ID DISPLAY_NAME Data CONTACT_ID NUMBER © 2012 - CEFRIEL
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). Creiamo e salviamo un CallSchedule nella lista Intent ContentResolver Cursor Cursor Contact _ID DISPLAY_NAME Data CONTACT_ID NUMBER © 2012 - CEFRIEL
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 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 FastCall CallScheduleHolder Mantiene i dati/componenti per una riga e ne nasconde i dettagli di aggiornamento e accesso (efficienza e disaccoppiamento) © 2012 - CEFRIEL
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: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"/> © 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; Dati row view holder row view holder row view holder row view holder © 2012 - CEFRIEL
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
ContentResolver: oltre alla query c’è di più! 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 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: TelephonyManager telMgr = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE); telMgr.listen(phoneListener, PhoneStateListener.LISTEN_CALL_STATE); onCreate 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: Cosa ci manca? 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: Servizi remoti: 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 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 BroadcastReceiver © 2012 - CEFRIEL
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
Abbiamo solamente iniziato: 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