JavascriptProva

martedì 30 agosto 2016

Analisi della funzione che ho creato per trasferire su Firebase il contenuto di un SQLiteDatabase

Ecco, ho trovato il modo...

Questa è la funzione che ho creato per archiviare un database SQLite su Firebase:
    void archivia(Cursor crs){
        crs.moveToFirst();

        int c=0;
        do{
            Map<String,Object> mappa=new HashMap<String,Object>();

            for(int i=0;i<crs.getColumnCount();i++) {

                mappa.put(crs.getColumnName(i), (Object) crs.getString(i));
                reference.child("" + c).push();
                reference.child("" + c).updateChildren(mappa);
            }

            c++;
        }while(crs.moveToNext());
    }
Come parametro viene fornito un oggetto Cursor che rappresenta la query del database.
 crs.moveToFirst();
resetto il Cursor alla sua posizione iniziale.

int c=0;
stabilisco un "contatore".

Ora mi leggo tutto il cursore, e per ogni record:
Map<String,Object> mappa=new HashMap<String,Object>();
creo una HashMap String, Object;

for(int i=0;i<crs.getColumnCount();i++) {
Per ogni colonna...

mappa.put(crs.getColumnName(i), (Object) crs.getString(i));
...inserisco nella HashMap il nome della colonna e, castandolo a Object, il valore del campo.

reference.child("" + c).push();
Aggiungo a un child del database Firebase il cui nome è specificato dal contatore (che se non esiste viene creato), un elemento ancora indeterminato;

reference.child("" + c).updateChildren(mappa);
Modifico l'elemento appena creato alla luce della HashMap che ho compilato in precedenza.

Così ogni nodo figlio del database rappresenta un record in quanto porta "attaccate" tutte le colonne di un record.

            c++;
        }while(crs.moveToNext());
    }
Aumento il contatore e vado al prossimo record del Cursor.

mercoledì 24 agosto 2016

Nota sull'installazione di Genymotion e AndroidSQLiteBrowser per la visualizzazione dei databases.

Meglio prendere appunti altrimenti me li dimentico, e queste cose potrebbero tornare utili.

Una volta scoperto come fare per creare un database JSON su Firebase, si è presentato il problema di convertire il database SQLite della mia applicazione per archiviarlo sul server.
Quindi avevo la necessità di visualizzare il database, e non avendo un cellulare rootato ho pensato di farlo con l'emulatore come facevo in Eclipse, anche in Android Studio.
Qui si sono presentati due tipi di problema:
  1. era necessario installare anche in Android Studio un'applicazione del tipo di Questoid per la visualizzazione dei database;
  2. L'emulatore "classico" si è rivelato ancora una volta, e forse ancor di più, troppo lento, rallentando terribilmente tutto il computer.
Così ho pensato di usare Genymotion, che fa uso di VirtualBox.
Ho scaricato Genymotion completo di VirtualBox, e l'emulatore non si apriva: appariva una dicitura del tipo "Virtualbox non può aprire Genymotion".
Virtualbox non si apriva nemmeno essa stessa cliccando sull'icona VirtualBox.exe.
Così, peregrinando un po', ho pensato di cercare una versione più recente di VirtualBox, e ho scaricato la versione 5.1.4 di VirtualBox disinstallando la versione fornita con Genymotion, che era, se non sbaglio, la 5.0.4.

Adesso, l'emulatore si apriva (sia pure essendo necessario che l'applicazione Genymotion abbia il focus).
Il problema adesso era che non sempre Android Studio riconosceva l'emulatore (solo in una certa sequenza, mi sembra), e così anche DDMS.
Studiando e cercando un po', accertato grazie a un articolo molto preciso su StackOverflow che il problema dipendeva dall'applicazione ADB.exe, sono arrivato alla conclusione che bisognava cambiare le impostazioni:



Ecco: di default era selezionato Use Genymotion Android tools (default), e invece va selezionato Use custom Android SDK tools specificando il percorso, che nel mio caso era C:/Users/Antonello/AppData/Local/Android/android-sdk (quando viene trovato l'SDK la scritta sotto mette il segno di spunta e diventa Android SDK tools found successfully, mentre in caso contrario vi appare una X nera e appare scritto Android SDK not found at this location),

In questo modo, funziona alla perfezione!
Resta il problema della visualizzazione del database.
Grazie a questo articolo di StackOverflow ho seguito la procedura di scaricamento di AndroidSQLiteBrowser_1.0.2.jar, ponendolo nella cartella indicata, e si visualizza un'icona molto simile a quella visualizzata in Eclipse con Questoid, mostrando il database come in Eclipse.

Genymotion è risultato molto più agile del vecchio emulatore, come prevedevo!

Ho scoperto anche un modo diverso, in Genymotion, di pushare e prelevare media dalle cartelle dell'emulatore.

lunedì 22 agosto 2016

Promemoria delle aggiunte ai files Gradle per Firebase.

Ecco le librerie che ho dovuto importare in Gradle:
Nel file build.gradle presente in App:
apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "24.0.1"

    defaultConfig {
        applicationId "com.example.antonello.studiofirebase"
        minSdkVersion 18
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.4.0'
    compile 'com.google.firebase:firebase-core:9.4.0'
    compile 'com.google.firebase:firebase-database:9.4.0'
}

apply plugin: 'com.google.gms.google-services'


Nel file presente in StudioFirebase (il nome della mia App) (si chiamerebbe root-level):
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.3'
        classpath 'com.google.gms:google-services:3.0.0'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}


Questa è la pagina di riferimento

Timidi approcci in Firebase...

Per vie traverse che riassumerò dopo, sono arrivato ad avere una giusta connessione (spero) con Firebase.
Sono adesso alla creazione di un riferimento per il database.
Ecco il sito dal quale sto cercando di farmi guidare ora...

Sto mettendo insieme un codice così:
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        FirebaseDatabase mDatabase=FirebaseDatabase.getInstance();
        


    }
E adesso mi si dice di avere un riferimento.
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        FirebaseDatabase mDatabase=FirebaseDatabase.getInstance();
        DatabaseReference reference=mDatabase.getReference();


    }


Che riferimento è reference?

Sarebbe un riferimento a tutto il database.
Facciamo una prova...

Poi mi sono andato a perdere fra eventi e tremila cazzi... Ho trovato ora questa pagina che mi sembra più "umana"... vediamo...

Eccone la sintassi, simile a quella di cui sopra:
private DatabaseReference mDatabase;
// ...
mDatabase = FirebaseDatabase.getInstance().getReference();
Sarebbe un riferimento a tutto il database, suppongo.
A differenza di prima, dove creo separatamente un oggetto FirebaseDatabase mediante il metodo getInstance() e poi un oggetto DatabaseReference mediante il getReference(), li creo tutti insieme.
Seguo questo stesso esempio nel mio codice:
public class MainActivity extends AppCompatActivity {

    TextView textView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView=(TextView)findViewById(R.id.textView);
        
        DatabaseReference mDatabase= FirebaseDatabase.getInstance().getReference();




Finalmente ci sono riuscito.
La procedura è piuttosto complessa, per cui sarà meglio ripeterla con un nuovo progetto e una nuova App.
Ho un nuovo progetto, MioProgetto, su Firebase.
Ho difficoltà nel mettere al posto giusto il file scaricato.
Mi annoto la segnalazione del messaggio di errore:
Error:Execution failed for task ':app:processDebugGoogleServices'.
> File google-services.json is missing. The Google Services Plugin cannot function without it. 
   Searched Location: 
  C:\Users\Antonello\AndroidStudioProjects\StudioNuovo2\app\src\debug\google-services.json
  C:\Users\Antonello\AndroidStudioProjects\StudioNuovo2\app\google-services.json
Mi è bastato posizionare in App mediante Esplora Risorse.



Dunque, facciamo una distinzione.
  1. associamo una App ottenendo il numero Sha mediante la finestra Gradle sulla destra
  2. posizioniamo il file scaricato nella cartella App mediante Esplora Risorse
  3. sistemiamo i Gradle e facciamo il Sync
  4. nel Manifest, impostare il permesso Internet.
Dovrebbe bastare...

sabato 20 agosto 2016

Connessione di app android con database JSON Firebase.

Un nome "Studio Nuovo" sarà al di fuori di ogni sospetto...

Bene.
La radice di tutto, su Esplora Risorse, è la sottodirectory di "Android Studio Projects" chiamata "Studio Nuovo".
App è una sottocartella di questa.
Ecco: sono andato con il DOS e ho ricavato la struttura.
 Il volume nell'unit… C Š Acer
 Numero di serie del volume: E27D-F746

