JavascriptProva

martedì 2 agosto 2016

Aggiunta della voce sintetica.al CoachCall.

Aggiunta dell'attesa che termini il parlato.
L'aggiunta di OnUtteranceCompletedListener è un po' contorta.

Ho dichiarato onUtteranceCompletedListener nel contesto del Service, per poi implementarla nel contesto di Telefono.
E' un comportamento sensato?
Che cosa è, qui, nella fattispecie, OnUtteranceCompletedListener?
Sarebbe un Listener, ossia un'interfaccia.
Con quesco codice:
   mService.onUtteranceCompletedListener=new TextToSpeech.OnUtteranceCompletedListener(){

    @Override
    public void onUtteranceCompleted(String utteranceId) {
     mService.startTime();
     finish();
     
    }
    
   };
ho istanziato la variabile onUtteranceCompletedListener, presente in Service, creando un nuovo Listener.
Ma è proprio necessario che la variabile si trovi in un'altra classe?
Non potrei metterla in onCreate di Telefono?

Provo a metterla nella classe Activity Telefono.
Funziona lo stesso.
Ecco il codice:
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_telefono);
  
  onUtteranceCompletedListener=new TextToSpeech.OnUtteranceCompletedListener(){

   @Override
   public void onUtteranceCompleted(String utteranceId) {
    mService.startTime();
    finish();
    
   }
   
  };
In pratica, per creare un listener (ora faccio un po' di analisi) bisogna avere un oggetto del tipo listener.
Questo oggetto si setta poi come listener.
La cosa si può fare anche in forma anonima.
Perché qui non lo posso fare in forma anonima, ossia senza usare una variabile?
Perché devo avere una variabile a livello di classe.
Provo pure a farlo, ma devo farlo nella stessa funzione in cui uso tts, cui va poi settato il listener.
  bttRispondi.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    
    mService.tts.setOnUtteranceCompletedListener(new OnUtteranceCompletedListener(){
     @Override
     public void onUtteranceCompleted(String utteranceId) {
      mService.startTime();
      finish();
      
     }
     
    });
    HashMap hashMap=new HashMap();
    hashMap.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "id");
    
    mService.tts.speak("Ciao, scemo",TextToSpeech.QUEUE_FLUSH,hashMap);
    
    
   }
  });
Funziona ugualmente, ovviamente.

Aggiunta del TTS al CoachCall

Bene. Adesso ho messo tutto l'essenziale.
RIassumo i passaggi:
  1. Creazione del Service foreGround (ricordare di aggiungere startForeground / Bindaggio delle activities con startService in aggiunta al binding.
  2. Creazione di startTime e stopTime con i riferimenti dei buttons che richiamano queste funzioni dalle altre activities
  3. Creazione del listener personalizzato di AlarmOn, e di onTaskRemoved per la chiusura del Service.


Detto questo, ora si aggiungono le funzioni più specifiche...

Per CoachCall, bisogna aggiungere la sintesi vocale e la suoneria.

Metto la variabile pubblica:
 public TextToSpeech tts;
e aggiungo il codice per l'inizializzazione del TextToSpeech nel Service:
 @Override
 public void onCreate(){
  Intent intent=new Intent(this,MainActivity.class);
  PendingIntent pi=PendingIntent.getActivity(this, 0, intent, 0);
  Notification notification=new NotificationCompat.Builder(this)
    .setSmallIcon(R.drawable.cornetta)
    .setContentTitle("CoachCall")
    .setContentText("Telefonate dal Coach")
    .setContentIntent(pi).build();
  startForeground(1,notification);
    
  tts=new TextToSpeech(this,new TextToSpeech.OnInitListener() {
   
   @Override
   public void onInit(int status) {
    if(status==TextToSpeech.SUCCESS){
     tts.setLanguage(Locale.ITALIAN);
    }
    
   }
  });


Quindi in Telefono vado al codice del pulsante bttRispondi e scrivo un codice per far pronunciare "qualcosa":
  bttRispondi.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    mService.tts.speak("Ciao, scemo",TextToSpeech.QUEUE_FLUSH,null);
    mService.startTime();
    finish();
    
   }
  });
E finora funziona.
Lo scopo successivo è attendere la fine del parlato.

Aggiunta dei listener, della chiusura del Service alla rimozione del recent Task, e correzione dell'errore di startService per ottenere un vero service Foreground.

Come terzo passaggio, sistemiamo il listener dello status del Service, a seconda che l'alarm sia on o off.

Scrivo l'interfaccia, la variabile e il setter.
 public interface OnAlarmChangeListener {
  public void AlarmChange();
 }
OnAlarmChangeListener onAlarmChangeListener;
 public void setOnAlarmChangeListener(OnAlarmChangeListener listener){
  onAlarmChangeListener=listener;
 }
