1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.example.android.bitmapfun.util;
18
19import android.content.Context;
20import android.graphics.Bitmap;
21import android.net.ConnectivityManager;
22import android.net.NetworkInfo;
23import android.os.Build;
24import android.util.Log;
25import android.widget.Toast;
26
27import com.example.android.bitmapfun.BuildConfig;
28import com.example.android.bitmapfun.R;
29
30import java.io.BufferedInputStream;
31import java.io.BufferedOutputStream;
32import java.io.File;
33import java.io.FileDescriptor;
34import java.io.FileInputStream;
35import java.io.IOException;
36import java.io.OutputStream;
37import java.net.HttpURLConnection;
38import java.net.URL;
39
40/**
41 * A simple subclass of {@link ImageResizer} that fetches and resizes images fetched from a URL.
42 */
43public class ImageFetcher extends ImageResizer {
44    private static final String TAG = "ImageFetcher";
45    private static final int HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10MB
46    private static final String HTTP_CACHE_DIR = "http";
47    private static final int IO_BUFFER_SIZE = 8 * 1024;
48
49    private DiskLruCache mHttpDiskCache;
50    private File mHttpCacheDir;
51    private boolean mHttpDiskCacheStarting = true;
52    private final Object mHttpDiskCacheLock = new Object();
53    private static final int DISK_CACHE_INDEX = 0;
54
55    /**
56     * Initialize providing a target image width and height for the processing images.
57     *
58     * @param context
59     * @param imageWidth
60     * @param imageHeight
61     */
62    public ImageFetcher(Context context, int imageWidth, int imageHeight) {
63        super(context, imageWidth, imageHeight);
64        init(context);
65    }
66
67    /**
68     * Initialize providing a single target image size (used for both width and height);
69     *
70     * @param context
71     * @param imageSize
72     */
73    public ImageFetcher(Context context, int imageSize) {
74        super(context, imageSize);
75        init(context);
76    }
77
78    private void init(Context context) {
79        checkConnection(context);
80        mHttpCacheDir = ImageCache.getDiskCacheDir(context, HTTP_CACHE_DIR);
81    }
82
83    @Override
84    protected void initDiskCacheInternal() {
85        super.initDiskCacheInternal();
86        initHttpDiskCache();
87    }
88
89    private void initHttpDiskCache() {
90        if (!mHttpCacheDir.exists()) {
91            mHttpCacheDir.mkdirs();
92        }
93        synchronized (mHttpDiskCacheLock) {
94            if (ImageCache.getUsableSpace(mHttpCacheDir) > HTTP_CACHE_SIZE) {
95                try {
96                    mHttpDiskCache = DiskLruCache.open(mHttpCacheDir, 1, 1, HTTP_CACHE_SIZE);
97                    if (BuildConfig.DEBUG) {
98                        Log.d(TAG, "HTTP cache initialized");
99                    }
100                } catch (IOException e) {
101                    mHttpDiskCache = null;
102                }
103            }
104            mHttpDiskCacheStarting = false;
105            mHttpDiskCacheLock.notifyAll();
106        }
107    }
108
109    @Override
110    protected void clearCacheInternal() {
111        super.clearCacheInternal();
112        synchronized (mHttpDiskCacheLock) {
113            if (mHttpDiskCache != null && !mHttpDiskCache.isClosed()) {
114                try {
115                    mHttpDiskCache.delete();
116                    if (BuildConfig.DEBUG) {
117                        Log.d(TAG, "HTTP cache cleared");
118                    }
119                } catch (IOException e) {
120                    Log.e(TAG, "clearCacheInternal - " + e);
121                }
122                mHttpDiskCache = null;
123                mHttpDiskCacheStarting = true;
124                initHttpDiskCache();
125            }
126        }
127    }
128
129    @Override
130    protected void flushCacheInternal() {
131        super.flushCacheInternal();
132        synchronized (mHttpDiskCacheLock) {
133            if (mHttpDiskCache != null) {
134                try {
135                    mHttpDiskCache.flush();
136                    if (BuildConfig.DEBUG) {
137                        Log.d(TAG, "HTTP cache flushed");
138                    }
139                } catch (IOException e) {
140                    Log.e(TAG, "flush - " + e);
141                }
142            }
143        }
144    }
145
146    @Override
147    protected void closeCacheInternal() {
148        super.closeCacheInternal();
149        synchronized (mHttpDiskCacheLock) {
150            if (mHttpDiskCache != null) {
151                try {
152                    if (!mHttpDiskCache.isClosed()) {
153                        mHttpDiskCache.close();
154                        mHttpDiskCache = null;
155                        if (BuildConfig.DEBUG) {
156                            Log.d(TAG, "HTTP cache closed");
157                        }
158                    }
159                } catch (IOException e) {
160                    Log.e(TAG, "closeCacheInternal - " + e);
161                }
162            }
163        }
164    }
165
166    /**
167    * Simple network connection check.
168    *
169    * @param context
170    */
171    private void checkConnection(Context context) {
172        final ConnectivityManager cm =
173                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
174        final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
175        if (networkInfo == null || !networkInfo.isConnectedOrConnecting()) {
176            Toast.makeText(context, R.string.no_network_connection_toast, Toast.LENGTH_LONG).show();
177            Log.e(TAG, "checkConnection - no connection found");
178        }
179    }
180
181    /**
182     * The main process method, which will be called by the ImageWorker in the AsyncTask background
183     * thread.
184     *
185     * @param data The data to load the bitmap, in this case, a regular http URL
186     * @return The downloaded and resized bitmap
187     */
188    private Bitmap processBitmap(String data) {
189        if (BuildConfig.DEBUG) {
190            Log.d(TAG, "processBitmap - " + data);
191        }
192
193        final String key = ImageCache.hashKeyForDisk(data);
194        FileDescriptor fileDescriptor = null;
195        FileInputStream fileInputStream = null;
196        DiskLruCache.Snapshot snapshot;
197        synchronized (mHttpDiskCacheLock) {
198            // Wait for disk cache to initialize
199            while (mHttpDiskCacheStarting) {
200                try {
201                    mHttpDiskCacheLock.wait();
202                } catch (InterruptedException e) {}
203            }
204
205            if (mHttpDiskCache != null) {
206                try {
207                    snapshot = mHttpDiskCache.get(key);
208                    if (snapshot == null) {
209                        if (BuildConfig.DEBUG) {
210                            Log.d(TAG, "processBitmap, not found in http cache, downloading...");
211                        }
212                        DiskLruCache.Editor editor = mHttpDiskCache.edit(key);
213                        if (editor != null) {
214                            if (downloadUrlToStream(data,
215                                    editor.newOutputStream(DISK_CACHE_INDEX))) {
216                                editor.commit();
217                            } else {
218                                editor.abort();
219                            }
220                        }
221                        snapshot = mHttpDiskCache.get(key);
222                    }
223                    if (snapshot != null) {
224                        fileInputStream =
225                                (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
226                        fileDescriptor = fileInputStream.getFD();
227                    }
228                } catch (IOException e) {
229                    Log.e(TAG, "processBitmap - " + e);
230                } catch (IllegalStateException e) {
231                    Log.e(TAG, "processBitmap - " + e);
232                } finally {
233                    if (fileDescriptor == null && fileInputStream != null) {
234                        try {
235                            fileInputStream.close();
236                        } catch (IOException e) {}
237                    }
238                }
239            }
240        }
241
242        Bitmap bitmap = null;
243        if (fileDescriptor != null) {
244            bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth,
245                    mImageHeight, getImageCache());
246        }
247        if (fileInputStream != null) {
248            try {
249                fileInputStream.close();
250            } catch (IOException e) {}
251        }
252        return bitmap;
253    }
254
255    @Override
256    protected Bitmap processBitmap(Object data) {
257        return processBitmap(String.valueOf(data));
258    }
259
260    /**
261     * Download a bitmap from a URL and write the content to an output stream.
262     *
263     * @param urlString The URL to fetch
264     * @return true if successful, false otherwise
265     */
266    public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
267        disableConnectionReuseIfNecessary();
268        HttpURLConnection urlConnection = null;
269        BufferedOutputStream out = null;
270        BufferedInputStream in = null;
271
272        try {
273            final URL url = new URL(urlString);
274            urlConnection = (HttpURLConnection) url.openConnection();
275            in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
276            out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
277
278            int b;
279            while ((b = in.read()) != -1) {
280                out.write(b);
281            }
282            return true;
283        } catch (final IOException e) {
284            Log.e(TAG, "Error in downloadBitmap - " + e);
285        } finally {
286            if (urlConnection != null) {
287                urlConnection.disconnect();
288            }
289            try {
290                if (out != null) {
291                    out.close();
292                }
293                if (in != null) {
294                    in.close();
295                }
296            } catch (final IOException e) {}
297        }
298        return false;
299    }
300
301    /**
302     * Workaround for bug pre-Froyo, see here for more info:
303     * http://android-developers.blogspot.com/2011/09/androids-http-clients.html
304     */
305    public static void disableConnectionReuseIfNecessary() {
306        // HTTP connection reuse which was buggy pre-froyo
307        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) {
308            System.setProperty("http.keepAlive", "false");
309        }
310    }
311}
312