 Directory di C:\Users\Antonello\AndroidStudioProjects\StudioNuovo

20/08/2016  21.57    <DIR>          .
20/08/2016  21.57    <DIR>          ..
20/08/2016  21.48    <DIR>          .gradle
20/08/2016  21.53    <DIR>          .idea
20/08/2016  21.49    <DIR>          app
20/08/2016  21.48    <DIR>          build
20/08/2016  21.46    <DIR>          gradle
20/08/2016  21.46               105 .gitignore
20/08/2016  21.46               521 build.gradle
20/08/2016  21.46               872 gradle.properties
20/08/2016  21.46             4.971 gradlew
20/08/2016  21.46             2.404 gradlew.bat
20/08/2016  21.57                 0 lista.txt
20/08/2016  21.46               470 local.properties
20/08/2016  21.46                16 settings.gradle
20/08/2016  21.48               942 StudioNuovo.iml
               9 File         10.301 byte
               7 Directory  172.789.342.208 byte disponibili 
Andando più giù nella cartella App:
C:\Users\Antonello\AndroidStudioProjects\StudioNuovo\app>dir/p/o
 Il volume nell'unità C è Acer
 Numero di serie del volume: E27D-F746

 Directory di C:\Users\Antonello\AndroidStudioProjects\StudioNuovo\app

20/08/2016  21.49    <DIR>          .
20/08/2016  21.49    <DIR>          ..
20/08/2016  21.48    <DIR>          build
20/08/2016  21.46    <DIR>          libs
20/08/2016  21.46    <DIR>          src
20/08/2016  21.46                 8 .gitignore
20/08/2016  21.49             8.922 app.iml
20/08/2016  21.46               660 build.gradle
20/08/2016  21.46               700 proguard-rules.pro
               4 File         10.290 byte
               5 Directory  172.781.301.760 byte disponibili 
Ora vediamo come appare in Android Studio:



Ecco, quindi App, che qui viene mostrata isolata, è "compagna" delle cartelle che appaiono più sotto, ossia .idea, build, gradle: tutte "figlie" della cartella StudioNuovo.
Manca la cartella .gradle...
Comunque abbiamo due sedi in cui appaia questo ancora misterioso nome.
  1. Come file all'interno di App
  2. Come file "fratello" di App
Vediamo quale di questi due presenta quei requisiti.


Intanto devo rimontare il tutto con questo nuovo progetto.
Cancello i vari files scaricati in Download così evito quel numeretto fra parentesi...

Cancello tutti i progetti su Firebase e ne ricreo uno di sana pianta.

Vado alla console.
Associo al progetto JaccaProgetto il mio nuovo package, mi appare quella schermata ma me ne appare un'altra:



Prima probabilmente avevo saltato e non l'avevo vista.

Riprocediamo a salvare il file, che mi è stato già automaticamente scaricato, nella cartella App.
Eccolo, e senza numeretti.
Ora mi trovo al punto di prima


Cerchiamo il file gradle...eccetera che ha quella parola buildscript.
Bene. Si tratta del file "fratello" di App.
Eccolo:
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.3'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
Dunque devo trasformarlo in questo senso:
buildscript {
    // ...
    dependencies {
        // ...
        classpath 'com.google.gms:google-services:3.0.0'
    }
}
Ecco:nonostante il "don't place"...
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.3'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
        classpath 'com.google.gms:google-services:3.0.0'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}


Quindi questo:
apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "24.0.0"

    defaultConfig {
        applicationId "com.example.antonello.studionuovo"
        minSdkVersion 18
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.2.1'
}
...va modificato in questo senso:
apply plugin: 'com.android.application'

android {
  // ...
}

dependencies {
  //apply plugin: 'com.android.application'

android {
  // ...
}

dependencies {
  // ...
  compile 'com.google.firebase:firebase-core:9.4.0'
}

// ADD THIS AT THE BOTTOM
apply plugin: 'com.google.gms.google-services' ...
  compile 'com.google.firebase:firebase-core:9.4.0'
}

// ADD THIS AT THE BOTTOM
apply plugin: 'com.google.gms.google-services'
E quindi:
apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "24.0.0"

    defaultConfig {
        applicationId "com.example.antonello.studionuovo"
        minSdkVersion 18
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.2.1'
    compile 'com.google.firebase:firebase-core:9.4.0'
}

apply plugin: 'com.google.gms.google-services'


A questo punto dovrebbe essere pronto, anche se non so cosa farne...

Lo faccio girare... giusto per vedere che non ci siano errori.

Ecco, non mi riconosceva una libreria. Ho fatto un lungo update di SDK e alla fine è partito! E questo già è consolante!

Tentativo di modifica di build.gradle e perplessità sulla struttura del progetto.

Veniamo al Gradle, grosso mistero, ancora, per me.
Il Tutorial dice che adesso bisogna aggiungere le regole al livello radice build.gradle
Ho risalvato nella cartella App il file google-services, senza il numeretto fra parentesi, per paura che non mi venisse riconosciuto il nome (lo avevo già scaricato in miei precedenti tentativi).

Troviamo il build.gradle.
Qui in App ce n'è un file solo, che si chiama così.
Vado a leggerlo:
apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "24.0.0"

    defaultConfig {
        applicationId "com.example.antonello.studiofirebase2"
        minSdkVersion 18
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.2.1'
}


Il codice che bisogna modificare viene invece presentato così:
buildscript {
    // ...
    dependencies {
        // ...
        classpath 'com.google.gms:google-services:3.0.0'
    }
}
Quel buildscript non lo vedo da nessuna parte...

Un file che contiene buildscript si trova invece in StudioFirebase2.
Che faccio, modifico questo?
Ma in questa struttura dei progetti android non ci ho capito niente...
Non è che magari ci sono problemi col fatto che il nome di questo progetto è simile a quello di un progetto fatto prima?...

Costruisco da capo un progetto con un nome che non c'entra niente e cerco di capire qualcosa sulla sua struttura.

Connettere il database con l'app

Come si collega un'applicazione Android con il database?

Ho trovato un tutorial piuttosto datato che però potrebbe essere utile...

Andiamo a vedere il codice che dal sito moderno si dice di dover copiare. Intanto, come requisiti quali si pongono?
L'unico che mi sembra di non avere è Google Play Services 9.4.0 o più recente.
Che roba è?

Vado su SDK manager.
Trovo Google Play services 23.0.0, con un update available.
Installiamolo... non dovrebbe prendere troppo tempo.

In Android Studio viene chiuso l'IDE quando si fanno questi aggiornamenti...

Fatto.
Vado avanti.
Aggiungi Firebase alla tua App.
Come si fa?

Per aggiungere Firebase alla tua App hai bisogno di un progetto (e ce l'ho) e di un file di configurazione Firebase per la tua App. Ed è questo che presenta problemi...

Ora, il progetto (se si intende un database-storage eccetera) io ce l'ho già.
E allora dovrei cliccare su Import Google project.
Vediamo dove...

Creo un nuovo progetto per la confusione che ho combinato col primo.

Ma no, è giusto: devo cliccare sul simbolo verde: Add Firebase to Android App.
e Immagino di dover aggiungere Firebase all'applicazione che ho "di qua".
E devo inserire il nome del package.
Bene, l'ho fatto, ho digitato ADD APP, e mi è comparso un riquadro e contemporaneamente mi si è downloadato un file.
Il riquadro è questo:




E dice che lo devo copiare nella cartella App dell'applicazione.
Procedo subito.
Credo di averlo fatto decentemente...

Veniamo ora ad Add the SDK... Questo è il riferimento

Primissimi approcci con JSON su Firebase

Creo un progetto in Firebase.
Trovo scritto: "Un progetto è il tuo contenitore per Apps". Dunque non si tratta di un'App.
Vediamo di che si tratta, allora: sarà un insieme di progetti?

Ecco il primo form:



Ho aggiunto il nome di fantasia di un "progetto", ora inserisco la nazione corretta e vado...

Ottengo una pagina con questo elenco:



Ecco, a me interessa in particolare il database, quindi andiamo a vedere se se ne può fare qualcosa...

Intanto, ecco un link che dovrebbe indirizzare al mio database, forse... Ci sono dati personali in mezzo, per cui ritengo sia l'indirizzo di qualcosa di mio personale.



Ma mi rendo conto che la finestra principale della pagina presenta tre schede: DATA, RULES e USAGE.
Vado sulla pagina linkata da LEARN MORE, e scopro che bisogna innanzitutto definire le REGOLE. Andiamo su RULES e vediamo...

Apprendo che le regole sono scritte in una sintassi JavaScript-like.
Ecco la prima regola, che dovrebbe avere a che fare con l'accesso autorizzato dell'utente a dati privati:
{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null"
  }
}
Non conosco questi comandi.
Si dovrebbe approfondire...