Quindi devo impostare una variabile booleana per il Service che dica se l'alarm è on o off:
boolean alarmOn;
...e devo vedere dove questa variabile cambia, in modo da piazzarci un innesco dell'evento AlarmChange:

in startTime:
 public void startTime(){
  Runnable runnable=new Runnable(){

   @Override
   public void run() {
    Intent intent =new Intent(getApplicationContext(),Telefono.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    startActivity(intent);
    
   }
   
  };
  handler.postDelayed(runnable, 7000);
  alarmOn=true;
  if(onAlarmChangeListener!=null)onAlarmChangeListener.AlarmChange();


in stopTime:
 public void stopTime(){
  handler.removeCallbacksAndMessages(null);
  alarmOn=false;
  if(onAlarmChangeListener!=null)onAlarmChangeListener.AlarmChange();
 }
Oltre a notificare lo stato dell'Alarm quando questo cambia, è il caso di notificarlo quando si setta il listener.
Per questo modifico il setter:
 public void setOnAlarmChangeListener(OnAlarmChangeListener listener){
  onAlarmChangeListener=listener;
  onAlarmChangeListener.AlarmChange();
 }
Ora vado a gestire l'evento in MainActivity:
  @Override
  public void onServiceConnected(ComponentName name, IBinder service) {
   LocalBinder bnd=(LocalBinder)service;
   mService=bnd.getService();
   mBound=true;
   mService.setOnAlarmChangeListener(new OnAlarmChangeListener(){

    @Override
    public void AlarmChange() {
     // TODO Auto-generated method stub
     
    }
    
   });
E ora si imposta l'aspetto di MainActivity a seconda che l'alarm sia on o off. Quel circoletto rosso l'ho chiamato in xml "semaforo" e serve a indicare visivamente se l'alarm è attivo o no.
Lo istanzio...
View semaforo;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  semaforo=(View)findViewById(R.id.semaforo);
   mService.setOnAlarmChangeListener(new OnAlarmChangeListener(){

    @Override
    public void AlarmChange() {
     if(mService.alarmOn){
      semaforo.setBackgroundResource(R.drawable.tondinoverde);
     }
     else {
      semaforo.setBackgroundResource(R.drawable.tondinorosso);
     }
      
     
    }
    
   });
Ma oltre al semaforo mi interessa anche attivare e disattivare i pulsanti a seconda che siano utili o inutili, disattivando il tasto "Attiva" se l'alarm è attivato, e il tasto "Disattiva" se l'alarm è disattivato:
   mService.setOnAlarmChangeListener(new OnAlarmChangeListener(){

    @Override
    public void AlarmChange() {
     if(mService.alarmOn){
      semaforo.setBackgroundResource(R.drawable.tondinoverde);
      bttAttiva.setEnabled(false);
      bttDisattiva.setEnabled(true);
     }
     else {
      semaforo.setBackgroundResource(R.drawable.tondinorosso);
      bttAttiva.setEnabled(true);
      bttDisattiva.setEnabled(false);
     }
      
     
    }
    
   });
E proviamo...

Funziona!

Una cosa importante è che il TimerService si disattivi qualora venga chiusa l'applicazione.
Per fare in modo che allo swipe dalla finestra delle app recenti si elimini tutto c'è questo metodo nel Service:
 @Override
 public void onTaskRemoved(Intent rootIntent){
  stopSelf();
 }
Sperimentiamola.

Sì, okay.
E ho scoperto anche un'altra cosa fondamentale: che se non inserisco startService quando apro il service, questo non è foreground, in quanto sparisce con la sparizione delle activities..
Ecco la correzione in onStart di MainActivity:
 @Override
 public void onStart(){
  super.onStart();
  
  Intent intent=new Intent(this,TimerService.class);
  startService(intent);
  bindService(intent,mConnection,Service.BIND_AUTO_CREATE);
 }

Ricostruzione di CoachCall: secondo passaggio: impostare startTime e stopTime, e i cicli con la terza activity.

Dunque i primi passi sono quelli:
  • Creazione di due activities
  • Creazione di un service foreground;
  • Bindaggio fra activities e service.

Il passo successivo è definire il metodo startTime e stopTime.
Per prima cosa è necessario avere un Handler, all'inizio, fra le dichiarazioni di variabili:
Handler handler=new Handler();
Quindi procedo alla startTime:
 public void startTime(){
  Runnable runnable=new Runnable(){

   @Override
   public void run() {
    Intent intent =new Intent(getApplicationContext(),Telefono.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    startActivity(intent);
    
   }
   
  };
  handler.postDelayed(runnable, 7000);
  
 }
(ho impostato il ritardo a 7 secondi fissi solo temporaneamente e per provare). Questo metodo deve essere innescato dal pulsante "Attiva" della MainActivity:
  bttAttiva=(Button)findViewById(R.id.button1);
  bttAttiva.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    mService.startTime();
    
   }
  });
Il listener viene impostato in onCreate, prima che sia stato istanziato mService, ma siccome si userà sicuramente dopo, è lecito.

Nel frattempo, vado a impostare anche il bottone bttRispondi nella terza Activity, ossia Telefono, in modo che questa possa rimandare a startTime del Service per poi chiudersi, innescando il ciclo.
  bttRispondi=(Button)findViewById(R.id.button1);
  bttRifiuta=(Button)findViewById(R.id.button2);
  
  bttRispondi.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    mService.startTime();
                                finish();
    
   }
  });
