Abbiamo già parlato in un precedente post di Volley, la libreria Google che aiuta lo sviluppatore nell’implementazione di client REST basati su richieste HTTP asincrone. Fra i vari componenti del toolbox ce ne è uno, la classe NetworkImageView, che consente di caricare in modo asincrono immagini e mostrarle in una UI Android. Questa classe infatti estende ImageView e mette a disposizione un comodo metodo setImageUrl che si preoccupa di caricare asincronamente la risorsa immagine e di mostrarla nell’interfaccia.
Questo componente è molto flessibile e potente, ma soffre di una limitazione che risulta essere bloccante nel caso lo si voglia utilizzare per mostrare immagini la cui URL si rivela essere un redirect 302 o 301 ad una URL diversa. Un esempio di immagini con URL che forzano un redirect 301 o 302 è quello delle immagini profilo estratte attraverso le Graph API di Facebook. Vediamo come patchare il sorgente di Volley per superare questo inconveniente.
NetworkImageView e URL redirect 301/302
La classe NetworkImageView è una delle classi più importanti del toolbox Volley. Il suo scopo è consentire in modo semplice, veloce, testato ed ottimizzato il caricamento asincrono di immagini in una UI Android.
La logica di funzionamento è molto semplice, in quanto NetworkImageView estende la classe Android ImageView e può quindi essere usata come suo replacement.
Nel file di definizione del layout:
<com.android.volley.toolbox.NetworkImageView android:id="@+id/user_picture" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/user_placeholder" />
Nel sorgente Java della activity:
public class MyProfileActivity { private NetworkImageView userPic; private String profileImageUrl; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_my_profile); bind(); loadImage(); } private void bind() { // use volley to retrieve the user pic // bind the NetworkImageView instance userPic = (NetworkImageView) findViewById(R.id.user_picture); // specify a placeholder which will be displayed while the image is loadig userPic.setDefaultImageResId(R.drawable.user_placeholder); } private void loadImage() { profileImageUrl = "http://elbuild.it/images/luca.png"; // this is a picture of me :) if(profileImageUrl!=null) { userPic.setImageUrl(profileImageUrl, AppController.getInstance().getImageLoader()); } } }
In questo caso tutto funziona correttamente, ma se la URL passata al metodo setImageUrl è un redirect 301/302 la NetworkImageView non mostra niente, e non produce un log apprezzabile del perchè.
Proviamo ad esempio ad utilizzare una classica URL per estrarre una foto profilo pubblica di un qualsiasi utente Facebook di cui conosciamo il Facebook ID. Le API Facebook forniscono come riferimento la seguente URL:
http://graph.facebook.com/{USER_ID}/picture?type=large
Visitando questa URL con un browser si viene rediretti su una URL differente che è la URL diretta verso l’immagine. In questo contesto risulta impossibile, nel tipico caso di login via Facebook, utilizzare Volley e la classe NetworkImageView per mostrare in modo asincrono ed ottimizzato la foto profilo dell’utente.
Fortunatamente Google ha messo a disposizione il codice sorgente di Volley per cui è possibile farne un fork ed intervenire per patchare le classi che gestiscono il livello che implementa il client HTTP per far sì che in presenza di redirect 301/302 il comportamento sia quello desiderato.
Analizzando il codice sorgente infatti, all’interno della classe src/com/android/volley/toolbox/BasicNetwork.java notiamo il seguente snippet di codice:
public NetworkResponse performRequest(Request<?> request) throws VolleyError { long requestStart = SystemClock.elapsedRealtime(); while (true) { … code missing here .. if (statusLine.getStatusCode() != HttpStatus.SC_OK) { throw new IOException(); } return new NetworkResponse(HttpStatus.SC_OK, responseContents, responseHeaders, false); } catch (SocketTimeoutException e) { attemptRetryOnException("socket", request, new TimeoutError()); } catch (ConnectTimeoutException e) { attemptRetryOnException("connection", request, new TimeoutError()); } catch (MalformedURLException e) { throw new RuntimeException("Bad URL " + request.getUrl(), e); } catch (IOException e) { int statusCode = 0; NetworkResponse networkResponse = null; if (httpResponse != null) { statusCode = httpResponse.getStatusLine().getStatusCode(); } else { throw new NoConnectionError(e); } VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl()); if (responseContents != null) { networkResponse = new NetworkResponse(statusCode, responseContents, responseHeaders, false); if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) { attemptRetryOnException("auth", request, new AuthFailureError(networkResponse)); } else { // TODO: Only throw ServerError for 5xx status codes. throw new ServerError(networkResponse); } } else { throw new NetworkError(networkResponse); } } } }
Quindi per tutti i codici di risposta diversi da 200 (HttpStatus.SC_OK) Volley lancia una IOException, che viene catchata e gestita nel blocco sotto, con un log a sistema e, nel caso di 301/302 senza eseguire la redirezione alla pagina indicata nell’header. La soluzione quindi sta nel gestire correttamente i codici di risposta 301 e 302 e settare la URL su cui redirigere la request. Un approccio è descritto dal blocco di codice sotto:
@Override public NetworkResponse performRequest(Request<?> request) throws VolleyError { long requestStart = SystemClock.elapsedRealtime(); while (true) { HttpResponse httpResponse = null; … code missing here … // Handle moved resources if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || statusCode == HttpStatus.SC_MOVED_TEMPORARILY) { String newUrl = responseHeaders.get("Location"); request.setRedirectUrl(newUrl); } // Some responses such as 204s do not have content. We must check. if (httpResponse.getEntity() != null) { responseContents = entityToBytes(httpResponse.getEntity()); } else { // Add 0 byte response as a way of honestly representing a // no-content request. responseContents = new byte[0]; } // if the request is slow, log it. long requestLifetime = SystemClock.elapsedRealtime() - requestStart; logSlowRequests(requestLifetime, request, responseContents, statusLine); if (statusCode < 200 || statusCode > 299) { throw new IOException(); } return new NetworkResponse(statusCode, responseContents, responseHeaders, false); } catch (SocketTimeoutException e) { attemptRetryOnException("socket", request, new TimeoutError()); } catch (ConnectTimeoutException e) { attemptRetryOnException("connection", request, new TimeoutError()); } catch (MalformedURLException e) { throw new RuntimeException("Bad URL " + request.getUrl(), e); } catch (IOException e) { int statusCode = 0; NetworkResponse networkResponse = null; if (httpResponse != null) { statusCode = httpResponse.getStatusLine().getStatusCode(); } else { throw new NoConnectionError(e); } if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || statusCode == HttpStatus.SC_MOVED_TEMPORARILY) { VolleyLog.e("Request at %s has been redirected to %s", request.getOriginUrl(), request.getUrl()); } else { VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl()); } if (responseContents != null) { networkResponse = new NetworkResponse(statusCode, responseContents, responseHeaders, false); if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) { attemptRetryOnException("auth", request, new AuthFailureError(networkResponse)); } else if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || statusCode == HttpStatus.SC_MOVED_TEMPORARILY) { attemptRetryOnException("redirect", request, new AuthFailureError(networkResponse)); } else { // TODO: Only throw ServerError for 5xx status codes. throw new ServerError(networkResponse); } } else { throw new NetworkError(networkResponse); } } } }
E’ interessante notare il metodo setRedirectUrl che non fa parte della Request definita dalle API della libreria Volley originale. Questo metodo server per tener traccia contemporaneamente della URL originale e di quella su cui redirigere la richiesta. Il metodo attemptRetryOnException invece è quello che fisicamente recupera la redirezione e provvede a ripetere la richiesta verso la URL corretta ed a restituire la risposta all’utente.
Sul nostro GitHub trovate un fork di Volley opportunamente modificato per supportare le URL con redirect 301/302.
https://github.com/elbuild/volley-plus
ELbuild, sviluppo app per Android e iOS
Hai un’idea originale per una app e vorresti vederla realizzata? Contattaci per capire come possiamo aiutarti. Sviluppiamo app per iOS ed Android, utilizzando librerie e framework sempre aggiornati per offrire il massimo delle performance e della manutenibilità.