Android Code Camp, Urbino — 2012
SimpleAudioPlayer
Realizzazione di un semplice player Audio.
Introduzione
Programmiamo una semplice applicazione Android che ci consenta di
riprodurre dei file audio locali e/o remoti. Per rendere l’applicazione più
completa ed utilizzabile nella vita quotidiana di uno smartphone utilizzeremo
un service per la riproduzione in background e registreremo dei
BroadcastReceiver per reagire a degli eventi di sistema particolarmente
importanti per la nostra applicazione.
Lo sviluppo sarà diviso in passi incrementali. I file coinvolti ad ogni passo
saranno indicati insieme alla descrizione del procedimento.
Questo tutorial è incentrato sulla programmazione di applicazioni su
piattaforma Android perciò alcune classi responsabili di funzionalità che
esulano dallo scopo saranno già fornite nel progetto baseSimpleFeedReader.
Semplice riproduzione di un file negli asset
File coinvolti:
Version1.java
Nel nostro progetto sono presenti due sample audio in formato .ogg (prelevati
dal sito wikipedia) che utilizzeremo per le nostre prove. Android mette a
disposizione nel proprio SDK un oggetto chiamato MediaPlayer che consente
con discreta facilità la riproduzione di vari formati multimediali: per un
dettaglio
dei
formati
disponibili
consultare
la
pagina
http://developer.android.com/guide/appendix/media-formats.html.
In questa prima versione provvediamo all’inizializzazione dell’oggetto
MediaPlayer di Android nel metodo onStart() dell’activity e successivamente
provvediamo a far partire la riproduzione nel metodo onResume() dell’activity.
@Override
public void onStart(){
super.onStart();
mp = MediaPlayer.create(this, R.raw.yesterdaysample);
}
@Override
public void onResume(){
Android Code Camp, Urbino — 2012
TITOLO DEL DOCUMENTO
}
super.onResume();
mp.start();
A questo punto se facciamo partire l’app sentiremo riprodurre il file audio ma
se chiudiamo l’activity la riproduzione non si interromperà. In questo caso
dobbiamo uccidere a mano il processo e possiamo farlo tramite la prospettiva
DDMS in Eclipse.
Se perdiamo il riferimento in Java all’oggetto MediaPlayer abbiamo memory
leak e non riusciremo più ad ottenere il controllo sull’oggetto appena perso.
Pagina 2 di 13
Android Code Camp, Urbino — 2012
TITOLO DEL DOCUMENTO
Interfaccia grafica e stop del MediaPlayer
File coinvolti:
Version2.java
Per evitare il memory leak fermiamo la riproduzione del MediaPlayer e
rilasciamolo per liberarne le risorse nel motodo onStop() dell’activity.
@Override
public void onStop (){
super.onStop();
mp.stop();
mp.release();
mp = null;
}
Aggiungiamo inoltre un layout all’activity con i classici pulsanti di pausa, play,
avanti, indietro. Per ora attiveremo solo il pulsante di play/pausa. Per gestire
meglio il ciclo di vita del media player aggiungiamo inoltre una variabile intera
in cui annoteremo lo stato del riproduttore.
@Override
public void onCreate (Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.version2);
this.prevButton = (Button) this.findViewById(R.id.PrevButton);
this.playPauseButton = (Button)
this.findViewById(R.id.PlayPauseButton);
this.nextButton = (Button) this.findViewById(R.id.NextButton);
this.playPauseButton.setOnClickListener(new View.OnClickListener() {
});
}
@Override
public void onClick(View v) {
if(mpState == STATE_PLAYING){
mp.pause();
mpState = STATE_PAUSED;
playPauseButton.setText("Play");
}else{
mp.start();
mpState = STATE_PLAYING;
playPauseButton.setText("Pause");
}
}
mpState = STATE_UNDEFINED;
Pagina 3 di 13
Android Code Camp, Urbino — 2012
TITOLO DEL DOCUMENTO
Interfaccia grafica evoluta
File coinvolti:
Version3.java
In questa nuova versione aggiungeremo al layout una SeekBar e due TextView
per avere un impatto visivo dell’istante corrente di riproduzione. Quindi dopo
aver modificato il layout dovremo ottenere il riferimento ai nuovo widget anche
nell’activity Version3.java. Oltre a questo registreremo un listener per reagire
agli spostamenti sulla SeekBar richiesti dall’utente in modo da scorrere il file
riprodotto all’istante desiderato.
@Override
public void onCreate (Bundle savedInstanceState){
// …
this.progressBar.setOnSeekBarChangeListener(new
SeekBar.OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
}
});
@Override
public void onProgressChanged(SeekBar seekBar,
int progress, boolean fromUser) {
// we should distinguish between a seek caused by an
// user, or by an update through the polling thread
if (fromUser) {
mp.seekTo(progress);
}
}
È necessario inoltre inizializzare correttamente la SeekBar con il massimo
valore che può raggiungere (nel nostro caso la durata del file audio) ed il suo
stato iniziale. Vogliamo anche che la SeekBar si resetti quando il file audio
finisce: per questo l’oggetto MediaPlayer permette di registrare un listener che
viene invocato nel momento in cui il termine del file viene raggiunto.
@Override
public void onStart (){
// …
mp.setOnCompletionListener(new
MediaPlayer.OnCompletionListener() {
Pagina 4 di 13
Android Code Camp, Urbino — 2012
TITOLO DEL DOCUMENTO
});
}
@Override
public void onCompletion(MediaPlayer mp) {
playPauseButton.setText("Play");
progressBar.setProgress(0);
mpState = STATE_PREPARED;
}
mpState = STATE_PREPARED;
// initialize properly the progress bar
// with the info about the current track
progressBar.setMax(mp.getDuration());
progressBar.setProgress(mp.getCurrentPosition());
// …
A questo punto se avviassimo l’applicazione noteremmo che la SeekBar non
viene aggiornata durante la riproduzione del file: ed effettivamente è giusto
così, perché in nessun punto abbiamo codificato il concetto di aggiornarla
periodicamente. Per questo compito il MediaPlayer non prevede nessun
metodo o listener particolare, perciò l’unica alternativa che abbiamo è creare
un thread che vada periodicamente a valutare lo stato di riproduzione del
MediaPlayer.
Il concetto di thread in Android non ha differenze rispetto ai concetti classici:
quello su cui dobbiamo fare attenzione è il seguente segmento di codice:
// …
if (mp != null && (mpState == STATE_PLAYING || mpState == STATE_PAUSED)) {
Version3.this.runOnUiThread(new Runnable() {
@Override
public void run() {
// update progress bar
progressBar.setProgress(mp.getCurrentPosition());
// and progress/duration textview
currentProgressTV.setText(
TimeUtil.formatProgress(mp.getCurrentPosition()));
durationTV.setText(
TimeUtil.formatProgress(mp.getDuration()));
}
});
} else {
Version3.this.runOnUiThread(new Runnable() {
@Override
public void run() {
progressBar.setProgress(0);
currentProgressTV.setText(TimeUtil.formatProgress(-1));
durationTV.setText(TimeUtil.formatProgress(-1));
}
});
}
Pagina 5 di 13
Android Code Camp, Urbino — 2012
TITOLO DEL DOCUMENTO
// …
Questa parte di codice appena mostrata viene eseguita nel metodo run() di una
sottoclasse di Thread, quindi viene eseguita da un Thread differente rispetto a
quello dell’interfaccia grafica. In Android, l’interfaccia grafica NON può essere
modificata da un Thread che non sia quello principale dell’UI: per questo se
provassimo a modificarla direttamente dal Thread in polling sul MediaPlayer
verrebbe sollevata un’eccezione.
Il sistema ci aiuta mettendoci a disposizione alcune tecniche per far eseguire
del codice nel Thread UI: la tecnica utilizzata in questo caso è la possibilità di
passare un Runnable con il codice da eseguire nel metodo runOnUiThread()
della classe Activity. Il codice dell’esempio è molto semplice ed auto
esplicativo: semplicemente viene aggiornato il progresso nella SeekBar e
l’istante di riproduzione nelle TextView.
Pagina 6 di 13
Android Code Camp, Urbino — 2012
TITOLO DEL DOCUMENTO
Disaccoppiamento grafica-servizio
File coinvolti:
Version4.java
ServiceVersion4.java
Il software fin qui creato permette di riprodurre una canzone, scorrerla e
metterla in pausa: ma per farlo dobbiamo per forza continuare ad avere la
nostra activity attiva. Per risolvere questo problema, in questa nuova versione
introdurremo un Service in cui sposteremo la gestione del MediaPlayer in
modo che continui a funzionare correttamente anche chiudendo l’activity
principale.
In particolare vogliamo un foreground service, tramite il quale magari
ripristinare l’activity di gestione del player. Per questo, nel file
ServiceVersion4.java, i passi cruciali sono riportati nel codice seguente:
// …
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// make the service "foreground", so it acquires max priority
Intent i = new Intent(this, Version4.class);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_SINGLE_TOP);
pi = PendingIntent.getActivity(this, 1, i, 0);
// create the notification that will be shown in the system bar
notification = new Notification(android.R.drawable.ic_media_play,
"Audio Tutorial", System.currentTimeMillis());
notification.setLatestEventInfo(this, "Audio Tutorial",
"Service started", pi);
notification.flags |= Notification.FLAG_ONGOING_EVENT;
startForeground(NOTIFICATION_ID, notification);
// we want this service to continue running until it is explicitly
// stopped, so return sticky
return START_STICKY;
}
// …
@Override
public IBinder onBind(Intent arg0) {
clientsBound = true;
return binder;
}
@Override
public boolean onUnbind(Intent i) {
clientsBound = false;
return super.onUnbind(i);
}
/**
Pagina 7 di 13
Android Code Camp, Urbino — 2012
TITOLO DEL DOCUMENTO
* Custom binder for the AudioTutorial service
*
*/
public class ServiceBinder extends Binder {
ServiceVersion4 getService() {
return ServiceVersion4.this;
}
}
Nel metodo onStartCommand() creiamo dapprima una notifica che verrà
visualizzato nella StatusBar di Android: la cosa interessante è che passiamo
un PendingIntent che fa riferimento a Version4.class che conterrà l’UI per
controllare la riproduzione audio, in modo che quando un utente aprirà la
notifica verrà avviata questa Activity. Impostiamo inoltre il flag
FLAG_ONGOING_EVENT per indicare che la notifica non deve essere rimossa
dalla StatusBar; infine, chiamando il metodo startForeground() avvieremo il
servizio come foreground service.
I servizi in Android possono essere legati ad un’activity: i metodi onBind e
onUnbind forniscono le chiamate eseguite rispettivamente all’atto di
instaurazione e rilascio del legame. In particolare nel metodo onBind dobbiamo
ritornare un oggetto che fornisca un Binder con il servizio attuale, in modo che
il chiamante possa operare in qualche maniera diretta con il servizio. Nel
nostro caso abbiamo creato una sottoclasse di Binder che restituisce
semplicemente il servizio di cui si è effettuato il legame, in modo che il
chiamante possa accedere alla sua interfaccia. Vedremo poi nell’activity come
questo legame ci sarà utile.
Oltre a queste implementazioni, il resto del servizio non presenta nulla di
particolare dato che presenta fondamentale un semplice wrapper dell’oggetto
MediaPlayer.
Passando ora in esamine l’activity, le novità interessanti sono riportate qui di
seguito:
// …
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder s) {
// called when the connection is made
Log.i(TAG, "Getting a reference to the foreground service");
service = ((ServiceVersion4.ServiceBinder) s).getService();
}
@Override
public void onServiceDisconnected(ComponentName name) {
Pagina 8 di 13
Android Code Camp, Urbino — 2012
TITOLO DEL DOCUMENTO
}
Log.i(TAG, "Service disconnected");
// received when the service unexpectedly disconnects
service = null;
};
@Override
public void onStart() {
super.onStart();
// initialize a polling thread with an interval of a second
pollingThread = new PollingThread(POLLING_DELAY);
// start the polling thread
pollingThread.start();
// start service
Intent serviceIntent = new Intent(this, ServiceVersion4.class);
startService(serviceIntent);
// and bind it
bindService(serviceIntent, mConnection, Context.BIND_AUTO_CREATE);
}
@Override
public void onStop() {
super.onStop();
// stop the polling thread
pollingThread.stopPolling();
pollingThread = null;
// unbind the service
unbindService(mConnection);
}
// …
Notiamo subito che i metodi onStart() e onStop() presentano delle chiamate
per avviare il servizio (startService()), effettuarne il binding (bindService()) e
rilasciare il binding(unbindService()).
In particolare la chiamata bindService() si aspetta tra i parametri un oggetto
molto importante che è il gestore della connessione (ServiceConnection). Nel
nostro caso passiamo mConnection che semplicemente inizializzerà l’attributo
service al momento della connessione del servizio e lo renderà null nel
momento della disconnessione: tra la connessione e la disconnessione,
potremo chiamare direttamente i metodi pubblici del nostro servizio. Ed infatti,
il resto delle modifiche prevede semplicemente di chiamare i vari metodi
(play/pause/seekTo/etc..) direttamente sul servizio.
Pagina 9 di 13
Android Code Camp, Urbino — 2012
TITOLO DEL DOCUMENTO
Reagire ad eventi broadcast
File coinvolti:
ServiceVersion5.java
ExtActivity.java
NoiseBroadcastReceiver.java
AndroidManifest.xml
Ora provvediamo ad implementare alcune caratteristiche particolari che ci
permetteranno di valutare l’utilizzo di un importante componente di Android: il
BroadcastReceiver.
Nel nostro caso vogliamo semplicemente registrare un BroadcastReceiver che
venga chiamato quando l’audio del device potrebbe diventare rumoroso, ad
esempio perché sono stati appena scollegati gli auricolari e quindi l’audio
verrebbe riprodotto direttamente dagli speaker del device: in questa caso,
vogliamo che la nostra applicazione semplicemente metta in pausa il player
per evitare possibili situazione di imbarazzo. Il codice (all’interno di
NoiseBroadcastReceiver.java) che svolgerà questa funzionalità è molto
semplice.
// …
@Override
public void onReceive(Context c, Intent intent) {
if (intent.getAction().equals(
android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
// retrieve our service if it's running
Intent serviceIntent = new Intent(c, ServiceVersion4.class);
IBinder serviceBinder = this.peekService(c, serviceIntent);
if (serviceBinder != null) {
ServiceVersion5 service = ((ServiceVersion5.ServiceBinder)
serviceBinder).getService();
if (service.getState() == PlayerState.STATE_PLAYING) {
// since the audio is becoming noisy and the player
// is playing, we should pause it, otherwise the
// user may be angry with us
service.pauseSong();
}
} else {
Log.i(TAG, "Service not started, so do nothing");
}
}
}
// …
Il metodo onReceive() di un BroadcastReceiver viene chiamato quando accade
un evento broadcast per il quale lo abbiamo registrato. Nel nostro caso
Pagina 10 di 13
Android Code Camp, Urbino — 2012
TITOLO DEL DOCUMENTO
l’evento è android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY: in
questa situazione cerchiamo di ottenere il binder al nostro servizio e, se il
servizio è attivo e sta riproducendo, mettere in pausa la riproduzione. Per
registrare un BroadcastReceiver abbiamo due possibilità: farlo in maniera
dinamica dal codice Java o in maniera statica nell’AndroidManifest. Vediamo
qui di seguito la seconda possibilità, il Manifest:
<!-- … -->
<receiver android:name=".NoiseBroadcastReceiver" >
<intent-filter>
<action android:name="android.media.AUDIO_BECOMING_NOISY" />
</intent-filter>
</receiver>
<!-- … -->
Anche le applicazioni possono lanciare Intent Broadcast; per questo
modifichiamo il servizio creato precedentemente inserendo un nuovo metodo
come di seguito:
// …
public static final String BROADCAST_INTENT =
"it.uniurb.androidcoursecamp.audioplayer.STATUS_CHANGED";
public static final String EXTRA_STATE = "state";
private void broadcastState() {
// send broadcast intent
Intent bi = new Intent(ServiceVersion5.BROADCAST_INTENT);
bi.putExtra(EXTRA_STATE, mpState);
this.sendBroadcast(bi);
}
// …
Chiameremo questo metodo ogni qual volta i metodi all’interno del servizio
facciano cambiare di stato il MediaPlayer: in questa maniera eventuali
applicazioni esterne potranno accorgersi di questo cambiamento ed operare di
conseguenza.
Per esemplificare questa possibilità e mostrare anche la tecnica di
registrazione dinamica dei BroadcastReceiver osserviamo il codice della
classe ExtActivity.java:
// …
@Override
public void onStart() {
super.onStart();
// register our receiver for ServiceVersion4 intent
IntentFilter f = new IntentFilter(ServiceVersion5.BROADCAST_INTENT);
this.registerReceiver(mReceiver, f);
}
Pagina 11 di 13
Android Code Camp, Urbino — 2012
TITOLO DEL DOCUMENTO
@Override
public void onStop() {
super.onStop();
// unregister our receiver
this.unregisterReceiver(mReceiver);
}
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent i) {
if(i.getAction().equals(ServiceVersion5.BROADCAST_INTENT)){
String msg;
// retrieve state from the intent
int state=i.getIntExtra(ServiceVersion5.EXTRA_STATE, -1);
if (state == PlayerState.STATE_PLAYING) {
msg = "Now playing";
} else if (state == PlayerState.STATE_PAUSED) {
msg = "Player paused";
} else if (state == PlayerState.STATE_STOPPED) {
msg = "Player stopped";
} else {
msg = "Undefined state";
Log.w(TAG, “I have not found any state information
in the broadcast intent!");
}
};
// …
}
}
// notify user the new state
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
Quando l’activity viene avviata (onStart()) creiamo un IntentFilter per filtrare
solo
gli
eventi
a
cui
siamo
interessati,
nel
nostro
caso
ServiceVersion5.BROADCAST_INTENT. Quindi registriamo il BroadcastReceiver
(che è stato definito nelle linee di codice successive) con il filtro appena
specificato tramite la chiamata registerReceiver().
Specularmente, quando l’activity viene fermata (onStop()) rimuoviamo la
registrazione del BroadcastReceiver perché non siamo più interessati ad
essere notificati per l’evento.
Il BroadcastReceiver definito è molto semplice e segue la struttura del
BroadcastReceiver spiegato precedentemente: quando capita l’evento per cui
ci siamo registrati, controlliamo lo stato del servizio e emaniamo un Toast
informativo appropriato. Ovviamente questo esempio è molto banale, ma il
Pagina 12 di 13
Android Code Camp, Urbino — 2012
TITOLO DEL DOCUMENTO
funzionamento di base è uguale per tutti i BroadcastReceiver, quindi prevedere
comportamenti più complessi ed allo stesso tempo utili è solo questione di
fantasia ed ingegno.
Pagina 13 di 13