Date Archives

Settembre 2019

Timber Android: loggare con serenità!

Oggi voglio parlare con voi di un aspetto che ogni sviluppatore, sia mobile che web, ha a che fare ogni giorno. Scrivere ed organizzare i propri log applicativi è importantissimo. Ancora più importante è offuscarli negli ambienti di produzione, sia per questioni di pulizia che per questioni di sicurezza (loggare lo stato di chiamate http con tutti i parametri, quali username, password o secret key ad esempio). Una libreria che ha reso questa gestione più serena in Android è Timber, ed oggi voglio parlarvene!

In Android la logica di decidere quali log tenere in produzione o meno è responsabilità del programmatore. Per un gran periodo della mia carriera, avevo creato una classe “LogUtils” che funzionava da intermediario con il mio codice e la classe android.util.Log. Essa conteneva tutti i metodi per loggare come la relativa classe nativa, ma prima di scriverli controllava se la costante d’ambiente BuildConfig.DEBUG fosse positiva. In caso contrario, non scriveva il log.

Questa grezza logica riusciva si a fare il proprio lavoro, ma non mi dava molte libertà. In alcuni casi mi poteva far comodo tenere dei log in produzione.
Inoltre, dopo anni di sviluppo (e forse anche voi!), comincio ad infastidirmi verso alcuni processi ripetitivi che noi sviluppatori effettuiamo ogni giorno: Ogni volta che dovevo scrivere un log su una classe nuova, la famosissima ed irritante variabile statica TAG mancava ed il perdere quei 10 secondi ogni volta mi urtava il sistema nervoso.

Per tutte queste problematiche ci viene incontro Timber, una libreria Android che ci facilita la scrittura e la logica dei nostri log!

Android Timber, che cosa è

Timber è una piccola libreria Android che contiene dei metodi che facilitano la gestione dei log all’interno dell’applicazione. è possibile infatti “piantare” Timber all’inizio dell’applicazione, e decidere quali log devono essere stampati o meno. Potrete così decidere di offuscare determinati log con la build di produzione e invece stampare qualsiasi informazione in quella di debug. La logica con cui vengono oscurati o meno i log è decisa interamente da voi, dandovi così la più completa libertà, insieme alla semplicità di utilizzo.

Implementazione ed uso di Timber in un applicazione Android

Per importare la libreria nel vostro progetto basterà aggiungere l’implementazione nel vostro gradle:

implementation 'com.jakewharton.4.7.1'

Vi consiglio di controllare la guida nella pagina di Github poichè il numero di versione potrebbe essere aggiornato.
Sincronizzate il progetto ed ecco fatto, siete pronti per “piantare” i vostri log!

Per configurare Timber andrà “piantato” un “albero” (Tree), vale a dire creare una istanza. Di solito il posto migliore è nel OnCreate della vostra classe Application:

public class ExampleApp extends Application {

@Override public void onCreate() {
     super.onCreate();

     if (BuildConfig.DEBUG) {
        Timber.plant(new DebugTree());
     } else {
        Timber.plant(new CrashReportingTree());
     }
}

}

Questo esempio “pianta” un DebugTree in caso si tratti di una build di Debug. In caso di build di produzione, invece, Timber crea un CrashReportingTree.

DebugTree è una classe che si trova all’interno della libreria che permette di loggare tutto. In ambiente di Debug è molto importante loggare qualsiasi cosa, per agevolare sia lo sviluppo che il bug fixing.
In produzione la situazione è molto diversa, vogliamo che i log siano il meno possibile, per motivi di sicurezza.


Ogni applicazione può necessitare diverse logiche di log in produzione: Alcune non vogliono loggare niente, mentre altre solo i log di livello ERROR o di determinate classi. Per avere il pieno controllo sarà necessario creare una classe che estende Timber.Tree ed gestire la logica all’interno del metodo log:

/** A tree which logs important information for crash reporting. */
   private static class CrashReportingTree extends Timber.Tree {
     @Override protected void log(int priority, String tag, @NonNull String message, Throwable t) {

       if (priority == Log.VERBOSE || priority == Log.DEBUG) {
         return;
       }  

       FakeCrashLibrary.log(priority, tag, message);

       if (t != null) {
          if (priority == Log.ERROR) {
             FakeCrashLibrary.logError(t);
       } else if (priority == Log.WARN) {
          FakeCrashLibrary.logWarning(t);
       }
    }
  }
}

