JavascriptProva

sabato 30 luglio 2016

Aggiunta di OnUtteranceListener con il TextToSpeech globale statico.

E ho inserito anche il detector della fine del parlato.
Ho creato la variabile globale onUtteranceCompletedListener:
public class Global {

 public static TextToSpeech tts;
 public static OnUtteranceCompletedListener onUtteranceCompletedListener;
}
Ho inizializzato questa variabile di tipo listener sempre in MainActivity:
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  Log.d("MAIN", "ONCREATE");
  
  Global.tts=new TextToSpeech(getApplicationContext(),new TextToSpeech.OnInitListener() {
   
   @Override
   public void onInit(int status) {
    if(status==TextToSpeech.SUCCESS){
     Global.tts.setLanguage(Locale.ITALIAN);
    }
    
   }
  });
  
  Global.onUtteranceCompletedListener=new TextToSpeech.OnUtteranceCompletedListener(){

   @Override
   public void onUtteranceCompleted(String utteranceId) {
    Log.d("ONUTTERANCE", "MESSAGGIO");
    
   }
   
  };
e poi in Form, dove c'è la vocalizzazione:
  Global.tts.setOnUtteranceCompletedListener(Global.onUtteranceCompletedListener);
  HashMap map=new HashMap();
  map.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "id");
  Global.tts.speak("Ciao, bello, stupido, cretino, aspettiamo che passi il tempo", TextToSpeech.QUEUE_FLUSH, map);
E ho testato la funzione con il messaggio che arriva dopo che la voce ha terminato di parlare:
01-01 04:17:36.808: D/ONUTTERANCE(25944): MESSAGGIO

Inizio dei lavori con il TextToSpeech. Creazione della variabile statica pubblica tts per evitare la latenza di inizializzazione del TextToSpeech.

Impostiamo il TextToSpeech.
Creo una classe chiamata Global, al solo scopo di dichiararvi una variabile pubblica e statica:
import android.speech.tts.TextToSpeech;

public class Global {

 public static TextToSpeech tts;
}
...che sarà inizializzata nella prima Activity ad essere aperta, ossia in MainActivity.
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  Log.d("MAIN", "ONCREATE");
  
  Global.tts=new TextToSpeech(getApplicationContext(),new TextToSpeech.OnInitListener() {
   
   @Override
   public void onInit(int status) {
    if(status==TextToSpeech.SUCCESS){
     Global.tts.setLanguage(Locale.ITALIAN);
    }
    
   }
  });
Bene.
Ora faccio in modo che parli all'apertura di Form, dicendo una cazzata qualunque.
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_form);
  
  Global.tts.speak("Ciao, Stronzo", TextToSpeech.QUEUE_FLUSH, null);
zittendo provvisoriamente il play della suoneria:
  ringtone=RingtoneManager.getRingtone(getApplicationContext(), ringtoneUri);
  //ringtone.play();
E funziona.
In particolare, è notevole il fatto che la voce inizi immediatamente al comparire dell'Activity, segno che il mio workaround per evitare la latenza dovuta all'inizializzazione del TextToSpeech è stata azzeccatissima!

Activity RingtoneSettings con scelta della suoneria personalizzata e durata dell'attesa di risposta

Impostato anche il codice per la seekBar che specifica l'intervallo di attesa della suoneria prima di desistere.
Ecco il codice completo:
public class RingtoneSettings extends Activity {

 TimerService mService;
 boolean mBound;
 SharedPreferences SP;
 Button bttRingtones;
 SeekBar seekBarAttesa;
 TextView txtAttesa;
 
 int attesaMinima=10;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_ringtone_settings);
  seekBarAttesa=(SeekBar)findViewById(R.id.seekBar1);
  txtAttesa=(TextView)findViewById(R.id.textView1);
  SP=(SharedPreferences)getApplicationContext().getSharedPreferences("settings", Context.MODE_PRIVATE);
  
  bttRingtones=(Button)findViewById(R.id.button1);
  bttRingtones.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    mService.StopTime();
    Intent intent=new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
    startActivityForResult(intent,0);
    
   }
  });
  
  seekBarAttesa.setMax(120-attesaMinima);
  int numero=SP.getInt("attesa",attesaMinima);
  int realProgress=numero-attesaMinima;
  txtAttesa.setText(""+numero);
  seekBarAttesa.setProgress(realProgress);
  seekBarAttesa.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   
   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {
    // TODO Auto-generated method stub
    
   }
   
   @Override
   public void onStartTrackingTouch(SeekBar seekBar) {
    // TODO Auto-generated method stub
    
   }
   
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    int realProgress=5*(progress/5);
    int numero=realProgress+attesaMinima;
    txtAttesa.setText(""+numero);
    SharedPreferences.Editor editor=SP.edit();
    editor.putInt("attesa", numero);
    editor.commit();
    
   }
  });
 }
 
 @Override
 public void onActivityResult(int requestCode, int resultCode, Intent data){
  if(resultCode!=RESULT_OK)return;
  Uri ringtoneUri=data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
  SharedPreferences.Editor editor=SP.edit();
  editor.putString("suoneria", ringtoneUri.toString());
  editor.commit();
 }
 
 
 
 @Override
 public void onStart(){
  super.onStart();
  Intent i=new Intent(getApplicationContext(),TimerService.class);
  bindService(i,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;
   
  }
  
 };
  
}

Impostazione di suoneria personalizzata per la mia App.

Come organizzo la mia "schermata" per la regolazione dei settings delle suonerie?
Per prima cosa, mi serve un pulsante che imposti la suoneria personalizzata dell'App, ossia di quella "telefonata virtuale" che viene fatta dal Coach.
Quindi impostare il tempo di attesa prima che il Coach desista.
Dunque mi servono un pulsante e una SeekBar.
Disegno anzitutto il pulsante.

E veniamo al codice:
public class RingtoneSettings extends Activity {

 SharedPreferences SP;
 Button bttRingtones;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_ringtone_settings);
  
  SP=(SharedPreferences)getApplicationContext().getSharedPreferences("settings", Context.MODE_PRIVATE);
  bttRingtones=(Button)findViewById(R.id.button1);
  bttRingtones.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    Intent intent=new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
    startActivityForResult(intent,0);
    
   }
  });
 }
 
 @Override
 public void onActivityResult(int requestCode, int resultCode, Intent data){
  Uri ringtoneUri=data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
  SharedPreferences.Editor editor=SP.edit();
  editor.putString("suoneria", ringtoneUri.toString());
  editor.commit();
 }
 

  
}
Sperimentato e funzionante!

Invece nell'Activity Form, che esegue la suoneria:
  SP=(SharedPreferences)getApplicationContext().getSharedPreferences("settings", Context.MODE_PRIVATE);
  
  String suoneria=SP.getString("suoneria", "");
  if(TextUtils.isEmpty(suoneria)){
   ringtoneUri=RingtoneManager.getActualDefaultRingtoneUri(getApplicationContext(), RingtoneManager.TYPE_RINGTONE);
  }
  else{
   ringtoneUri=Uri.parse(suoneria);
  }
  ringtone=RingtoneManager.getRingtone(getApplicationContext(), ringtoneUri);
  ringtone.play();
  
  TimerTask task=new TimerTask(){

   @Override
   public void run() {
    ringtone.stop(); 
   }
  };
  
  Timer timer=new Timer();
  timer.schedule(task, 10000);
Apre la suoneria memorizzata, se non trova nulla esegue la suoneria di default, mentre se la trova la esegue.

Suoneria personalizzata per l'applicazione.

Ora bisogna personalizzare le suonerie per l'applicazione.
Ci si avvale di SharedPreferences.

Ecco il codice:
public class MainActivity extends Activity {

 Button button;
 Button bttPlay;
 SharedPreferences SP;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
 
  SP=(SharedPreferences)getApplicationContext().getSharedPreferences("settings", Context.MODE_PRIVATE);
  
  
  button=(Button)findViewById(R.id.button1);
  button.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    Intent intent=new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
    intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALL);
    startActivityForResult(intent,0);
    
   }
  });
  
  bttPlay=(Button)findViewById(R.id.button2);
  bttPlay.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    Uri ringtoneUri;
    String suoneria=SP.getString("suoneria", "");
    if(TextUtils.isEmpty(suoneria)){
     ringtoneUri=RingtoneManager.getActualDefaultRingtoneUri(getApplicationContext(), RingtoneManager.TYPE_RINGTONE);
    }else{
     ringtoneUri=Uri.parse(suoneria);
    }
    Ringtone ringtone=RingtoneManager.getRingtone(getApplicationContext(), ringtoneUri);
    ringtone.play();
   }
  });
  
 }
 
 @Override
 public void onActivityResult(int requestCode, int resultCode, Intent data){
  Uri ringtoneUri=data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
  SharedPreferences.Editor editor=SP.edit();
  editor.putString("suoneria", ringtoneUri.toString());
  editor.commit();
 }



}
Funzionante.

Studio sull'impostazione generale delle suonerie.

Adesso dobbiamo fare un bel po' di pratica per quanto riguarda le suonerie.
Questo è il codice di base:
   @Override
   public void onClick(View v) {
    Intent intent=new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
    startActivityForResult(intent,0);
    
   }
  });
  
 }
 @Override
 public void onActivityResult(int requestCode, int resultCode, Intent data){
  Uri ringtoneUri=data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
  Ringtone ringtone=RingtoneManager.getRingtone(getApplicationContext(), ringtoneUri);
  ringtone.play();
 }
Per la scelta della suoneria bisogna scrivere ACTION_RINGTONE_PICKER.
Per "cacciare fuori" la suoneria dai data bisogna scrivere EXTRA_RINGTONE_PICKED_URI, che da un risultato in Uri, dal quale poi si può ricavare la suoneria e suonarla o istituirla come suoneria di default.


Scomponiamo le parti:
Parto dall'Intent. Se io scrivo:
    Intent intent=new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
    startActivity(intent);
mi appare la lista delle Suonerie telefoniche.
Ora vediamo di far apparire la lista delle Suonerie della sveglia, ossia se è sensata la mia ipotesi che ponendo un putExtra con dei particolari parametri si può ottenere questo risultato...

    Intent intent=new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
    intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM);
    startActivity(intent);
Vediamo...

Sì! E' sensata!

Facciamo la stessa cosa con le notifiche:
    Intent intent=new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
    intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION);
    startActivity(intent);
Perfetto! Corrisponde anche qui! Qui vengono elencati i toni per le notifiche.
Proviamo a inserire come parametro value di putExtra TYPE_ALL...
   @Override
   public void onClick(View v) {
    Intent intent=new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
    intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALL);
    startActivity(intent);
    
   }
Perfetto! Così mi appaiono tutte! Identifico il tono CHIME che è una suoneria telefonica, MORNING FLOWER ALARM che è una suoneria della sveglia e WHISTLE che è una suoneria di notifica.

E per quanto riguarda la scelta delle suonerie, abbiamo chiarito!

Ora vediamo la seconda parte, nella quale sarebbe logico ci fosse la possibilità di assegnare un certo tono a diventare suoneria telefonica o di sveglia o di notifica... vediamo... Intanto correggo startActivity con startActivityForResult (quella di prima mi serviva solo per mostrare le liste, senza alcuna conseguenza della scelta)

