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. is there a way to save the inputstream to /data/data/file.png for example?

    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