Vado a vedere su altri eventuali tutorial...

Mi pare di aver capito che questo linguaggio si chiami JSON.

Voglio archiviare sul database una piccola stronzata.
Come faccio?

Questa cosa mi sembra promettente.

Il mio "indirizzo" è:
https://jacproject-c5cfa.firebaseio.com/
L'esempio riportato sul tutorial opera sull'indirizzo:
https://docs-examples.firebaseio.com/web/data
E' necessario aggiungere web/data?
Va aggiunto nell'albero?...



Per cominciare, cerchiamo di espandere questo database.

Se ho capito bene, la radice è quella, https://jacproject-c5cfa.firebaseio.com/
Provo questo approccio: tento di lavorare con json partendo dal javascript dove mi sento più a mio agio...

Ho trovato questo codice da aggiungere al codice HTML (credo):

<script src="https://www.gstatic.com/firebasejs/3.3.0/firebase.js"></script>
<script>
  // Initialize Firebase
  var config = {
    apiKey: "AIzaSyDAplxRFl1HcMMo5QYqt8RropQCx6no3Ok",
    authDomain: "jacproject-c5cfa.firebaseapp.com",
    databaseURL: "https://jacproject-c5cfa.firebaseio.com",
    storageBucket: "jacproject-c5cfa.appspot.com",
  };
  firebase.initializeApp(config);
</script>
Non succede letteralmente un tubo.
AH... mi rimanda a un link per usare il database... andiamo a vedere...

Sono ancora in fase indeterminata.
Come faccio ad aggiungere altri "campi"?

Ho sfondato! Decrittato il sistema!
Sulla scheda DATA della pagina relativa al database, posso aggiungere dei nodi al mio database in modo immediato, con una funzione che prima mi era sfuggita.

Ora vediamo che dati si possono inserire in JSON.

sabato 13 agosto 2016

Movimento di un oggetto nell'ambito di una activity.

Per il posizionamento dell'immagine nella finestra: voglio vedere se in WindowManager.LayoutParams vi sono indicazioni per il posizionamento preciso in base a un sistema di coordinate.
Ho già provato con setX() e non funziona.
E se provassi con setRawX()?
Proviamo...

Non esiste proprio. Devo fare una ricerca su WindowManager.LayoutParams. Ecco, ho trovato: esistono le proprietà param.x e param.y, nelle quali però la x esprime il posizionamento relativo a quello in cui l'oggetto viene posizionato inizialmente.
        Immagine immagine=new Immagine(this);

        WindowManager wm=(WindowManager)getSystemService(WINDOW_SERVICE);
                WindowManager.LayoutParams params=new WindowManager.LayoutParams(
                200,
                200,
                WindowManager.LayoutParams.TYPE_SYSTEM_ERROR,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
                        WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
                PixelFormat.TRANSPARENT);
        params.x=100;
        wm.addView(immagine, params);
(sempre in onCreate).
L'oggetto appare spostato di 100 rispetto al centro dove è stato caricato inizialmente.
Se è così, allora conviene metterlo in alto a sinistra, così il posizionamento relativo dovrebbe diventare assoluto facilitando il compito.

Ho fatto un po' di prove, e tutto è ok.

Ora provo a buttare giù qualcosa che lo sposti.


Con gli strumenti classici non mi sembra possibile spostare un oggetto posizionato direttamente sullo schermo.
Ma prima, a scopo formativo, è bene che mi ristudi un sistema per muovere oggetti entro un'activity.

Come funzionava quello per scrollare un'immagine entro una ImageView?
Ripasso!


Innanzitutto creo una classe:
public class Immagine extends ImageView {

    Context context;
    public Immagine(Context context) {
        super(context);
        this.context=context;
    }
}
Ora voglio metterci un'immagine da risorse.
Prendo un'immagine e la copio nelle risorse, come facevo per Eclipse.

Fatto!

Ora devo inserirla nella mia classe che eredita ImageView.
public class Immagine extends ImageView {

    Context context;
    public Immagine(Context context) {
        super(context);
        this.context=context;
        
        setImageResource(R.drawable.pupazzo);
    }
}
Devo istanziare un oggetto di questa classe, ovviamente...
In MainActivity:
public class MainActivity extends AppCompatActivity {

    RelativeLayout mainLayout;
    Immagine immagine;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mainLayout=(RelativeLayout)findViewById(R.id.mainLayout);
        immagine=new Immagine(this);
        mainLayout.addView(immagine);
        
    }

}
(meglio istanziare oggetti dichiarandone le variabili come variabili di istanza anziché come variabili di metodo, locali) E vediamo il risultato...

Bene, ho ottenuto un'immagine molto grande.
Come fare per ridimensionarla?
public class MainActivity extends AppCompatActivity {

    RelativeLayout mainLayout;
    Immagine immagine;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mainLayout=(RelativeLayout)findViewById(R.id.mainLayout);
        immagine=new Immagine(this);

        mainLayout.addView(immagine);
        immagine.getLayoutParams().width=100;

    }

}
Se metto quest'ultima riga prima dell'aggiunta dell'immagine a mainLayout ottengo un errore NullPointerException perché l'immagine non esiste ancora non essendo ancora stata aggiunta).


Per demarcare l'immagine dal resto, coloro l'activity principale in modo diverso dal bianco.

Bene, sono riuscito alla fine a posizionare l'immagine, non senza qualche problemino dovuto al fatto che non ricordo lo ScaleType da impostare all'immagine.
Alla fine il codice è questo:
public class MainActivity extends AppCompatActivity {

    RelativeLayout mainLayout;
    Immagine immagine;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mainLayout=(RelativeLayout)findViewById(R.id.mainLayout);
        immagine=new Immagine(this);
        immagine.setScaleType(ImageView.ScaleType.FIT_START);
        immagine.setY(0);
        mainLayout.addView(immagine);
        immagine.getLayoutParams().width=200;
        immagine.getLayoutParams().height=200;

    }

}
Per il momento tralascio quel FIT_START, agirò su di esso solo se ci saranno problemi per quanto riguarda lo scrolling.

Ora voglio iniziare, non ricordando bene i codici, dalle cose più semplici: imposto la cosa in modo tale che al semplice tocco la coordinata X cambi.

        immagine.setOnTouchListener(new View.OnTouchListener(){

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                v.setX(100);
                return true;
            }
        });
E riesce perfettamente.
Dunque, facciamo che al movimento i valori attuali di X e Y (tipo float) vengano immagazzinati in due variabili, dopo aver calcolato lo spostamento in rapporto ai valori precedenti delle due variabili.
La prima variabile va impostata all'azione DOWN anziché MOVE.


Sì, ho realizzato lo spostamento, ma non c'è correlazione fra lo spostamento della figura e lo spostamento del mio dito.
Forse il RelativeLayout non fornisce, con il setX e il setY, le giuste coordinate.
Infatti ho ritrovato un mio vecchio codice in cui usavo parametri derivati dal RelativeLayout, e li prendevo a ogni getAction dell'evento Touch.
Provo a rifare adesso la stessa cosa...

Ecco, ho ristudiato il mio vecchio codice.
Il principio è che
  1. Il punto di contatto viene preso una volta sola.
  2. All'azione DOWN viene rilevata la distanza fra il punto di contatto e il margine dell'oggetto.
  3. All'azione MOVE l'oggetto viene riposizionato in base alla distanza del margine dal punto di contatto.
public class MainActivity extends AppCompatActivity {

    float deltaX,deltaY;
    float X,Y;
    RelativeLayout mainLayout;
    Immagine immagine;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mainLayout=(RelativeLayout)findViewById(R.id.mainLayout);
        immagine=new Immagine(this);
        immagine.setScaleType(ImageView.ScaleType.FIT_START);
        RelativeLayout.LayoutParams rl=new RelativeLayout.LayoutParams(0,0);
        mainLayout.addView(immagine,rl);
        immagine.getLayoutParams().width=200;
        immagine.getLayoutParams().height=200;


