Thursday, January 27, 2011

Follow 302 redirects with AndroidHttpClient

Recently I had to download images in Android mobile application. I found a fantastic tutorial by Tim Bray in android developers blog - Multithreading For Performance. You are downloading images using AndroidHttpClient and AsyncTask-s.

All was perfect before I realize that some of my images doesn't load because of HTTP code 302 - moved temporary or "The data requested actually resides under a different URL". So this means my request is redirected with "Location" header. I found that Android do not support any redirect following - with RedirectHandler or anything else (in SDK v2.3).

I solved my problem with simply reading headers, finding Location header and recursively call downloading method again. Warning: This technique does not prevent circular redirects, so you have to check for them on your own. Here is my method:

private static Bitmap downloadBitmap(String url) {
    final AndroidHttpClient client = 
        AndroidHttpClient.newInstance("Android");
    final HttpGet request = new HttpGet(url);
    
    try {
        HttpResponse response = client.execute(request);
        final int statusCode = 
            response.getStatusLine().getStatusCode();
        
        if (statusCode != HttpStatus.SC_OK) {
            Header[] headers = response.getHeaders("Location");
            
            if (headers != null && headers.length != 0) {
                String newUrl = 
                    headers[headers.length - 1].getValue();
                // call again with new URL
                return downloadBitmap(newUrl);
            } else {
                return null;
            }
        }
        
        final HttpEntity entity = response.getEntity();
        if (entity != null) {
            InputStream inputStream = null;
            try {
                inputStream = entity.getContent();
                
                // do your work here
                return BitmapFactory.decodeStream(inputStream);
            } finally {
                if (inputStream != null) {
                    inputStream.close();  
                }
                entity.consumeContent();
            }
        }
    } catch (Exception e) {
        request.abort();
    } finally {
        if (client != null) {
            client.close();
        }
    }
    
    return null;
}

Lets all hope Google engineers will fix following redirects in future versions of Android SDK

Edit

I have found that you shouldn't use AndroidHttpClient but instead use HttpUrlConnection. This class follows up to 5 redirects and supports cache. Here is the improved code snippet:
private static Bitmap downloadBitmap(String stringUrl) {
    URL url = null;
    HttpURLConnection connection = null;
    InputStream inputStream = null;
    
    try {
        url = new URL(stringUrl);
        connection = (HttpURLConnection) url.openConnection();
        connection.setUseCaches(true);
        inputStream = connection.getInputStream();
        
        return BitmapFactory.decodeStream(new FlushedInputStream(inputStream));
    } catch (Exception e) {
        Log.w(TAG, "Error while retrieving bitmap from " + stringUrl, e);
    } finally {
        if (connection != null) {
            connection.disconnect();
        }
    }
    
    return null;
}

6 comments:

  1. Problem of url.openConnection() is that it doesn't allow redirect from http to https and vice versa :-(

    But AndroidHttpClient support redirect. You can use this
    HttpClientParams.setRedirecting(client.getParams(), true);

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. thank you so much. still works today, nearly 20 months since it's post.

    ReplyDelete
  4. Still getting error decoder-decode false


    private static Bitmap downloadBitmap(String stringUrl) {
    URL url = null;
    HttpURLConnection connection = null;
    InputStream inputStream = null;

    try {
    url = new URL(stringUrl);
    connection = (HttpURLConnection) url.openConnection();
    connection.setUseCaches(true);
    inputStream = connection.getInputStream();

    return BitmapFactory.decodeStream(new FlushedInputStream(inputStream));
    } catch (Exception e) {
    Log.w("e", "Error while retrieving bitmap from " + stringUrl, e);
    } finally {
    if (connection != null) {
    connection.disconnect();
    }
    }

    return null;
    }
    static class FlushedInputStream extends FilterInputStream {
    public FlushedInputStream(InputStream inputStream) {
    super(inputStream);
    }

    @Override
    public long skip(long n) throws IOException {
    long totalBytesSkipped = 0L;
    while (totalBytesSkipped < n) {
    long bytesSkipped = in.skip(n - totalBytesSkipped);
    if (bytesSkipped == 0L) {
    int byteValue = read();
    if (byteValue < 0) {
    break; // we reached EOF
    } else
    {
    bytesSkipped = 1; // we read one byte
    }
    }
    totalBytesSkipped += bytesSkipped;
    }
    return totalBytesSkipped;
    }
    }

    ReplyDelete
  5. thank you so very much!! solved my problem!!

    ReplyDelete