Fin qui nessun problema:
 @Override
 public void onActivityResult(int requestCode, int resultCode, Intent data){
  Uri ringtoneUri=data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
  
 }
Anzi, faccio un test e provo a farmi scrivere in LogCat l'Uri della suoneria per vedere se le estrae tutte.
  button=(Button)findViewById(R.id.button1);
  button.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    Intent intent=new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
    intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALL);
    startActivityForResult(intent,0);
    
   }
  });
  
 }
 
 @Override
 public void onActivityResult(int requestCode, int resultCode, Intent data){
  Uri ringtoneUri=data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
  Log.d("SUONERIA", ringtoneUri.toString());
 }
Ecco:
07-30 13:24:06.831: D/SUONERIA(19604): content://media/internal/audio/media/41
07-30 13:24:38.552: D/SUONERIA(19604): content://media/internal/audio/media/5
07-30 13:24:53.877: D/SUONERIA(19604): content://media/internal/audio/media/30
...che corrispondono alle suonerie:
  • Chime (suoneria telefonica);
  • Morning Flower Alarm (suoneria sveglia);
  • Whistle (suoneria notifica).
Perfetto!

Andiamo avanti...

Ecco, probabilmente è sull'altra riga che decidiamo in quale serie assegnare una suoneria, mediante il TYPE che diamo come secondo parametro a RingtoneManager.setActualDefaultRingtoneUri.
 @Override
 public void onActivityResult(int requestCode, int resultCode, Intent data){
  Uri ringtoneUri=data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
  RingtoneManager.setActualDefaultRingtoneUri(getApplicationContext(), RingtoneManager.TYPE_RINGTONE, ringtoneUri);
 }
Posso fare la prova.
Assegno come default alla suoneria telefonica Whistle, sarà possibile?

Errore! Ci vuole il permesso WRITE_SETTINGS.
Vado al Manifest...

<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
E assegno anche Chime alle suonerie di notifica...
 @Override
 public void onActivityResult(int requestCode, int resultCode, Intent data){
  Uri ringtoneUri=data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
  RingtoneManager.setActualDefaultRingtoneUri(getApplicationContext(), RingtoneManager.TYPE_NOTIFICATION, ringtoneUri);
 }
Ora vado a vedere fra le impostazioni del cellulare:



Mi sembra perfetto.
Aggiungo Chime alle suonerie della sveglia:
 @Override
 public void onActivityResult(int requestCode, int resultCode, Intent data){
  Uri ringtoneUri=data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
  RingtoneManager.setActualDefaultRingtoneUri(getApplicationContext(), RingtoneManager.TYPE_ALARM, ringtoneUri);
 }
E qui mi trovo in crisi perché esistono tante sveglie ognuna con il suo tono che non è comunque Chime... bene, altra cosa che forse si capirà in seguito, e che comunque adesso poco mi interessa.

Voce sintetica dal ricevitore del cellulare anziché dall'altoparlante.

L'idea successiva è stata quella di mantenere sì la voce, ma di inviarla come una normale telefonata, tramite l'altoparlante del ricevitore telefonico, in modo che possa essere sentita da tutti. Simulare una vera e propria telefonata!
In un paio di giorni al massimo voglio riuscirci.
La sequenza è questa:

  • squilla il telefono
  • l'utente ha facoltà di vedere se la chiamata sia una chiamata reale o una chiamata simulata dalla mia applicazione.
  • se l'utente non risponde, entro un tempo settabile la chiamata cessa.
  • l'utente può, con la pressione di un tasto, rifiutare la chiamata
  • l'utente risponde tramite la pressione di un tasto
  • l'utente sente il messaggio vocale e si comporta di conseguenza.
Creo una Palestra dedicata.
Ecco il codice:
public class MainActivity extends Activity {

 Button button;
 TextToSpeech tts;
 AudioManager audioManager;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  

  
  tts=new TextToSpeech(getApplicationContext(),new TextToSpeech.OnInitListener(){

   @Override
   public void onInit(int status) {
    if(status==TextToSpeech.SUCCESS){
     tts.setLanguage(Locale.ITALIAN);
    }
    
   }
   
  });
  

  
  button=(Button)findViewById(R.id.button1);
  button.setOnClickListener(new View.OnClickListener() {
   
  
   @Override
   public void onClick(View v) {
    audioManager=(AudioManager)getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
    audioManager.setMode(AudioManager.MODE_IN_CALL);
    audioManager.setSpeakerphoneOn(false);
    tts.speak("Nel mezzo del cammin di nostra vita mi ritrovai per una selva oscura", TextToSpeech.QUEUE_FLUSH, null);
    
   }
  });
  

  
 }

 @Override
 public void onDestroy(){
  super.onDestroy();
  audioManager.setSpeakerphoneOn(true);
  audioManager.setMode(AudioManager.MODE_NORMAL);
 }

}
Alla fine ho messo una specie di "riparazione", che non so fino a che punto sia efficace, devo sperimentarla.
Ecco, sperimentazione:
 @Override
 public void onDestroy(){
  super.onDestroy();
  audioManager.setSpeakerphoneOn(true);
  audioManager.setMode(AudioManager.MODE_NORMAL);
  tts.speak("Fine del messaggio", TextToSpeech.QUEUE_FLUSH, null);
 }
Funziona, funziona...

venerdì 29 luglio 2016

Workaround per disabilitare il tasto Recent Tasks.

Ho trovato il codice per disabilitare il tasto Recent Tasks (almeno, spero che funzioni sempre e non ci siano "bug" inaspettati).
 @Override
 public void onPause(){
  super.onPause();
  ActivityManager activityManager=(ActivityManager)getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
  activityManager.moveTaskToFront(getTaskId(), 0);
  Log.d("TASKID", ""+getTaskId());
 }
Con il permesso nel Manifest:
<uses-permission android:name="android.permission.REORDER_TASKS" />
Così posso evitare quelle incongrue interruzioni del programma che creano uno scompaginamento totale del flusso dell'applicazione non chiudendo a dovere le componenti secondo le modalità prestabilite. E ora posso andare avanti!

OnDestroy e StartTime

Ci sono un po' di cose da sistemare.
Vediamo l'effetto del tasto Indietro sul Form.
Quando inizia la suoneria e appare Form, il tasto indietro produce l'evento OnDestroy:
07-29 13:28:31.029: D/FORM(20401): ONDESTROY
Perché dunque la suoneria continua a suonare?
Perché non è arrivato il messaggio ringtone.stop(), il quale è stato impostato per il Button insieme a finish(), ma non per il finish() dovuto alla pressione del tasto Indietro.
Posso ovviare mettendo ringtone.stop() in associazione a onDestroy() in modo che chiamando onDestroy(), sia con il Button virtuale tramite il finish(), sia con il tasto fisico Indietro, l'effetto sia sempre quello di stoppare la suoneria.
07-29 13:33:32.022: D/MAIN(20401): ONDESTROY
Esatto!
Procedo alla modifica: nel codice del button lascio solo finish():
  button=(Button)findViewById(R.id.button1);
  button.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    mService.StartTime();
    finish();
    
   }
  });
...e metto ringtone.stop() nell'evento onDestroy():
 @Override
 public void onDestroy(){
  super.onDestroy();
  ringtone.stop();
  Log.d("FORM","ONDESTROY");
 }
E ora provo: funziona!
Ma c'è un altro problema: sia con il tasto indietro sia spazzando via la finestrella recent dopo la pressione del tasto Overview, l'Alarm rimane impostato ma non si avvia un nuovo ciclo di tempo.
Ovviamente, devo fare lo stesso procedimento di cui sopra con mService.startTime().
Lo faccio:
  button=(Button)findViewById(R.id.button1);
  button.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    finish();
    
   }
  });
 @Override
 public void onDestroy(){
  super.onDestroy();
  ringtone.stop();
  mService.StartTime();
  Log.d("FORM","ONDESTROY");
 }
E vediamo...

Sì, funziona.
Ma c'è una differenza di fondo fra Indietro e Overview.
Nel primo caso, posso anche usare il tasto per togliere di mezzo la finestra e aspettare il prossimo intervallo, mentre nel secondo caso in genere uso questa manovra per cessare l'applicazione.
L'unico modo che avevo previsto per cessare l'applicazione era impostare Disattiva per l'Alarm e quindi chiudere l'activity Main... ma come faccio adesso a differenziare il tasto Indietro con lo spazzamento del Task dalla finestra dei Recents?

"Risponditore automatico" per la suoneria nell'activity Form

Bene.
Impostiamo una suoneria limitata nel tempo per la mia app.
Se qualcuno ci telefona, dopo qualche minuto si scoccerà di aspettare, e smetterà, salvo poi andare noi a vedere dopo chi ci ha telefonato.
Cerco di fare uguale.

Mi avvalgo del TimerTask.
TimerTask e Timer si istenziano con un costruttore semplice semplice, senza parametri.

Ecco, io metto nel codice di Form, alla onCreate, questo:
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_form);
  
  ringtoneUri=RingtoneManager.getActualDefaultRingtoneUri(getApplicationContext(), RingtoneManager.TYPE_RINGTONE);
  ringtone=RingtoneManager.getRingtone(getApplicationContext(), ringtoneUri);
  ringtone.play();
  
  TimerTask task=new TimerTask(){

   @Override
   public void run() {
    ringtone.stop(); 
   }
  };
  
  Timer timer=new Timer();
  timer.schedule(task, 5000);
che dovrebbe lasciare che la suoneria inizi a suonare, e venga bloccata dopo 5 secondi automaticamente.
Proviamo...

Sì, funziona egregiamente! 5 secondi sono pochi, forse lo sono anche 10, come ho poi impostato... ma l'importante è che il principio funzioni, poi magari il tempo di attesa si può impostare.

DialogFragment

Ecco i Dialog.
Mi pare di aver toccato in passato questo argomento, a proposito delle finestre di dialogo con le opzioni OK e Annulla e simili... ma non tanto da aver memorizzato qualcosa di utile.

Quello che può fare al caso mio è il DialogFragment, in modo da porgere all'utente l'informazione in modo obbligatorio, senza che magari una pressione involontaria di tasti possa far passare in secondo piano l'oggetto che presenta l'informazione.

Ci provo, poi a seconda dell'uso modificherò la cosa.

Questo è il tutorial di riferimento Ho una classe che estende DialogFragment.
public class FireMissilesDialogFragment extends DialogFragment {
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
Ecco la classe che estende DialogFragment.
Ha un metodo onCreateDialog, che ha per parametro Bundle SavedInstanceState. Come per le activities.
E' di tipo Dialog, però, non void.
E vediamola...
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // Use the Builder class for convenient dialog construction
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setMessage(R.string.dialog_fire_missiles)
Istanzia un oggetto builder di classe AlertDialog.Builder, mettendo come parametro getActivity().
Quindi usa il metodo setMessage con una stringa per parametro.
Quindi, in sintesi, istanzia AlertDialog.Builder con parametro getActivity() e usa il metodo setMessage di questo oggetto con una stringa per parametro.

Quindi abbiamo i metodi setPositiveButton e setNegativeButton che (ecco l'anomalia che ricordavo!) vanno scritti di seguito a .setMessage, e hanno come parametro una stringa e new DialogInterface.onClickListener.
public class FireMissilesDialogFragment extends DialogFragment {
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // Use the Builder class for convenient dialog construction
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setMessage(R.string.dialog_fire_missiles)
               .setPositiveButton(R.string.fire, new DialogInterface.OnClickListener() {
                   public void onClick(DialogInterface dialog, int id) {
                       // FIRE ZE MISSILES!
                   }
               })
               .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
                   public void onClick(DialogInterface dialog, int id) {
                       // User cancelled the dialog
                   }
               });
        // Create the AlertDialog object and return it
        return builder.create();
    }
}
Ma a questo punto forse si fa prima a scriverlo...