        immagine.setOnTouchListener(new View.OnTouchListener(){

            public boolean onTouch(View v, MotionEvent event) {

                X=event.getRawX();
                Y=event.getRawY();

                switch(event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) v.getLayoutParams();
                        deltaX = X - lp.leftMargin;
                        deltaY = Y - lp.topMargin;
                        break;

                    case MotionEvent.ACTION_MOVE:
                        lp = (RelativeLayout.LayoutParams) v.getLayoutParams();
                        lp.leftMargin = (int) (X - deltaX);
                        lp.topMargin = (int) (Y - deltaY);
                        v.setLayoutParams(lp);
                        break;
                }

                return true;
            }
        });
    }



}

venerdì 12 agosto 2016

Aggiunta di azione al bottone isolato

Ora dovrei aggiungere un'azione a quella immagine che ho creato sovrapposta allo schermo.
Ho trovato il modo modificando in tal modo il codice:
        WindowManager wm=(WindowManager)getSystemService(WINDOW_SERVICE);
                WindowManager.LayoutParams params=new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.TYPE_SYSTEM_ERROR,
                0,
                PixelFormat.TRANSLUCENT);
        params.gravity= Gravity.RIGHT | Gravity.TOP;
altrimenti l'azione non parte.
L'azione è come evento TouchEvent nel codice della View:
public class Immagine extends View {
    Paint paint;
    Context context;
    public Immagine(Context context) {
        super(context);
        this.context=context;
        paint=new Paint();
        paint.setStyle(Paint.Style.FILL);
        paint.setStrokeWidth(5);
        paint.setColor(Color.RED);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event){
        Uri ringtoneUri= RingtoneManager.getActualDefaultRingtoneUri(context,RingtoneManager.TYPE_RINGTONE);
        Ringtone ringtone=RingtoneManager.getRingtone(context,ringtoneUri);
        ringtone.play();
        return true;
    }


    @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);
        canvas.drawCircle(100, 100, 50, paint);
    }.


}
Il toast non ha una durata illimitata. Ciò è strano.

Ci ho messo altre gestioni dell'evento come il cambiamento di colore dell'activity, e, come qui, una suoneria.

Adesso dovrei esercitarmi a creare un pulsante libero e dargli una funzione.

Però voglio creare un Service di quelli "stabili". Il codice non lo ricordo per niente...

Ricerche sul bottone isolato: primo successo

WindowManager cosa è?
Proviamo a istanziare un oggetto di tipo WindowManager. Ho reso WindowManager wm come variabile di istanza.
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Immagine immagine=new Immagine(this);
        wm=(WindowManager)this.getSystemService(Context.WINDOW_SERVICE);
        wm.addView(immagine);
Ottengo un errore perché il metodo addView prevede necessariamente due parametri di cui uno i parametri.
Devo quindi studiare WindowManager.LayoutParams.
Vediamoli!

Soprattutto, quanti parametri hanno questi LayoutParams?
Qui ne figurano cinque.
  1. larghezza
  2. altezza
  3. tipo
  4. flags
  5. format
Come vengono specificati?
        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
                0,
//              WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
//                      | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
                PixelFormat.TRANSLUCENT);
Ci provo.
Ci si aggiunge anche la proprietà Gravity.
        params=new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
                0,
                PixelFormat.TRANSLUCENT);
        params.gravity= Gravity.RIGHT | Gravity.TOP;
(scritti da me).

E ora ci provo...

Niente da fare!

Mi rendo conto che si tratta di un Service, non di un'Activity.
Costruisco un Service.
Il codice è posto nel metodo onCreate.

E funziona!!!

Continuazione dello studio per i bottoni isolati

Bene.
Ho capito che quella parte del codice non è altro che la creazione di una classe che estende View con il codice per il disegno su di essa.

La parte fondamentale del codice è questa:
    @Override
    public void onCreate() {
        super.onCreate();
        Toast.makeText(getBaseContext(),"onCreate", Toast.LENGTH_LONG).show();
        mView = new HUDView(this);
        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
                0,
//              WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
//                      | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
                PixelFormat.TRANSLUCENT);
        params.gravity = Gravity.RIGHT | Gravity.TOP;
        params.setTitle("Load Average");
        WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
        wm.addView(mView, params);
Prima viene istanziata la classe derivata dalla View:
mView = new HUDView(this);
quindi viene aggiunta non al layout principale, ma a un oggetto di classe WindowManager, dopo una "preparazione" specifica.
Vediamo di analizzarla...

Vengono settati dei parametri di tipo WindowManager.LayoutParams.
Quindi viene istanziato un oggetto di classe WindowManager.
E' necessario andare a vedere le due classi WindowManager e WindowManager.LayoutParams.

Primo ripasso di Paint, Canvas e onDraw

Ecco.
Il passo fondamentale è overridare onDraw.

Però prima bisogna fare una distinzione.
  • Cosa disegnare (Canvas);
  • Come disegnare (Paint).
Dunque prima bisogna creare il Paint, per decidere come disegnare, quindi il Canvas provvederà il cosa disegnare.

Vediamo se per istanziare un oggetto della classe Paint è sufficiente un costruttore senza parametri:
Paint paint=new Paint();
Non ottengo segnalazioni di errore, e per ora ci siamo.

Quindi attribuisco delle proprietà a questo Paint.
        Paint paint=new Paint();
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.RED);
...forse può bastare.
Ora si overridda onDraw, con Canvas per parametro, come si overridda qualunque metodo.
Ma un'activity non ha come metodo onDraw...

Creo una classe che estende View.
public class Immagine extends View {
}
Mi viene fatta la richiesta del costruttore...
public class Immagine extends View {
    public Immagine(Context context) {
        super(context);
    }
}
Mi conviene quindi istanziare Paint nel costruttore:
public class Immagine extends View {
    public Immagine(Context context) {
        super(context);
        Paint paint=new Paint();
    }
}
E quindi ricominciare il discorso dell'overriddare onDraw.
    @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);
    }
Rimetto le proprietà al Paint:
        Paint paint=new Paint();
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.RED);
E ora vediamo se possiamo disegnare qualcosa integrando onDraw: scopro che devo usare nuovamente l'oggetto paint istanziato, e visto che mi trovo in un'altra funzione devo mettere la variabile di questa all'inizio della classe.
    @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);
        canvas.drawCircle(100,100,50,paint);
    }
}
E vediamo...

Istanzio una view della mia classe:
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        Immagine immagine=new Immagine(this);
        

    }
}
e poi vado...

Bene: non succede niente!
Credo di aver dimenticato qualcosa...

Ecco, sì, ho dimenticato di aggiungere la View al RelativeLayout.
Chiamando questo mainLayout, otteniamo:
public class MainActivity extends AppCompatActivity {

    RelativeLayout mainLayout;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mainLayout = (RelativeLayout)findViewById(R.id.mainLayout);
        Immagine immagine=new Immagine(this);
        mainLayout.addView(immagine);


    }
}
E adesso funziona: ho un cerchio rosso sulla mia activity.

Approccio al "button isolato"

Credo di aver trovato il codice che mi interessa per quel pulsantino isolato che sovrasta tutto...

Analizziamo il codice traducendolo in termini umani...

public class HUD extends Service {
    HUDView mView;

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
Viene creato un semplice Service, con il relativo metodo obbligatorio da istanziare onBind... e la dichiarazione di una variabile mView di tipo HUDView. Che tipo sarà questo? Presumibilmente una classe creata ad hoc. Andiamo a cercarla?

Eccola:
class HUDView extends ViewGroup {
    private Paint mLoadPaint;

    public HUDView(Context context) {
        super(context);
        Toast.makeText(getContext(),"HUDView", Toast.LENGTH_LONG).show();

        mLoadPaint = new Paint();
        mLoadPaint.setAntiAlias(true);
        mLoadPaint.setTextSize(10);
        mLoadPaint.setARGB(255, 255, 0, 0);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawText("Hello World", 5, 15, mLoadPaint);
    }

    @Override
    protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) {
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //return super.onTouchEvent(event);
        Toast.makeText(getContext(),"onTouchEvent", Toast.LENGTH_LONG).show();
        return true;
    }
}
Spostiamo il focus su questa.
 private Paint mLoadPaint;
 private Paint mLoadPaint;
