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 
17 package com.example.android.displayingbitmaps.util;
18 
19 import android.content.Context;
20 import android.graphics.Bitmap;
21 import android.net.ConnectivityManager;
22 import android.net.NetworkInfo;
23 import android.os.Build;
24 import android.widget.Toast;
25 
26 import com.example.android.common.logger.Log;
27 import com.example.android.displayingbitmaps.BuildConfig;
28 import com.example.android.displayingbitmaps.R;
29 
30 import java.io.BufferedInputStream;
31 import java.io.BufferedOutputStream;
32 import java.io.File;
33 import java.io.FileDescriptor;
34 import java.io.FileInputStream;
35 import java.io.IOException;
36 import java.io.OutputStream;
37 import java.net.HttpURLConnection;
38 import java.net.URL;
39 
40 /**
41  * A simple subclass of {@link ImageResizer} that fetches and resizes images fetched from a URL.
42  */
43 public 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      */
ImageFetcher(Context context, int imageWidth, int imageHeight)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      */
ImageFetcher(Context context, int imageSize)73     public ImageFetcher(Context context, int imageSize) {
74         super(context, imageSize);
75         init(context);
76     }
77 
init(Context context)78     private void init(Context context) {
79         checkConnection(context);
80         mHttpCacheDir = ImageCache.getDiskCacheDir(context, HTTP_CACHE_DIR);
81     }
82 
83     @Override
initDiskCacheInternal()84     protected void initDiskCacheInternal() {
85         super.initDiskCacheInternal();
86         initHttpDiskCache();
87     }
88 
initHttpDiskCache()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
clearCacheInternal()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
flushCacheInternal()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
closeCacheInternal()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     */
checkConnection(Context context)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      */
processBitmap(String data)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
processBitmap(Object data)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      */
downloadUrlToStream(String urlString, OutputStream outputStream)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      */
disableConnectionReuseIfNecessary()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