Creiamo dunque la classe che estende DialogFragment:
 public class Dialogo extends DialogFragment{
  
 }
Ora va overridata onCreateDialog
 public class Dialogo extends DialogFragment{
  @Override
  public Dialog onCreateDialog(Bundle savedInstanceState){
   
   return null;
   
  }
 }
Ora si istanzia AlertDialog.Builder builder.
 public class Dialogo extends DialogFragment{
  @Override
  public Dialog onCreateDialog(Bundle savedInstanceState){
   AlertDialog.Builder builder=new AlertDialog.Builder(getActivity());
   return null;
   
  }
 }
(getActivity() è presentato come parametro di tipo Context. Forse andrà bene anche getApplicationContext()... sì, va bene, l'ho provato: non so se riferirsi al contesto dell'applicazione piuttosto che dell'activity possa creare qualche problema...)

Quindi si usa .setMessage con una stringa per parametro. Usando anche .setPositiveButton che oltre a una stringa richiede new DialogInterface.OnClickListener.
   builder.setMessage("Ciao, Ciccio Bello!")
   .setPositiveButton("Ciao", new DialogInterface.OnClickListener() {

    @Override
    public void onClick(DialogInterface dialog, int which) {
     // TODO Auto-generated method stub
     
    
Ecco, la cosa dovrebbe essere quasi completa.
Imposto anche il negative button:
   builder.setMessage("Ciao, Ciccio Bello!")
   .setPositiveButton("Ciao", new DialogInterface.OnClickListener() {

    @Override
    public void onClick(DialogInterface dialog, int which) {
     // TODO Auto-generated method stub
     
    }
    
   })
   .setNegativeButton("No", new DialogInterface.OnClickListener() {
    
    @Override
    public void onClick(DialogInterface dialog, int which) {
     // TODO Auto-generated method stub
     
    }
   });
   return null;
Quindi si corregge return null con return builder.create:
 public class Dialogo extends DialogFragment{
  @Override
  public Dialog onCreateDialog(Bundle savedInstanceState){
   AlertDialog.Builder builder=new AlertDialog.Builder(getApplicationContext());
   builder.setMessage("Ciao, Ciccio Bello!")
   .setPositiveButton("Ciao", new DialogInterface.OnClickListener() {

    @Override
    public void onClick(DialogInterface dialog, int which) {
     // TODO Auto-generated method stub
     
    }
    
   })
   .setNegativeButton("No", new DialogInterface.OnClickListener() {
    
    @Override
    public void onClick(DialogInterface dialog, int which) {
     // TODO Auto-generated method stub
     
    }
   });
   return builder.create();
   
  }
 }
Ed eccola completa!

Ora rompo e ricostruisco come esercizio mnemonico...

Eccola ricostruita sbirciando un poco:
 public class Dialogo extends DialogFragment{
  @Override
  public Dialog onCreateDialog(Bundle savedInstanceState){
   AlertDialog.Builder builder=new AlertDialog.Builder(getActivity());
   builder.setMessage("Vuoi fare questa azione?")
   .setPositiveButton("SI", new DialogInterface.OnClickListener() {
    
    @Override
    public void onClick(DialogInterface dialog, int which) {
     Log.d("SCELTA","POSITIVA");
     
    }
   })
   .setNegativeButton("NO", new DialogInterface.OnClickListener() {
    
    @Override
    public void onClick(DialogInterface dialog, int which) {
     Log.d("SCELTA", "NEGATIVA");
     
    }
   });
   
   return builder.create();
   
  }
 }
Ora come la uso?

Il tutorial dice "create an instance of this class and call show". Proviamoci.
  button=(Button)findViewById(R.id.button1);
  button.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    Dialogo mDialogo=new Dialogo();
    mDialogo.show(getFragmentManager(),"boh");
    
   }
  });
Non ho ben capito cosa siano i due parametri di show, che ho compilato un po' a cavolo, ma di fatto la finestra compare! E le scelte sono congruenti:
07-29 12:29:25.111: D/SCELTA(12881): NEGATIVA
07-29 12:29:33.569: D/SCELTA(12881): POSITIVA
07-29 12:29:56.431: D/SCELTA(12881): NEGATIVA
L'unica perplessità è che il tasto negativo viene a sinistra del positivo invece che a destra come mi aspettavo dall'ordine nel codice. Sarà così...

Ora, questa finestra di dialogo non viene eliminata se si clicca sull'activity in sottofondo, ma lo viene se si clicca sui tasti indietro, home e recents.
Praticamente non c'è differenza con un'activity a tutto schermo nella quale non si può cliccare su altra activity se non la stessa. Bisognerebbe trovare il modo di renderla veramente modale, ma adesso faccio la mia applicazione come avevo preventivato, senza cercare il modo di fare finestre modali, quindi si vedrà.

giovedì 28 luglio 2016

Studio sulla finestra dei recents.

Ora vediamo le modifiche degli status quando si passi da un'activity a un'altra mediante un Intent.

Dopo aver inserito nell'altra Activity FORM tutti i markers dei cambi di status, inserisco in MAIN il codice per passare a FORM.
 @Override
 public void onStart(){
  super.onStart();
  Log.v("MAIN", "ONSTART");
  Intent intent =new Intent(this,Form.class);
  startActivity(intent);
 }
Ecco:
07-28 18:44:55.776: V/MAIN(17395): ONCREATE
07-28 18:44:55.776: V/MAIN(17395): ONSTART
07-28 18:44:55.796: V/MAIN(17395): ONRESUME
07-28 18:44:55.836: V/MAIN(17395): ONPAUSE
07-28 18:44:56.046: V/FORM(17395): ONCREATE
07-28 18:44:56.046: V/FORM(17395): ONSTART
07-28 18:44:56.046: V/FORM(17395): ONRESUME
07-28 18:44:56.146: V/MAIN(17395): ONSTOP

Dunque MAIN va prima in "paused", quindi si crea FORM e una volta completate le tre fasi della creazione di FORM, MAIN va in "stopped".

Quando poi vado al tasto Overview:
07-28 18:47:47.774: V/FORM(17395): ONPAUSE
07-28 18:47:48.395: V/FORM(17395): ONSTOP
anche FORM va in "stopped".

Quindi spazzo via le activities dalla finestra "recenti":
07-28 18:51:55.325: V/MAIN(17986): ONDESTROY
07-28 18:51:55.365: V/FORM(17986): ONDESTROY
Ora, cosa succede con l'istruzione finish()?
Ci provo: la assegno a un Button.
  button=(Button)findViewById(R.id.button1);
  button.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    finish();
    
   }
  });
07-28 23:33:14.535: V/MAIN(10805): ONPAUSE
07-28 23:33:15.196: V/MAIN(10805): ONSTOP
07-28 23:33:15.196: V/MAIN(10805): ONDESTROY
Ciononostante, la finestra dell'app rimane sui recents... Quindi stare sui recents non implica l'esistenza della finestra.
Cliccando sulla finestra:
07-28 23:36:36.122: V/MAIN(11725): ONCREATE
07-28 23:36:36.122: V/MAIN(11725): ONSTART
07-28 23:36:36.122: V/MAIN(11725): ONRESUME
Si ricrea da capo.

Esperimenti sulle azioni dei tasti INDIETRO e OVERVIEW.

Digressione importante.
Dato che ho difficoltà con la gestione delle eventuali pressioni dei pulsanti che potrebbero mandare a pallino le mie applicazioni, credo di dover fare tutta una sessione sul modo di far entrare nel codice la programmazione degli eventi di questi pulsanti.

Costruisco un'applicazione inutile con un MainActivity e un Service bindato ad essa, ma con tutti i "marker" sugli eventi onCreate, onStart, onResume, onPause, onStop, onDestroy, per studiare le modalità di cambiamento degli "status" con la pressione dei diversi tasti del cellulare.

Questo è ciò che avviene all'inizio:
07-28 14:47:43.211: V/MAIN(3942): ONCREATE
07-28 14:47:43.211: V/MAIN(3942): ONSTART
07-28 14:47:43.221: V/MAIN(3942): ONRESUME
07-28 14:47:43.241: D/SERVIZIO(3942): ONCREATE
Main segue la progressione di creazione, start, resume. Service viene creato in quanto bindato dall'evento onStart di Main.

Ora schiaccio il tasto INDIETRO.
07-28 14:48:03.110: V/MAIN(3942): ONPAUSE
07-28 14:48:03.751: V/MAIN(3942): ONSTOP
07-28 14:48:03.751: V/MAIN(3942): ONDESTROY
07-28 14:48:03.761: D/SERVIZIO(3942): ONDESTROY
Sembra la perfetta reversibilità dell'avvio...

Riavvio Main:
07-28 14:49:09.765: V/MAIN(4195): ONCREATE
07-28 14:49:09.765: V/MAIN(4195): ONSTART
07-28 14:49:09.775: V/MAIN(4195): ONRESUME
07-28 14:49:09.795: D/SERVIZIO(4195): ONCREATE
Ora schiaccio il tasto OVERVIEW:
07-28 14:50:01.986: V/MAIN(4195): ONPAUSE
07-28 14:50:02.647: V/MAIN(4195): ONSTOP
07-28 14:50:02.657: D/SERVIZIO(4195): ONDESTROY
Bene, MAIN non viene distrutto, ma viene distrutto il SERVICE in relazione all'evento ONSTOP di MAIN.

Rischiaccio il tasto OVERVIEW:
07-28 14:51:09.682: V/MAIN(4195): ONSTART
07-28 14:51:09.682: V/MAIN(4195): ONRESUME
07-28 14:51:09.722: D/SERVIZIO(4195): ONCREATE
Esattamente reversibile al primo click.

Conclusioni per il momento:
  1. Il tasto INDIETRO distrugge completamente un'activity
  2. Il tasto OVERVIEW riporta l'activity allo stato "stoppato" ma non la distrugge completamente.