Viene dichiarata una variabile di tipo Paint.
E dato che non ricordo bene come funziona tutto il complesso delle istruzioni coinvolgenti Paint, Canvas e onDraw è bene che vada a rivedermelo.


Tengo presente il mio riferimento.
Il focus si sposta su Paint, Canvas e onDraw.

giovedì 11 agosto 2016

Giocherellando con la Toolbar

Ora vorrei rendermi conto di questa nuova struttura dell'activity
Che cosa è il CoordinatorLayout?
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.example.antonello.studiofab.MainActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/content_main" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        android:src="@android:drawable/ic_dialog_email" />

</android.support.design.widget.CoordinatorLayout> 
E' quello che avvolge il tutto.
Dentro di esso sono contenuti:
  1. AppBarLayout
  2. content_main
  3. FloatingActionButton
A sua volta AppBarLayout comprende la ToolBar.

Vediamo di trovare qualche definizione.


Bene.
Prendiamo in considerazione AppBarOverlay.
    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout> 
Ora Toolbar è presa in considerazione nel codice Java.
Vediamo...

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
Solo queste, di cui una è l'istanziazione della variabile Toolbar.
Che significa SetSupportActionBar?
Vediamo.

Praticamente serve solo per far sì che questa Toolbar funzioni da ActionBar.
Ah, ecco, ci sono altri metodi:
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        toolbar.setSubtitle("Ciao, ciccio");
Ecco, ho provato a modificare le dimensioni della Toolbar:
    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="300dp"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout>




Funziona!

Ora cerco di modificarne anche il colore...
    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="300dp"
            android:background="#FAA"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout>




Perfetto!

Floating Action Button, primi approcci.

Mi conviene fare uno studio del Floating Point, adesso.

Veniamo a esaminare il codice.
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });
All'azione click segue questa fantomatica Snackbar che sarebbe uno spazio che si apre sotto all'activity.
E poi setAction... Il codice mi giunge nuovo.
Meglio andare a vedere...

Ecco: intanto ho commentizzato tutta la parte relativa alla Snackbar, e ho creato come codice un semplice Toast:
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(getApplicationContext(),"Ciao floating button",Toast.LENGTH_LONG).show();
                /*Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();*/
            }
        });
Adesso trovo conferma di quanto avevo capito, che la Snackbar è un erede un po' più complesso del Toast.
Vado a provarla...

La scommentizzo e ci gioco un po'...

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                Snackbar.make(view, "Questa è la Snackbar con la sua faccia di cazzo", Snackbar.LENGTH_LONG)
                        .setAction("AZIONEEEE!!!", null).show();
            }
        });
E vediamo il risultato di questi giochini...



Ecco, non mi viene invece "Azioneeee", forse perché c'è quel null davanti...

Vediamo come si agisce su quel null...

Si istanzia un'interfaccia eventListener in forma anonima.
Provo...

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
               
                Snackbar.make(view, "Questa è la Snackbar con la sua faccia di cazzo", Snackbar.LENGTH_LONG)
                        .setAction("AZIONEEEE!!!", new View.OnClickListener() {

                            @Override
                            public void onClick(View v) {
                                Toast.makeText(getApplicationContext(),"Toast derivato dalla Snackbar",Toast.LENGTH_LONG).show();
                            }
                        }).show();
            }
        });
Ecco...



Come volevasi realizzare!

Disabilitare i tasti Recent Tasks e Indietro

Ho trovato il modo di disabilitare sia il tasto Recent Tasks sia il tasto Indietro.
La cosa va sperimentata.
 @Override
 protected void onPause() {
  super.onPause();
  ActivityManager activityManager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE);
  activityManager.moveTaskToFront(getTaskId(), 0);
 }

 @Override
 public void onBackPressed(){
Il primo blocca il tasto Recent Tasks, il secondo metodo blocca il tasto Indietro.

Con tutta la ricerca fatta per trovare questi due metodi, è bene conservarli scrupolosamente.

martedì 9 agosto 2016

Analisi del codice dell'Activity "Telefono".

Questo è il codice della schermata "Telefono":
public class Telefono extends Activity {

	AudioManager audioManager;
	Handler HndNonRisposta;
	Handler handler;
	String frase;
	Ringtone ringtone;
	TimerService mService;
	boolean mBound;
	Button bttRispondi, bttRifiuta;
	Helper helper;
	Timer timerNoRisposta;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_telefono);
		helper=new Helper(getApplicationContext());
		final AudioManager audioManager=(AudioManager)this.getSystemService(Context.AUDIO_SERVICE);
//--------------------------------------------------------------------------------
		//GESTIONE DELLE STRINGHE
		Cursor crs=helper.query();
		if(crs.getCount()!=0){
			Random rnd=new Random();
			int indice=rnd.nextInt(crs.getCount());
			crs.moveToPosition(indice);
			frase=crs.getString(0);
		}else{
			frase="Questa è una frase di prova";
		}
	
		
//--------------------------------------------------------------------------------
		
		
		
//-------------------------------------------------------------------------------
		//SUONERIA
//---------------------------------------------------------------------------------

		Uri ringtoneUri=RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_RINGTONE);
		ringtone=RingtoneManager.getRingtone(this, ringtoneUri);
		ringtone.play();

		bttRispondi=(Button)findViewById(R.id.button1);
		bttRifiuta=(Button)findViewById(R.id.button2);
		
		TimerTask noRisposta=new TimerTask(){

			@Override
			public void run() {
				ringtone.stop();
				mService.startTime();
				finish();
				
			}
			
		};
		timerNoRisposta=new Timer();
		timerNoRisposta.schedule(noRisposta, 10000);
		
//BOTTONE RISPOSTA

		bttRispondi.setOnClickListener(new View.OnClickListener() {
			
			
			@Override
			public void onClick(View v) {
				
				bttRispondi.setEnabled(false);
				bttRifiuta.setEnabled(false);
				timerNoRisposta.cancel();
				timerNoRisposta.purge();
				ringtone.stop();

				TimerTask tt=new TimerTask(){

					@Override
					public void run() {
						mService.tts.setOnUtteranceCompletedListener(new OnUtteranceCompletedListener(){

							@Override
							public void onUtteranceCompleted(String utteranceId) {
								audioManager.setSpeakerphoneOn(true);
								audioManager.setMode(AudioManager.MODE_NORMAL);
								mService.startTime();
								finish();
								
							}
							
						});
						HashMap map=new HashMap();
						map.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "value");
						audioManager.setMode(AudioManager.MODE_IN_CALL);
						audioManager.setSpeakerphoneOn(false);
						mService.tts.speak(frase, TextToSpeech.QUEUE_FLUSH, map);
						
					}
					
				};
				Timer Timer = new Timer();
				Timer.schedule(tt, 1500);

			}
		});

//BOTTONE RIFIUTA

		bttRifiuta.setOnClickListener(new View.OnClickListener() {
			
			@Override
			public void onClick(View v) {
				timerNoRisposta.cancel();
				timerNoRisposta.purge();
				ringtone.stop();
				mService.startTime();
				finish();
				
			}
		});
	}
//-------------------------------------------------------------------------
	
	@Override
	public void onWindowFocusChanged(boolean hasFocus){
		if(!hasFocus){
			ringtone.stop();

		}else{
			ringtone.play();
		}	
	}
	
	
	@Override
	public void onStart(){
		super.onStart();
		Intent intent=new Intent(this,TimerService.class);
		bindService(intent,mConnection,Service.BIND_AUTO_CREATE);
	}
	
	@Override
	public void onStop(){
		super.onStop();
		if(mBound){
			unbindService(mConnection);
			mBound=false;
		}
	}
	
	ServiceConnection mConnection=new ServiceConnection(){

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

		@Override
		public void onServiceDisconnected(ComponentName name) {
			mBound=false;
		}
	};
}
Ci sono diversi "blocchi".
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_telefono); helper=new Helper(getApplicationContext()); final AudioManager audioManager=(AudioManager)this.getSystemService(Context.AUDIO_SERVICE); Qui, subito dopo l'inizio di onCreate, si inizializzano Helper, per la gestione del database, e AudioManager, per la gestione delle telefonate.

