target, DataSource dataSource, boolean isFirstResource) {
123 | if (listener != null) {
124 | if (resource instanceof BitmapDrawable) {
125 | listener.onCompleted(((BitmapDrawable) resource).getBitmap());
126 | }
127 | }
128 | return false;
129 | }
130 | });
131 | }
132 |
133 | @Override
134 | public void destroy() {
135 | Glide.tearDown();
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/imageloader/src/main/java/com/sxu/imageloader/instance/ImageLoaderInstance.java:
--------------------------------------------------------------------------------
1 | package com.sxu.imageloader.instance;
2 |
3 | import android.content.Context;
4 |
5 | import com.sxu.imageloader.listener.ImageLoaderListener;
6 | import com.sxu.imageloader.WrapImageView;
7 |
8 | /**
9 |
10 | * 类或接口的描述信息
11 | *
12 | * @author Freeman
13 | * @date 2017/12/5
14 | */
15 |
16 | public interface ImageLoaderInstance {
17 |
18 | void init(Context context);
19 |
20 | void displayImage(String url, WrapImageView imageView);
21 |
22 | void displayImage(String url, WrapImageView imageView, final ImageLoaderListener listener);
23 |
24 | void downloadImage(Context context, String url, ImageLoaderListener listener);
25 |
26 | void destroy();
27 | }
28 |
--------------------------------------------------------------------------------
/imageloader/src/main/java/com/sxu/imageloader/instance/UILInstance.java:
--------------------------------------------------------------------------------
1 | package com.sxu.imageloader.instance;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.view.View;
6 |
7 | import com.nostra13.universalimageloader.cache.disc.naming.Md5FileNameGenerator;
8 | import com.nostra13.universalimageloader.core.DisplayImageOptions;
9 | import com.nostra13.universalimageloader.core.ImageLoader;
10 | import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
11 | import com.nostra13.universalimageloader.core.assist.FailReason;
12 | import com.nostra13.universalimageloader.core.assist.ImageScaleType;
13 | import com.nostra13.universalimageloader.core.assist.QueueProcessingType;
14 | import com.nostra13.universalimageloader.core.display.CircleBitmapDisplayer;
15 | import com.nostra13.universalimageloader.core.display.RoundedBitmapDisplayer;
16 | import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
17 | import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
18 | import com.nostra13.universalimageloader.core.process.BitmapProcessor;
19 | import com.sxu.imageloader.listener.ImageLoaderListener;
20 | import com.sxu.imageloader.WrapImageView;
21 | import com.sxu.utils.FastBlurUtil;
22 |
23 | /**
24 |
25 | * 类或接口的描述信息
26 | *
27 | * @author Freeman
28 | * @date 2017/12/5
29 | */
30 |
31 |
32 | public class UILInstance implements ImageLoaderInstance {
33 |
34 | private DisplayImageOptions options;
35 |
36 | private void initOptions(final WrapImageView imageView) {
37 | DisplayImageOptions.Builder builder = new DisplayImageOptions.Builder()
38 | .showImageOnLoading(imageView.getPlaceHolder())
39 | .showImageForEmptyUri(imageView.getPlaceHolder())
40 | .showImageOnFail(imageView.getFailureHolder())
41 | .cacheInMemory(true)
42 | .cacheOnDisk(true)
43 | .imageScaleType(ImageScaleType.IN_SAMPLE_INT)
44 | .bitmapConfig(Bitmap.Config.RGB_565)
45 | .considerExifParams(true)
46 | .preProcessor(!imageView.isBlur() ? null : new BitmapProcessor() {
47 | @Override
48 | public Bitmap process(Bitmap bitmap) {
49 | return FastBlurUtil.doBlur(bitmap, 2, imageView.getBlurRadius() * 4);
50 | }
51 | });
52 | int shape = imageView.getShape();
53 | if (shape == WrapImageView.SHAPE_CIRCLE) {
54 | builder.displayer(new CircleBitmapDisplayer(imageView.getBorderColor(), imageView.getBorderWidth()));
55 | } else if (shape == WrapImageView.SHAPE_ROUND) {
56 | builder.displayer(new RoundedBitmapDisplayer(imageView.getRadius()));
57 | }
58 | options = builder.build();
59 | }
60 |
61 | @Override
62 | public void init(Context context) {
63 | int memCacheSize = (int) Math.min(Runtime.getRuntime().maxMemory() / 8, 64 * 1024 * 1024);
64 | ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context.getApplicationContext())
65 | .denyCacheImageMultipleSizesInMemory()
66 | .memoryCacheSize(memCacheSize)
67 | .diskCacheFileNameGenerator(new Md5FileNameGenerator())
68 | .threadPriority(10)
69 | .tasksProcessingOrder(QueueProcessingType.LIFO)
70 | .diskCacheFileCount(Integer.MAX_VALUE)
71 | .imageDownloader(new BaseImageDownloader(context, 5 * 1000, 5 * 1000))
72 | .writeDebugLogs()
73 | .build();
74 | ImageLoader.getInstance().init(config);
75 | }
76 |
77 | @Override
78 | public void displayImage(String url, WrapImageView imageView) {
79 | initOptions(imageView);
80 | ImageLoader.getInstance().displayImage(url, imageView, options);
81 | }
82 |
83 | @Override
84 | public void displayImage(String url, WrapImageView imageView, final ImageLoaderListener listener) {
85 | initOptions(imageView);
86 | ImageLoader.getInstance().displayImage(url, imageView, options, new ImageLoadingListener() {
87 | @Override
88 | public void onLoadingStarted(String imageUri, View view) {
89 | if (listener != null) {
90 | listener.onStart();
91 | }
92 | }
93 |
94 | @Override
95 | public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
96 | if (listener != null) {
97 | listener.onFailure(new Exception(failReason.getCause()));
98 | }
99 | }
100 |
101 | @Override
102 | public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
103 | if (listener != null) {
104 | listener.onCompleted(loadedImage);
105 | }
106 | }
107 |
108 | @Override
109 | public void onLoadingCancelled(String imageUri, View view) {
110 | if (listener != null) {
111 | listener.onFailure(new Exception("task is cancelled"));
112 | }
113 | }
114 | });
115 | }
116 |
117 | @Override
118 | public void downloadImage(Context context, String url, final ImageLoaderListener listener) {
119 | ImageLoader.getInstance().loadImage(url, new ImageLoadingListener() {
120 | @Override
121 | public void onLoadingStarted(String imageUri, View view) {
122 | if (listener != null) {
123 | listener.onStart();
124 | }
125 | }
126 |
127 | @Override
128 | public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
129 | if (listener != null) {
130 | listener.onFailure(new Exception(failReason.getCause()));
131 | }
132 | }
133 |
134 | @Override
135 | public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
136 | if (listener != null) {
137 | listener.onCompleted(loadedImage);
138 | }
139 | }
140 |
141 | @Override
142 | public void onLoadingCancelled(String imageUri, View view) {
143 | if (listener != null) {
144 | listener.onFailure(new Exception("task is cancelled"));
145 | }
146 | }
147 | });
148 | }
149 |
150 | @Override
151 | public void destroy() {
152 | if (ImageLoader.getInstance().isInited()) {
153 | ImageLoader.getInstance().destroy();
154 | }
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/imageloader/src/main/java/com/sxu/imageloader/listener/ImageLoaderListener.java:
--------------------------------------------------------------------------------
1 | package com.sxu.imageloader.listener;
2 |
3 | import android.graphics.Bitmap;
4 |
5 | /**
6 |
7 | * 类或接口的描述信息
8 | *
9 | * @author Freeman
10 | * @date 2017/12/5
11 | */
12 |
13 |
14 | public interface ImageLoaderListener {
15 |
16 | void onStart();
17 |
18 | void onProcess(int completedSize, int totalSize);
19 |
20 | void onCompleted(Bitmap bitmap);
21 |
22 | void onFailure(Exception e);
23 | }
24 |
--------------------------------------------------------------------------------
/imageloader/src/main/java/com/sxu/imageloader/listener/SimpleImageLoaderListener.java:
--------------------------------------------------------------------------------
1 | package com.sxu.imageloader.listener;
2 |
3 | import android.graphics.Bitmap;
4 |
5 | import com.sxu.imageloader.ImageLoaderManager;
6 |
7 | /**
8 |
9 | * 类或接口的描述信息
10 | *
11 | * @author Freeman
12 | * @date 2017/12/5
13 | */
14 |
15 |
16 | public class SimpleImageLoaderListener implements ImageLoaderListener {
17 |
18 | @Override
19 | public void onStart() {
20 |
21 | }
22 |
23 | @Override
24 | public void onProcess(int completedSize, int totalSize) {
25 |
26 | }
27 |
28 | @Override
29 | public void onCompleted(Bitmap bitmap) {
30 |
31 | }
32 |
33 | @Override
34 | public void onFailure(Exception e) {
35 |
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/imageloader/src/main/java/com/sxu/transform/GlideBlurTransform.java:
--------------------------------------------------------------------------------
1 | package com.sxu.transform;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.support.annotation.NonNull;
6 |
7 | import com.bumptech.glide.disklrucache.DiskLruCache;
8 | import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
9 | import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
10 | import com.sxu.utils.DiskLruCacheManager;
11 | import com.sxu.utils.FastBlurUtil;
12 |
13 | import java.security.MessageDigest;
14 |
15 | /**
16 |
17 | * 类或接口的描述信息
18 | *
19 | * @author Freeman
20 | * @date 2017/12/19
21 | */
22 |
23 |
24 | public class GlideBlurTransform extends BitmapTransformation {
25 |
26 | private String key;
27 | private Context context;
28 | private int blurRadius;
29 |
30 | public GlideBlurTransform(Context context, String key, int blurRadius) {
31 | this.context = context;
32 | this.key = key;
33 | this.blurRadius = blurRadius;
34 | }
35 |
36 | @Override
37 | protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {
38 | Bitmap bitmap = FastBlurUtil.doBlur(toTransform, 8, blurRadius);
39 | // 缓存高斯模糊图片
40 | DiskLruCacheManager.getInstance(context).put(key, bitmap);
41 | return bitmap;
42 | }
43 |
44 | @Override
45 | public void updateDiskCacheKey(MessageDigest messageDigest) {
46 |
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/imageloader/src/main/java/com/sxu/transform/GlideCircleBitmapTransform.java:
--------------------------------------------------------------------------------
1 | package com.sxu.transform;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.graphics.BitmapShader;
6 | import android.graphics.Canvas;
7 | import android.graphics.Paint;
8 | import android.support.annotation.NonNull;
9 |
10 | import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
11 | import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
12 | import com.sxu.utils.DiskLruCacheManager;
13 |
14 | import java.security.MessageDigest;
15 |
16 | /**
17 |
18 | * 类或接口的描述信息
19 | *
20 | * @author Freeman
21 | * @date 2017/12/19
22 | */
23 |
24 |
25 | public class GlideCircleBitmapTransform extends BitmapTransformation {
26 |
27 | private int mBorderWidth;
28 | private int mBorderColor;
29 | private String mKey;
30 | private Context mContext;
31 |
32 | public GlideCircleBitmapTransform(Context context, String key, int borderWidth, int borderColor) {
33 | this.mContext = context;
34 | this.mKey = key;
35 | this.mBorderWidth = borderWidth;
36 | this.mBorderColor = borderColor;
37 | }
38 |
39 | @Override
40 | protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {
41 | int size = Math.min(toTransform.getWidth(), toTransform.getHeight());
42 | int x = (toTransform.getWidth() - size) / 2 + mBorderWidth;
43 | int y = (toTransform.getHeight() - size) / 2 + mBorderWidth;
44 | int newSize = size - mBorderWidth * 2;
45 | int radius = newSize / 2;
46 | Bitmap bitmap = Bitmap.createBitmap(toTransform, x, y, newSize, newSize);
47 | Bitmap result = pool.get(newSize, newSize, toTransform.getConfig());
48 | if (result == null) {
49 | result = Bitmap.createBitmap(newSize, newSize, toTransform.getConfig());
50 | }
51 |
52 | Canvas canvas = new Canvas(result);
53 | if (mBorderWidth > 0) {
54 | Paint borderPaint = new Paint();
55 | borderPaint.setStyle(Paint.Style.STROKE);
56 | borderPaint.setStrokeWidth(mBorderWidth);
57 | borderPaint.setColor(mBorderColor);
58 | borderPaint.setAntiAlias(true);
59 | canvas.drawCircle(radius, radius, radius - mBorderWidth/2, borderPaint);
60 | }
61 | Paint paint = new Paint();
62 | paint.setShader(new BitmapShader(bitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
63 | paint.setAntiAlias(true);
64 | canvas.drawCircle(radius, radius, radius - mBorderWidth, paint);
65 |
66 | DiskLruCacheManager.getInstance(mContext).put(mKey, result);
67 |
68 | return result;
69 | }
70 |
71 | @Override
72 | public void updateDiskCacheKey(MessageDigest messageDigest) {
73 |
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/imageloader/src/main/java/com/sxu/transform/GlideRoundBitmapTransform.java:
--------------------------------------------------------------------------------
1 | package com.sxu.transform;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.graphics.BitmapShader;
6 | import android.graphics.Canvas;
7 | import android.graphics.Paint;
8 | import android.graphics.RectF;
9 | import android.support.annotation.NonNull;
10 |
11 | import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
12 | import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
13 | import com.sxu.utils.DiskLruCacheManager;
14 |
15 | import java.security.MessageDigest;
16 |
17 | /**
18 |
19 | * 类或接口的描述信息
20 | *
21 | * @author Freeman
22 | * @date 2017/12/19
23 | */
24 |
25 |
26 | public class GlideRoundBitmapTransform extends BitmapTransformation {
27 |
28 | private int mRadius;
29 | private int mBorderWidth;
30 | private int mBorderColor;
31 | private String mKey;
32 | private Context mContext;
33 |
34 | public GlideRoundBitmapTransform(Context context, String key, int radius, int borderWidth, int borderColor) {
35 | this.mContext = context;
36 | this.mKey = key;
37 | this.mRadius = radius;
38 | this.mBorderWidth = borderWidth;
39 | this.mBorderColor = borderColor;
40 | }
41 |
42 | @Override
43 | protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {
44 | if (mRadius == 0 && mBorderWidth == 0) {
45 | return toTransform;
46 | }
47 | int width = toTransform.getWidth();
48 | int height = toTransform.getHeight();
49 | RectF rectF = new RectF(mBorderWidth, mBorderWidth, width - mBorderWidth, height - mBorderWidth);
50 | Bitmap result = pool.get(width, height, toTransform.getConfig());
51 | if (result == null) {
52 | result = toTransform.copy(toTransform.getConfig(), true);
53 | }
54 | Canvas canvas = new Canvas(result);
55 | Paint paint = new Paint();
56 | paint.setShader(new BitmapShader(toTransform, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP ));
57 | paint.setAntiAlias(true);
58 | canvas.drawRoundRect(rectF, mRadius, mRadius, paint);
59 | if (mBorderWidth > 0) {
60 | Paint borderPaint = new Paint();
61 | borderPaint.setStyle(Paint.Style.STROKE);
62 | borderPaint.setStrokeWidth(mBorderWidth);
63 | borderPaint.setColor(mBorderColor);
64 | borderPaint.setAntiAlias(true);
65 | canvas.drawRoundRect(rectF, mRadius, mRadius, borderPaint);
66 | }
67 | DiskLruCacheManager.getInstance(mContext).put(mKey, result);
68 |
69 | return result;
70 | }
71 |
72 | @Override
73 | public void updateDiskCacheKey(MessageDigest messageDigest) {
74 |
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/imageloader/src/main/java/com/sxu/utils/DiskLruCache.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2011 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 | package com.sxu.utils;
17 |
18 | import java.io.BufferedWriter;
19 | import java.io.Closeable;
20 | import java.io.EOFException;
21 | import java.io.File;
22 | import java.io.FileInputStream;
23 | import java.io.FileNotFoundException;
24 | import java.io.FileOutputStream;
25 | import java.io.FilterOutputStream;
26 | import java.io.IOException;
27 | import java.io.InputStream;
28 | import java.io.InputStreamReader;
29 | import java.io.OutputStream;
30 | import java.io.OutputStreamWriter;
31 | import java.io.Writer;
32 | import java.util.ArrayList;
33 | import java.util.Iterator;
34 | import java.util.LinkedHashMap;
35 | import java.util.Map;
36 | import java.util.concurrent.Callable;
37 | import java.util.concurrent.LinkedBlockingQueue;
38 | import java.util.concurrent.ThreadPoolExecutor;
39 | import java.util.concurrent.TimeUnit;
40 | import java.util.regex.Matcher;
41 | import java.util.regex.Pattern;
42 |
43 | /**
44 | * A cache that uses a bounded amount of space on a filesystem. Each cache
45 | * entry has a string key and a fixed number of values. Each key must match
46 | * the regex [a-z0-9_-]{1,64}. Values are byte sequences,
47 | * accessible as streams or files. Each value must be between {@code 0} and
48 | * {@code Integer.MAX_VALUE} bytes in length.
49 | *
50 | * The cache stores its data in a directory on the filesystem. This
51 | * directory must be exclusive to the cache; the cache may delete or overwrite
52 | * files from its directory. It is an error for multiple processes to use the
53 | * same cache directory at the same time.
54 | *
55 | *
This cache limits the number of bytes that it will store on the
56 | * filesystem. When the number of stored bytes exceeds the limit, the cache will
57 | * remove entries in the background until the limit is satisfied. The limit is
58 | * not strict: the cache may temporarily exceed it while waiting for files to be
59 | * deleted. The limit does not include filesystem overhead or the cache
60 | * journal so space-sensitive applications should set a conservative limit.
61 | *
62 | *
Clients call {@link #edit} to create or update the values of an entry. An
63 | * entry may have only one editor at one time; if a value is not available to be
64 | * edited then {@link #edit} will return null.
65 | *
66 | * - When an entry is being created it is necessary to
67 | * supply a full set of values; the empty value should be used as a
68 | * placeholder if necessary.
69 | *
- When an entry is being edited, it is not necessary
70 | * to supply data for every value; values default to their previous
71 | * value.
72 | *
73 | * Every {@link #edit} call must be matched by a call to {@link Editor#commit}
74 | * or {@link Editor#abort}. Committing is atomic: a read observes the full set
75 | * of values as they were before or after the commit, but never a mix of values.
76 | *
77 | * Clients call {@link #get} to read a snapshot of an entry. The read will
78 | * observe the value at the time that {@link #get} was called. Updates and
79 | * removals after the call do not impact ongoing reads.
80 | *
81 | *
This class is tolerant of some I/O errors. If files are missing from the
82 | * filesystem, the corresponding entries will be dropped from the cache. If
83 | * an error occurs while writing a cache value, the edit will fail silently.
84 | * Callers should handle other problems by catching {@code IOException} and
85 | * responding appropriately.
86 | */
87 | final class DiskLruCache implements Closeable {
88 | static final String JOURNAL_FILE = "journal";
89 | static final String JOURNAL_FILE_TEMP = "journal.tmp";
90 | static final String JOURNAL_FILE_BACKUP = "journal.bkp";
91 | static final String MAGIC = "libcore.io.DiskLruCache";
92 | static final String VERSION_1 = "1";
93 | static final long ANY_SEQUENCE_NUMBER = -1;
94 | static final Pattern LEGAL_KEY_PATTERN = Pattern.compile("[a-z0-9_-]{1,64}");
95 | private static final String CLEAN = "CLEAN";
96 | private static final String DIRTY = "DIRTY";
97 | private static final String REMOVE = "REMOVE";
98 | private static final String READ = "READ";
99 |
100 | /*
101 | * This cache uses a journal file named "journal". A typical journal file
102 | * looks like this:
103 | * libcore.io.DiskLruCache
104 | * 1
105 | * 100
106 | * 2
107 | *
108 | * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
109 | * DIRTY 335c4c6028171cfddfbaae1a9c313c52
110 | * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
111 | * REMOVE 335c4c6028171cfddfbaae1a9c313c52
112 | * DIRTY 1ab96a171faeeee38496d8b330771a7a
113 | * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
114 | * READ 335c4c6028171cfddfbaae1a9c313c52
115 | * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
116 | *
117 | * The first five lines of the journal form its header. They are the
118 | * constant string "libcore.io.DiskLruCache", the disk cache's version,
119 | * the application's version, the value count, and a blank line.
120 | *
121 | * Each of the subsequent lines in the file is a record of the state of a
122 | * cache entry. Each line contains space-separated values: a state, a key,
123 | * and optional state-specific values.
124 | * o DIRTY lines track that an entry is actively being created or updated.
125 | * Every successful DIRTY action should be followed by a CLEAN or REMOVE
126 | * action. DIRTY lines without a matching CLEAN or REMOVE indicate that
127 | * temporary files may need to be deleted.
128 | * o CLEAN lines track a cache entry that has been successfully published
129 | * and may be read. A publish line is followed by the lengths of each of
130 | * its values.
131 | * o READ lines track accesses for LRU.
132 | * o REMOVE lines track entries that have been deleted.
133 | *
134 | * The journal file is appended to as cache operations occur. The journal may
135 | * occasionally be compacted by dropping redundant lines. A temporary file named
136 | * "journal.tmp" will be used during compaction; that file should be deleted if
137 | * it exists when the cache is opened.
138 | */
139 |
140 | private final File directory;
141 | private final File journalFile;
142 | private final File journalFileTmp;
143 | private final File journalFileBackup;
144 | private final int appVersion;
145 | private long maxSize;
146 | private int maxFileCount;
147 | private final int valueCount;
148 | private long size = 0;
149 | private int fileCount = 0;
150 | private Writer journalWriter;
151 | private final LinkedHashMap lruEntries =
152 | new LinkedHashMap(0, 0.75f, true);
153 | private int redundantOpCount;
154 |
155 | /**
156 | * To differentiate between old and current snapshots, each entry is given
157 | * a sequence number each time an edit is committed. A snapshot is stale if
158 | * its sequence number is not equal to its entry's sequence number.
159 | */
160 | private long nextSequenceNumber = 0;
161 |
162 | /** This cache uses a single background thread to evict entries. */
163 | final ThreadPoolExecutor executorService =
164 | new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue());
165 | private final Callable cleanupCallable = new Callable() {
166 | public Void call() throws Exception {
167 | synchronized (DiskLruCache.this) {
168 | if (journalWriter == null) {
169 | return null; // Closed.
170 | }
171 | trimToSize();
172 | trimToFileCount();
173 | if (journalRebuildRequired()) {
174 | rebuildJournal();
175 | redundantOpCount = 0;
176 | }
177 | }
178 | return null;
179 | }
180 | };
181 |
182 | private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize, int maxFileCount) {
183 | this.directory = directory;
184 | this.appVersion = appVersion;
185 | this.journalFile = new File(directory, JOURNAL_FILE);
186 | this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
187 | this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
188 | this.valueCount = valueCount;
189 | this.maxSize = maxSize;
190 | this.maxFileCount = maxFileCount;
191 | }
192 |
193 | /**
194 | * Opens the cache in {@code directory}, creating a cache if none exists
195 | * there.
196 | *
197 | * @param directory a writable directory
198 | * @param valueCount the number of values per cache entry. Must be positive.
199 | * @param maxSize the maximum number of bytes this cache should use to store
200 | * @param maxFileCount the maximum file count this cache should store
201 | * @throws IOException if reading or writing the cache directory fails
202 | */
203 | public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize, int maxFileCount)
204 | throws IOException {
205 | if (maxSize <= 0) {
206 | throw new IllegalArgumentException("maxSize <= 0");
207 | }
208 | if (maxFileCount <= 0) {
209 | throw new IllegalArgumentException("maxFileCount <= 0");
210 | }
211 | if (valueCount <= 0) {
212 | throw new IllegalArgumentException("valueCount <= 0");
213 | }
214 |
215 | // If a bkp file exists, use it instead.
216 | File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
217 | if (backupFile.exists()) {
218 | File journalFile = new File(directory, JOURNAL_FILE);
219 | // If journal file also exists just delete backup file.
220 | if (journalFile.exists()) {
221 | backupFile.delete();
222 | } else {
223 | renameTo(backupFile, journalFile, false);
224 | }
225 | }
226 |
227 | // Prefer to pick up where we left off.
228 | DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize, maxFileCount);
229 | if (cache.journalFile.exists()) {
230 | try {
231 | cache.readJournal();
232 | cache.processJournal();
233 | cache.journalWriter = new BufferedWriter(
234 | new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));
235 | return cache;
236 | } catch (IOException journalIsCorrupt) {
237 | System.out
238 | .println("DiskLruCache "
239 | + directory
240 | + " is corrupt: "
241 | + journalIsCorrupt.getMessage()
242 | + ", removing");
243 | cache.delete();
244 | }
245 | }
246 |
247 | // Create a new empty cache.
248 | directory.mkdirs();
249 | cache = new DiskLruCache(directory, appVersion, valueCount, maxSize, maxFileCount);
250 | cache.rebuildJournal();
251 | return cache;
252 | }
253 |
254 | private void readJournal() throws IOException {
255 | StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
256 | try {
257 | String magic = reader.readLine();
258 | String version = reader.readLine();
259 | String appVersionString = reader.readLine();
260 | String valueCountString = reader.readLine();
261 | String blank = reader.readLine();
262 | if (!MAGIC.equals(magic)
263 | || !VERSION_1.equals(version)
264 | || !Integer.toString(appVersion).equals(appVersionString)
265 | || !Integer.toString(valueCount).equals(valueCountString)
266 | || !"".equals(blank)) {
267 | throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
268 | + valueCountString + ", " + blank + "]");
269 | }
270 |
271 | int lineCount = 0;
272 | while (true) {
273 | try {
274 | readJournalLine(reader.readLine());
275 | lineCount++;
276 | } catch (EOFException endOfJournal) {
277 | break;
278 | }
279 | }
280 | redundantOpCount = lineCount - lruEntries.size();
281 | } finally {
282 | Util.closeQuietly(reader);
283 | }
284 | }
285 |
286 | private void readJournalLine(String line) throws IOException {
287 | int firstSpace = line.indexOf(' ');
288 | if (firstSpace == -1) {
289 | throw new IOException("unexpected journal line: " + line);
290 | }
291 |
292 | int keyBegin = firstSpace + 1;
293 | int secondSpace = line.indexOf(' ', keyBegin);
294 | final String key;
295 | if (secondSpace == -1) {
296 | key = line.substring(keyBegin);
297 | if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
298 | lruEntries.remove(key);
299 | return;
300 | }
301 | } else {
302 | key = line.substring(keyBegin, secondSpace);
303 | }
304 |
305 | Entry entry = lruEntries.get(key);
306 | if (entry == null) {
307 | entry = new Entry(key);
308 | lruEntries.put(key, entry);
309 | }
310 |
311 | if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
312 | String[] parts = line.substring(secondSpace + 1).split(" ");
313 | entry.readable = true;
314 | entry.currentEditor = null;
315 | entry.setLengths(parts);
316 | } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
317 | entry.currentEditor = new Editor(entry);
318 | } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
319 | // This work was already done by calling lruEntries.get().
320 | } else {
321 | throw new IOException("unexpected journal line: " + line);
322 | }
323 | }
324 |
325 | /**
326 | * Computes the initial size and collects garbage as a part of opening the
327 | * cache. Dirty entries are assumed to be inconsistent and will be deleted.
328 | */
329 | private void processJournal() throws IOException {
330 | deleteIfExists(journalFileTmp);
331 | for (Iterator i = lruEntries.values().iterator(); i.hasNext(); ) {
332 | Entry entry = i.next();
333 | if (entry.currentEditor == null) {
334 | for (int t = 0; t < valueCount; t++) {
335 | size += entry.lengths[t];
336 | fileCount++;
337 | }
338 | } else {
339 | entry.currentEditor = null;
340 | for (int t = 0; t < valueCount; t++) {
341 | deleteIfExists(entry.getCleanFile(t));
342 | deleteIfExists(entry.getDirtyFile(t));
343 | }
344 | i.remove();
345 | }
346 | }
347 | }
348 |
349 | /**
350 | * Creates a new journal that omits redundant information. This replaces the
351 | * current journal if it exists.
352 | */
353 | private synchronized void rebuildJournal() throws IOException {
354 | if (journalWriter != null) {
355 | journalWriter.close();
356 | }
357 |
358 | Writer writer = new BufferedWriter(
359 | new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
360 | try {
361 | writer.write(MAGIC);
362 | writer.write("\n");
363 | writer.write(VERSION_1);
364 | writer.write("\n");
365 | writer.write(Integer.toString(appVersion));
366 | writer.write("\n");
367 | writer.write(Integer.toString(valueCount));
368 | writer.write("\n");
369 | writer.write("\n");
370 |
371 | for (Entry entry : lruEntries.values()) {
372 | if (entry.currentEditor != null) {
373 | writer.write(DIRTY + ' ' + entry.key + '\n');
374 | } else {
375 | writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
376 | }
377 | }
378 | } finally {
379 | writer.close();
380 | }
381 |
382 | if (journalFile.exists()) {
383 | renameTo(journalFile, journalFileBackup, true);
384 | }
385 | renameTo(journalFileTmp, journalFile, false);
386 | journalFileBackup.delete();
387 |
388 | journalWriter = new BufferedWriter(
389 | new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
390 | }
391 |
392 | private static void deleteIfExists(File file) throws IOException {
393 | if (file.exists() && !file.delete()) {
394 | throw new IOException();
395 | }
396 | }
397 |
398 | private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
399 | if (deleteDestination) {
400 | deleteIfExists(to);
401 | }
402 | if (!from.renameTo(to)) {
403 | throw new IOException();
404 | }
405 | }
406 |
407 | /**
408 | * Returns a snapshot of the entry named {@code key}, or null if it doesn't
409 | * exist is not currently readable. If a value is returned, it is moved to
410 | * the head of the LRU queue.
411 | */
412 | public synchronized Snapshot get(String key) throws IOException {
413 | checkNotClosed();
414 | validateKey(key);
415 | Entry entry = lruEntries.get(key);
416 | if (entry == null) {
417 | return null;
418 | }
419 |
420 | if (!entry.readable) {
421 | return null;
422 | }
423 |
424 | // Open all streams eagerly to guarantee that we see a single published
425 | // snapshot. If we opened streams lazily then the streams could come
426 | // from different edits.
427 | File[] files = new File[valueCount];
428 | InputStream[] ins = new InputStream[valueCount];
429 | try {
430 | File file;
431 | for (int i = 0; i < valueCount; i++) {
432 | file = entry.getCleanFile(i);
433 | files[i] = file;
434 | ins[i] = new FileInputStream(file);
435 | }
436 | } catch (FileNotFoundException e) {
437 | // A file must have been deleted manually!
438 | for (int i = 0; i < valueCount; i++) {
439 | if (ins[i] != null) {
440 | Util.closeQuietly(ins[i]);
441 | } else {
442 | break;
443 | }
444 | }
445 | return null;
446 | }
447 |
448 | redundantOpCount++;
449 | journalWriter.append(READ + ' ' + key + '\n');
450 | if (journalRebuildRequired()) {
451 | executorService.submit(cleanupCallable);
452 | }
453 |
454 | return new Snapshot(key, entry.sequenceNumber, files, ins, entry.lengths);
455 | }
456 |
457 | /**
458 | * Returns an editor for the entry named {@code key}, or null if another
459 | * edit is in progress.
460 | */
461 | public Editor edit(String key) throws IOException {
462 | return edit(key, ANY_SEQUENCE_NUMBER);
463 | }
464 |
465 | private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
466 | checkNotClosed();
467 | validateKey(key);
468 | Entry entry = lruEntries.get(key);
469 | if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
470 | || entry.sequenceNumber != expectedSequenceNumber)) {
471 | return null; // Snapshot is stale.
472 | }
473 | if (entry == null) {
474 | entry = new Entry(key);
475 | lruEntries.put(key, entry);
476 | } else if (entry.currentEditor != null) {
477 | return null; // Another edit is in progress.
478 | }
479 |
480 | Editor editor = new Editor(entry);
481 | entry.currentEditor = editor;
482 |
483 | // Flush the journal before creating files to prevent file leaks.
484 | journalWriter.write(DIRTY + ' ' + key + '\n');
485 | journalWriter.flush();
486 | return editor;
487 | }
488 |
489 | /** Returns the directory where this cache stores its data. */
490 | public File getDirectory() {
491 | return directory;
492 | }
493 |
494 | /**
495 | * Returns the maximum number of bytes that this cache should use to store
496 | * its data.
497 | */
498 | public synchronized long getMaxSize() {
499 | return maxSize;
500 | }
501 |
502 | /** Returns the maximum number of files that this cache should store */
503 | public synchronized int getMaxFileCount() {
504 | return maxFileCount;
505 | }
506 |
507 | /**
508 | * Changes the maximum number of bytes the cache can store and queues a job
509 | * to trim the existing store, if necessary.
510 | */
511 | public synchronized void setMaxSize(long maxSize) {
512 | this.maxSize = maxSize;
513 | executorService.submit(cleanupCallable);
514 | }
515 |
516 | /**
517 | * Returns the number of bytes currently being used to store the values in
518 | * this cache. This may be greater than the max size if a background
519 | * deletion is pending.
520 | */
521 | public synchronized long size() {
522 | return size;
523 | }
524 |
525 | /**
526 | * Returns the number of files currently being used to store the values in
527 | * this cache. This may be greater than the max file count if a background
528 | * deletion is pending.
529 | */
530 | public synchronized long fileCount() {
531 | return fileCount;
532 | }
533 |
534 | private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
535 | Entry entry = editor.entry;
536 | if (entry.currentEditor != editor) {
537 | throw new IllegalStateException();
538 | }
539 |
540 | // If this edit is creating the entry for the first time, every index must have a value.
541 | if (success && !entry.readable) {
542 | for (int i = 0; i < valueCount; i++) {
543 | if (!editor.written[i]) {
544 | editor.abort();
545 | throw new IllegalStateException("Newly created entry didn't create value for index " + i);
546 | }
547 | if (!entry.getDirtyFile(i).exists()) {
548 | editor.abort();
549 | return;
550 | }
551 | }
552 | }
553 |
554 | for (int i = 0; i < valueCount; i++) {
555 | File dirty = entry.getDirtyFile(i);
556 | if (success) {
557 | if (dirty.exists()) {
558 | File clean = entry.getCleanFile(i);
559 | dirty.renameTo(clean);
560 | long oldLength = entry.lengths[i];
561 | long newLength = clean.length();
562 | entry.lengths[i] = newLength;
563 | size = size - oldLength + newLength;
564 | fileCount++;
565 | }
566 | } else {
567 | deleteIfExists(dirty);
568 | }
569 | }
570 |
571 | redundantOpCount++;
572 | entry.currentEditor = null;
573 | if (entry.readable | success) {
574 | entry.readable = true;
575 | journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
576 | if (success) {
577 | entry.sequenceNumber = nextSequenceNumber++;
578 | }
579 | } else {
580 | lruEntries.remove(entry.key);
581 | journalWriter.write(REMOVE + ' ' + entry.key + '\n');
582 | }
583 | journalWriter.flush();
584 |
585 | if (size > maxSize || fileCount > maxFileCount || journalRebuildRequired()) {
586 | executorService.submit(cleanupCallable);
587 | }
588 | }
589 |
590 | /**
591 | * We only rebuild the journal when it will halve the size of the journal
592 | * and eliminate at least 2000 ops.
593 | */
594 | private boolean journalRebuildRequired() {
595 | final int redundantOpCompactThreshold = 2000;
596 | return redundantOpCount >= redundantOpCompactThreshold //
597 | && redundantOpCount >= lruEntries.size();
598 | }
599 |
600 | /**
601 | * Drops the entry for {@code key} if it exists and can be removed. Entries
602 | * actively being edited cannot be removed.
603 | *
604 | * @return true if an entry was removed.
605 | */
606 | public synchronized boolean remove(String key) throws IOException {
607 | checkNotClosed();
608 | validateKey(key);
609 | Entry entry = lruEntries.get(key);
610 | if (entry == null || entry.currentEditor != null) {
611 | return false;
612 | }
613 |
614 | for (int i = 0; i < valueCount; i++) {
615 | File file = entry.getCleanFile(i);
616 | if (file.exists() && !file.delete()) {
617 | throw new IOException("failed to delete " + file);
618 | }
619 | size -= entry.lengths[i];
620 | fileCount--;
621 | entry.lengths[i] = 0;
622 | }
623 |
624 | redundantOpCount++;
625 | journalWriter.append(REMOVE + ' ' + key + '\n');
626 | lruEntries.remove(key);
627 |
628 | if (journalRebuildRequired()) {
629 | executorService.submit(cleanupCallable);
630 | }
631 |
632 | return true;
633 | }
634 |
635 | /** Returns true if this cache has been closed. */
636 | public synchronized boolean isClosed() {
637 | return journalWriter == null;
638 | }
639 |
640 | private void checkNotClosed() {
641 | if (journalWriter == null) {
642 | throw new IllegalStateException("cache is closed");
643 | }
644 | }
645 |
646 | /** Force buffered operations to the filesystem. */
647 | public synchronized void flush() throws IOException {
648 | checkNotClosed();
649 | trimToSize();
650 | trimToFileCount();
651 | journalWriter.flush();
652 | }
653 |
654 | /** Closes this cache. Stored values will remain on the filesystem. */
655 | public synchronized void close() throws IOException {
656 | if (journalWriter == null) {
657 | return; // Already closed.
658 | }
659 | for (Entry entry : new ArrayList(lruEntries.values())) {
660 | if (entry.currentEditor != null) {
661 | entry.currentEditor.abort();
662 | }
663 | }
664 | trimToSize();
665 | trimToFileCount();
666 | journalWriter.close();
667 | journalWriter = null;
668 | }
669 |
670 | private void trimToSize() throws IOException {
671 | while (size > maxSize) {
672 | Map.Entry toEvict = lruEntries.entrySet().iterator().next();
673 | remove(toEvict.getKey());
674 | }
675 | }
676 |
677 | private void trimToFileCount() throws IOException {
678 | while (fileCount > maxFileCount) {
679 | Map.Entry toEvict = lruEntries.entrySet().iterator().next();
680 | remove(toEvict.getKey());
681 | }
682 | }
683 |
684 | /**
685 | * Closes the cache and deletes all of its stored values. This will delete
686 | * all files in the cache directory including files that weren't created by
687 | * the cache.
688 | */
689 | public void delete() throws IOException {
690 | close();
691 | Util.deleteContents(directory);
692 | }
693 |
694 | private void validateKey(String key) {
695 | Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
696 | if (!matcher.matches()) {
697 | throw new IllegalArgumentException("keys must match regex [a-z0-9_-]{1,64}: \"" + key + "\"");
698 | }
699 | }
700 |
701 | private static String inputStreamToString(InputStream in) throws IOException {
702 | return Util.readFully(new InputStreamReader(in, Util.UTF_8));
703 | }
704 |
705 | /** A snapshot of the values for an entry. */
706 | public final class Snapshot implements Closeable {
707 | private final String key;
708 | private final long sequenceNumber;
709 | private File[] files;
710 | private final InputStream[] ins;
711 | private final long[] lengths;
712 |
713 | private Snapshot(String key, long sequenceNumber, File[] files, InputStream[] ins, long[] lengths) {
714 | this.key = key;
715 | this.sequenceNumber = sequenceNumber;
716 | this.files = files;
717 | this.ins = ins;
718 | this.lengths = lengths;
719 | }
720 |
721 | /**
722 | * Returns an editor for this snapshot's entry, or null if either the
723 | * entry has changed since this snapshot was created or if another edit
724 | * is in progress.
725 | */
726 | public Editor edit() throws IOException {
727 | return DiskLruCache.this.edit(key, sequenceNumber);
728 | }
729 |
730 | /** Returns file with the value for {@code index}. */
731 | public File getFile(int index) {
732 | return files[index];
733 | }
734 |
735 | /** Returns the unbuffered stream with the value for {@code index}. */
736 | public InputStream getInputStream(int index) {
737 | return ins[index];
738 | }
739 |
740 | /** Returns the string value for {@code index}. */
741 | public String getString(int index) throws IOException {
742 | return inputStreamToString(getInputStream(index));
743 | }
744 |
745 | /** Returns the byte length of the value for {@code index}. */
746 | public long getLength(int index) {
747 | return lengths[index];
748 | }
749 |
750 | public void close() {
751 | for (InputStream in : ins) {
752 | Util.closeQuietly(in);
753 | }
754 | }
755 | }
756 |
757 | private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
758 | @Override
759 | public void write(int b) throws IOException {
760 | // Eat all writes silently. Nom nom.
761 | }
762 | };
763 |
764 | /** Edits the values for an entry. */
765 | public final class Editor {
766 | private final Entry entry;
767 | private final boolean[] written;
768 | private boolean hasErrors;
769 | private boolean committed;
770 |
771 | private Editor(Entry entry) {
772 | this.entry = entry;
773 | this.written = (entry.readable) ? null : new boolean[valueCount];
774 | }
775 |
776 | /**
777 | * Returns an unbuffered input stream to read the last committed value,
778 | * or null if no value has been committed.
779 | */
780 | public InputStream newInputStream(int index) throws IOException {
781 | synchronized (DiskLruCache.this) {
782 | if (entry.currentEditor != this) {
783 | throw new IllegalStateException();
784 | }
785 | if (!entry.readable) {
786 | return null;
787 | }
788 | try {
789 | return new FileInputStream(entry.getCleanFile(index));
790 | } catch (FileNotFoundException e) {
791 | return null;
792 | }
793 | }
794 | }
795 |
796 | /**
797 | * Returns the last committed value as a string, or null if no value
798 | * has been committed.
799 | */
800 | public String getString(int index) throws IOException {
801 | InputStream in = newInputStream(index);
802 | return in != null ? inputStreamToString(in) : null;
803 | }
804 |
805 | /**
806 | * Returns a new unbuffered output stream to write the value at
807 | * {@code index}. If the underlying output stream encounters errors
808 | * when writing to the filesystem, this edit will be aborted when
809 | * {@link #commit} is called. The returned output stream does not throw
810 | * IOExceptions.
811 | */
812 | public OutputStream newOutputStream(int index) throws IOException {
813 | synchronized (DiskLruCache.this) {
814 | if (entry.currentEditor != this) {
815 | throw new IllegalStateException();
816 | }
817 | if (!entry.readable) {
818 | written[index] = true;
819 | }
820 | File dirtyFile = entry.getDirtyFile(index);
821 | FileOutputStream outputStream;
822 | try {
823 | outputStream = new FileOutputStream(dirtyFile);
824 | } catch (FileNotFoundException e) {
825 | // Attempt to recreate the cache directory.
826 | directory.mkdirs();
827 | try {
828 | outputStream = new FileOutputStream(dirtyFile);
829 | } catch (FileNotFoundException e2) {
830 | // We are unable to recover. Silently eat the writes.
831 | return NULL_OUTPUT_STREAM;
832 | }
833 | }
834 | return new FaultHidingOutputStream(outputStream);
835 | }
836 | }
837 |
838 | /** Sets the value at {@code index} to {@code value}. */
839 | public void set(int index, String value) throws IOException {
840 | Writer writer = null;
841 | try {
842 | writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);
843 | writer.write(value);
844 | } finally {
845 | Util.closeQuietly(writer);
846 | }
847 | }
848 |
849 | /**
850 | * Commits this edit so it is visible to readers. This releases the
851 | * edit lock so another edit may be started on the same key.
852 | */
853 | public void commit() throws IOException {
854 | if (hasErrors) {
855 | completeEdit(this, false);
856 | remove(entry.key); // The previous entry is stale.
857 | } else {
858 | completeEdit(this, true);
859 | }
860 | committed = true;
861 | }
862 |
863 | /**
864 | * Aborts this edit. This releases the edit lock so another edit may be
865 | * started on the same key.
866 | */
867 | public void abort() throws IOException {
868 | completeEdit(this, false);
869 | }
870 |
871 | public void abortUnlessCommitted() {
872 | if (!committed) {
873 | try {
874 | abort();
875 | } catch (IOException ignored) {
876 | }
877 | }
878 | }
879 |
880 | private class FaultHidingOutputStream extends FilterOutputStream {
881 | private FaultHidingOutputStream(OutputStream out) {
882 | super(out);
883 | }
884 |
885 | @Override public void write(int oneByte) {
886 | try {
887 | out.write(oneByte);
888 | } catch (IOException e) {
889 | hasErrors = true;
890 | }
891 | }
892 |
893 | @Override public void write(byte[] buffer, int offset, int length) {
894 | try {
895 | out.write(buffer, offset, length);
896 | } catch (IOException e) {
897 | hasErrors = true;
898 | }
899 | }
900 |
901 | @Override public void close() {
902 | try {
903 | out.close();
904 | } catch (IOException e) {
905 | hasErrors = true;
906 | }
907 | }
908 |
909 | @Override public void flush() {
910 | try {
911 | out.flush();
912 | } catch (IOException e) {
913 | hasErrors = true;
914 | }
915 | }
916 | }
917 | }
918 |
919 | private final class Entry {
920 | private final String key;
921 |
922 | /** Lengths of this entry's files. */
923 | private final long[] lengths;
924 |
925 | /** True if this entry has ever been published. */
926 | private boolean readable;
927 |
928 | /** The ongoing edit or null if this entry is not being edited. */
929 | private Editor currentEditor;
930 |
931 | /** The sequence number of the most recently committed edit to this entry. */
932 | private long sequenceNumber;
933 |
934 | private Entry(String key) {
935 | this.key = key;
936 | this.lengths = new long[valueCount];
937 | }
938 |
939 | public String getLengths() throws IOException {
940 | StringBuilder result = new StringBuilder();
941 | for (long size : lengths) {
942 | result.append(' ').append(size);
943 | }
944 | return result.toString();
945 | }
946 |
947 | /** Set lengths using decimal numbers like "10123". */
948 | private void setLengths(String[] strings) throws IOException {
949 | if (strings.length != valueCount) {
950 | throw invalidLengths(strings);
951 | }
952 |
953 | try {
954 | for (int i = 0; i < strings.length; i++) {
955 | lengths[i] = Long.parseLong(strings[i]);
956 | }
957 | } catch (NumberFormatException e) {
958 | throw invalidLengths(strings);
959 | }
960 | }
961 |
962 | private IOException invalidLengths(String[] strings) throws IOException {
963 | throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
964 | }
965 |
966 | public File getCleanFile(int i) {
967 | return new File(directory, key + "" + i);
968 | }
969 |
970 | public File getDirtyFile(int i) {
971 | return new File(directory, key + "" + i + ".tmp");
972 | }
973 | }
974 | }
975 |
--------------------------------------------------------------------------------
/imageloader/src/main/java/com/sxu/utils/DiskLruCacheManager.java:
--------------------------------------------------------------------------------
1 | package com.sxu.utils;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.graphics.BitmapFactory;
6 | import android.text.TextUtils;
7 |
8 | import java.io.File;
9 | import java.io.IOException;
10 | import java.io.InputStream;
11 | import java.io.OutputStream;
12 | import java.math.BigInteger;
13 | import java.security.MessageDigest;
14 |
15 | /*******************************************************************************
16 | * Description: 用于缓存经过高斯模糊的图片
17 | *
18 | * Author: Freeman
19 | *
20 | * Date: 2018/9/4
21 | *
22 | * Copyright: all rights reserved by Freeman.
23 | *******************************************************************************/
24 | public class DiskLruCacheManager {
25 |
26 | private DiskLruCache diskLruCache;
27 | private static DiskLruCacheManager instance;
28 |
29 | private final int MAX_CACHE_SIZE = 64 * 1024 * 1024;
30 |
31 | private DiskLruCacheManager(Context context) {
32 | try {
33 | diskLruCache = DiskLruCache.open(context.getCacheDir(), 1, 1,
34 | MAX_CACHE_SIZE, Integer.MAX_VALUE);
35 | } catch (Exception e) {
36 | e.printStackTrace(System.err);
37 | }
38 | }
39 |
40 | public static DiskLruCacheManager getInstance(Context context) {
41 | if (instance == null) {
42 | synchronized (DiskLruCacheManager.class) {
43 | if (instance == null) {
44 | instance = new DiskLruCacheManager(context.getApplicationContext());
45 | }
46 | }
47 | }
48 |
49 | return instance;
50 | }
51 |
52 | public void put(String url, Bitmap bitmap) {
53 | if (TextUtils.isEmpty(url) || bitmap == null || bitmap.isRecycled()) {
54 | return;
55 | }
56 |
57 | try {
58 | DiskLruCache.Editor editor = diskLruCache.edit(getKey(url));
59 | OutputStream outputStream = editor.newOutputStream(0);
60 | if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)) {
61 | editor.commit();
62 | }
63 | diskLruCache.flush();
64 | } catch (Exception e) {
65 | e.printStackTrace(System.err);
66 | }
67 | }
68 |
69 | public Bitmap get(String url) {
70 | try {
71 | DiskLruCache.Snapshot snapshot = diskLruCache.get(getKey(url));
72 | if (snapshot != null) {
73 | InputStream inputStream = snapshot.getInputStream(0);
74 | return BitmapFactory.decodeStream(inputStream);
75 | }
76 | } catch (Exception e) {
77 | e.printStackTrace(System.err);
78 | }
79 |
80 | return null;
81 | }
82 |
83 | public static String getKey(String url) {
84 | try {
85 | MessageDigest digest = MessageDigest.getInstance("MD5");
86 | byte[] md5 = digest.digest(url.getBytes());
87 | BigInteger bigInteger = new BigInteger(1, md5);
88 | return bigInteger.toString(16);
89 | } catch (Exception e) {
90 | e.printStackTrace(System.err);
91 | }
92 |
93 | return null;
94 | }
95 |
96 | public void close() {
97 | try {
98 | diskLruCache.close();
99 | } catch (Exception e) {
100 | e.printStackTrace(System.err);
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/imageloader/src/main/java/com/sxu/utils/FastBlurUtil.java:
--------------------------------------------------------------------------------
1 | package com.sxu.utils;
2 |
3 | import android.graphics.Bitmap;
4 |
5 | /**
6 | * Created by jay on 11/7/15.
7 | */
8 | public class FastBlurUtil {
9 |
10 | public static Bitmap doBlur(Bitmap sentBitmap, int scaleRadius, int radius) {
11 |
12 | // Stack Blur v1.0 from
13 | // http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html
14 | //
15 | // Java Author: Mario Klingemann
16 | // http://incubator.quasimondo.com
17 | // created Feburary 29, 2004
18 | // Android port : Yahel Bouaziz
19 | // http://www.kayenko.com
20 | // ported april 5th, 2012
21 |
22 | // This is a compromise between Gaussian Blur and Box blur
23 | // It creates much better looking blurs than Box Blur, but is
24 | // 7x faster than my Gaussian Blur implementation.
25 | //
26 | // I called it Stack Blur because this describes best how this
27 | // filter works internally: it creates a kind of moving stack
28 | // of colors whilst scanning through the image. Thereby it
29 | // just has to add one new block of color to the right side
30 | // of the stack and remove the leftmost color. The remaining
31 | // colors on the topmost layer of the stack are either added on
32 | // or reduced by one, depending on if they are on the right or
33 | // on the left side of the stack.
34 | //
35 | // If you are using this algorithm in your code please add
36 | // the following line:
37 | //
38 | // Stack Blur Algorithm by Mario Klingemann
39 | if (scaleRadius > 0) {
40 | sentBitmap = Bitmap.createScaledBitmap(sentBitmap, sentBitmap.getWidth() / scaleRadius,
41 | sentBitmap.getHeight() / scaleRadius, false);
42 | }
43 | Bitmap bitmap = sentBitmap.copy(sentBitmap.getConfig(), true);
44 |
45 | if (radius < 1) {
46 | return (null);
47 | }
48 | int w = bitmap.getWidth();
49 | int h = bitmap.getHeight();
50 |
51 | int[] pix = new int[w * h];
52 | bitmap.getPixels(pix, 0, w, 0, 0, w, h);
53 |
54 | int wm = w - 1;
55 | int hm = h - 1;
56 | int wh = w * h;
57 | int div = radius + radius + 1;
58 |
59 | int r[] = new int[wh];
60 | int g[] = new int[wh];
61 | int b[] = new int[wh];
62 | int rsum, gsum, bsum, x, y, i, p, yp, yi, yw;
63 | int vmin[] = new int[Math.max(w, h)];
64 |
65 | int divsum = (div + 1) >> 1;
66 | divsum *= divsum;
67 | int dv[] = new int[256 * divsum];
68 | for (i = 0; i < 256 * divsum; i++) {
69 | dv[i] = (i / divsum);
70 | }
71 |
72 | yw = yi = 0;
73 |
74 | int[][] stack = new int[div][3];
75 | int stackpointer;
76 | int stackstart;
77 | int[] sir;
78 | int rbs;
79 | int r1 = radius + 1;
80 | int routsum, goutsum, boutsum;
81 | int rinsum, ginsum, binsum;
82 |
83 | for (y = 0; y < h; y++) {
84 | rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
85 | for (i = -radius; i <= radius; i++) {
86 | p = pix[yi + Math.min(wm, Math.max(i, 0))];
87 | sir = stack[i + radius];
88 | sir[0] = (p & 0xff0000) >> 16;
89 | sir[1] = (p & 0x00ff00) >> 8;
90 | sir[2] = (p & 0x0000ff);
91 | rbs = r1 - Math.abs(i);
92 | rsum += sir[0] * rbs;
93 | gsum += sir[1] * rbs;
94 | bsum += sir[2] * rbs;
95 | if (i > 0) {
96 | rinsum += sir[0];
97 | ginsum += sir[1];
98 | binsum += sir[2];
99 | } else {
100 | routsum += sir[0];
101 | goutsum += sir[1];
102 | boutsum += sir[2];
103 | }
104 | }
105 | stackpointer = radius;
106 |
107 | for (x = 0; x < w; x++) {
108 |
109 | r[yi] = dv[rsum];
110 | g[yi] = dv[gsum];
111 | b[yi] = dv[bsum];
112 |
113 | rsum -= routsum;
114 | gsum -= goutsum;
115 | bsum -= boutsum;
116 |
117 | stackstart = stackpointer - radius + div;
118 | sir = stack[stackstart % div];
119 |
120 | routsum -= sir[0];
121 | goutsum -= sir[1];
122 | boutsum -= sir[2];
123 |
124 | if (y == 0) {
125 | vmin[x] = Math.min(x + radius + 1, wm);
126 | }
127 | p = pix[yw + vmin[x]];
128 |
129 | sir[0] = (p & 0xff0000) >> 16;
130 | sir[1] = (p & 0x00ff00) >> 8;
131 | sir[2] = (p & 0x0000ff);
132 |
133 | rinsum += sir[0];
134 | ginsum += sir[1];
135 | binsum += sir[2];
136 |
137 | rsum += rinsum;
138 | gsum += ginsum;
139 | bsum += binsum;
140 |
141 | stackpointer = (stackpointer + 1) % div;
142 | sir = stack[(stackpointer) % div];
143 |
144 | routsum += sir[0];
145 | goutsum += sir[1];
146 | boutsum += sir[2];
147 |
148 | rinsum -= sir[0];
149 | ginsum -= sir[1];
150 | binsum -= sir[2];
151 |
152 | yi++;
153 | }
154 | yw += w;
155 | }
156 | for (x = 0; x < w; x++) {
157 | rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
158 | yp = -radius * w;
159 | for (i = -radius; i <= radius; i++) {
160 | yi = Math.max(0, yp) + x;
161 |
162 | sir = stack[i + radius];
163 |
164 | sir[0] = r[yi];
165 | sir[1] = g[yi];
166 | sir[2] = b[yi];
167 |
168 | rbs = r1 - Math.abs(i);
169 |
170 | rsum += r[yi] * rbs;
171 | gsum += g[yi] * rbs;
172 | bsum += b[yi] * rbs;
173 |
174 | if (i > 0) {
175 | rinsum += sir[0];
176 | ginsum += sir[1];
177 | binsum += sir[2];
178 | } else {
179 | routsum += sir[0];
180 | goutsum += sir[1];
181 | boutsum += sir[2];
182 | }
183 |
184 | if (i < hm) {
185 | yp += w;
186 | }
187 | }
188 | yi = x;
189 | stackpointer = radius;
190 | for (y = 0; y < h; y++) {
191 | // Preserve alpha channel: ( 0xff000000 & pix[yi] )
192 | pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum];
193 |
194 | rsum -= routsum;
195 | gsum -= goutsum;
196 | bsum -= boutsum;
197 |
198 | stackstart = stackpointer - radius + div;
199 | sir = stack[stackstart % div];
200 |
201 | routsum -= sir[0];
202 | goutsum -= sir[1];
203 | boutsum -= sir[2];
204 |
205 | if (x == 0) {
206 | vmin[y] = Math.min(y + r1, hm) * w;
207 | }
208 | p = x + vmin[y];
209 |
210 | sir[0] = r[p];
211 | sir[1] = g[p];
212 | sir[2] = b[p];
213 |
214 | rinsum += sir[0];
215 | ginsum += sir[1];
216 | binsum += sir[2];
217 |
218 | rsum += rinsum;
219 | gsum += ginsum;
220 | bsum += binsum;
221 |
222 | stackpointer = (stackpointer + 1) % div;
223 | sir = stack[stackpointer];
224 |
225 | routsum += sir[0];
226 | goutsum += sir[1];
227 | boutsum += sir[2];
228 |
229 | rinsum -= sir[0];
230 | ginsum -= sir[1];
231 | binsum -= sir[2];
232 |
233 | yi += w;
234 | }
235 | }
236 |
237 | bitmap.setPixels(pix, 0, w, 0, 0, w, h);
238 |
239 | return (bitmap);
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/imageloader/src/main/java/com/sxu/utils/StrictLineReader.java:
--------------------------------------------------------------------------------
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 | package com.sxu.utils;
17 |
18 | import java.io.ByteArrayOutputStream;
19 | import java.io.Closeable;
20 | import java.io.EOFException;
21 | import java.io.IOException;
22 | import java.io.InputStream;
23 | import java.io.UnsupportedEncodingException;
24 | import java.nio.charset.Charset;
25 |
26 | /**
27 | * Buffers input from an {@link InputStream} for reading lines.
28 | *
29 | * This class is used for buffered reading of lines. For purposes of this class, a line ends
30 | * with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated
31 | * line at end of input is invalid and will be ignored, the caller may use {@code
32 | * hasUnterminatedLine()} to detect it after catching the {@code EOFException}.
33 | *
34 | *
This class is intended for reading input that strictly consists of lines, such as line-based
35 | * cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
36 | * with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
37 | * end-of-input reporting and a more restrictive definition of a line.
38 | *
39 | *
This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
40 | * and 10, respectively, and the representation of no other character contains these values.
41 | * We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
42 | * The default charset is US_ASCII.
43 | */
44 | class StrictLineReader implements Closeable {
45 | private static final byte CR = (byte) '\r';
46 | private static final byte LF = (byte) '\n';
47 |
48 | private final InputStream in;
49 | private final Charset charset;
50 |
51 | /*
52 | * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
53 | * and the data in the range [pos, end) is buffered for reading. At end of input, if there is
54 | * an unterminated line, we set end == -1, otherwise end == pos. If the underlying
55 | * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
56 | */
57 | private byte[] buf;
58 | private int pos;
59 | private int end;
60 |
61 | /**
62 | * Constructs a new {@code LineReader} with the specified charset and the default capacity.
63 | *
64 | * @param in the {@code InputStream} to read data from.
65 | * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
66 | * supported.
67 | * @throws NullPointerException if {@code in} or {@code charset} is null.
68 | * @throws IllegalArgumentException if the specified charset is not supported.
69 | */
70 | public StrictLineReader(InputStream in, Charset charset) {
71 | this(in, 8192, charset);
72 | }
73 |
74 | /**
75 | * Constructs a new {@code LineReader} with the specified capacity and charset.
76 | *
77 | * @param in the {@code InputStream} to read data from.
78 | * @param capacity the capacity of the buffer.
79 | * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
80 | * supported.
81 | * @throws NullPointerException if {@code in} or {@code charset} is null.
82 | * @throws IllegalArgumentException if {@code capacity} is negative or zero
83 | * or the specified charset is not supported.
84 | */
85 | public StrictLineReader(InputStream in, int capacity, Charset charset) {
86 | if (in == null || charset == null) {
87 | throw new NullPointerException();
88 | }
89 | if (capacity < 0) {
90 | throw new IllegalArgumentException("capacity <= 0");
91 | }
92 | if (!(charset.equals(Util.US_ASCII))) {
93 | throw new IllegalArgumentException("Unsupported encoding");
94 | }
95 |
96 | this.in = in;
97 | this.charset = charset;
98 | buf = new byte[capacity];
99 | }
100 |
101 | /**
102 | * Closes the reader by closing the underlying {@code InputStream} and
103 | * marking this reader as closed.
104 | *
105 | * @throws IOException for errors when closing the underlying {@code InputStream}.
106 | */
107 | public void close() throws IOException {
108 | synchronized (in) {
109 | if (buf != null) {
110 | buf = null;
111 | in.close();
112 | }
113 | }
114 | }
115 |
116 | /**
117 | * Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
118 | * this end of line marker is not included in the result.
119 | *
120 | * @return the next line from the input.
121 | * @throws IOException for underlying {@code InputStream} errors.
122 | * @throws EOFException for the end of source stream.
123 | */
124 | public String readLine() throws IOException {
125 | synchronized (in) {
126 | if (buf == null) {
127 | throw new IOException("LineReader is closed");
128 | }
129 |
130 | // Read more data if we are at the end of the buffered data.
131 | // Though it's an error to read after an exception, we will let {@code fillBuf()}
132 | // throw again if that happens; thus we need to handle end == -1 as well as end == pos.
133 | if (pos >= end) {
134 | fillBuf();
135 | }
136 | // Try to find LF in the buffered data and return the line if successful.
137 | for (int i = pos; i != end; ++i) {
138 | if (buf[i] == LF) {
139 | int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
140 | String res = new String(buf, pos, lineEnd - pos, charset.name());
141 | pos = i + 1;
142 | return res;
143 | }
144 | }
145 |
146 | // Let's anticipate up to 80 characters on top of those already read.
147 | ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
148 | @Override
149 | public String toString() {
150 | int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
151 | try {
152 | return new String(buf, 0, length, charset.name());
153 | } catch (UnsupportedEncodingException e) {
154 | throw new AssertionError(e); // Since we control the charset this will never happen.
155 | }
156 | }
157 | };
158 |
159 | while (true) {
160 | out.write(buf, pos, end - pos);
161 | // Mark unterminated line in case fillBuf throws EOFException or IOException.
162 | end = -1;
163 | fillBuf();
164 | // Try to find LF in the buffered data and return the line if successful.
165 | for (int i = pos; i != end; ++i) {
166 | if (buf[i] == LF) {
167 | if (i != pos) {
168 | out.write(buf, pos, i - pos);
169 | }
170 | pos = i + 1;
171 | return out.toString();
172 | }
173 | }
174 | }
175 | }
176 | }
177 |
178 | /**
179 | * Reads new input data into the buffer. Call only with pos == end or end == -1,
180 | * depending on the desired outcome if the function throws.
181 | */
182 | private void fillBuf() throws IOException {
183 | int result = in.read(buf, 0, buf.length);
184 | if (result == -1) {
185 | throw new EOFException();
186 | }
187 | pos = 0;
188 | end = result;
189 | }
190 | }
191 |
192 |
--------------------------------------------------------------------------------
/imageloader/src/main/java/com/sxu/utils/Util.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2010 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 | package com.sxu.utils;
17 |
18 | import java.io.Closeable;
19 | import java.io.File;
20 | import java.io.IOException;
21 | import java.io.Reader;
22 | import java.io.StringWriter;
23 | import java.nio.charset.Charset;
24 |
25 | /** Junk drawer of utility methods. */
26 | final class Util {
27 | static final Charset US_ASCII = Charset.forName("US-ASCII");
28 | static final Charset UTF_8 = Charset.forName("UTF-8");
29 |
30 | private Util() {
31 | }
32 |
33 | static String readFully(Reader reader) throws IOException {
34 | try {
35 | StringWriter writer = new StringWriter();
36 | char[] buffer = new char[1024];
37 | int count;
38 | while ((count = reader.read(buffer)) != -1) {
39 | writer.write(buffer, 0, count);
40 | }
41 | return writer.toString();
42 | } finally {
43 | reader.close();
44 | }
45 | }
46 |
47 | /**
48 | * Deletes the contents of {@code dir}. Throws an IOException if any file
49 | * could not be deleted, or if {@code dir} is not a readable directory.
50 | */
51 | static void deleteContents(File dir) throws IOException {
52 | File[] files = dir.listFiles();
53 | if (files == null) {
54 | throw new IOException("not a readable directory: " + dir);
55 | }
56 | for (File file : files) {
57 | if (file.isDirectory()) {
58 | deleteContents(file);
59 | }
60 | if (!file.delete()) {
61 | throw new IOException("failed to delete file: " + file);
62 | }
63 | }
64 | }
65 |
66 | static void closeQuietly(/*Auto*/Closeable closeable) {
67 | if (closeable != null) {
68 | try {
69 | closeable.close();
70 | } catch (RuntimeException rethrown) {
71 | throw rethrown;
72 | } catch (Exception ignored) {
73 | }
74 | }
75 | }
76 | }
--------------------------------------------------------------------------------
/imageloader/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/imageloader/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | imageloader
3 |
4 |
--------------------------------------------------------------------------------
/imageloader/src/test/java/com/sxu/imageloader/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.sxu.imageloader;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':imageloader'
2 |
--------------------------------------------------------------------------------