Ora provo solo con una MAIN svincolata da qualsiasi SERVICE (rimuovo il codice per il bindaggio)
07-28 14:54:26.654: V/MAIN(4401): ONCREATE
07-28 14:54:26.654: V/MAIN(4401): ONSTART
07-28 14:54:26.654: V/MAIN(4401): ONRESUME
(come di prassi, all'apertura, senza eventi del SERVICE).

Tasto INDIETRO:
07-28 14:55:24.421: V/MAIN(4401): ONPAUSE
07-28 14:55:25.261: V/MAIN(4401): ONSTOP
07-28 14:55:25.261: V/MAIN(4401): ONDESTROY
che è giusto...

Ora riavvio e poi premo il tasto OVERVIEW:
07-28 14:56:12.998: V/MAIN(4401): ONCREATE
07-28 14:56:12.998: V/MAIN(4401): ONSTART
07-28 14:56:12.998: V/MAIN(4401): ONRESUME

..........

07-28 14:56:35.350: V/MAIN(4401): ONPAUSE
07-28 14:56:35.950: V/MAIN(4401): ONSTOP
Ho le finestrelle impilate...
Ora ripremo il tasto OVERVIEW:
07-28 14:57:12.016: V/MAIN(4401): ONSTART
07-28 14:57:12.016: V/MAIN(4401): ONRESUME
Bene: comportamento perfettamente univoco.

mercoledì 27 luglio 2016

Aggiustamento dati letti dalle SharedPreferences sugli intervalli e correzione di un bug dovuto all'interdipendenza delle SeekBars.

Devo aggiustare i tempi in relazione ai dati letti nelle SharedPreferences.
Leggo le SharedPreferences direttamente in TimerService.
  AlarmManager alarmManager=(AlarmManager)getApplicationContext().getSystemService(Context.ALARM_SERVICE);
  int intervalloMin=SP.getInt("intervalloMin", 0);
  int intervalloMax=SP.getInt("intervalloMax",0);
  int delta=intervalloMax-intervalloMin;
Ho preso i due intervalli e ne calcolo la differenza.
Ecco come ho sistemato la cosa:
 public void StartTime(){
  Intent intent=new Intent(getApplicationContext(),
        Form.class);
  PendingIntent pendingIntent=PendingIntent.getActivity(getApplicationContext(), 
                0,
                intent, 
                0);
  AlarmManager alarmManager=(AlarmManager)getApplicationContext().getSystemService(Context.ALARM_SERVICE);
  int intervalloMin=SP.getInt("intervalloMin", 0);
  int intervalloMax=SP.getInt("intervalloMax",0);
  int delta=intervalloMax-intervalloMin;
  Random rnd=new Random();
  int casuale=rnd.nextInt(delta+1);
  int tempo=intervalloMin+casuale;
  
  alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 
      SystemClock.elapsedRealtime()+tempo*60*1000, 
      pendingIntent);
  if(mAlarmChangeListener!=null) mAlarmChangeListener.AlarmChange();
Il fatto di prendere la differenza più uno mi salva dal bug della differenza pari a zero, che si manifesta quando il parametro di Random.nextInt sia zero.
Comunque funziona.

Ora devo rivedere come si fa a scegliere la suoneria...

Ma prima ecco un altro erroraccio!
Ho ottenuto ugualmente un errore dovuto al fatto che la differenza fra intervalloMin e intervalloMax era negativa.
Semplicemente avevo omesso di salvare le SharedPreferencesi di ambedue le SeekBars una volta mossa una di queste: infatti esse sono interdipendenti con lo scopo di evitare il paradosso di un intervalloMin superiore a un intervalloMax.
Ecco il codice corretto:
  seekBarMin.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   
   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {
    SharedPreferences.Editor editor=SP.edit();
    editor.putInt("intervalloMin", seekBar.getProgress());
    editor.putInt("intervalloMax",seekBarMax.getProgress());
    editor.commit();

.....................

  seekBarMax.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   
   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {
    SharedPreferences.Editor editor=SP.edit();
    editor.putInt("intervalloMax", seekBar.getProgress());
    editor.putInt("intervalloMin", seekBarMin.getProgress());
    editor.commit();
E ora funziona.

Bindaggio dell'Activity Impostazioni con il TimerService per l'interruzione dell'AlarmManager quando vengono regolate le impostazioni degli intervalli

Voglio che la regolazione delle SeekBar blocchi l'AlarmManager se è in funzione.
Per far questo devo bindare l'attività TimeSettings al TimerService.

public class TimeSettings extends Activity {

 SeekBar seekBarMin, seekBarMax;
 TextView textViewMin,textViewMax;
 SharedPreferences SP;
 
 TimerService mService;
 boolean mBound;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_time_settings);
  
  SP=getApplicationContext().getSharedPreferences("settings", Context.MODE_PRIVATE);
  seekBarMin=(SeekBar)findViewById(R.id.seekBar1);
  seekBarMin.setMax(240);
  
  
  seekBarMax=(SeekBar)findViewById(R.id.seekBar2);
  seekBarMax.setMax(240);
  
  int intervalloMin=SP.getInt("intervalloMin", 0);
  int intervalloMax=SP.getInt("intervalloMax", 0);
  
  seekBarMin.setProgress(intervalloMin);
  seekBarMax.setProgress(intervalloMax);
  
  textViewMin=(TextView)findViewById(R.id.textView1);
  setTextViewFromSeekBar(intervalloMin,textViewMin);
  textViewMax=(TextView)findViewById(R.id.textView2);
  setTextViewFromSeekBar(intervalloMax,textViewMax);
  
  seekBarMin.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   
   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {
    SharedPreferences.Editor editor=SP.edit();
    editor.putInt("intervalloMin", seekBar.getProgress());
    editor.commit();
    
   }
   
   @Override
   public void onStartTrackingTouch(SeekBar seekBar) {
    mService.StopTime();
    
   }
   
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    setTextViewFromSeekBar(progress,textViewMin);
    if(progress>seekBarMax.getProgress())seekBarMax.setProgress(progress);
   }
  });
  
  
  seekBarMax.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   
   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {
    SharedPreferences.Editor editor=SP.edit();
    editor.putInt("intervalloMax", seekBar.getProgress());
    editor.commit();
    
   }
   
   @Override
   public void onStartTrackingTouch(SeekBar seekBar) {
    mService.StopTime();
    
   }
   
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    setTextViewFromSeekBar(progress,textViewMax);
    if(progress<seekBarMin.getProgress())seekBarMin.setProgress(progress);
   }
  });
 }
 
 private void setTextViewFromSeekBar(int numero,TextView textView){
  TextView t=textView;
  int intero=10*(numero/10);
  int ore=intero/60;
  int minuti=intero%60;
  
  String stringaDelleOre;
  String stringaDeiMinuti;
  String stringaEt="";
  
  if(ore==0){
   stringaDelleOre="";
  }
  else if(ore==1){
   stringaDelleOre=ore+" ORA "; 
  }
  else{
   stringaDelleOre=ore+" ORE ";     
  }    
  if(minuti==0){
   stringaDeiMinuti="";     
  }
  else if(minuti==1){
   stringaDeiMinuti=minuti+" MINUTO ";          
  }
  else{
   stringaDeiMinuti=minuti+" MINUTI ";
  }    
  if(ore!=0 && minuti!=0) stringaEt=" E ";
  if(ore==0 && minuti==0) {
   stringaDelleOre=ore+" ORE";
   stringaEt= " E ";
   stringaDeiMinuti=minuti+" MINUTI";
  }

  t.setText(stringaDelleOre+stringaEt+stringaDeiMinuti);
 }
 
 @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==true){
   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;
   
  }
  
 };


}
Proviamolo...

Funziona! Riuscito al primo colpo!

Sistemazione delle SharedPreferences delle SeekBars

Bene. Ora non devo fare altro che salvare i settings quando si muovono le SeekBars.
Non sono più tanto sicuro se sia meglio memorizzare in minuti l'intervallo minimo e l'intervallo massimo, oppure l'intervallo minimo e il range.
Credo che sia meglio memorizzare i primi, per cui devo rivedere la nomenclatura e la gestione dei miei SharedPreferences...

Ecco, ho corretto.
  seekBarMin.setProgress(intervalloMin);
  seekBarMax.setProgress(intervalloMax);
  
  textViewMin=(TextView)findViewById(R.id.textView1);
  setTextViewFromSeekBar(intervalloMin,textViewMin);
  textViewMax=(TextView)findViewById(R.id.textView2);
  setTextViewFromSeekBar(intervalloMax,textViewMax);
Farò poi i conti quando si dovrà usare matematicamente la differenza fra i due intervalli.

Ora provvedo al salvataggio. Il salvataggio dovrà avvenire una volta che si lasci la pressione sulla SeekBar.
  seekBarMin.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   
   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {
    SharedPreferences.Editor editor=SP.edit();
    editor.putInt("intervalloMin", seekBar.getProgress());
    editor.commit();

............

  seekBarMax.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   
   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {
    SharedPreferences.Editor editor=SP.edit();
    editor.putInt("intervalloMax", seekBar.getProgress());
    editor.commit();
E vediamo se funziona...

FUNZIONA! Mantiene le impostazioni! E le TextViews sono in linea con i valori!

Aggiustamento iniziale delle TextView che mostrano i valori delle SeekBar, con creazione di una funzione specifica.

Ora devo pensare ai Settings da salvare in memoria.
Trattandosi di memoria accessibile da ogni parte del programma, si può agire in modo indipendente dalle varia activities.

Inizio a definire le SharedPreferences all'activity TimeSettings.
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_time_settings);
  
  SP=getApplicationContext().getSharedPreferences("settings", Context.MODE_PRIVATE);
Ora dovrei incrociare questi dati con le SeekBars.
Il nome generale delle impostazioni è "settings", ma in particolare i dati devono contenere il tempo in minuti dell'intervallo fra gli avvisi e il tempo in minuti della variabilità dell'intervallo.
Quindi li chiamerò "intervallo" e "range".
Detto questo, bisogna innanzitutto settare i valori delle SeekBars a questi valori: la prima va settata a "intervallo", mentre la seconda a "intervallo" + "range".
I valori di default saranno zero e zero.

Inserisco il codice necessario nell'attività TimeSettings...

Ho spostato la riga che regola il valore massimo della SeekBar per ragioni di ordine, e ho aggiunto il codice che legge intervallo e range dai settings, con valore di default zero.
  seekBarMin=(SeekBar)findViewById(R.id.seekBar1);
  seekBarMin.setMax(240);
  
  
  seekBarMax=(SeekBar)findViewById(R.id.seekBar2);
  seekBarMax.setMax(240);
  
  int intervallo=SP.getInt("intervallo", 0);
  int range=SP.getInt("range", 0);
Ecco poi quello che regola la posizione delle SeekBars, tutto insieme, con qualche riga di codice che permette di stabilire dove ci troviamo:
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_time_settings);

                SP=getApplicationContext().getSharedPreferences("settings", Context.MODE_PRIVATE);
  seekBarMin=(SeekBar)findViewById(R.id.seekBar1);
  seekBarMin.setMax(240);
  
  
  seekBarMax=(SeekBar)findViewById(R.id.seekBar2);
  seekBarMax.setMax(240);
  
  int intervallo=SP.getInt("intervallo", 0);
  int range=SP.getInt("range", 0);
  
  seekBarMin.setProgress(intervallo);
  seekBarMax.setProgress(intervallo+range);
Quindi anche il valore iniziale delle TextView va stabilito. Ma vediamo se si stabilisce da solo, ossia se il setProgress delle SeekBars equivale a una modifica dei valori di esse tale da impostare le TextViews secondo il codice scritto in precedenza...