In questo caso, qualsiasi log di livello VERBOSE o DEBUG viene completamente ignorato, mentre gli altri vengono gestiti da una logica personalizzata dell’applicazione.

Iniziamo a Timber – loggare!

Ora che abbiamo configurato la nostra libreria, possiamo iniziare a loggare!
La procedura di log è semplicissima! Timber ha gli stessi metodi statici della classe Log di Android, basterà sostituire “Log.METODO” con “Timber.METODO” (Timber.d, Timber.w, Timber.v e così via):

Timber.d("Ciao, sono un log!")

TAG ? No grazie!

Eccoci qui . ad una grande funzionalità di questa libreria! Siete stanchi, ogni volta che loggate in una nuova classe, di creare la solita variabile statica privata TAG?
Nessun problema! Richiamando i metodi di Timber, esso riuscirà ad intuire da quale classe è stato chiamato il metodo e metterà in automatico il nome della classe. Un problema in meno!

Nel caso invece si voglia mettere un TAG personalizzato, è sempre possibile farlo con il seguente modo:

Timber.tag("TagPersonalizzato").d("Ciao sono un log con un tag personalizzato!");

Concatenazione di Stringhe e variabili

Nel caso in cui il testo dei log contiene stringhe e variabili (la maggior parte dei casi), non sarà necessario scrivere “stringa” + variabile, oppure usare String.format().
Timber gestisce la formattazione in maniera automatica:

Timber.d("Ciao, mi chiamo %s %s", firstName, lastName);

Un’ altra funzionalità che ci permettere di perdere meno tempo nella scrittura di log!

Lint, che passione!

All’interno di Timber sono gestite delle regole Lint che ci mostreranno dei warning che potremmo risolvere manualmente o che verranno automaticamente risolte dalla libreria:

  • Numero incorretto di argomenti
  • Argomenti di tipo diverso da quello specificato
  • Lunghezza dei TAG superiori a 23 (lunghezza massima di Android)
  • Metodi che usano ancora Log.* invece di Timber.*
  • Concatenazione di stringhe al posto della formattazione automatica di Timber
  • Uso di log con messaggio nullo o vuoto. Notifica anche i log che contengono come messaggio solo un eccezione

Tutti questi warning possono essere automaticamente risolti da Timber cliccando sulla lampadina gialla.

Spero che il mio articolo vi abbia incuriosito, vi lascio qui il link della pagina di Github. Il mio consiglio è quello di provarlo, la sua semplicità ed immediatezza ha fatto scattare in me amore a prima vista!

Alla prossima,
Buon coding!

Guida DownloadManager: Componenti Jetpack #1

Oggi vi voglio parlare di un componente della raccolta Jetpack di Android, “DownloadManager”, insieme ad una breve panoramica e guida del suo utilizzo.
Potrete consultare un esempio completo attraverso questo repository di Github. Gli snippet di codice successi saranno presi da questo progetto.
Inoltre, è disponibile su PlayStore l’applicazione di prova che ho creato appositamente come supporto a questo articolo.

Guida DownloadManager: Di cosa tratta?

DownloadManager è un servizio di sistema Android che gestisce i download HTTP di lunga/breve durata. Non richiede implementazioni di librerie aggiuntive, poiché si trova all’interno del’ SDK di Android.
Le applicazioni possono richiedere a questo servizio di scaricare un Uri e di salvarlo in uno specifico percorso, che sia privato dell’applicazione o pubblico.

DownloadManager si occuperà infine di gestire il processo in background di download, delle interazioni HTTP e ripartirà dall’inizio in caso di fallimento o di riavvio del dispositivo.

I permessi necessari

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
  • android.permission.INTERNET
    Necessario per le applicazioni per accedere ad internet.
  • (OPZIONALE) android.permission.DOWNLOAD_WITHOUT_NOTIFICATION
    Necessario per usare DownloadManager senza rendere visibile la notifica (DownloadManager.Request.VISIBILITY_HIDDEN) di download del file. Nel caso si lasciasse la notifica visibile, tale permesso non è necessario.