(Ci è capitato in mezzo un pezzo del codice di bttRifiuta, che implementerò in seguito...)
Facciamo la prova e vediamo se si innesca il ciclo.

Funziona perfettamente come previsto!


Ora devo poter cancellare la funzione a tempo.


Ma un momento!!!!! L'icona del Service non mi appare!!! Indagando qualche minuto mi rendo conto che ho dimenticato l'istruzione più importante per il Service foreGround! Quindi io stavo lavorando su un service che per pura fortuna non era stato killato, ma che avrebbe potuto venire killato da un momento all'altro!!!

Rifaccio l'onCreate del TimerService...

 public void onCreate(){
  Intent intent=new Intent(this,MainActivity.class);
  PendingIntent pi=PendingIntent.getActivity(this, 0, intent, 0);
  Notification notification=new NotificationCompat.Builder(this)
    .setSmallIcon(R.drawable.cornetta)
    .setContentTitle("CoachCall")
    .setContentText("Telefonate dal Coach")
    .setContentIntent(pi).build();
  startForeground(1,notification);
    
 }
Okay: vediamo se appare la cornetta come icona...

Bene, ora gestiamo la cancellazione del postDelayed.
Creo in TimerService la funzione stopTime:
 public void stopTime(){
  handler.removeCallbacksAndMessages(null);
 }
Semplicissima.
Quindi ne collego l'azione al pulsante bttDisattiva di MainActivity.
  bttDisattiva=(Button)findViewById(R.id.button2);
  bttDisattiva.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    mService.stopTime();
    
   }
  });
Dovrebbe funzionare...

E infatti funziona benissimo!
Imposto la risposta anche per il pulsante bttRifiuta dell'activity Telefono:
  bttRifiuta=(Button)findViewById(R.id.button2);
  bttRifiuta.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    mService.startTime();
    finish();
    
   }
  });
...che per il momento è uguale alla risposta per il pulsante bttRispondi, ossia ricomincia il ciclo, ma successivamente, ovviamente, vi saranno introdotte le dovute differenze.

Ricostruzione da zero della mia App CoachCall, con l'impostazione di un Service in foreground: creazione del server e binding da parte delle due activities.

Ricomincio praticamente da zero.
Opto per un Service in foreground, che rimanga il fulcro di tutta l'applicazione.
Per crearlo, bisogna inserire nel metodo onCreate del Service un intent e un pendingintent, e quindi un oggetto Notification, che è quello che presenta la maggior novità.

Il costruttore è NotificationCompat.Builder(this).
A questo seguono setSmallIcon, setContentText, setContentTitle e setContentIntent.
Il primo fa riferimento a un'immagine, i successivi due a testo, e l'ultimo deve avere come parametro pendingIntent e quindi viene scritto il suo membro build().
Non è una nomenclatura tecnica, ma è efficace per ricordare.
Quindi si invoca startForeground con un numero identificativo che può essere anche 1, e la notifica per parametro.
Quindi la notifica ha bisogno del pendingIntent che ha bisogno dell'intent, e alla fine il metodo startForeground si serve della notifica.
Provo a scriverlo a memoria...

 @Override
 public void onCreate(){
  Intent intent=new Intent(this,MainActivity.class);
  PendingIntent pi=PendingIntent.getActivity(this, 0, intent, 0);
  Notification notification=new NotificationCompat.Builder(this)
    .setSmallIcon(R.drawable.cornetta)
    .setContentTitle("CoachCall")
    .setContentText("Chiamate dal Coach")
    .setContentIntent(pi).build();
    
 }
Ci sono arrivato...

Con questo ho un Service che dovrebbe essere inattaccabile.
Ora, questo service deve essere innescato dall'apertura dell'Activity principale, e ciò si può fare mediante il binding, che è peraltro necessario per tutte le altre activities che debbano interagire con esso eseguendone le funzioni.
Secondo passaggio, quindi, è quello di effettuare il binding.