No. Devo provvedere io a impostare le TextViews inizialmente.
Ma ecco che in pochi secondi compatto tutto il codice che compila le TextView alla variazione di SeekBar, in modo da usare la funzione agilmente anche qui, oltre che nel codice delle SeekBar:
 private void setTextViewFromSeekBar(int numero,TextView textView){
  TextView t=textView;
  int intero=10*(numero/10);
  int ore=intero/60;
  int minuti=intero%60;
  
  String stringaDelleOre;
  String stringaDeiMinuti;
  String stringaEt="";
  
  if(ore==0){
   stringaDelleOre="";
  }
  else if(ore==1){
   stringaDelleOre=ore+" ORA "; 
  }
  else{
   stringaDelleOre=ore+" ORE ";     
  }    
  if(minuti==0){
   stringaDeiMinuti="";     
  }
  else if(minuti==1){
   stringaDeiMinuti=minuti+" MINUTO ";          
  }
  else{
   stringaDeiMinuti=minuti+" MINUTI ";
  }    
  if(ore!=0 && minuti!=0) stringaEt=" E ";
  if(ore==0 && minuti==0) {
   stringaDelleOre=ore+" ORE";
   stringaEt= " E ";
   stringaDeiMinuti=minuti+" MINUTI";
  }

  t.setText(stringaDelleOre+stringaEt+stringaDeiMinuti);
 }
e il codice delle SeekBars diventa:
        seekBarMin.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   
   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {
    // TODO Auto-generated method stub
    
   }
   
   @Override
   public void onStartTrackingTouch(SeekBar seekBar) {
    // TODO Auto-generated method stub
    
   }
   
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    setTextViewFromSeekBar(progress,textViewMin);
    if(progress>seekBarMax.getProgress())seekBarMax.setProgress(progress);
   }
  });
  
  
  seekBarMax.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   
   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {
    // TODO Auto-generated method stub
    
   }
   
   @Override
   public void onStartTrackingTouch(SeekBar seekBar) {
    // TODO Auto-generated method stub
    
   }
   
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    setTextViewFromSeekBar(progress,textViewMax);
    if(progress<seekBarMin.getProgress())seekBarMin.setProgress(progress);
   }
  });
        }
e funziona...

Ecco quindi la modifica iniziale delle TextView:
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_time_settings);
  
  SP=getApplicationContext().getSharedPreferences("settings", Context.MODE_PRIVATE);
  seekBarMin=(SeekBar)findViewById(R.id.seekBar1);
  seekBarMin.setMax(240);
  
  
  seekBarMax=(SeekBar)findViewById(R.id.seekBar2);
  seekBarMax.setMax(240);
  
  int intervallo=SP.getInt("intervallo", 0);
  int range=SP.getInt("range", 0);
  
  seekBarMin.setProgress(intervallo);
  seekBarMax.setProgress(intervallo+range);
  
  textViewMin=(TextView)findViewById(R.id.textView1);
  setTextViewFromSeekBar(intervallo,textViewMin);
  textViewMax=(TextView)findViewById(R.id.textView2);
  setTextViewFromSeekBar(intervallo+range,textViewMax);
che funziona egregiamente anch'essa!

Sistemazione delle SeekBars per gli intervalli fra le segnalazioni.

Con qualche "colpo di copia-incolla" ho tolto il codice XML che rappresentava le SeekBars dall'attività principale e lo ho messe su una nuova attività chiamata TimeSettings.
Dato che le shapes sono già nella cartella drawables vengono recepite anche qui, e così le due bars mi vengono già ben formattate da un punto di vista estetico.

Ho impostato il massimo a 240 minuti (4 ore)
Ora, per rendere la scala graduata a 10 minuti, devo fare, come già fatto prima nell'altra mia prova, la divisione intera del valore della barra per 10, e poi moltiplicare per 10.
Ecco:
public class TimeSettings extends Activity {

 SeekBar seekBarMin, seekBarMax;
 TextView textViewMin,textViewMax;
 int intero;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_time_settings);
  seekBarMin=(SeekBar)findViewById(R.id.seekBar1);
  seekBarMax=(SeekBar)findViewById(R.id.seekBar2);
  textViewMin=(TextView)findViewById(R.id.textView1);
  textViewMax=(TextView)findViewById(R.id.textView2);
  
  seekBarMin.setMax(240);  //4 ore * 60 minuti
  seekBarMin.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   
   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {
    // TODO Auto-generated method stub
    
   }
   
   @Override
   public void onStartTrackingTouch(SeekBar seekBar) {
    // TODO Auto-generated method stub
    
   }
   
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    intero=10*(progress/10);
    textViewMin.setText(""+intero);
    
   }
  });
 }


}
E funziona: sulla TextBarMin appaiono i minuti, in cambiamenti discreti di 10 minuti per volta.

Ora devo commutare in ore e minuti, tremite un altro calcolo matematico: divisione intera per 60 e modulo 60.
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    intero=10*(progress/10);
    int ore=intero/60;
    int minuti=intero%60;
    textViewMin.setText(ore+" ORE : "+minuti+" MINUTI");
E verifichiamo...

Funziona.

Una finezza: se si tratta di 1 ora, appare scritto "ORA" e non "ORE". Più "umano" e attraente:
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    intero=10*(progress/10);
    int ore=intero/60;
    int minuti=intero%60;
    String strOra="ORE";
    if(ore==1)strOra="ORA";
    textViewMin.setText(ore+" "+strOra+" E "+minuti+" MINUTI");
    
   }
Okay.


Ho sistemato le due SeekBars, con una serie di finezze che rendono il testo molto "human friendly":
  seekBarMin.setMax(240);  //4 ore * 60 minuti
  seekBarMin.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   
   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {
    // TODO Auto-generated method stub
    
   }
   
   @Override
   public void onStartTrackingTouch(SeekBar seekBar) {
    // TODO Auto-generated method stub
    
   }
   
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    int intero=10*(progress/10);
    int ore=intero/60;
    int minuti=intero%60;
    
    String stringaDelleOre;
    String stringaDeiMinuti;
    String stringaEt="";
    
    if(ore==0){
     stringaDelleOre="";
    }
    else if(ore==1){
     stringaDelleOre=ore+" ORA "; 
    }
    else{
     stringaDelleOre=ore+" ORE ";     
    }    
    if(minuti==0){
     stringaDeiMinuti="";     
    }
    else if(minuti==1){
     stringaDeiMinuti=minuti+" MINUTO ";          
    }
    else{
     stringaDeiMinuti=minuti+" MINUTI ";
    }    
    if(ore!=0 && minuti!=0) stringaEt=" E ";
    if(ore==0 && minuti==0) {
     stringaDelleOre=ore+" ORE";
     stringaEt= " E ";
     stringaDeiMinuti=minuti+" MINUTI";
    }

    textViewMin.setText(stringaDelleOre+stringaEt+stringaDeiMinuti);
    
    if(progress>seekBarMax.getProgress())seekBarMax.setProgress(progress);
   }
  });
  
  seekBarMax.setMax(240);
  seekBarMax.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   
   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {
    // TODO Auto-generated method stub
    
   }
   
   @Override
   public void onStartTrackingTouch(SeekBar seekBar) {
    // TODO Auto-generated method stub
    
   }
   
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    int intero=10*(progress/10);
    int ore=intero/60;
    int minuti=intero%60;
    
    String stringaDelleOre;
    String stringaDeiMinuti;
    String stringaEt="";
    
    if(ore==0){
     stringaDelleOre="";
    }
    else if(ore==1){
     stringaDelleOre=ore+" ORA "; 
    }
    else{
     stringaDelleOre=ore+" ORE ";     
    }    
    if(minuti==0){
     stringaDeiMinuti="";     
    }
    else if(minuti==1){
     stringaDeiMinuti=minuti+" MINUTO ";          
    }
    else{
     stringaDeiMinuti=minuti+" MINUTI ";
    }    
    if(ore!=0 && minuti!=0) stringaEt=" E ";
    if(ore==0 && minuti==0) {
     stringaDelleOre=ore+" ORE";
     stringaEt= " E ";
     stringaDeiMinuti=minuti+" MINUTI";
    }

    textViewMax.setText(stringaDelleOre+stringaEt+stringaDeiMinuti);
    if(progress<seekBarMin.getProgress())seekBarMin.setProgress(progress);
   }
  });
 }

Ripasso dei menu per rimaneggiamento dell'interfaccia di FFD

Ora dobbiamo aggiungere i settings.
Devo far entrare in causa le SeekBars.
Sono un po' indeciso se mettere le SeekBars su un'altra activity per evitare che possano essere manomesse distrattamente, e anche per orientare le SeekBars in modalità Landscape consentendo una più fine regolazione dei tempi.
Mi sembra il caso.
Per farlo, devo creare un menu che apra l'attività Impostazioni.
Un'occasione per ripassare i Menu.


Ci sono due eventi, per la gestione dei Menu. Uno è la creazione del Menu e l'altro è la selezione del Menu.

Quindi il primo è onCreateOptionsMenu. Come faccio per ricordare il nome? Ricordando che si tratta di un Menu di opzioni.
Ha come parametro un oggetto menu di tipo Menu.
Quindi chiama la funzione getMenuInflater, che ha come parametro un menu già presente nella cartella menu (o che si può anche creare) che lo inflata e credo inizializzi la variabile menu ad esso.
Restituisce un valore booleano, quindi il metodo è booleano.
Provo a scriverlo.
 @Override
 public boolean onCreateOptionsMenu(Menu menu){
  getMenuInflater().inflate(R.menu.prova, menu);
  return true;
 }
Questa dovrebbe creare il menu. Vediamo quale ne è l'effetto scrivendola su un'attività della mia app...

Mi appaiono i tre puntini, e cliccandoci sopra mi appare un menu con scritto "Settings".
Vediamo la struttura xml del menu contenuto nella cartella...

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.example.ffd.MainActivity" >

    <item
        android:id="@+id/action_settings"
        android:orderInCategory="100"
        android:showAsAction="never"
        android:title="@string/action_settings"/>

</menu> 
La scritta "Settings" deve provenire dal file string, contenuto nella cartella values.
Vediamolo...

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="app_name">FFD</string>
    <string name="hello_world">Hello world!</string>
    <string name="action_settings">Settings</string>
    <string name="title_activity_form">Form</string>
    <string name="title_activity_prova">Prova</string>

</resources> 
Ecco: la scritta di nome action_settings reca "Settings".
Provo a cambiarla con "Impostazioni" scritto in maiuscolo per vedere se cambia.

PERFETTO!

Ora tolgo questo metodo per vedere se mi spariscono i tre puntini...
Ecco, sì, spariscono.
E se invece faccio dare al metodo un risultato false?
 @Override
 public boolean onCreateOptionsMenu(Menu menu){
  getMenuInflater().inflate(R.menu.main, menu);
  return false;
 }
Ecco, se restituisce false è come se non ci fosse: i tre puntini spariscono.

Ora vediamo il secondo metodo, quello che intercetta la selezione del menu.
Questa volta il centro di tutto non è il Menu ma l'elemento del Menu, ossia l'Item.
Quindi se quello di prima era un OptionsMenu, questo è un OptionsItem.
E quindi il nome del metodo è onOptionsItemSelected. Come parametro abbiamo MenuItem, ossia l'elemento del Menu.
Lo scopo è determinare quale MenuItem è stato selezionato, per cui se ne deve ricavare l'identità, ossia l'Id.
Poi si seleziona: a seconda di quale id corrisponda all'id del MenuItem selezionato si determina la conseguenza e segue la restituzione di true, altrimenti la funzione non termina e si restituisce super.onOptionsItemSelected con item per parametro.
La funzione che restituisce l'id del MenuItem selezionato è getItemId().
Quindi si confronta questo id con l'id del MenuItem scritto nel file xml.