Come potete vedere, l’utilizzo di questo componente non prevede l’utilizzo di particolari permessi (soprattutto quelli catalogati da Android come “pericolosi”).

Come richiedere un download

Per prima cosa, è necessario creare una istanza della classe DownloadManager. Come tutti i servizi di sistema Android, non viene creata un istanza dal costruttore ma viene richiesta dal contesto:

val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager

Ora che abbiamo creato l’oggetto, possiamo inviargli una richiesta di download, creando un oggetto di classe DownloadManager.Request.
Questa classe ha l’obiettivo di contenere tutte le informazioni necessarie a DownloadManager per poter gestire il download di un File:

private fun generateDownloadRequest():DownloadManager.Request{

//Creiamo un oggetto DownloadManager.Request e 
//gli passiamo l'url per il download del file
val request:DownloadManager.Request = DownloadManager.Request(Uri.parse("http://url_file.pdf"))
//Impostiamo il titolo della notifica che verrà visualizzata    
request.setTitle(txt_notification_title.editText?.text.toString())
//Impostiamo la descrizione della notifica che verrà visualizzata        
request.setDescription(txt_notification_description.editText?.text.toString())
//impostiamo la destinazione del file, in questo caso sarà nella cartella privata dell'applicazione "Downloads". Il nome del file sarà testFile
request.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS,"testFile")
//impostiamo se il download ha il permesso di usare il traffico della rete mobile
request.setAllowedOverMetered(true)
 //impostiamo se il download ha il permesso di scaricare anche in caso fossimo in roaming       
request.setAllowedOverRoaming(true)
 
//Build.VERSION_CODES.N o maggiore: Impostiamo se il 
//download deve partire solo se il dispositivo è in carica         
request.setRequiresCharging(false)
//Build.VERSION_CODES.N o maggiore: Impostiamo se il 
//download deve partire solo se il dispositivo è in
//modalità Idle (inattivo)
request.setRequiresDeviceIdle(false)

//Impostiamo se si visualizza o meno la notifica di download
//
//DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
//Notifica visibile sia durante che a completamento del download
//
//DownloadManager.Request.VISIBILITY_HIDDEN
//Mai visualizzata. Ricordarsi di aggiungere al manifest il permesso
//android.permission.DOWNLOAD_WITHOUT_NOTIFICATION
//
//DownloadManager.Request.VISIBILITY_VISIBLE
//Notifica visibile solo durante il download. A download completato
//scompare
//
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)

  return request
 }

In questo esempio ho cercato di includere tutti i vari parametri che vi può essere utile modificare, con relativa descrizione nei commenti,

Una volta creata la richiesta, sarà possibile passarla al DownloadManager in questo modo:

val id:Long = downloadManager.enqueue(request)

Molto importante è il valore restituito da DownloadManager.enqueue: Si tratta dell’identificativo che il sistema ha assegnato alla richiesta. Sarà importante per capire quando il download è terminato (Sia nel bene che nel male).

Intercettare il completamento del download

Per capire se il download richiesto è terminato è necessario registriare un receiver nel manifest (oppure anche runtime):

<receiver android:name=".receiver.DownloadManagerReceiver">
     <intent-filter>
        <action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
        <action
android:name="android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED"/>
     </intent-filter>
</receiver>

Le azioni registrate ed inviate da DownloadManager sono:

  • android.intent.action.DOWNLOAD_COMPLETE
    Invia questo evento quando un Download è stato completato.
  • android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED
    Invia questo evento quando l’utente clicca sulla notifica mentre il download è ANCORA in corso.
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import android.widget.Toast
import com.danielebachicchi.jetpackdownloadmanagerexample.utils.DownloadManagerUtils
import com.danielebachicchi.jetpackdownloadmanagerexample.item.DownloadObject

class DownloadManagerReceiver: BroadcastReceiver() {
 