Solito schema: nell'activity creo le variabili...
 TimerService mService;
 boolean mBound;
quindi procedo...
 @Override
 public void onStart(){
  super.onStart();
  Intent intent=new Intent(this,TimerService.class);
  bindService(intent,mConnection,Service.BIND_AUTO_CREATE);
 }
 
 @Override
 public void onStop(){
  super.onStop();
  if(mBound){
   unbindService(mConnection);
   mBound=false;
  }
 }
 
 ServiceConnection mConnection=new ServiceConnection(){

  @Override
  public void onServiceConnected(ComponentName name, IBinder service) {
   LocalBinder bnd=(LocalBinder)service;
   mService=bnd.getService();
                        mBound=true;
   
  }

  @Override
  public void onServiceDisconnected(ComponentName name) {
   mBound=false;
   
  }
  
 };
E su TimerService:
 IBinder mBinder=new LocalBinder();
e poi...
 @Override
 public IBinder onBind(Intent intent) {
  // TODO Auto-generated method stub
  return mBinder;
 }
 
 public class LocalBinder extends Binder {
  TimerService getService(){
   return TimerService.this;
  }
 }
importando quindi LocalBinder su MainActivity (di solito faccio prima nel Service e poi nell'Activity).
Una volta fatto il bindaggio posso creare le funzioni che chiamano la terza Activity "a tempo".
Per far questo creo la terza Activity.

Ho dato a questa il formato XML di quella che era la mia originale (come del resto ho fatto con MainActivity)...
Faccio il binding.
Una volta fatto il binding, creo i "movimenti circolari" del meccanismo a tempo.

lunedì 1 agosto 2016

Regolazione di AlarmOn.

A ogni cambiamento in TimerService viene attivato il cambiamento dello status di AlarmOn.
 public void StartTime(){
  
  Log.d("STARTTIME", "STARTTIME");
  Runnable runnable=new Runnable(){

   @Override
   public void run() {
    Intent intent=new Intent(getApplicationContext(),Form.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    startActivity(intent); 
   }
  };
  Global.handler.postDelayed(runnable, 5000);
  SharedPreferences.Editor editor=SP.edit();
  editor.putBoolean("alarmon", true);
  editor.commit();
  if(onAlarmChangeListener!=null)onAlarmChangeListener.AlarmChange();
In blu ho marcato il cambiamento del setting "alarmon" a "true" quando viene inizializzato l'alarm.
In rosso, invece, l'attivazione dell'evento AlarmChange.

In MainActivity, non appena si ha il binding del TimerService bisogna desumere lo stato di AlarmOn del TimerService stesso.
Viene impostato il listener su TimerService e si dà corpo all'evento AlarmChange.
 ServiceConnection mConnection=new ServiceConnection(){

  @Override
  public void onServiceConnected(ComponentName name, IBinder service) {
   LocalBinder bnd=(LocalBinder)service;
   mService=bnd.getService();
   mBound=true;
   mService.setOnAlarmChangeListener(new OnAlarmChangeListener(){

    @Override
    public void AlarmChange() {
     if(SP.getBoolean("alarmon", true)){
      semaforo.setBackgroundResource(R.drawable.tondinoverde);
      bttStart.setEnabled(false);
      bttStop.setEnabled(true);
     }else{
      semaforo.setBackgroundResource(R.drawable.tondinorosso);
      bttStart.setEnabled(true);
      bttStop.setEnabled(false);
     }
     
    }
    
   });
Al binding, ossia alla riapertura di MainActivity, si entra in ascolto di AlarmChange.
Ora, in TimerService, AlarmChange() entra in azione anche quando viene impostato il listener:
 public void setOnAlarmChangeListener(OnAlarmChangeListener listener){
  onAlarmChangeListener=listener;
  onAlarmChangeListener.AlarmChange();
 }
Qui è inutile porre la condizione "se onAlarmChangeListener esiste", in quanto questa è proprio la funzione che lo imposta. Dovrebbe funzionare...

Primo approccio allo studio degli handler

Poniamo domande:

Partiamo dall'ipotesi che un Handler metta un Runnable in una catena di messaggi chiamata MessageQueue.
Proviamo a stoppare l'handler senza chiudere l'activity in modo da essere sicuri che si tratti dello stesso handler.
  button=(Button)findViewById(R.id.button1);
  button.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    handler.postDelayed(r, 10000);

   }
  });
  
  button2=(Button)findViewById(R.id.button2);
  button2.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    handler.removeCallbacksAndMessages(null);
    
   }
  });
Sì, è perfettamente riuscito.
Il problema è che, da un'altra activity, il riferimento all'handler si è perso, e non lo si può più ripescare per togliere la callback.