Provo a cancellare e riscrivere...

Okay, sembra che ci sia riuscito piuttosto bene.

Ora scrivo nella mia App anche questa, insieme alla prima.
 @Override
 public boolean onCreateOptionsMenu(Menu menu){
  getMenuInflater().inflate(R.menu.main, menu);
  return true;
 }
 
 @Override
 public boolean onOptionsItemSelected(MenuItem item){
  int id=item.getItemId();
  if(id==R.id.action_settings){
   Log.d("Menu selezionato", ""+item.getTitle());
   return true;
  }
  return super.onOptionsItemSelected(item);
 }
Vediamo se mi viene scritto in LogCat il nome del MenuItem.

Perfetto!
07-27 13:13:49.361: D/Menu selezionato(25304): IMPOSTAZIONI

Correzione dell'errore dovuto alla non istanziazione dell'interfaccia listener da parte di Form.

Ed ecco che mi si ripresenta l'errore quando torno dal Form al Service.
Credo di aver capito, con gli impazzimenti di ieri, che l'errore sia dovuto al fatto che soltanto il Main istanzia la variabile che ospita l'interfaccia nel Service.
Riprovo a eseguire...

Ecco: quando viene terminato Main, nel momento in cui ritorno a StartTime nel TimerService, ottengo un messaggio di errore con questo LogCat:
07-27 10:25:51.360: E/AndroidRuntime(31345): java.lang.NullPointerException
07-27 10:25:51.360: E/AndroidRuntime(31345):  at com.example.ffd.TimerService.StartTime(TimerService.java:54)
Ed ecco che il NullPointerException credo sia dovuto al fatto che non ho chiamato il setter da Form, per cui:
  1. Quando termino Main, TimerService termina;
  2. L'alarm mi evoca Form al momento stabilito, ma stavolta non viene evocato nessun setter che istanzi la variabile interfaccia;
  3. Quando deve essere innescato l'evento della variabile interfaccia di Service, questo non esiste.
Pongo allora la condizione di innescare l'evento soltanto se questo esiste, in StartTime e in StopTime:

TimerService:
if(mAlarmChangeListener!=null) mAlarmChangeListener.AlarmChange();
E vediamo...

Funziona egregiamente!

martedì 26 luglio 2016

Inserimento nell'applicazione del CustomEventListener sullo stato di AlarmManager

Ora si inserisce il CustomEvent.
Per cominciare, creo un'interfaccia in TimerService:
 public interface OnAlarmChangeListener{
  public void AlarmChange();
 }
Quindi creo una variabile in TimerService, di tipo OnAlarmChangeListener:
public class TimerService extends Service {

 IBinder mBinder=new LocalBinder();
 
 OnAlarmChangeListener mAlarmChangeListener;
 
 @Override
 public IBinder onBind(Intent intent) {
  // TODO Auto-generated method stub
  return mBinder;
 }

......
e quindi nel corpo di TimerService creo il setter:
 public void setOnAlarmChangeListener(OnAlarmChangeListener onAlarmChangeListener){
  mAlarmChangeListener= onAlarmChangeListener;
 }
Questo dovrebbe consentirmi di inserire in Main il codice del setter: lo inserisco appena realizzata la connessione.

Main:
  @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() {
     Log.d("ALARM", "CHANGED");
     
    }
    
   });
   
  }
E vediamo se mi dà il giusto segnale...

Niente... ma io, nel corpo di TimerService, non ho dato da nessuna parte l'ordine di innescare l'evento AlarmChange()...
Lo faccio:
 public void StartTime(){
  Intent intent=new Intent(getApplicationContext(),
        Form.class);
  PendingIntent pendingIntent=PendingIntent.getActivity(getApplicationContext(), 
                0,
                intent, 
                0);
  AlarmManager alarmManager=(AlarmManager)getApplicationContext().getSystemService(Context.ALARM_SERVICE);
  alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 
      SystemClock.elapsedRealtime()+5*1000, 
      pendingIntent);
  mAlarmChangeListener.AlarmChange();
 }
 
 public void StopTime(){
  Intent intent=new Intent(getApplicationContext(),
    Form.class);
  PendingIntent pendingIntent=PendingIntent.getActivity(getApplicationContext(), 
            0,
            intent, 
            0);
  AlarmManager alarmManager=(AlarmManager)getApplicationContext().getSystemService(Context.ALARM_SERVICE);
  alarmManager.cancel(pendingIntent);
  if(pendingIntent!=null)pendingIntent.cancel();
  mAlarmChangeListener.AlarmChange();
 }
...e rivediamo.

Funziona!
Ecco il LogCat:
07-27 06:26:55.876: D/ALARM(15540): CHANGED
07-27 06:27:00.711: D/ALARM(15540): CHANGED
07-27 06:27:02.332: D/ALARM(15540): CHANGED
07-27 06:27:03.994: D/ALARM(15540): CHANGED
Bene.
Adesso posso manovrare la mia piccola View chiamata "semaforo" che cambia colore a seconda del fatto che alarmManager sia settato o no.
Creo la variabile oggetto e la inizializzo:
 View semaforo;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  Log.d("MAIN", "ONCREATE");
  
  semaforo=(View)findViewById(R.id.semaforo);
E aggiusto il codice del setter:
  @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(mService.AlarmOn()){
      semaforo.setBackgroundResource(R.drawable.tondinoverde);
     }else{
      semaforo.setBackgroundResource(R.drawable.tondinorosso);
     }
     
    }
    
   });
E vediamo...

Perfetto!
Aggiusto anche la disabilitazione dei pulsanti "Attiva" e "Disattiva".
  @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(mService.AlarmOn()){
      semaforo.setBackgroundResource(R.drawable.tondinoverde);
      bttStart.setEnabled(false);
      bttStop.setEnabled(true);
     }else{
      semaforo.setBackgroundResource(R.drawable.tondinorosso);
      bttStart.setEnabled(true);
      bttStop.setEnabled(false);
     }
     
    }
    
   });
Proviamo...

Funziona, ma quando si apre il programma, o quando si riapre l'attività Main dopo che era stata chiusa, non viene messo in atto nessun comportamento adeguato allo stato di Alarm.
Metto nel TimerService il codice per l'innesco dell'evento AlarmChange direttamente nel setter.

TimerService:
 public void setOnAlarmChangeListener(OnAlarmChangeListener onAlarmChangeListener){
  mAlarmChangeListener= onAlarmChangeListener;
  mAlarmChangeListener.AlarmChange();
 }
E proviamo...

Funziona egregiamente!

Impostazione del ciclo base fra due activities e un service (tecnica mia)

Riscrivo tutto il codice del mio sistema di interazione fra due Activities e un Service per generare intervalli di tempo casuali...

Ho già l'XML pronto per la precedente prova, che ho distrutto in seguito a un errore che mi ha fatto impazzire. L'XML lo riciclo.
Ho preparato un po' il codice di MainActivity:
public class MainActivity extends Activity {

 Button bttStart, bttStop, bttHide;
 SeekBar seekBarMin, seekBarMax;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  
 }

}
Ma per prima cosa mi conviene scrivere il Service chiamato TimerService, e bindarlo.
Scrivo il codice per bindarlo alle due attività:

TimerService:
class TimerService extends Service {

 IBinder mBinder=new LocalBinder();
 
 @Override
 public IBinder onBind(Intent intent) {
  // TODO Auto-generated method stub
  return mBinder;
 }
 
 public class LocalBinder extends Binder{
  TimerService getService(){
   return TimerService.this;
  }
 }
 
}


Ora bindo con MainActivity
public class MainActivity extends Activity {

 TimerService mService;
 boolean mBound;
 
 Button bttStart, bttStop, bttHide;
 SeekBar seekBarMin, seekBarMax;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  
 }
 
 @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!=false){
   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;
   
  }
  
 };

}
Ecco, credo che ci sia tutto.
Passo a creare il binding anche con Form, l'altra activity:
public class Form extends Activity {

 TimerService mService;
 boolean mBound;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_form);
 }
 
 @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!=false){
   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;
   
  }
  
 };


}
Ecco. Ora, però, vorrei testare il corretto funzionamento.
Inserisco dei "segnali" negli eventi onCreate e onDestroy.
 @Override
 public void onCreate(){
  super.onCreate();
  Log.d("TIMERSERVICE","ONCREATE");
 }
 
 @Override
 public void onDestroy(){
  super.onDestroy();
  Log.d("TIMERSERVICE", "ONDESTROY");
 }
Metto dei segnali anche nei codici delle attività:

Main:
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  Log.d("MAIN", "ONCREATE");
  
 }

..........

 @Override
 public void onDestroy(){
  super.onDestroy();
  Log.d("MAIN", "ONDESTROY");
 }


Form:
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_form);
  
  Log.d("FORM", "ONCREATE");
 }

........

 @Override
 public void onDestroy(){
  super.onDestroy();
  Log.d("FORM","ONDESTROY");
 }

Ora devo inserire il codice per gli intervalli di tempo in TimerService:
 public void StartTime(){
  Intent intent=new Intent(getApplicationContext(),
        Form.class);
  PendingIntent pendingIntent=PendingIntent.getActivity(getApplicationContext(), 
                0,
                intent, 
                0);
  AlarmManager alarmManager=(AlarmManager)getApplicationContext().getSystemService(Context.ALARM_SERVICE);
  alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 
      SystemClock.elapsedRealtime()+5*1000, 
      pendingIntent);
 }
 
 public void StopTime(){
  Intent intent=new Intent(getApplicationContext(),
    Form.class);
  PendingIntent pendingIntent=PendingIntent.getActivity(getApplicationContext(), 
            0,
            intent, 
            0);
  AlarmManager alarmManager=(AlarmManager)getApplicationContext().getSystemService(Context.ALARM_SERVICE);
  alarmManager.cancel(pendingIntent);
  if(pendingIntent!=null)pendingIntent.cancel();
 }
 
 boolean AlarmOn(){
  Intent intent=new Intent(getApplicationContext(),
    Form.class);
  boolean al=(PendingIntent.getActivity(getApplicationContext(),
            0,
            intent,
            PendingIntent.FLAG_NO_CREATE)!=null);
  return al;
         
 }
Ora scrivo il codice dei bottoni, e posiziono un bottone in Form, per muovermi fra attività e services.

Main:
  bttStart=(Button)findViewById(R.id.button1);
  
  bttStart.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    mService.StartTime();
    
   }
  });
  bttStop=(Button)findViewById(R.id.button2);
  
  bttStop.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    mService.StopTime();
    
   }
  });
 }


Form:
  button=(Button)findViewById(R.id.button1);
  button.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    mService.StartTime();
    
   }
  });
Credo sia pronto per la prova.
Registro il Service:
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <service android:name=".TimerService" />
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" > 
E vediamo...

Problemino iniziale: IllegalAccess: accesso alla classe TimerService non consentito.
Avevo dimenticato public:
public class TimerService extends Service {

 IBinder mBinder=new LocalBinder();
Semplice distrazione!
Corretto questo, il programma parte.

Secondo intoppo: da Form non torna indietro: ho dimenticato di mettere finish() nel codice del button di Form.
   @Override
   public void onClick(View v) {
    mService.StartTime();
    finish();
    
   }
Okay.
Funziona anche lo StopTime.

Ma adesso devo fare la prova relativa al problema che ieri mi ha fatto impazzire, terminando Main una volta che questo ha impostato alarmManager sul TimerService.
A tale scopo, metto un codice nel button che deve terminare il Main.
  bttHide=(Button)findViewById(R.id.bttHide);
  bttHide.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    finish();
    
   }
  });
