Reputation: 5764
I got an own ContentProvider implementation that supports saving/loading images to each record by overriding the ContentProvider.openInputStream and openOutputStream methods. Locally everything works fine. But now I'm downloading images from an URL and then saving them into the ContentProvider.
Optimal solution, but does not work: To avoid creating the huge bitmaps in memory, I would like to write the incoming HTTPS stream directly into the file (option 1). But when I then load the bitmap, the BitmapFactory throws an error.
Works, but no optimal solution: If I load the bitmap from the incoming HTTPS stream (option 2) into memory and then save (compress) it to the ContentProvider - then loading the bitmap later on works fine.
So I wonder what I'm doing wrong?
Here some URLs to test:
https://lh5.ggpht.com/goggZXKLiJst1uSWPmgzk9j2WqdNiPAQZyb59tddL1WIHQgb-cPV7uqGuqECdu7ChiW8vve_2UC-Ta16YfbLlA=s192 https://lh4.ggpht.com/EizCbwoyAndISCf1b2tjPkOSMEl-jJZoPJ386RtQ7Q4kJ-1tUDEhqweXrPP-jX7pbCAoCUYN7iw1beyiI9JTFAo=s160
Sample Code (downloadDirect results in wrong bitmap, downloadIndirect works):
private void downloadDirect(String url, int key, Context context)
throws MalformedURLException, IOException {
InputStream is = download(url);
OutputStream os = openOutputStream(context, key);
final byte[] buffer = new byte[BUFFER_SIZE];
while (is.read(buffer) >= 0) {
os.write(buffer);
}
os.close();
is.close();
}
private void downloadIndirect(String url, int key, Context context)
throws MalformedURLException, IOException {
InputStream is = download(url);
Bitmap bitmap = BitmapFactory.decodeStream(is);
saveBitmap(context, key, bitmap);
}
private InputStream download(String url) throws MalformedURLException,
IOException {
URL newUrl = new URL(url);
HttpURLConnection con = (HttpURLConnection) newUrl.openConnection();
return con.getInputStream();
}
Those are the methods of the ContentProvider:
public static InputStream openInputStream(Context context, Uri contentUri,
int key) throws FileNotFoundException {
Uri uri = ContentUris.withAppendedId(contentUri, key);
return context.getContentResolver().openInputStream(uri);
}
public static OutputStream openOutputStream(Context context,
Uri contentUri, int key) throws FileNotFoundException {
Uri uri = ContentUris.withAppendedId(contentUri, key);
return context.getContentResolver().openOutputStream(uri);
}
protected static void saveBitmap(Context context, Uri contentUri,
String basePath, int key, Bitmap value, boolean updateDatabase) {
Uri uri = ContentUris.withAppendedId(contentUri, key);
try {
if (value == null) {
deleteFile(uri, basePath, context, true);
return;
}
OutputStream outStream;
try {
outStream = openOutputStream(context, contentUri, key);
ImageUtils.saveToStream(value, outStream,
Bitmap.CompressFormat.PNG);
outStream.close();
Log.d(TAG,
"Image (" + value.getWidth() + "x" + value.getHeight()
+ "pixels) saved to " + uri.toString());
} catch (FileNotFoundException e) {
e.printStackTrace();
Log.e(TAG, "Could not save image to " + uri.toString());
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "Could not save image to " + uri.toString());
}
} finally {
if (updateDatabase) {
ContentValues values = new ContentValues();
// modified column will be added automatically
context.getContentResolver().update(uri, values, null, null);
}
}
}
The openFile method of the ContentProvider is overriden like that:
public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException {
ContextWrapper cw = new ContextWrapper(getContext());
// path to /data/data/yourapp/app_data/dir
File directory = cw.getDir(basePath, Context.MODE_MULTI_PROCESS);
directory.mkdirs();
long id = ContentUris.parseId(uri);
File path = new File(directory, String.valueOf(id));
int imode = 0;
if (mode.contains("w")) {
imode |= ParcelFileDescriptor.MODE_WRITE_ONLY;
if (!path.exists()) {
try {
path.createNewFile();
} catch (IOException e) {
Log.e(tag, "Could not create file: " + path.toString());
e.printStackTrace();
}
}
}
if (mode.contains("r"))
imode |= ParcelFileDescriptor.MODE_READ_ONLY;
if (mode.contains("+"))
imode |= ParcelFileDescriptor.MODE_APPEND;
return ParcelFileDescriptor.open(path, imode);
}
Upvotes: 2
Views: 2740
Reputation: 5764
I was reading the HTTP input stream in a wrong way. There are two correct ways that are working perfectly:
a) write directly to the output stream
private void downloadDirect(String url, int key, Context context)
throws MalformedURLException, IOException {
InputStream is = download(url);
OutputStream os = openOutputStream(context, key);
final byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead = 0;
while ((bytesRead = is.read(buffer, 0, buffer.length)) >= 0) {
os.write(buffer, 0, bytesRead);
}
os.close();
is.close();
}
b) write into a buffered stream and then into the output stream
private void downloadDirect(String url, int key, Context context)
throws MalformedURLException, IOException {
InputStream is = download(url);
OutputStream os = openOutputStream(context, key);
BufferedInputStream bis = new BufferedInputStream(is);
ByteArrayBuffer baf = new ByteArrayBuffer(50);
int current = 0;
while ((current = bis.read()) != -1) {
baf.append((byte) current);
}
os.write(baf.toByteArray());
os.close();
is.close();
}
The advantage of this solution is, that even large image files can be downloaded and directly stored into a file. Later on, you can load those (potentially large) images into a bitmap using sampling.
Most articles that I found on the internet used the HTTP input stream to decode a bitmap using the BitmapFactory. This is a very bad approach because you might get OutOfMemoryExceptions if the image is too large.
And it's very slow as well, because first you are decoding the incoming stream into a bitmap and then you would encode the bitmap into an output stream again. Bad performance.
Upvotes: 3