    override fun onReceive(p0: Context?, p1: Intent?) {
        val action:String? = p1?.action
 
        var downloadID:Long = -1

        if(DownloadManager.ACTION_DOWNLOAD_COMPLETE == action){
            //ritirare l'id del download completato
            downloadID = p1.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID,-1)

            val downloadManager:DownloadManager = p0!!.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
                
            val downloadObject: DownloadObject? =
                DownloadManagerUtils.queryDownloadManagerObject(
                    downloadManager,
                    downloadID
                )
            //Ora abbiamo l'oggetto con tutte le 
            //informazioni del download, potete
           //fare tutto quello che volete!

        }else if (DownloadManager.ACTION_NOTIFICATION_CLICKED == action){
            downloadID = p1.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID,-1)

          //Gestite il caso in cui la notifica del 
          //download di downloadID è stata cliccata
          //dall'utente

        }
    }
}
fun queryDownloadManagerObject(downloadManager:DownloadManager, id:Long): DownloadObject?{

           val cursor:Cursor = downloadManager.query(DownloadManager.Query().setFilterById(id))

            var result: DownloadObject? = null
            if (cursor.moveToFirst())
                result = DownloadObject.fromCursor(cursor)


           return result


        }

DownloadManager.query ritorna un Cursor con le informazioni del download richiesto (tramite l’id dell’oggetto, passato da DownloadManager alla richiesta del download). Se ritorna almeno un risultato si procede alla conversione del Cursor ad oggetto:

import android.app.DownloadManager
import android.database.Cursor
import java.util.*

class DownloadObject (val uri:String,
                      val localUri:String,
                      val bytesDownloaded:Long,
                      val bytesTotal:Long,
                      val id:Long,
                      val title:String,
                      val description:String,
                      val date:Date,
                      val mediaProviderUri:String?,
                      val mediaType:String,
                      val reason:String,
                      val status:Int) {



    companion object{
        /*
        --- DownloadManager Row Example ---
        _id : 45
        mediaprovider_uri : null
        destination : 2
        title : Download Test
        description : Downloading a file for testing Download Manager API
        uri : http://ipv4.download.thinkbroadband.com/10MB.zip
        status : 200
        hint : null
        media_type : application/zip
        total_size : 10485760
        last_modified_timestamp : 1565972803944
        bytes_so_far : 10485760
        allow_write : 0
        local_uri : content://downloads/all_downloads/45
        reason : placeholder
         --- End Row ---*/
        fun fromCursor(cursor:Cursor): DownloadObject {
            val uri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_URI))
            
            val localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
            
            val bytesDownloaded = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
            
            val description = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION))
            
            val id = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_ID))
            
            val date = Date(cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP)))
            
            val mediaProviderUri:String? = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_MEDIAPROVIDER_URI))
            
            val mediaType = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE))
            
            val reason = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_REASON))
            
            val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
            
            val title = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_TITLE))
            
            val bytesTotal = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))

            return DownloadObject(
                uri,
                localUri,
                bytesDownloaded,
                bytesTotal,
                id,
                title,
                description,
                date,
                mediaProviderUri,
                mediaType,
                reason,
                status
            )

        }
    }

    override fun toString(): String {
        return "DownloadObject(\nuri='$uri'\nlocalUri='$localUri'\nbytesDownloaded=$bytesDownloaded\nbytesTotal=$bytesTotal\nid=$id\ntitle='$title'\ndescription='$description'\ndate=$date\nmediaProviderUri=$mediaProviderUri\nmediaType='$mediaType'\nreason='$reason'\nstatus=$status\n)"
    }


}

Questo è un oggetto di utility creato da me con l’obiettivo di racchiudere tutte le informazioni che DownloadManager possiede per un determinato download, con relativo metodo di wrapping dal Cursor.

Guida DownloadManager: il repository Git e l’applicazione di esempio!

Vi consiglio caldamente di consultare il codice del repository Github o scaricare l’applicazione esempio direttamente dal PlayStore (lasciatemi qualche stella se vi è stata almeno un poco utile e lasciate un commento con il vostro parere, mi piacerebbe sentire la vostra opinione)

Anche oggi è tutto, fatemi sapere se questa guida del componente Android DownloadManager vi è stata utile!

Buon codice a tutti voi!