E riproviamo...

Funziona anche questo!

OnUtteranceCompletedListener

Ho ripassato nuovamente il modo di intercettare la fine della sintesi vocale.
Già l'avevo visto in precedenza

Ecco il codice:
public class MainActivity extends Activity {

 TextToSpeech tts;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  tts=new TextToSpeech(this,new TextToSpeech.OnInitListener() {
   
   
   
   @Override
   public void onInit(int status) {
    if(status==TextToSpeech.SUCCESS){
     int result=tts.setLanguage(Locale.ITALIAN);
     OnUtteranceCompletedListener utteranceListener=new OnUtteranceCompletedListener(){

      @Override
      public void onUtteranceCompleted(String utteranceId) {
       Log.d("UTTERANCE", "COMPLETED");
       
      }
      
     };
     tts.setOnUtteranceCompletedListener(utteranceListener);
     HashMap hashMap=new HashMap();
     hashMap.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "qualcosa");
     

     tts.speak("Ciao, bellissimo",TextToSpeech.QUEUE_FLUSH,hashMap);
    }
    
   }
  });
 }


}
Salvo e rifaccio...

sabato 23 luglio 2016

Codice per gli intervalli minimo e massimo tramite SeekBar.

Codice definitivo per gli intervalli minimo e massimo:
public class MainActivity extends Activity {

 SeekBar seekBar1, seekBar2;
 TextView textView,textView2;
 int step;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  step=15;
  textView=(TextView)findViewById(R.id.textView1);
  textView2=(TextView)findViewById(R.id.textView2);
  
  seekBar1=(SeekBar)findViewById(R.id.seekBar1);
  seekBar2=(SeekBar)findViewById(R.id.seekBar2);
  
  seekBar1.setMax(60*4);
  seekBar2.setMax(60*4);
  
  

  textView.setText(0+" "+0);
  textView2.setText(0+" "+0);

  
  seekBar1.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   
   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {
    
   }
   
   @Override
   public void onStartTrackingTouch(SeekBar seekBar) {
    // TODO Auto-generated method stub
    
   }
   
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    int numero=step*(progress/step);
    int ore=numero/60;
    int minuti=numero%60;
    textView.setText(ore+" "+minuti);
    if(progress>seekBar2.getProgress()) seekBar2.setProgress(progress);
   }
  });
  
  seekBar2.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   
   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {
    // TODO Auto-generated method stub
    
   }
   
   @Override
   public void onStartTrackingTouch(SeekBar seekBar) {
    // TODO Auto-generated method stub
    
   }
   
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    int numero=step*(progress/step);
    int ore=numero/60;
    int minuti=numero%60;
    textView2.setText(ore+" "+minuti);
    if(progress<seekBar1.getProgress()) seekBar1.setProgress(progress);
    
   }
  });
 }

}

Due SeekBar con intervalli minimo e massimo di un intervallo di tempo.

Una SeekBar con valori discreti.
Ecco.
Per rendere più "praticabile" la scelta, ho deciso di inserire i valori di 5 in 5. Potrei decidere anche di immetterli di 10 in 10, e forse è meglio.
Per fare questo, divido il numero progress della SeekBar per il "salto" della numerazione (divisione intera) e poi moltiplicare il risultato per 5, ottenendo questo:
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    int numero=5*(progress/5);
che funziona.
Quindi calcolo su questo ore e minuti: le ore facendo la divisione intera del numero per 60, i minuti facendo il modulo:
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    int numero=5*(progress/5);
    int ore=numero/60;
    int minuti=numero%60;
E quindi riporto sulla TextBox:
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    int numero=5*(progress/5);
    int ore=numero/60;
    int minuti=numero%60;
    textView.setText(ore+" "+minuti);


Stessa cosa per l'altra SeekBar in cui voglio mettere il valore massimo dell'intervallo di campionamento:
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    int numero=5*(progress/5);
    int ore=numero/60;
    int minuti=numero%60;
    textView2.setText(ore+" "+minuti);
Ora metto i vincoli:
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    int numero=5*(progress/5);
    int ore=numero/60;
    int minuti=numero%60;
    textView.setText(ore+" "+minuti);
    if(progress>seekBar2.getProgress()) seekBar2.setProgress(progress);


   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    int numero=5*(progress/5);
    int ore=numero/60;
    int minuti=numero%60;
    textView2.setText(ore+" "+minuti);
    if(progress<seekBar1.getProgress()) seekBar1.setProgress(progress);
E funziona praticamente egregiamente.

Codice per due SeekBars sincronizzate

Intanto metto da parte questo codice per due SeekBar sincronizzate:
public class MainActivity extends Activity {

 SeekBar seekBar1, seekBar2;
 TextView textView,textView2;
 int Incremento;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  textView=(TextView)findViewById(R.id.textView1);
  textView2=(TextView)findViewById(R.id.textView2);
  
  seekBar1=(SeekBar)findViewById(R.id.seekBar1);
  seekBar2=(SeekBar)findViewById(R.id.seekBar2);
  
  seekBar1.setMax(3*3600*1000);
  seekBar2.setMax(3*3600*1000);
  
  int ore=seekBar1.getProgress()/(60*60*1000);
  int minuti=(seekBar1.getProgress()%(60*60*1000)/(60*1000));
  textView.setText(ore+" ORE : "+minuti+" MINUTI");
  ore=seekBar2.getProgress()/(60*60*1000);
  minuti=(seekBar2.getProgress()%(60*60*1000)/(60*1000));
  textView2.setText(ore+" ORE : "+minuti+" MINUTI");
  
  seekBar1.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   
   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {
    
   }
   
   @Override
   public void onStartTrackingTouch(SeekBar seekBar) {
    // TODO Auto-generated method stub
    
   }
   
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    int ore=progress/(60*60*1000);
    int minuti=(progress%(60*60*1000)/(60*1000));
    textView.setText(ore+" ORE : "+minuti+" MINUTI");
    if(progress>seekBar2.getProgress()) seekBar2.setProgress(progress);
   }
  });
  
  seekBar2.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   
   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {
    // TODO Auto-generated method stub
    
   }
   
   @Override
   public void onStartTrackingTouch(SeekBar seekBar) {
    // TODO Auto-generated method stub
    
   }
   
   @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
   
    int ore=progress/(60*60*1000);
    int minuti=(progress%(60*60*1000)/(60*1000));
    textView2.setText(ore+" ORE : "+minuti+" MINUTI");
    if(progress<seekBar1.getProgress()) seekBar1.setProgress(progress);
   }
  });
 }

}

venerdì 22 luglio 2016

Custom Events e un Service bindato con Custom Events

Cerchiamo di chiarire le idee sulla creazione di custom events.

Per prima cosa, creo l'interfaccia, che contiene l'evento, al di fuori della classe che conterrà il metodo stesso.
 public void cambiaVerita(){
  mInterfaccia.evento();
 }
Poi bisogna considerare la classe.
public class Oggetto{
 Interfaccia mInterfaccia;

 
}
...che contiene una variabile del tipo dell'interfaccia.
Questa variabile conterrà pure l'evento ( o meglio il metodo) contenuto nell'interfaccia.
Quindi si scrive il Setter:
public class Oggetto{
 Interfaccia mInterfaccia;

 public void Setter(Interfaccia interfaccia){
  mInterfaccia=interfaccia;
 
 } 
}
e quindi le condizioni che attivano l'evento, che possono essere manipolate eventualmente anche dall'esterno della classe stessa:
public class Oggetto{
 Interfaccia mInterfaccia;

 public void Setter(Interfaccia interfaccia){
  mInterfaccia=interfaccia;
 
 }
 public void cambiaVerita(){
  mInterfaccia.evento();
 }
 
}
Riprovo a scrivere il tutto...

La classe:
public class Oggetto{
 private int numero;
 Interfaccia mInterfaccia;
 
 public void Setter(Interfaccia interfaccia){
  mInterfaccia=interfaccia;
 }
 
 public void setNumero(int num){
  numero+=num;
  if(numero>10)mInterfaccia.evento();
 }
 
}


 interface Interfaccia{
 public void evento();
}


L'istanziazione:
public class MainActivity extends Activity {

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  Oggetto oggetto=new Oggetto();
  oggetto.Setter(new Interfaccia(){

   @Override
   public void evento() {
    Log.d("NUMERO","SUPERATO");
    
   }
   
  });
  oggetto.setNumero(3);
  oggetto.setNumero(4);
  oggetto.setNumero(4);
 }

}

Ora scriviamo un Service che abbia un evento personalizzato.

Ce l'ho fatta!
public class Servizio extends Service {

 Interfaccia mInterfaccia;
 IBinder mBinder=new LocalBinder();
 
 @Override
 public IBinder onBind(Intent intent) {
  // TODO Auto-generated method stub
  return mBinder;
 }
 
 public class LocalBinder extends Binder {
  Servizio getService(){
   return Servizio.this;
  }
 }
 
 public void StartTime(){
  Intent intent=new Intent(this,RecordService.class);
  PendingIntent pendingIntent=PendingIntent.getActivity(getApplicationContext(),0,intent,0);
  AlarmManager alarmManager=(AlarmManager)getApplicationContext().getSystemService(Context.ALARM_SERVICE);
  alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime()+60*1000, pendingIntent);
  mInterfaccia.AlarmStateChange();
 }
 
 public void StopTime(){
  Intent intent=new Intent(this,RecordService.class);
  PendingIntent pendingIntent=PendingIntent.getActivity(getApplicationContext(),0,intent,0);
  AlarmManager alarmManager=(AlarmManager)getApplicationContext().getSystemService(Context.ALARM_SERVICE);
  alarmManager.cancel(pendingIntent);
  if(pendingIntent!=null)pendingIntent.cancel();
  mInterfaccia.AlarmStateChange();
 }
 
 public boolean AlarmOn(){
  Intent intent=new Intent(this,RecordService.class);
  boolean check=(PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_NO_CREATE)!=null);
  return check;
 }
 
 public void Setter(Interfaccia interfaccia){
  mInterfaccia=interfaccia;
 }
 


}

interface Interfaccia{
 public void AlarmStateChange();
}

mercoledì 20 luglio 2016

TimerTask e primo approccio con gli Handler.

Devo fare una digressione sul TimerTask, che ho usato recentemente ma che non ricordo come si usi.

Sicuramente conviene rivedere quello che ho già usato:
  if(ringtone!=null){
   ringtone.play();
   TimerTask timerTask=new TimerTask(){

    @Override
    public void run() {
     ringtone.stop();
     
    }
    
   };
   Timer timer=new Timer();
   timer.schedule(timerTask, 2000);
  }
Si dichiara come qualunque istanza di classe che abbia dei metodi da implementare.
  TimerTask timerTask=new TimerTask(){
   
  }
A questo punto vengono da sole le richieste di mettere il punto e virgola finale e di implementare i metodi.
E si trasforma così:
  TimerTask timerTask=new TimerTask(){

   @Override
   public void run() {
    // TODO Auto-generated method stub
    
   }
   
  };
