Skip to content

Commit 499fccd

Browse files
committed
Updates to cloud icon fetching to reduce redundant calls
1 parent 6ab9aec commit 499fccd

7 files changed

Lines changed: 746 additions & 37 deletions

File tree

app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,15 @@ public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
171171
private final int dragAndDropPreference;
172172
private final boolean isGrid;
173173

174+
/**
175+
* Cached thumbnail-preference values, refreshed once per {@link #setItems} call to avoid per-bind
176+
* {@code Resources.getIntArray()} allocations and {@code SharedPreferences} reads.
177+
*/
178+
private boolean cachedShowThumb;
179+
180+
private int[] cachedMaxSizes;
181+
private int cachedCapIndex;
182+
174183
@IntDef({VIEW_GENERIC, VIEW_PICTURE, VIEW_APK, VIEW_THUMB})
175184
public @interface ViewType {}
176185

@@ -216,6 +225,7 @@ public RecyclerAdapter(
216225
grey_color = Utils.getColor(context, R.color.grey);
217226
apkColor = Utils.getColor(context, R.color.apk_item);
218227

228+
refreshThumbnailPreferences();
219229
setItems(recyclerView, itemsRaw, false);
220230
}
221231

@@ -580,6 +590,8 @@ private void setItems(
580590
@NonNull RecyclerView recyclerView,
581591
@NonNull List<LayoutElementParcelable> elements,
582592
boolean invalidate) {
593+
refreshThumbnailPreferences();
594+
583595
if (preloader != null) {
584596
recyclerView.removeOnScrollListener(preloader);
585597
preloader = null;
@@ -796,6 +808,9 @@ private void bindViewHolderList(@NonNull final ItemViewHolder holder, int positi
796808
final LayoutElementParcelable rowItem =
797809
getItemsDigested().get(position).layoutElementParcelable;
798810

811+
// Compute once per bind to avoid repeated resource/SharedPreferences reads
812+
final boolean shouldLoad = shouldLoadThumbnail(rowItem.longSize, rowItem.getMode());
813+
799814
if (mainFragment.getMainFragmentViewModel() != null && position == getItemCount() - 1) {
800815
holder.baseItemView.setMinimumHeight((int) minRowHeight);
801816
if (getItemsDigested().size() == (getBoolean(PREFERENCE_SHOW_GOBACK_BUTTON) ? 1 : 0))
@@ -907,7 +922,7 @@ && getItemsDigested().get(holder.getAdapterPosition()).getChecked()
907922
switch (rowItem.filetype) {
908923
case Icons.IMAGE:
909924
case Icons.VIDEO:
910-
if (shouldLoadThumbnail(rowItem.longSize, rowItem.getMode())) {
925+
if (shouldLoad) {
911926
if (getBoolean(PREFERENCE_USE_CIRCULAR_IMAGES)) {
912927
showThumbnailWithBackground(
913928
holder, rowItem.iconData, holder.pictureIcon, rowItem.iconData::setImageBroken);
@@ -923,7 +938,7 @@ && getItemsDigested().get(holder.getAdapterPosition()).getChecked()
923938
}
924939
break;
925940
case Icons.APK:
926-
if (shouldLoadThumbnail(rowItem.longSize, rowItem.getMode())) {
941+
if (shouldLoad) {
927942
showThumbnailWithBackground(
928943
holder, rowItem.iconData, holder.apkIcon, rowItem.iconData::setImageBroken);
929944
} else {
@@ -966,7 +981,7 @@ && getItemsDigested().get(holder.getAdapterPosition()).getChecked()
966981
if ((rowItem.filetype != Icons.IMAGE
967982
&& rowItem.filetype != Icons.APK
968983
&& rowItem.filetype != Icons.VIDEO)
969-
|| !shouldLoadThumbnail(rowItem.longSize, rowItem.getMode())) {
984+
|| !shouldLoad) {
970985
holder.apkIcon.setVisibility(View.GONE);
971986
holder.pictureIcon.setVisibility(View.GONE);
972987
holder.genericIcon.setVisibility(View.VISIBLE);
@@ -980,7 +995,7 @@ && getItemsDigested().get(holder.getAdapterPosition()).getChecked()
980995
if (!((rowItem.filetype == Icons.APK
981996
|| rowItem.filetype == Icons.IMAGE
982997
|| rowItem.filetype == Icons.VIDEO)
983-
&& shouldLoadThumbnail(rowItem.longSize, rowItem.getMode()))) {
998+
&& shouldLoad)) {
984999
holder.genericIcon.setVisibility(View.VISIBLE);
9851000
GradientDrawable gradientDrawable = (GradientDrawable) holder.genericIcon.getBackground();
9861001

@@ -1021,6 +1036,9 @@ private void bindViewHolderGrid(@NonNull final ItemViewHolder holder, int positi
10211036
final LayoutElementParcelable rowItem =
10221037
getItemsDigested().get(position).layoutElementParcelable;
10231038

1039+
// Compute once per bind to avoid repeated resource/SharedPreferences reads
1040+
final boolean shouldLoad = shouldLoadThumbnail(rowItem.longSize, rowItem.getMode());
1041+
10241042
holder.baseItemView.setOnLongClickListener(
10251043
p1 -> {
10261044
if (hasPendingPasteOperation()) return false;
@@ -1056,8 +1074,7 @@ && getItemsDigested().get(holder.getAdapterPosition()).getChecked()
10561074
holder.checkImageViewGrid.setVisibility(View.INVISIBLE);
10571075

10581076
if (rowItem.filetype == Icons.IMAGE || rowItem.filetype == Icons.VIDEO) {
1059-
if (getBoolean(PREFERENCE_SHOW_THUMB)
1060-
&& shouldLoadThumbnail(rowItem.longSize, rowItem.getMode())) {
1077+
if (shouldLoad) {
10611078
holder.imageView1.setVisibility(View.VISIBLE);
10621079
holder.imageView1.setImageDrawable(null);
10631080
if (utilsProvider.getAppTheme().equals(AppTheme.DARK)
@@ -1066,7 +1083,6 @@ && shouldLoadThumbnail(rowItem.longSize, rowItem.getMode())) {
10661083
showRoundedThumbnail(
10671084
holder,
10681085
rowItem.longSize,
1069-
rowItem.getMode(),
10701086
rowItem.iconData,
10711087
holder.imageView1,
10721088
rowItem.iconData::setImageBroken);
@@ -1076,12 +1092,10 @@ && shouldLoadThumbnail(rowItem.longSize, rowItem.getMode())) {
10761092
else holder.genericIcon.setImageResource(R.drawable.ic_doc_video_am);
10771093
}
10781094
} else if (rowItem.filetype == Icons.APK) {
1079-
if (getBoolean(PREFERENCE_SHOW_THUMB)
1080-
&& shouldLoadThumbnail(rowItem.longSize, rowItem.getMode()))
1095+
if (shouldLoad)
10811096
showRoundedThumbnail(
10821097
holder,
10831098
rowItem.longSize,
1084-
rowItem.getMode(),
10851099
rowItem.iconData,
10861100
holder.genericIcon,
10871101
rowItem.iconData::setImageBroken);
@@ -1100,8 +1114,7 @@ && shouldLoadThumbnail(rowItem.longSize, rowItem.getMode()))
11001114
} else {
11011115
switch (rowItem.filetype) {
11021116
case Icons.VIDEO:
1103-
if (!shouldLoadThumbnail(rowItem.longSize, rowItem.getMode()))
1104-
iconBackground.setBackgroundColor(videoColor);
1117+
if (!shouldLoad) iconBackground.setBackgroundColor(videoColor);
11051118
break;
11061119
case Icons.AUDIO:
11071120
iconBackground.setBackgroundColor(audioColor);
@@ -1122,12 +1135,10 @@ && shouldLoadThumbnail(rowItem.longSize, rowItem.getMode()))
11221135
iconBackground.setBackgroundColor(genericColor);
11231136
break;
11241137
case Icons.APK:
1125-
if (!shouldLoadThumbnail(rowItem.longSize, rowItem.getMode()))
1126-
iconBackground.setBackgroundColor(apkColor);
1138+
if (!shouldLoad) iconBackground.setBackgroundColor(apkColor);
11271139
break;
11281140
case Icons.IMAGE:
1129-
if (!shouldLoadThumbnail(rowItem.longSize, rowItem.getMode()))
1130-
iconBackground.setBackgroundColor(videoColor);
1141+
if (!shouldLoad) iconBackground.setBackgroundColor(videoColor);
11311142
break;
11321143
default:
11331144
iconBackground.setBackgroundColor(iconSkinColor);
@@ -1146,7 +1157,7 @@ && shouldLoadThumbnail(rowItem.longSize, rowItem.getMode()))
11461157
if ((rowItem.filetype != Icons.IMAGE
11471158
&& rowItem.filetype != Icons.APK
11481159
&& rowItem.filetype != Icons.VIDEO)
1149-
|| !shouldLoadThumbnail(rowItem.longSize, rowItem.getMode())) {
1160+
|| !shouldLoad) {
11501161
View iconBackground =
11511162
getBoolean(PREFERENCE_USE_CIRCULAR_IMAGES) ? holder.genericIcon : holder.iconLayout;
11521163
iconBackground.setBackgroundColor(goBackColor);
@@ -1196,7 +1207,7 @@ public int getCorrectView(IconDataParcelable item, int adapterPosition) {
11961207

11971208
if (mainFragment.getMainFragmentViewModel() != null
11981209
&& mainFragment.getMainFragmentViewModel().isList()) {
1199-
if (getBoolean(PREFERENCE_SHOW_THUMB)) {
1210+
if (cachedShowThumb) {
12001211
int filetype =
12011212
getItemsDigested().get(adapterPosition).requireLayoutElementParcelable().filetype;
12021213

@@ -1365,7 +1376,6 @@ public boolean onResourceReady(
13651376
private void showRoundedThumbnail(
13661377
ItemViewHolder viewHolder,
13671378
long longSize,
1368-
OpenMode mode,
13691379
IconDataParcelable iconData,
13701380
AppCompatImageView view,
13711381
OnImageProcessed errorListener) {
@@ -1541,11 +1551,16 @@ private boolean getBoolean(String key) {
15411551
}
15421552

15431553
private boolean shouldLoadThumbnail(long longSize, OpenMode mode) {
1544-
int[] maxSizes =
1545-
preferenceActivity.getResources().getIntArray(R.array.thumbnailDisplaySizeLimitPreference);
1546-
int idx = preferenceActivity.getPrefs().getInt(PREFERENCE_SHOW_REMOTE_THUMB_MAX_SIZE, 0);
15471554
return shouldLoadThumbnailStatic(
1548-
getBoolean(PREFERENCE_SHOW_THUMB), maxSizes, idx, longSize, mode);
1555+
cachedShowThumb, cachedMaxSizes, cachedCapIndex, longSize, mode);
1556+
}
1557+
1558+
/** Re-reads preference / resource values into the cached fields. */
1559+
private void refreshThumbnailPreferences() {
1560+
cachedShowThumb = getBoolean(PREFERENCE_SHOW_THUMB);
1561+
cachedMaxSizes =
1562+
preferenceActivity.getResources().getIntArray(R.array.thumbnailDisplaySizeLimitPreference);
1563+
cachedCapIndex = preferenceActivity.getPrefs().getInt(PREFERENCE_SHOW_REMOTE_THUMB_MAX_SIZE, 0);
15491564
}
15501565

15511566
/**
@@ -1658,7 +1673,7 @@ public void toggleShouldToggleDragChecked() {
16581673
}
16591674

16601675
public void setAnimate(boolean animating) {
1661-
if (specialType == -1) this.animate = animating;
1676+
if (specialType == TYPE_ITEM || specialType == TYPE_BACK) this.animate = animating;
16621677
}
16631678

16641679
public boolean getAnimating() {

app/src/main/java/com/amaze/filemanager/adapters/glide/RecyclerPreloadModelProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public RequestBuilder<Drawable> getPreloadRequestBuilder(IconDataParcelable icon
7373
requestBuilder = request.load(iconData.path);
7474
break;
7575
case IconDataParcelable.IMAGE_FROMCLOUD:
76-
requestBuilder = request.load(iconData.path).diskCacheStrategy(DiskCacheStrategy.NONE);
76+
requestBuilder = request.load(iconData.path).diskCacheStrategy(DiskCacheStrategy.RESOURCE);
7777
break;
7878
default:
7979
requestBuilder = request.load(iconData.image);

app/src/main/java/com/amaze/filemanager/adapters/glide/cloudicon/CloudIconDataFetcher.kt

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import com.bumptech.glide.load.DataSource
3030
import com.bumptech.glide.load.data.DataFetcher
3131
import java.io.IOException
3232
import java.io.InputStream
33+
import java.util.concurrent.atomic.AtomicBoolean
3334

3435
class CloudIconDataFetcher(
3536
private val context: Context,
@@ -39,22 +40,80 @@ class CloudIconDataFetcher(
3940
) : DataFetcher<Bitmap> {
4041
companion object {
4142
private val TAG = CloudIconDataFetcher::class.java.simpleName
43+
44+
/**
45+
* Calculate an [BitmapFactory.Options.inSampleSize] value that keeps the
46+
* decoded bitmap larger than the requested [reqWidth]×[reqHeight] while
47+
* still reducing memory usage significantly for oversized sources.
48+
*/
49+
@JvmStatic
50+
internal fun calculateInSampleSize(
51+
outWidth: Int,
52+
outHeight: Int,
53+
reqWidth: Int,
54+
reqHeight: Int,
55+
): Int {
56+
var inSampleSize = 1
57+
if (outHeight > reqHeight || outWidth > reqWidth) {
58+
val halfHeight = outHeight / 2
59+
val halfWidth = outWidth / 2
60+
while (halfHeight / inSampleSize >= reqHeight &&
61+
halfWidth / inSampleSize >= reqWidth
62+
) {
63+
inSampleSize *= 2
64+
}
65+
}
66+
return inSampleSize
67+
}
4268
}
4369

4470
private var inputStream: InputStream? = null
71+
private val cancelled = AtomicBoolean(false)
4572

4673
override fun loadData(
4774
priority: Priority,
4875
callback: DataFetcher.DataCallback<in Bitmap?>,
4976
) {
50-
inputStream = CloudUtil.getThumbnailInputStreamForCloud(context, path)
51-
val options =
52-
BitmapFactory.Options().also {
53-
it.outWidth = width
54-
it.outHeight = height
77+
try {
78+
inputStream = CloudUtil.getThumbnailInputStreamForCloud(context, path)
79+
if (inputStream == null || cancelled.get()) {
80+
callback.onDataReady(null)
81+
return
82+
}
83+
84+
// Buffer the full stream so we can do a two-pass decode.
85+
// Pass 1 reads only the dimensions; pass 2 decodes with inSampleSize.
86+
val bytes = inputStream!!.readBytes()
87+
if (cancelled.get()) {
88+
callback.onDataReady(null)
89+
return
90+
}
91+
92+
// --- Pass 1: decode bounds only ---
93+
val boundsOptions = BitmapFactory.Options().apply { inJustDecodeBounds = true }
94+
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, boundsOptions)
95+
96+
// --- Pass 2: decode with appropriate down-sampling ---
97+
val decodeOptions =
98+
BitmapFactory.Options().apply {
99+
inSampleSize =
100+
calculateInSampleSize(
101+
boundsOptions.outWidth,
102+
boundsOptions.outHeight,
103+
width,
104+
height,
105+
)
106+
}
107+
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, decodeOptions)
108+
callback.onDataReady(bitmap)
109+
} catch (e: Exception) {
110+
if (cancelled.get()) {
111+
callback.onDataReady(null)
112+
} else {
113+
Log.e(TAG, "Error loading cloud icon for $path", e)
114+
callback.onLoadFailed(e)
55115
}
56-
val drawable = BitmapFactory.decodeStream(inputStream, null, options)
57-
callback.onDataReady(drawable)
116+
}
58117
}
59118

60119
override fun cleanup() {
@@ -65,7 +124,17 @@ class CloudIconDataFetcher(
65124
}
66125
}
67126

68-
override fun cancel() = Unit
127+
override fun cancel() {
128+
cancelled.set(true)
129+
// Close the stream to interrupt any in-progress network read so the
130+
// background thread doesn't keep downloading a file whose result will
131+
// never be used.
132+
try {
133+
inputStream?.close()
134+
} catch (_: IOException) {
135+
// Best-effort; the stream may already be closed.
136+
}
137+
}
69138

70139
override fun getDataClass(): Class<Bitmap> = Bitmap::class.java
71140

app/src/main/java/com/amaze/filemanager/adapters/glide/cloudicon/CloudIconModelLoader.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ class CloudIconModelLoader(private val context: Context) : ModelLoader<String, B
3535
height: Int,
3636
options: Options,
3737
): ModelLoader.LoadData<Bitmap> {
38-
// we put key as current time since we're not disk caching the images for cloud,
39-
// as there is no way to differentiate input streams returned by different cloud services
40-
// for future instances and they don't expose concrete paths either
38+
// Use the path as the cache key so Glide's memory (and disk) cache can
39+
// recognise repeated loads of the same remote file and serve them from
40+
// cache instead of re-downloading. The previous implementation used
41+
// System.currentTimeMillis() which made every request unique, defeating
42+
// all caching and causing repeated full-file downloads on every bind.
4143
return ModelLoader.LoadData(
42-
ObjectKey(System.currentTimeMillis()),
44+
ObjectKey(s),
4345
CloudIconDataFetcher(context, s, width, height),
4446
)
4547
}

0 commit comments

Comments
 (0)