C'è poi la gestione delle stringhe, che sono state salvate nel database da un'altra schermata:
		//GESTIONE DELLE STRINGHE
		Cursor crs=helper.query();
		if(crs.getCount()!=0){
			Random rnd=new Random();
			int indice=rnd.nextInt(crs.getCount());
			crs.moveToPosition(indice);
			frase=crs.getString(0);
		}else{
			frase="Questa è una frase di prova";
		}


Ora c'è la suoneria: questo codice viene eseguito sul momento e innesca la suoneria:
//-------------------------------------------------------------------------------
		//SUONERIA
//---------------------------------------------------------------------------------

		Uri ringtoneUri=RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_RINGTONE);
		ringtone=RingtoneManager.getRingtone(this, ringtoneUri);
		ringtone.play();


Ora, dopo l'istanziazione dei due buttons (che dovrei spostare prima) c'è la chiusura ritardata della suoneria nel caso in cui non vi sia risposta:

		bttRispondi=(Button)findViewById(R.id.button1);
		bttRifiuta=(Button)findViewById(R.id.button2);
		
		TimerTask noRisposta=new TimerTask(){

			@Override
			public void run() {
				ringtone.stop();
				mService.startTime();
				finish();
				
			}
			
		};
		timerNoRisposta=new Timer();
		timerNoRisposta.schedule(noRisposta, 10000);
Questo codice di interruzione della suoneria va a finire alla via finale comune: mService.startTime e finish().

