blob: 28f5d80faadf845c598f3fcc7ac7ea16302d371a [file] [log] [blame]
David Morrissey5770dd72015-01-10 19:00:13 +00001package com.davemorrissey.labs.subscaleview.decoder;
2
3import android.content.ContentResolver;
4import android.content.Context;
5import android.content.pm.PackageManager;
6import android.content.res.AssetManager;
7import android.content.res.Resources;
8import android.graphics.*;
David Morrissey5770dd72015-01-10 19:00:13 +00009import android.net.Uri;
David Morrissey6bb6ea72017-11-24 12:46:45 +000010import android.os.Build;
David Morrissey5770dd72015-01-10 19:00:13 +000011import android.text.TextUtils;
12
David Morrisseyabffe7d2015-11-07 18:17:40 +000013import java.io.InputStream;
David Morrissey5770dd72015-01-10 19:00:13 +000014import java.util.List;
David Morrissey6bb6ea72017-11-24 12:46:45 +000015import java.util.concurrent.locks.Lock;
David Morrisseyc947ca42017-11-23 13:52:04 +000016import java.util.concurrent.locks.ReadWriteLock;
17import java.util.concurrent.locks.ReentrantReadWriteLock;
David Morrissey5770dd72015-01-10 19:00:13 +000018
19/**
20 * Default implementation of {@link com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder}
21 * using Android's {@link android.graphics.BitmapRegionDecoder}, based on the Skia library. This
22 * works well in most circumstances and has reasonable performance due to the cached decoder instance,
David Morrisseyf06b6c02015-01-12 21:04:09 +000023 * however it has some problems with grayscale, indexed and CMYK images.
David Morrisseyc947ca42017-11-23 13:52:04 +000024 *
25 * A {@link ReadWriteLock} is used to delegate responsibility for multi threading behaviour to the
David Morrissey6bb6ea72017-11-24 12:46:45 +000026 * {@link BitmapRegionDecoder} instance on SDK >= 21, whilst allowing this class to block until no
27 * tiles are being loaded before recycling the decoder. In practice, {@link BitmapRegionDecoder} is
28 * synchronized internally so this has no real impact on performance.
David Morrissey5770dd72015-01-10 19:00:13 +000029 */
30public class SkiaImageRegionDecoder implements ImageRegionDecoder {
31
32 private BitmapRegionDecoder decoder;
David Morrisseyc947ca42017-11-23 13:52:04 +000033 private final ReadWriteLock decoderLock = new ReentrantReadWriteLock(true);
David Morrissey5770dd72015-01-10 19:00:13 +000034
David Morrisseyf06b6c02015-01-12 21:04:09 +000035 private static final String FILE_PREFIX = "file://";
36 private static final String ASSET_PREFIX = FILE_PREFIX + "/android_asset/";
David Morrissey5770dd72015-01-10 19:00:13 +000037 private static final String RESOURCE_PREFIX = ContentResolver.SCHEME_ANDROID_RESOURCE + "://";
38
Khabensky Denisc95f6e22017-10-10 12:57:13 +030039 private final Bitmap.Config bitmapConfig;
40
41 public SkiaImageRegionDecoder() {
42 this(null);
43 }
44
45 public SkiaImageRegionDecoder(Bitmap.Config bitmapConfig) {
David Morrisseyaca42712017-11-22 19:47:20 +000046 if (bitmapConfig == null) {
Khabensky Denisc95f6e22017-10-10 12:57:13 +030047 this.bitmapConfig = Bitmap.Config.RGB_565;
David Morrisseyaca42712017-11-22 19:47:20 +000048 } else {
Khabensky Denisc95f6e22017-10-10 12:57:13 +030049 this.bitmapConfig = bitmapConfig;
David Morrisseyaca42712017-11-22 19:47:20 +000050 }
Khabensky Denisc95f6e22017-10-10 12:57:13 +030051 }
52
David Morrissey5770dd72015-01-10 19:00:13 +000053 @Override
54 public Point init(Context context, Uri uri) throws Exception {
55 String uriString = uri.toString();
56 if (uriString.startsWith(RESOURCE_PREFIX)) {
57 Resources res;
58 String packageName = uri.getAuthority();
59 if (context.getPackageName().equals(packageName)) {
60 res = context.getResources();
61 } else {
62 PackageManager pm = context.getPackageManager();
63 res = pm.getResourcesForApplication(packageName);
64 }
65
66 int id = 0;
67 List<String> segments = uri.getPathSegments();
68 int size = segments.size();
69 if (size == 2 && segments.get(0).equals("drawable")) {
70 String resName = segments.get(1);
71 id = res.getIdentifier(resName, "drawable", packageName);
72 } else if (size == 1 && TextUtils.isDigitsOnly(segments.get(0))) {
73 try {
74 id = Integer.parseInt(segments.get(0));
75 } catch (NumberFormatException ignored) {
76 }
77 }
78
79 decoder = BitmapRegionDecoder.newInstance(context.getResources().openRawResource(id), false);
80 } else if (uriString.startsWith(ASSET_PREFIX)) {
81 String assetName = uriString.substring(ASSET_PREFIX.length());
82 decoder = BitmapRegionDecoder.newInstance(context.getAssets().open(assetName, AssetManager.ACCESS_RANDOM), false);
David Morrisseyf06b6c02015-01-12 21:04:09 +000083 } else if (uriString.startsWith(FILE_PREFIX)) {
84 decoder = BitmapRegionDecoder.newInstance(uriString.substring(FILE_PREFIX.length()), false);
David Morrissey5770dd72015-01-10 19:00:13 +000085 } else {
David Morrisseyabffe7d2015-11-07 18:17:40 +000086 InputStream inputStream = null;
87 try {
88 ContentResolver contentResolver = context.getContentResolver();
89 inputStream = contentResolver.openInputStream(uri);
90 decoder = BitmapRegionDecoder.newInstance(inputStream, false);
91 } finally {
92 if (inputStream != null) {
93 try { inputStream.close(); } catch (Exception e) { }
94 }
95 }
David Morrissey5770dd72015-01-10 19:00:13 +000096 }
97 return new Point(decoder.getWidth(), decoder.getHeight());
98 }
99
100 @Override
101 public Bitmap decodeRegion(Rect sRect, int sampleSize) {
David Morrissey6bb6ea72017-11-24 12:46:45 +0000102 getDecodeLock().lock();
David Morrisseyc947ca42017-11-23 13:52:04 +0000103 try {
104 if (decoder != null && !decoder.isRecycled()) {
105 BitmapFactory.Options options = new BitmapFactory.Options();
106 options.inSampleSize = sampleSize;
107 options.inPreferredConfig = bitmapConfig;
108 Bitmap bitmap = decoder.decodeRegion(sRect, options);
109 if (bitmap == null) {
110 throw new RuntimeException("Skia image decoder returned null bitmap - image format may not be supported");
111 }
112 return bitmap;
113 } else {
114 throw new IllegalStateException("Cannot decode region after decoder has been recycled");
David Morrisseyc431b2a2015-06-14 17:55:11 +0100115 }
David Morrisseyc947ca42017-11-23 13:52:04 +0000116 } finally {
David Morrissey6bb6ea72017-11-24 12:46:45 +0000117 getDecodeLock().unlock();
David Morrissey5770dd72015-01-10 19:00:13 +0000118 }
119 }
120
121 @Override
David Morrissey6bb6ea72017-11-24 12:46:45 +0000122 public synchronized boolean isReady() {
123 return decoder != null && !decoder.isRecycled();
David Morrissey5770dd72015-01-10 19:00:13 +0000124 }
125
126 @Override
David Morrissey6bb6ea72017-11-24 12:46:45 +0000127 public synchronized void recycle() {
David Morrisseyc947ca42017-11-23 13:52:04 +0000128 decoderLock.writeLock().lock();
129 try {
130 decoder.recycle();
David Morrissey6bb6ea72017-11-24 12:46:45 +0000131 decoder = null;
David Morrisseyc947ca42017-11-23 13:52:04 +0000132 } finally {
133 decoderLock.writeLock().unlock();
134 }
David Morrissey5770dd72015-01-10 19:00:13 +0000135 }
David Morrissey6bb6ea72017-11-24 12:46:45 +0000136
137 /**
138 * Before SDK 21, BitmapRegionDecoder was not synchronized internally. Any attempt to decode
139 * regions from multiple threads with one decoder instance causes a segfault. For old versions
140 * use the write lock to enforce single threaded decoding.
141 */
142 private Lock getDecodeLock() {
143 if (Build.VERSION.SDK_INT < 21) {
144 return decoderLock.writeLock();
145 } else {
146 return decoderLock.readLock();
147 }
148 }
David Morrissey5770dd72015-01-10 19:00:13 +0000149}