Quindi ci mettiamo l'azione da svolgere dopo il tempo stabilito.
  TimerTask timerTask=new TimerTask(){

   @Override
   public void run() {
    Toast.makeText(getApplicationContext(), 
        "QUESTA E' L'AZIONE DA SVOLGERE DOPO IL TEMPO STABILITO", 
        Toast.LENGTH_LONG).show();
    
   }
   
  };


Fin qui non dice niente. Bisogna impostare il TEMPO dopo il quale va eseguito questo TimerTask.
E si introduce la classe Timer.
  Timer timer=new Timer();
  timer.schedule(timerTask, 5000);
quindi si usa il metodo schedule dell'istanza di Timer, il quale può avere come secondo parametro un ritardo in millisecondi come in questo caso, ma vedo adesso che può anche avere una data alla quale far eseguire il TimerTask (e credo che nel nome della classe Date sia implicita anche la possibilità di impostare un'ora anziché una data).
Proviamo:

Bene. Con il Toast ottengo un messaggio di errore:
Can't create handler inside thread that has not called Looper.prepare()
. Vado ad approfondire e trovo questo e risolvo così:
  mHandler=new Handler(Looper.getMainLooper()){
   @Override
   public void handleMessage(Message message){
    Toast.makeText(getApplicationContext(), "QUESTA E' L'AZIONE DA SVOLGERE DOPO IL TEMPO STABILITO",Toast.LENGTH_LONG).show();
   }
  };
  TimerTask timerTask=new TimerTask(){

   @Override
   public void run() {
    Message message=mHandler.obtainMessage();
    message.sendToTarget();
    
    
   }
   
  };
  Timer timer=new Timer();
  timer.schedule(timerTask, 5000);
Devo approfondire questi Handler.

Progetto Kouros: interfacce a un bottone e a due bottoni.

Ho studiato lo schema a un solo bottone:
  button.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    if(mService.AlarmOn()){
     mService.StopTime();
     ((Button)v).setText("ATTIVA");
     
    }
    else{
     mService.StartTime();
     ((Button)v).setText("DISATTIVA");
     
    }
    
   }
  });
In questo modo, se Alarm è On la pressione del pulsante disattiva e vi compare la scritta ATTIVA; mentre se Alarm è Off la pressione del pulsante attiva l'alarm e vi compare la scritta DISATTIVA:

Adesso realizzo uno schema a due bottoni.
Eccolo:
  bttStart.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    if(!mService.AlarmOn()){
     mService.StartTime();
     ((Button)v).setEnabled(false);
     bttStop.setEnabled(true);
    }
    
   }
  });
  
  bttStop.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    if(mService.AlarmOn()){
     mService.StopTime();
     ((Button)v).setEnabled(false);
     bttStart.setEnabled(true);
    }
    
   }
  });
ponendo nell'XML questo:
        <Button
            android:id="@+id/bttStart"
            android:layout_width="120dp"
            android:layout_height="50dp"
            android:layout_alignParentTop="true"
            android:layout_alignParentLeft="true"
            android:layout_marginLeft="10dp"
            android:layout_marginTop="20dp"
            android:background="@drawable/buttonshape"
            android:text="ATTIVA" />
        
        <Button 
            android:id="@+id/bttStop"
            android:layout_width="120dp"
            android:layout_height="50dp"
            android:layout_alignParentTop="true"
            android:layout_alignParentRight="true"
            android:layout_marginRight="10dp"
            android:layout_marginTop="20dp"
            android:background="@drawable/buttonshape"
            android:enabled="false"
            android:text="DISATTIVA" /> 
...che è una stronzata!
Quando chiudo l'attività con un AlarmOn vero, poi la riapro e trovo bttStop disabilitato come se AlarmOn fosse falso, mentre bttStart è abilitato quando dovrebbe essere disabilitato.
Se io faccio disabilitare bttStart alla sua pressione e do bttStop come disabilitato all'apertura, non faccio seguire l'abilitazione del bottone al reale status di Alarm.
Ci tornerò perché mi si sono aperti altri campi di studio in correlazione a questo...

Studio grafico dell'interfaccia (progetto Kouros)

Bisognerebbe studiare anche l'interfaccia che imposta l'innesco e il disinnesco dell'Alarm.
O un solo bottone che cambia funzione a seconda che l'Alarm sia innescato o disinnescato, o due bottoni dei quali l'uno si disabilita e l'altro si abilita a seconda dello status.
Proviamo con questa seconda possibilità...

Iniziamo a studiare l'XML...

Inserirei un Layout orizzontale con i due tasti...
Vado a guardare quello che ho già fatto con TimeManager e mi ritrovo un elemento molto importante per dare un certo "formato" al Layout:
    <RelativeLayout
        android:id="@+id/relativeLayout1"
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:background="@drawable/shap"
        android:orientation="horizontal" >
Visto questo, vado a rivedermi la forma shap.
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >


<corners
    android:bottomLeftRadius="10dp"
    android:bottomRightRadius="10dp"
    android:topLeftRadius="10dp"
    android:topRightRadius="10dp" />

<stroke android:width="2px" 
    android:color="#000" />

</shape> 
Vista questa, cerco di riprodurre una nuova forma...

Cliccando sulla cartella drawable, e poi new -> android XML file -> shape ottengo questo:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
    

</shape>
 
da riempire adesso con ciò che più mi aggrada.
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" 
    android:shape="oval">
    
<stroke android:width="5dp"
    android:color="#5f5" />
</shape>
 
E vediamo cosa ne esce impostando questa shape come background di un RelativeLayout...

Perfetto!
E' bene che mi faccia un po' di esperienza con le forme.

Ora, però, meglio metterci il classico rettangolo arrotondato, non prima però di aver provato a metterci dentro l'ovale un Button.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.kouros.MainActivity" >



    <RelativeLayout
        android:id="@+id/pulsantiera"
        android:layout_width="match_parent"
        android:layout_height="100dp"
     android:layout_alignParentTop="true"
     android:layout_alignParentLeft="true"
     android:layout_marginTop="50dp"
     android:layout_marginLeft="10dp"
     android:background="@drawable/forma" >
        
        <Button
            android:id="@+id/bttStart"
            android:layout_width="100dp"
            android:layout_height="50dp"
            android:layout_centerHorizontal="true"
            android:layout_centerVertical="true"
            android:text="Start" />
        
        
    </RelativeLayout>


</RelativeLayout> 


Ecco:




Questa veste grafica potrebbe essere buona per una soluzione a un solo pulsante dell'innesco e della cancellazione dell'Alarm.
Se invece di "oval" metto "rectangle" ottengo un rettangolo.
In questo caso posso agire sugli angoli inserendo il tag corners.
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" 
    android:shape="rectangle">
    
    <corners 
        android:topLeftRadius="15dp" 
        android:topRightRadius="15dp"
        android:bottomLeftRadius="15dp"
        android:bottomRightRadius="15dp"/>
    
 <stroke android:width="5dp"
     android:color="#5f5" />
</shape> 
Proviamo...

Progetto Kouros: creazione dell'AlarmManager nel servizio TimerService.

Innesco e cancellazione dell'AlarmManager.
Questo è il codice che nel mio progetto TimeManager gestisce la cancellazione dell'AlarmManager nel Service:
  bttStop.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    mService.StopTime();
    if(mService.AlarmOn()){
     textView.setTextColor(Color.GREEN);
     textView.setText("ATTIVO");
    }else{
     textView.setTextColor(Color.RED);
     textView.setText("INATTIVO");
    }
    
   }
  });
Fa riferimento a due funzioni del Service: StopTime e AlarmOn.
Vediamo...

StopTime:
 public void StopTime(){
  Log.e("CONTESTO STOP",getApplicationContext()+"");
  Intent intent=new Intent(getApplicationContext(),Form.class);
  PendingIntent pendingIntent=PendingIntent.getActivity(getApplicationContext(),0,intent,0);
  AlarmManager alarmManager=(AlarmManager)getApplicationContext().getSystemService(Context.ALARM_SERVICE);
  alarmManager.cancel(pendingIntent);
  if(pendingIntent!=null)pendingIntent.cancel();
 }
Se ricordo bene, ricrea esattamente le stesse classi di StartTime:
 public void StartTime(){
  Log.e("CONTESTO START",getApplicationContext()+"");
  SharedPreferences SP=getApplicationContext().getSharedPreferences("tempi", Context.MODE_PRIVATE);

  Intent intent=new Intent(getApplicationContext(),Form.class);
  PendingIntent pendingIntent=PendingIntent.getActivity(getApplicationContext(),0,intent,0);
  AlarmManager alarmManager=(AlarmManager)getApplicationContext().getSystemService(Context.ALARM_SERVICE);
  int base=(int) (SystemClock.elapsedRealtime()+(SP.getInt("ore",0)*60+SP.getInt("minuti", 0))*60*1000);
  int range=(SP.getInt("rndOre",0)*60+SP.getInt("rndMinuti", 0))*60*1000;
  Random rand=new Random();
  int tempo=base-range+rand.nextInt(range*2+1);
  alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, tempo, pendingIntent);
 }


Confronto:
  Intent intent=new Intent(getApplicationContext(),Form.class);
  PendingIntent pendingIntent=PendingIntent.getActivity(getApplicationContext(),0,intent,0);
  AlarmManager alarmManager=(AlarmManager)getApplicationContext().getSystemService(Context.ALARM_SERVICE);


  Intent intent=new Intent(getApplicationContext(),Form.class);
  PendingIntent pendingIntent=PendingIntent.getActivity(getApplicationContext(),0,intent,0);
  AlarmManager alarmManager=(AlarmManager)getApplicationContext().getSystemService(Context.ALARM_SERVICE);
Poi, mentre l'uno usa il metodo set, l'altro usa il metodo cancel.

Invece AlarmOn verifica se l'AlarmManager sia attivo...

Ecco, ho ricreato la triade:
 public void StartTime(){
  Intent intent=new Intent(getApplicationContext(),Form.class);
  PendingIntent pendingIntent=PendingIntent.getActivity(getApplicationContext(),0, intent, 0);
  AlarmManager alarmManager=(AlarmManager)getApplicationContext().getSystemService(Context.ALARM_SERVICE);
  alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime()+10*1000, pendingIntent);
 }
 
 public void StopTime(){
  Intent intent=new Intent(getApplicationContext(),Form.class);
  PendingIntent pendingIntent=PendingIntent.getActivity(getApplicationContext(),0, intent,0);
  AlarmManager alarmManager=(AlarmManager)getApplicationContext().getSystemService(Context.ALARM_SERVICE);
  alarmManager.cancel(pendingIntent);
  if(pendingIntent!=null)pendingIntent=null;
 }
 
 public boolean AlarmOn(){
  Intent intent=new Intent(getApplicationContext(),Form.class);
  boolean isAlarmOn=(PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_NO_CREATE)!=null);
  return isAlarmOn;
  
 }
Dunque, chiamando dalla MainActivity, il processo può essere attivato o deattivato, e si può avere il feedback se sia attivo o non attivo.

Lo schema è sempre quello. Potrei impostare anche i tempi, sempre a partire dalla MainActivity.
Dovrei studiare un sistema migliore per l'impostazione dei tempi...

Comunque per il momento lasciamolo così.

Adesso imposto una semplice suoneria alla scadenza del tempo.