Ed ecco quindi i codici dei pulsanti.
//BOTTONE RISPOSTA

		bttRispondi.setOnClickListener(new View.OnClickListener() {
			
			
			@Override
			public void onClick(View v) {
				
				bttRispondi.setEnabled(false);
				bttRifiuta.setEnabled(false);
				timerNoRisposta.cancel();
				timerNoRisposta.purge();
				ringtone.stop();

				TimerTask tt=new TimerTask(){

					@Override
					public void run() {
						mService.tts.setOnUtteranceCompletedListener(new OnUtteranceCompletedListener(){

							@Override
							public void onUtteranceCompleted(String utteranceId) {
								audioManager.setSpeakerphoneOn(true);
								audioManager.setMode(AudioManager.MODE_NORMAL);
								mService.startTime();
								finish();
								
							}
							
						});
						HashMap map=new HashMap();
						map.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "value");
						audioManager.setMode(AudioManager.MODE_IN_CALL);
						audioManager.setSpeakerphoneOn(false);
						mService.tts.speak(frase, TextToSpeech.QUEUE_FLUSH, map);
						
					}
					
				};
				Timer Timer = new Timer();
				Timer.schedule(tt, 1500);
Disabilita i pulsanti per evitarne pressioni accidentali con l'orecchio;
Elimina il task della non-risposta;
Blocca la suoneria;

Incapsula in un TimerTask tutto il codice per il parlato: dapprima il listener per il termine del parlato, quindi crea l'HashMap, imposta il microfono del cellulare e parla. Dopo il parlato OnUtteranceCompletedListener torna alla via finale comune mService.startTime e finish().


Quindi il pulsante "Rifiuta":
//BOTTONE RIFIUTA

		bttRifiuta.setOnClickListener(new View.OnClickListener() {
			
			@Override
			public void onClick(View v) {
				timerNoRisposta.cancel();
				timerNoRisposta.purge();
				ringtone.stop();
				mService.startTime();
				finish();
				
			}
		});
	}
Questo elimina il Task della "Non risposta", blocca la suoneria, e torna alla via finale comune mService.startTime e finish().

Mi sembra abbastanza stabile.
Quindi in pratica ogni evenienza esegue nell'ordine:
  1. Blocco della suoneria;
  2. mService.startTime;
  3. finish();
Fra i primi due si inserisce, nel caso di Risposta, il codice del parlato.

Si inizia a studiare i FAB.

Cerco di impadronirmi della scienza necessaria per costruire un FAB, che sarebbe una bellissima cosa.

Ricordare sempre di comportarsi come l'acqua, e mai come un sasso.

Intanto si definisce la classe, che estende View:
public class FloatingActionButton extends View {

 public FloatingActionButton(Context context) {
  super(context);
  // TODO Auto-generated constructor stub
 }

}
Questo è ciò che mi viene inizialmente, insieme al costruttore.
Ora dichiariamo, come di prassi, una variabile Context e vi poniamo il context preso come parametro del costruttore.
public class FloatingActionButton extends View {

 Context context;
 public FloatingActionButton(Context context) {
  super(context);
  this.context=context;
 }

}
Ecco che la cosa si va complicando.
Nel costruttore viene chiamata una funzione init(Color.WHITE).
Andando più avanti la scopro:
 public void init(int FloatingActionButtonColor) {
    setWillNotDraw(false);
    setLayerType(View.LAYER_TYPE_SOFTWARE, null);

    mButtonPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mButtonPaint.setColor(FloatingActionButtonColor);
    mButtonPaint.setStyle(Paint.Style.FILL);
    mButtonPaint.setShadowLayer(10.0f, 0.0f, 3.5f, Color.argb(100, 0, 0, 0));
    mDrawablePaint = new Paint(Paint.ANTI_ALIAS_FLAG);

...
Cosa sono queste? Funzioni e classi...
Dalla terza riga in poi è codice per il disegno, e quindi per il momento lo lascio stare. Mi interesso di setWillNotDraw e setLayerType, cercando se sono definite da qualche parte...

E invece sono metodi dell'oggetto View da cui questa classe eredita.
Per il momento lasciamoli stare, e vediamo oltre.
mButtonPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mi sembrano tutte istruzioni sulle quali posso avere le idee più chiare se vado a rivedermi Paint, Canvas onDraw eccetera... Vado a farlo.

domenica 7 agosto 2016

Studio della schermata Telefono non richiamabile dai Recent Tasks mediante l'apposito FLAG.

Attivo il programma.
Chiudo la MainActivity
Appare la schermata Telefono a intervalli.
Schiacciando Rispondi o Rifiuta funziona come deve e poi sparisce.

Ora provo, dopo che è sparita, a richiamarla in Recent Tasks.
Non vi rimane.
Per disattivare il programma devo riaprire dall'icona.

Ora provo a eliminare il flag:
   @Override
   public void run() {
    Intent intent =new Intent(getApplicationContext(),Telefono.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
    startActivity(intent);
    
   }


Adesso appare e sta lì ferma, quando la richiamo riappare e ricomincia a suonare.
Rimetto il flag...

Il comportamento è come prima.
In Manifest non ci sono elementi che governino la persistenza delle activities nel Recent Tasks: la caratteristica è governata esclusivamente dal FLAG di cui sopra.

Revisione critica dei settings per gli intervalli nella mia CoachCall.

Ripercorriamo il percorso delle SharedPreferences nella mia app...

Si apre MainActivity.
In onStart si avvia e si binda il Service:
 public void onStart() {
  super.onStart();

  Intent intent = new Intent(this, TimerService.class);
  startService(intent);
  bindService(intent, mConnection, Service.BIND_AUTO_CREATE);
 }
Il Service, una volta attivo, crea SharedPreferences:
public class TimerService extends Service {

 SharedPreferences SP; 
 
 public TextToSpeech tts;
 
 OnAlarmChangeListener onAlarmChangeListener;
 boolean alarmOn;

 Handler handler=new Handler();
 IBinder mBinder=new LocalBinder();
 
 @Override
 public void onCreate(){

 
  Intent intent=new Intent(this,MainActivity.class);
  PendingIntent pi=PendingIntent.getActivity(this, 0, intent, 0);
  Notification notification=new NotificationCompat.Builder(this)
    .setSmallIcon(R.drawable.cornetta)
    .setContentTitle("CoachCall")
    .setContentText("Telefonate dal Coach")
    .setContentIntent(pi).build();
  startForeground(1,notification);
    
  
  SP=getApplicationContext().getSharedPreferences("settings", Context.MODE_PRIVATE);
  SharedPreferences.Editor editor= SP.edit();
  editor.putInt("intervalloMin", 10);
  editor.putInt("intervalloMax", 10);
  editor.commit();
Qui viene direttamente impostato intervalloMin e intervalloMax.
Quindi in startTime, ossia quando già onCreate è stato chiamato, si usano intervalloMin e intervalloMax:
 public void startTime(){
  int intervalloMin=SP.getInt("intervalloMin", 0);
  int intervalloMax=SP.getInt("intervalloMax", 0);
  int delta=intervalloMax-intervalloMin;
  Random rnd=new Random();
  int differenza=(intervalloMax==intervalloMin)? 0 : rnd.nextInt(delta);
  int intervallo=intervalloMin+differenza;
  Runnable runnable=new Runnable(){

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

Quando si apre l'activity Impostazioni degli Intervalli, ecco il codice:
  SP=getApplicationContext().getSharedPreferences("settings",Context.MODE_PRIVATE);
  seekBar1=(SeekBar)findViewById(R.id.seekBar1);
  seekBar2=(SeekBar)findViewById(R.id.seekBar2);
  textView1=(TextView)findViewById(R.id.textView1);
  textView2=(TextView)findViewById(R.id.textView2);
  //codice che setta le seekBars in relazione ai valori letti nelle impostazioni
  
  seekBar1.setMax(230);
  intervalloMin=SP.getInt("intervalloMin", 10);
  setTextViewFromSeekBar(intervalloMin,textView1);
  seekBar1.setProgress(intervalloMin-10);
  
  seekBar2.setMax(230);
  intervalloMax=SP.getInt("intervalloMax", 10);
  setTextViewFromSeekBar(intervalloMax,textView2);
  seekBar2.setProgress(intervalloMax-10);
All'atto della regolazione degli intervalli, il valore che viene preso in caso di settings non inizializzati è 10.
In base a questo vengono compilati le TextView con i valori e le seekBars, la cui posizione viene settata a 10 in meno, in modo che per un valore di 10 vengano settate effettivamente a 0.
Il punto centrale è quello dell'utilizzo dei settings nella funzione setTime di TimerService.
Un codice come questo
  SharedPreferences.Editor editor= SP.edit();
  editor.putInt("intervalloMin", 10);
  editor.putInt("intervalloMax", 10);
  editor.commit();
seguito da questo che preleva i dati per l'utilizzo effettivo in startTime
 public void startTime(){
  int intervalloMin=SP.getInt("intervalloMin", 0);
  int intervalloMax=SP.getInt("intervalloMax", 0);
  int delta=intervalloMax-intervalloMin;
  Random rnd=new Random();
  int differenza=(intervalloMax==intervalloMin)? 0 : rnd.nextInt(delta);
  int intervallo=intervalloMin+differenza;
  Runnable runnable=new Runnable(){

   @Override
   public void run() {
    Intent intent =new Intent(getApplicationContext(),Telefono.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
    startActivity(intent);
    
   }
   
  };
non ha senso: meglio togliere l'impostazione iniziale e prendere direttamente come 10 il valore in startTime se il setting non è inizializzato.
Ossia, il primo diventa:
  SP=getApplicationContext().getSharedPreferences("settings", Context.MODE_PRIVATE);
mentre in startTime:
 public void startTime(){
  int intervalloMin=SP.getInt("intervalloMin", 10);
  int intervalloMax=SP.getInt("intervalloMax", 10);
  int delta=intervalloMax-intervalloMin;
  Random rnd=new Random();
....
Quindi quando andremo a regolare gli intervalli, lasciando tutto inalterato...
  seekBar1.setMax(230);
  intervalloMin=SP.getInt("intervalloMin", 10);
  setTextViewFromSeekBar(intervalloMin,textView1);
  seekBar1.setProgress(intervalloMin-10);
  
  seekBar2.setMax(230);
  intervalloMax=SP.getInt("intervalloMax", 10);
  setTextViewFromSeekBar(intervalloMax,textView2);
  seekBar2.setProgress(intervalloMax-10);
in caso di non inizializzazione dei settings, l'intervallo preso per buono sarà 10, che andrà messo pari pari nelle TextView mentre seekBars saranno settate al valore -10, ossia a zero.
Una volta che invece con le seekBars sarà settato un valore diverso, questo sarà quello utilizzato in startTime del Service, e quello letto dai settings nel form per la regolazione degli intervalli, sempre settando le seekBars al valore sottratto di 10.

giovedì 4 agosto 2016

Immissione e feedback delle frasi immesse in un database.

Creare un database di frasi con possibilità di inserire nuovi record digitando in una EditText, mostrarli in una ScrollView e cancellarli mediante un pulsante.

Ho questo file di layout:
<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.palestrascrollview.MainActivity" >
 

</RelativeLayout> 
Devo aggiungere una ScrollView che contiene un Layout. Scelgo un LinearLayout.
<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.palestrascrollview.MainActivity" >
 <ScrollView
     android:id="@+id/scrollView"
     android:fillViewport="true"
     android:layout_width="400dp"
     android:layout_height="100dp"
     android:layout_alignParentTop="true"
     android:layout_alignParentLeft="true">
     <LinearLayout
                android:orientation="vertical"
         android:id="@+id/scrollLayout"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:background="#afc" />
 </ScrollView>

</RelativeLayout> 
Bene: ho ottenuto un quadrato di colore verdino-azzurro (quello da me voluto), che dovrebbe comportarsi come una scrollView.
Ma vorrei aggiungervi anche altre cosette.
Una EditText e un pulsante per l'immissione.

Sono giunto a una soluzione ragionevole, usando LinearLayout, che al momento preferisco rispetto ai RelativeLayout:
<LinearLayout 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:orientation="vertical"
    tools:context="com.example.palestrascrollview.MainActivity" >
    
 <LinearLayout
     android:orientation="horizontal"
     android:layout_width="match_parent"
     android:layout_height="wrap_content" >
     
     <ScrollView
            android:id="@+id/scrollView"
            android:layout_width="400dp"
            android:layout_height="150dp"
            android:layout_alignParentLeft="true"
            android:layout_alignParentTop="true"
            android:fillViewport="true" >

            <LinearLayout
                android:id="@+id/scrollLayout"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="#afc"
                android:orientation="vertical" />
        </ScrollView>

 </LinearLayout>
 
 <LinearLayout
     android:orientation="horizontal"
     android:layout_width="match_parent"
     android:layout_height="wrap_content" >

     <EditText
         android:id="@+id/editText1"
         android:layout_marginTop="30dp"
         android:layout_width="400dp"
         android:layout_height="wrap_content"
         android:ems="10" >

     <requestFocus />
 </EditText>
 
 <Button
     android:id="@+id/button1"
     android:layout_marginTop="30dp"
     android:layout_marginLeft="30dp"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:text="Button" />    
 </LinearLayout>
 
</LinearLayout> 
Adesso occupiamoci del codice...

Ho creato un codice per una classe atta a generare il Database:
public class MainActivity extends Activity {

 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  

 }
 
 class Helper extends SQLiteOpenHelper{

  Context context;
  public Helper(Context context) {
   super(context, "database.db", null, 1);
   this.context=context;
  }
  @Override
  public void onCreate(SQLiteDatabase db) {
   db.execSQL("CREATE TABELLA(TESTO TEXT)");
   
  }
  
  public void save(String frase){
   SQLiteDatabase db=this.getWritableDatabase();
   ContentValues values=new ContentValues();
   values.put("TESTO", frase);
   db.insert("TABELLA", null, values);
  }
  
  public Cursor query(){
   SQLiteDatabase db=this.getWritableDatabase();
   Cursor crs=db.rawQuery("SELECT * FROM TABELLA", null);
   return crs;
   
   
  }
  @Override
  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
   // TODO Auto-generated method stub
   
  }
  
 }


}
Ora devo lavorare per l'immissione dei dati nel database, usando EditText e il Button.
Al momento ho trovato questo:
public class MainActivity extends Activity {

 EditText editText;
 Button button;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  final Helper helper=new Helper(this);
  editText=(EditText)findViewById(R.id.editText1);
  button=(Button)findViewById(R.id.button1);
  
  button.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    helper.save(editText.getText().toString());

    
    
   }
  });

 }
I dati mi risultano regolarmente immessi nel database.
Ora devo pensare a un modo di visualizzarli immediatamente nella ScrollView.
Per il momento mi visualizzo la frase in LogCat per vedere che sia tutto a posto:
  button.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    helper.save(editText.getText().toString());
    frase=helper.query().getString(2);
    Log.d("FRASE", frase);
    
   }
  });
Ottengo un errore IndexOutOfBound, e mi viene in mente che ho dimenticato il MoveToFirst...
  public Cursor query(){
   SQLiteDatabase db=this.getWritableDatabase();
   Cursor crs=db.rawQuery("SELECT * FROM TABELLA", null);
   crs.moveToFirst();
   return crs;
   
   
  }
Un'altra correzione per l'errore del numero di colonna nel metodo getString dell'oggetto query():
   @Override
   public void onClick(View v) {
    helper.save(editText.getText().toString());
    frase=helper.query().getString(1);
    Log.d("FRASE", frase);
    
   }
e la cosa riesce!
Ora voglio estrarre, però, non solo il primo record del Cursor, ma tutti:
   @Override
   public void onClick(View v) {
    helper.save(editText.getText().toString());
    Cursor c=helper.query();
    do{
     Log.d("frase", c.getString(1));
    }while(c.moveToNext());
    
   }
Vediamo...

08-04 10:42:22.708: D/frase(8361): cvgfddghjjjjjjjhvvcfdsdhhjk
08-04 10:42:22.708: D/frase(8361): sddfffggg
08-04 10:42:22.708: D/frase(8361): ciao
08-04 10:42:22.708: D/frase(8361): ciccia
08-04 10:42:22.708: D/frase(8361): fgggggggggg
08-04 10:42:22.708: D/frase(8361): ciccio bello non scassare LA minchia
Perfetto!

mercoledì 3 agosto 2016

Uso di una ScrollView nel mio CoachCall.

Ho riusato una ScrollView.
Il modo migliore di usarla è questo:
<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.palestrascrollview.MainActivity" >
 
<ScrollView 
    android:layout_width="200dp"
    android:layout_height="200dp" 
    android:fillViewport="true"
    android:layout_alignParentLeft="true"
    android:layout_alignParentTop="true"
    android:layout_marginLeft="30dp"
    android:layout_marginTop="30dp"

    android:id="@+id/scrollView" >
     
     <LinearLayout
         android:orientation="vertical"
            android:id="@+id/mainLayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#0cf" >
        </LinearLayout>
 </ScrollView>
</RelativeLayout> 
L'elemento cruciale è metterci dentro un LinearLayout (o anche un RelativeLayout o altro)
E ora ho una scrollView sulla quale aggiungere quello che voglio.

Voglio aggiungerci delle TextViews.
Voglio crearle però staccate le une dalle altre.
Ecco il codice:
public class MainActivity extends Activity {

 LinearLayout mainLayout;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  mainLayout=(LinearLayout)findViewById(R.id.mainLayout);
  
  TextView textView=new TextView(this);
  textView.setBackgroundColor(Color.WHITE);
  textView.setText("Nel mezzo del cammin di nostra vita\r\n mi ritrovai per una selva oscura\r\n che la diritta via era smarrita");
  LinearLayout.LayoutParams lp=new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.WRAP_CONTENT);
  lp.setMargins(0, 20, 0, 20);
  mainLayout.addView(textView, lp);
  TextView textView1=new TextView(this);
  textView1.setBackgroundColor(Color.WHITE);
  textView1.setText("Nel mezzo del cammin di nostra vita\r\n mi ritrovai per una selva oscura\r\n che la diritta via era smarrita");
  mainLayout.addView(textView1,lp);
  textView.setHeight(200);
  textView1.setHeight(200);
 }


}
e funziona perfettamente: appaiono i testi scrollabili su distinte caselle di testo.

