In questo articolo vedremo come realizzare un'applicazione per Android che azzittisca il telefono quando questi viene scosso con insistenza. Ovvero quando viene "shakerato" come un buon cocktail!
L'idea e il progetto
L'idea è di creare un'applicazione che rimanga "in ascolto" dello stato del telefono e, quando questo viene agitato in corrispondenza della chiamata, intervenga sulle impostazioni audio mettendo automaticamente il telefono in modalità silenziosa. Come vedremo, questa idea è percorribile ma, allo stato attuale, altamente perfettibile.
Un aspetto da tenere sempre presente, quando si progetta un'applicazione Android, è che il dispositivo su cui girerà l'app avrà risorse limitate. Perciò è molto importante progettare correttamente il software, usando i componenti adatti per ogni parte, per limitare l'uso della CPU (e della batteria!) al minimo indispensabile.
Per costruire un'applicazione Android ci si avvale di 4 componenti fondamentali, rappresentati ognuno da un'opportuna classe Java presente nelle librerie di sistema. Sviluppare significa estendere tali classi, creando delle istanze specifiche atte alle proprie esigenze. Una app è composta da una o più istanze di questi componenti. Brevemente, essi sono:
- Activities
- Service
- Broadcast Receiver
- Content Provider
Conoscendo il ciclo di vita di ciascuno di questi componenti è possibile intervenire, mediante il meccanismo dell'override, all'interno del ciclo stesso perché il componente reagisca secondo la volontà dello sviluppatore.
Activity, Servizi & altro
Una activity è il componente principale di un'applicazione poiché è l'unico responsabile dell'interfaccia con l'utente, l'unico ad avere una componente grafica. La sua logica, però, è di essere messa in pausa (e poi distrutta dal sistema) non appena 'utente passa ad una nuova activity, poiché l'activity con cui l'utente sta interagendo ha sempre priorità massima su tutti gli altri processi. Sebbene dunque sia un componente fondamentale non può essere usato per "rimanere in ascolto". Per il momento, dunque, lo mettiamo da parte.
Un service (servizio) è un componente che gira in background, nascosto all'utente, ma che viene mantenuto sempre attivo, con le giuste priorità (gestite dal sistema!) fino a che non abbia completato il suo task.
Un broadcast receiver, invece, è un componente che viene allertato a seguito di un evento di sistema, come la ricezione di un messaggio, il superamento di una certa soglia di livello di batteria, l'accensione o spegnimento del dispositivo o altro ancora.
Infine, un content provider è un componente che fornisce una sorgente di dati. Normalmente questi dati provengono da un database (Android è dotato di un database SQLite interno), ma potrebbero anche venire da un file o dalla rete. Il ruolo del content provider è di fornire un accesso standard ai dati e ottimizzare la loro organizzazione/lettura.
Il progetto
Visti i componenti a disposizione sembra evidente che quello più adatto alle nostre esigenze sia un servizio, il quale rimarrà in ascolto dello shake e, all'arrivo di una chiamata, intervenga sulle impostazioni audio del dispositivo.
Questa soluzione però è ancora imperfetta: un servizio che rimanga sempre attivo "brucia" continuamente risorse di sistema.
Lo scenario ottimale è che il servizio sia installato ma inattivo e venga attivato solo all'arrivo di una chiamata. Tale scenario è raggiungibile attraverso l'uso un Broadcast Receiver, impostato sull'ascolto dell'arrivo una chiamata. Sarà il Receiver a svegliare e fermare il servizio al momento opportuno, mentre quest'ultimo "ascolterà" se l'utente agita o meno il telefono.
Rimane un problema: come rilevare lo "shake". Per risolvere questo punto si può usare l'accelerometro, presente su molti dispositivi. Esso infatti rileva la posizione del telefono rispetto alla Terra valutandola in tre variabili secondo i tre classici assi cartesiani: X, Y e Z.
È possibile rilevare la posizione nell'intervallo di qualche millisecondo e, se questa cambia repentinamente su tutti e tre gli assi, possiamo supporre di essere in presenza di un evento di "shake".
Per il progetto in questione, quindi, faremo uso di un broadcast receiver ed un service. Infine, per "ascoltare" l'accelerometro progetteremo un'opportuno Listener Java.
La struttura dell'applicazione sarà la seguente: Il broadcast receiver sarà impostato sullo stato delle chiamate e reagirà sia quando il telefono inizia a squillare che quando smette. Il servizio, infine, rimarrà in ascolto della posizione del telefono attraverso un opportuno "Listener" creato ad hoc.
Ecco lo schema:
Creare il service
Cominciamo dal creare il componente principale: il servizio. Va detto che Android prevede due tipologie di servizi:
- servizi remoti
- servizi locali
Non ci si lasci ingannare dai nomi: un servizio remoto non significa che è sulla rete, entrambi girano in modo esclusivo sul dispositivo su cui sono installati.
La differenza sta nel fatto che i primi possono essere chiamati da una qualsiasi applicazione, mentre i secondi sono ad uso esclusivo dell'app che li ha installati. In questo caso useremo un servizio locale, che è anche il più semplice da usare.
Un servizio locale ha un ciclo di vita molto semplice: viene creato (viene eseguito il metodo onCreate), viene eseguito il metodo onStartCommand, quindi il servizio è in stato RUNNING fino alla sua interruzione.
Perché un servizio sia realmente indipendente e concorrente rispetto agli altri processi di sistema è necessario che il suo lavoro non venga svolto all'interno del thread principale di sistema, ma in un thread separato chiamato, per l'appunto, worker thread.
Per creare un servizio bisogna estendere la classe Service, presente nel pacchetto andoid.app
.
public class ShakeService extends Service
{
@Override
public void onCreate() {
super.onCreate();
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public IBinder onBind(Intent arg0) {
// Ritorniamo null in quanto non si vuole permettere
// l'accesso al servizio da una applicazione diversa
return null;
}
vediamo ora la creazione del worker thread
e la sua istanziazione da parte del servizio.
private final class BackgroundThread extends Thread
{
private static final long DELAY = 500L;
public boolean running = true;
public void run()
{
ShakeListener shakeListener = new ShakeListener() {
@Override
public void onShake()
{
audioManager.setRingerMode(AudioManager.RINGER_MODE_SILENT);
}
};
boolean supported = sensorManager.registerListener(shakeListener,
mAccelerometer, SensorManager.SENSOR_DELAY_GAME);
if (!supported) {
sensorManager.unregisterListener(shakeListener, mAccelerometer);
running = false;
throw new UnsupportedOperationException("Accelerometer not supported");
}
while(running) {
try {
Thread.sleep(DELAY);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
sensorManager.unregisterListener(shakeListener, mAccelerometer);
// Al termine del metodo run terminiamo il servizio
stopSelf();
}
È qui che avviene l'implementazione della classe ShakeListener
, ridefinito il metodo onShake e registrato il listener come ascoltatore.
Nel metodo onShake
avviene il cambio delle impostazioni audio, attraverso il riferimento all'AudioManager e l'imposizione della suoneria in modalità silenziosa. Il metodo entra quindi in un loop continuo, che viene controllato da una variabile impostata dal servizio.
All'uscita del metodo run
il worker thread muore autonomamente. in questo modo il thread rimarrà attivo fino a che non sarà il servizio a fermarlo, con pause di 500ms.
Vediamo dunque il codice completo:
package it.matteoavanzini.android.muteonshake;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.media.AudioManager;
import android.os.IBinder;
public class ShakeService extends Service
{
/*
* Tag per il Log
*/
private final static String LOG_TAG = "SHAKE_LISTENER";
private static BackgroundThread backgroundThread;
private AudioManager audioManager;
private SensorManager sensorManager;
private Sensor mAccelerometer;
private int ringerMode;
@Override
public void onCreate() {
super.onCreate();
audioManager = (AudioManager)
getSystemService(Context.AUDIO_SERVICE);
sensorManager = (SensorManager)
getSystemService(Context.SENSOR_SERVICE);
if (sensorManager == null) {
throw new UnsupportedOperationException("Sensors not supported");
}
mAccelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
ringerMode = audioManager.getRingerMode();
// Facciamo partire il BackgroundThread
backgroundThread = new BackgroundThread();
backgroundThread.start();
}
@Override
public void onDestroy() {
audioManager.setRingerMode(ringerMode);
backgroundThread.running = false;
audioManager = null;
mAccelerometer = null;
sensorManager = null;
super.onDestroy();
}
/* (non-Javadoc)
* @see android.app.Service#onBind(android.content.Intent)
*/
@Override
public IBinder onBind(Intent arg0) {
// Ritorniamo null in quanto non si vuole permettere
// l'accesso al servizio da una applicazione diversa
return null;
}
private final class BackgroundThread extends Thread {
private static final long DELAY = 500L;
public boolean running = true;
public void run()
{
ShakeListener shakeListener = new ShakeListener() {
@Override
public void onShake()
{
audioManager.setRingerMode(AudioManager.RINGER_MODE_SILENT);
}
};
boolean supported = sensorManager.registerListener(shakeListener,
mAccelerometer, SensorManager.SENSOR_DELAY_GAME);
if (!supported) {
sensorManager.unregisterListener(shakeListener, mAccelerometer);
running = false;
throw new UnsupportedOperationException("Accelerometer not supported");
}
while(running) {
try {
Thread.sleep(DELAY);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
sensorManager.unregisterListener(shakeListener, mAccelerometer);
// Al termine del metodo run terminiamo il servizio
stopSelf();
}
} // chiusura del worker thread
} // chiusura della classe ShakeService
La prima istruzione del metodo onCreate è una chiamata al metodo originale della superclasse. Questa chiamata è estremamente importante, poiché è necessario che il servizio venga correttamente inizializzato dal sistema. Questo non è valido solo per i servizi, ma per tutti i componenti in generale.
Le altre istruzioni del metodo onCreate creano un riferimento al servizio di suoneria ed uno all'accelerometro, verificando che il dispositivo lo supporti, pena il lancio di un'opportuna eccezione. Quindi viene istanziato e fatto partire il worker thread.
Entrambi i servizi vengono ottenuti grazie ad una chiamata al metodo getSystemService
della classe Context
. Si tratta di servizi di sistema e l'architettura del sistema Android
ci mette a disposizione dei facili metodi con cui manipolare questi oggetti. Nello specifico:
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
Nel caso dei sensori, però, la chiamata al metodo getSystemService()
non è sufficiente ed è necessaria una seconda istruzione per riferirsi al sensore specifico, poiché nella classe SensorManager
sono rapproesentati l'accelerometro, il giroscopio, il sensore di prossimità e quello della luce ambientale. Il riferimento al sensore accelerometro si ottiene quindi attraverso:
mAccelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
Nel metodo onDestroy, invece, liberiamo ognuna di queste risorse prima di chiamare il metodo onDestroy
della classe madre.
Il metodo onBind
, benché non utilizzato è comunque presente. Si tratta di un metodo astratto della classe Service e pertanto deve essere ridefinito nelle sue sottoclassi. Viene utilizzato dai servizi remoti ma il suo utilizzo non è scopo del presente articolo.
ShakeListener
Un listener android è una classe Java che implementa l'interfaccia SensorEventListener
e che viene utilizzata per rispondere e gestire gli eventi di sistema, secondo il classico meccanismo Osservatore/Ascoltatore.
In pratica una classe (il Listener) viene dedicata alla gestione di un particolare evento, con tutta la gestione degli errori e dei casi più comuni, ma in modo del tutto indipendente dal contesto, in modo da poter essere riutilizzabile. L'osservatore è una seconda classe che registra l'ascoltatore e ne ridefinisce le parti salienti, perché rispondano in modo coerente al contesto in cui il listener viene registrato.
package it.matteoavanzini.android.muteonshake;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
public abstract class ShakeListener implements SensorEventListener
{
private static final int FORCE_THRESHOLD = 350;
private static final int TIME_THRESHOLD = 100;
private static final int SHAKE_TIMEOUT = 500;
private static final int SHAKE_DURATION = 1000;
private static final int SHAKE_COUNT = 3;
private float mLastX=-1.0f, mLastY=-1.0f, mLastZ=-1.0f;
private long mLastTime;
private int mShakeCount = 0;
private long mLastShake;
private long mLastForce;
public abstract void onShake();
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
@Override
public void onSensorChanged(SensorEvent event)
{
if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER) return;
long now = System.currentTimeMillis();
if ((now - mLastForce) > SHAKE_TIMEOUT) {
mShakeCount = 0;
}
if ((now - mLastTime) > TIME_THRESHOLD) {
long diff = now - mLastTime;
float speed = Math.abs(event.values[SensorManager.DATA_X] +
event.values[SensorManager.DATA_Y] +
event.values[SensorManager.DATA_Z] - mLastX - mLastY - mLastZ) / diff * 10000;
if (speed > FORCE_THRESHOLD) {
if ((++mShakeCount >= SHAKE_COUNT) && (now - mLastShake > SHAKE_DURATION)) {
mLastShake = now;
mShakeCount = 0;
onShake();
}
mLastForce = now;
}
mLastTime = now;
mLastX = event.values[SensorManager.DATA_X];
mLastY = event.values[SensorManager.DATA_Y];
mLastZ = event.values[SensorManager.DATA_Z];
}
}
Vengono impostate diverse costanti, delle quali le più importanti sono la durata dello shake (SHAKE_DURATION
), il numero di shake (SHAKE_COUNT) e la forza con cui il dispositivo viene shakerato (FORCE_THRESOLD
).
Il lavoro del listener, quindi, è tutto nel metodo onSensorChanged, dove la posizione del dispositivo viene rilevata a intervalli regolari e salvata in opportune variabili. La posizione viene confrontata con l'ultima posizione conosciuta. Se questa è cambiata in tutte e tre gli assi e vengono superate le soglie specificate dalle costanti indicate sopra, allora viene invocato il metodo onShake, definito come astratto.
Broadcast Receiver
Veniamo ora al Broadcast Receiver, secondo componente del nostro progetto. Il telefono, inteso come servizio di telefonia e non come dispositivo intero, può avere solo tre stati possibili: RINGING (sta squillando), OFFHOOK (c'è una chiamata in corso) o IDLE (nessuna chiamata in corso né in arrivo/uscita). Impostando il Broadcast Receiver in ascolto dello stato del telefono il suo ruolo diventa tanto cruciale quanto semplice: dovrà attivare il servizio quando il telefono è in stato RINGING e fermarlo negli altri due casi. In questo modo il servizio non sarà sempre attivo ma verrà attivato solo quando ce n'è bisogno, con un notevole risparmio di risorse per il sistema.
Nel caso di un broadcast receiver, la classe di sistema da estendere è la classe BroadcastReceiver, presente nel pacchetto android.content. Il suo ciclo di vita è molto semplice e prevede un solo ed unico metodo: onReceive.
Vediamo il codice:
package it.matteoavanzini.android.muteonshake;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
public class MuteOnShake extends BroadcastReceiver
{
private final static String LOG_TAG = "MUTE_ON_SHAKE";
private static Intent serviceIntent;
private Context context;
@Override
public void onReceive(Context context, Intent intent)
{
this.context = context;
serviceIntent = new Intent(context, ShakeService.class);
TelephonyManager telephony = (TelephonyManager)
context.getSystemService(Context.TELEPHONY_SERVICE);
PhoneStateListener phoneListener = new PhoneStateListener() {
public void onCallStateChanged(int state, String incomingNumber) {
switch(state) {
case TelephonyManager.CALL_STATE_IDLE:
// riposo
stopLocalService();
break;
case TelephonyManager.CALL_STATE_RINGING:
// squillando
startLocalService();
break;
case TelephonyManager.CALL_STATE_OFFHOOK:
// in corso
stopLocalService();
break;
}
}
};
telephony.listen(phoneListener,PhoneStateListener.LISTEN_CALL_STATE);
}
public void startLocalService() {
context.startService(serviceIntent);
}
public void stopLocalService() {
context.stopService(serviceIntent);
}
Anche in questo caso abbiamo un riferimento ad un servizio di sistema ed un listener da implementare e registrare. Il servizio di telefonia, ottenuto con la chiamata:
TelephonyManager telephony = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
viene associato al listener dello stato del telefono (PhoneStateListener
) attraverso la chiamata:
telephony.listen(phoneListener,PhoneStateListener.LISTEN_CALL_STATE);
AndroidManifest
Per completare quanto fatto finora dobbiamo ancora dichiarare i componeti che abbiamo scritto all'interno del file manifesto della nostra applicazione. Il file "AndroidManifest.xml" contiene tutte le dichiarazioni di una app: i componenti di cui fa uso, i permessi, lo spazio di nomi delle variabili, ecc. E' un file di vitale imporrtanza poiché è il file cui il sistema fa iferimento per conoscere il nome dell'applicazione, le sue componenti e altro ancora.
Il file è scritto in XML e la sua compensione credo sia abbastanza immediata.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="it.matteoavanzini.android.muteonshake"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="5" />
<application android:icon="@drawable/icon" android:label="@string/app_name">
<receiver android:name="MuteOnShake">
<intent-filter>
<action android:name="android.intent.action.PHONE_STATE" />
</intent-filter>
</receiver>
<service android:name="ShakeService"></service>
</application>
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
</manifest>
Ho evidenziato nel codice alcune parti salienti: la prima è la dichiarazione del broadcast receiver e dell'intent cui risponde ovvero, trattandosi di un receiver, a quale evento deve rispondere. La seconda è la dichiarazione del servizio: si noti come sia sufficiente dichiararne il nome e nulla più, trattandosi di un servizio locale.
Infine abbiamo una dichiarazione di permesso, ovvero la dichiarazione di fare uso di risose particolari, come lo stato del telefono, in questo caso. Senza questa dichiarazione il Receiver andrebbe in errore. Si tratta di una dichiarazione di sicuezza: con questa, al momento dell'installazione della app l'utente verrà avvertito che il nostro software fa uso dello stato del telefono e potrà decidere se accettare o meno questa funzionalità. Se non accetta, comunque, la app non può essere installata. Altre azioni comuni per cui sono richiesti permessi sono l'uso del GPS, della navigazione in Internet o la lettura della rubrica.
Si noti infine la dichiarazione stessa dell'applicazione e gli attibuti del tag application:
android:icon="@drawable/icon" android:label="@string/app_name"
essi sono la dichiarazione, rispettivamente, dell'icona dell'applicazione ed il titolo dell'applicazione stessa. L'icona è definita da un file "icon.png", presente nella catella res/drawable
del progetto mentre il nome dell'applicazione è definito all'interno di un qualsiasi file XML all'interno di una sottocartella di res (tipicamente: res/values
) che definisca al suo interno un tag resources ed un tag string, con l'attibuto name impostato a "app_name"- Vediamo l'esempio:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Shake & Shut</string>
</resources>
Il file icon.png
, invece, è un'immagine che deve avere dimensioni specifiche a seconda del tipo di risoluzione dello schermo del dispositivo: da 36x36 fino a 96x96 pixel. Android prevede 4 tipologie di schermo, indicate dagli acronimi: ldpi, mdpi, hdpi, xhdpi.
Per ovviare al fatto che l'applicazione può essere installata su dispositivi diversi, con diverse risoluzioni è bene creare 4 cartelle drawable, chiamate rispettivamente: drawable-ldpi
, drawable-mdpi
, drawable-hdpi
e drawable-xhdpi
. Ognuna conterrà le stesse risorse ma con dimensioni adatte alla risoluzione. Esiste una guida ufficiale alla creazione delle icone, che consiglio vivamente di leggere.
.
Firmare le app
Con la dichiarazione dei componenti nell'AndroidManifest e l'aggiunta delle risorse nella cartella res la nostra applicazione è terminata. Non resta che pubblicarla sul market. Per fare questo occorre registrarsi come sviluppatori sul sito ufficiale di android.
Completata la registrazione è necessario pagare una cifra una tantum di 25 USD ed ottenere così una chiave con la quale firmare le proprie applicazioni. Tale chiave sarà valida per 25 anni. È molto importante conservarla, poiché versioni successive della stessa applicazione andranno firmate nuovamente con la stessa chiave, per far si che il market le riconosca come versioni successiva di una stessa app piuttosto che come app diverse. Se la chiave viene persa non può essere richiesta e bisogna rifare una registrazione daccapo (e ripagare nuovamente i 25 dollari).
Per firmare la app si può procedere tramite il comando keytool
dell'SDK oppure avvalersi dello strumento messo a disposizione da Eclipse: fate click col tasto destro del mouse sulla cartella del progettto Eclipse, quindi selezionate: Android Tools -> Export signed application
. Il risultato sarà un file .apk che sarà pronto per essere installato sui dispositivi e caricato sul market. Il file altro non è che il progetto intero compilato e compresso.
Se ci fosse la necessità di installare la app su un dispositivo di test è possibile evitare la registrazione ed il pagamento dei 25 dollari e firmare le app con una chiave temporanea, detta di debug. Le app così firmate però non possono essere caricate sul market Nè su dispositivi che non siano abilitati al "debug USB".
Non resta che installare l'app! Buon divertimento!