Aggiunta della suoneria e del meccanismo di cessazione attesa della risposta.

Ora, prima della voce sintetica va aggiunta la suoneria.
All'inizializzazione della schermata Telefono va innescata la suoneria, che poi va interrotta dal tasto Rispondi, il quale dà avvio alla voce.
Dunque lo metto in onCreate.
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_telefono);
  
  Uri ringtoneUri=RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_RINGTONE);
  ringtone=RingtoneManager.getRingtone(this, ringtoneUri);
  ringtone.play();
  bttRispondi=(Button)findViewById(R.id.button1);
  bttRispondi.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    ringtone.stop();
    mService.tts.setOnUtteranceCompletedListener(new OnUtteranceCompletedListener(){
     @Override
     public void onUtteranceCompleted(String utteranceId) {
      mService.startTime();
      finish(); 
     }
    });
La suoneria inizia a suonare e viene interrotta dal tasto Rispondi.
Proviamolo...

Sì, funziona.

Però sarebbe il caso di immettere un piccolo ritardo...
Proviamo:
    Handler handler=new Handler();
    handler.postDelayed(new Runnable(){

     @Override
     public void run() {
      HashMap hashMap=new HashMap();
      hashMap.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "id");
      
      mService.tts.speak("Ciao, scemo",TextToSpeech.QUEUE_FLUSH,hashMap);
      
     }
     
    },1000);
Proviamo...

Sì, funziona!

Prevedendo una non risposta da parte dell'utente, ossia né "accetta" né "rifiuta" ci dovrebbe essere l'abbandono della chiamata da parte del chiamante, come in tutte le normali telefonate.
Ho avuto delle difficoltà, però...

Vediamo di implementare questa funzione.
Ho "compattato" un po' il codice per la risposta: il listener per il termine del parlato viene inserito nel Runnable da lanciare con la latenza necessaria per portare il ricevitore all'orecchio:
  bttRispondi.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    ringtone.stop();
    handler=new Handler();
    handler.postDelayed(new Runnable(){

     @Override
     public void run() {
      mService.tts.setOnUtteranceCompletedListener(new OnUtteranceCompletedListener(){
       @Override
       public void onUtteranceCompleted(String utteranceId) {
        mService.startTime();
        finish(); 
       }
      });
      HashMap hashMap=new HashMap();
      hashMap.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "id");
      
      mService.tts.speak(frase,TextToSpeech.QUEUE_FLUSH,hashMap);
      handler.removeCallbacksAndMessages(null);
     }
    },1000);
   }
  });
Ho aggiunto anche il codice per la "ripulitura" dell'handler, anche se non credo ce ne sia proprio bisogno dal momento che con la distruzione dell'activity l'handler va distrutto... magari lo levo.

Metto ora un Runnable che viene lanciato dopo alcuni secondi dall'inizio della suoneria, con l'istruzione di interromperla e tornare al Service, distruggendo Telefono.
Vediamo se funziona...

  Uri ringtoneUri=RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_RINGTONE);
  ringtone=RingtoneManager.getRingtone(this, ringtoneUri);
  ringtone.play();
  HndNonRisposta=new Handler();
  HndNonRisposta.postDelayed(new Runnable(){

   @Override
   public void run() {
    ringtone.stop();
    mService.startTime();
    finish();
    
   }
   
  },10000);
Ora, se rispondo non ci sono problemi, mentre se rifiuto accade qualcosa di strano: si sovrappone un altro ciclo di schermate a ripetizione.
Questo perché il rifiuto fa tornare immediatamente il flusso del programma a startTime del Service, ma se resta attivo il ritardo per la chiusura spontanea questo torna a sua volta sempre a startTime del Service e tutto si impappa...

E' importante quindi aggiungere, qualora questo stop ritardato della suoneria venga anticipato dallo stop dovuto al rifiuto della chiamata, ma anche alla risposta (che non mi ha dato problemi forse per i tempi più lunghi...), un codice che "ripulisca" l'handler.
//BOTTONE RISPOSTA
  bttRispondi=(Button)findViewById(R.id.button1);
  bttRispondi.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    HndNonRisposta.removeCallbacksAndMessages(null);
    ringtone.stop();
    handler=new Handler();
    handler.postDelayed(new Runnable(){

     @Override
     public void run() {
      mService.tts.setOnUtteranceCompletedListener(new OnUtteranceCompletedListener(){
       @Override
       public void onUtteranceCompleted(String utteranceId) {
        mService.startTime();
        finish(); 
       }
      });
      HashMap hashMap=new HashMap();
      hashMap.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "id");
      
      mService.tts.speak(frase,TextToSpeech.QUEUE_FLUSH,hashMap);
      handler.removeCallbacksAndMessages(null);
     }
    },1000);
   }
  });

//BOTTONE RIFIUTA
  bttRifiuta=(Button)findViewById(R.id.button2);
  bttRifiuta.setOnClickListener(new View.OnClickListener() {
   
   @Override
   public void onClick(View v) {
    HndNonRisposta.removeCallbacksAndMessages(null);
    ringtone.stop();
    mService.startTime();
    finish();
    
   }
  });

martedì 2 agosto 2016

Aggiunta della voce sintetica.al CoachCall.

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

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

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

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

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

Aggiunta del TTS al CoachCall

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


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

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

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


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

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

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

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

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

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


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

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

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

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

Funziona!

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

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