Skip to content

Commit f6080a2

Browse files
committed
feat: use setImageViewUri instead of setImageViewBitmap
1 parent acfd6e4 commit f6080a2

6 files changed

Lines changed: 157 additions & 97 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
22
package="com.reactnativeandroidwidget">
3+
<application>
4+
<provider
5+
android:name=".RNWidgetImageProvider"
6+
android:authorities="${applicationId}.rnwidget.imageprovider"
7+
android:exported="true"
8+
/>
9+
</application>
310
</manifest>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,9 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
2+
<application>
3+
<provider
4+
android:name=".RNWidgetImageProvider"
5+
android:authorities="${applicationId}.rnwidget.imageprovider"
6+
android:exported="true"
7+
/>
8+
</application>
29
</manifest>

android/src/main/java/com/reactnativeandroidwidget/RNWidget.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ public void drawWidget(int widgetId) throws Exception {
7070
);
7171

7272
Bitmap bitmap = drawViewToBitmap(widgetWithViews.getRootView());
73-
remoteWidgetView.setImageViewBitmap(R.id.rn_widget_image, bitmap);
73+
Uri bitmapUri = saveBitmapToDisk(widgetId, bitmap);
74+
remoteWidgetView.setImageViewUri(R.id.rn_widget_image, bitmapUri);
7475

7576
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
7677
addClickableAreas(widgetId, remoteWidgetView, widgetWithViews);
@@ -106,6 +107,12 @@ private Bitmap drawViewToBitmap(View rootView) {
106107
return bitmap;
107108
}
108109

110+
private Uri saveBitmapToDisk(int widgetId, Bitmap bitmap) {
111+
String fileName = "widget_" + widgetId + ".png";
112+
RNWidgetImageProvider.writeImage(appContext, fileName, bitmap);
113+
return RNWidgetImageProvider.getImageUri(appContext, fileName);
114+
}
115+
109116
@RequiresApi(Build.VERSION_CODES.N)
110117
private void addClickableAreas(int widgetId, RemoteViews remoteWidgetView, WidgetWithViews widgetWithViews) {
111118
remoteWidgetView.removeAllViews(R.id.rn_widget_clickable_container);
@@ -212,10 +219,13 @@ private Intent createCollectionRemoteAdapterIntent(int widgetId, int collectionI
212219
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
213220

214221
ArrayList<Bundle> collectionItemsBundle = new ArrayList<>();
215-
for (CollectionViewItem collectionViewItem : collectionItems) {
222+
for (int i = 0; i < collectionItems.size(); i++) {
223+
CollectionViewItem collectionViewItem = collectionItems.get(i);
216224
Bundle bundle = new Bundle();
217225
bundle.putString("clickAction", collectionViewItem.getClickAction());
218226
bundle.putBundle("clickActionData", Arguments.toBundle(collectionViewItem.getClickActionData()));
227+
bundle.putInt("imageWidth", collectionViewItem.getBitmap().getWidth());
228+
bundle.putString("imageName", RNWidgetCollectionService.getImageName(widgetId, collectionId, i));
219229
collectionItemsBundle.add(bundle);
220230

221231
ArrayList<Bundle> clickableAreas = new ArrayList<>();

android/src/main/java/com/reactnativeandroidwidget/RNWidgetCollectionService.java

Lines changed: 8 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44
import android.appwidget.AppWidgetManager;
55
import android.content.ComponentName;
66
import android.content.Context;
7-
import android.content.ContextWrapper;
87
import android.content.Intent;
9-
import android.graphics.Bitmap;
10-
import android.graphics.BitmapFactory;
8+
import android.net.Uri;
119
import android.os.Build;
1210
import android.os.Bundle;
1311
import android.widget.RemoteViews;
@@ -18,66 +16,18 @@
1816
import com.facebook.react.bridge.ReactApplicationContext;
1917
import com.reactnativeandroidwidget.builder.CollectionViewItem;
2018

21-
import java.io.File;
22-
import java.io.FileInputStream;
23-
import java.io.FileOutputStream;
24-
import java.io.IOException;
2519
import java.util.ArrayList;
26-
import java.util.HashMap;
2720
import java.util.List;
28-
import java.util.Map;
29-
import java.util.Objects;
3021

3122
public class RNWidgetCollectionService extends RemoteViewsService {
3223
public static final int MAX_COLLECTION_WIDGETS = 2;
3324

34-
public static void deleteWidgetImages(Context context, int widgetId) {
35-
deleteImages(context, "widget_" + widgetId);
36-
}
37-
38-
private static void deleteCollectionImages(Context context, int widgetId, int collectionId) {
39-
deleteImages(context, "widget_" + widgetId + "_collection_" + collectionId);
40-
}
41-
42-
private static void deleteImages(Context context, String prefix) {
43-
ContextWrapper cw = new ContextWrapper(context);
44-
File directory = cw.getDir("widget_list_images", Context.MODE_PRIVATE);
45-
46-
File[] files = directory.listFiles(pathname -> pathname.getName().startsWith(prefix));
47-
48-
for (File f : Objects.requireNonNull(files)) {
49-
f.delete();
50-
}
51-
}
52-
5325
public static void storeCollection(ReactApplicationContext context, int widgetId, int collectionId, List<CollectionViewItem> collectionViewItems) {
54-
deleteCollectionImages(context, widgetId, collectionId);
26+
RNWidgetImageProvider.deleteCollectionImages(context, widgetId, collectionId);
5527
for (int i = 0; i < collectionViewItems.size(); i++) {
5628
CollectionViewItem item = collectionViewItems.get(i);
5729

58-
writeImage(context, widgetId, collectionId, i, item);
59-
}
60-
}
61-
62-
private static void writeImage(ReactApplicationContext context, int widgetId, int collectionId, int position, CollectionViewItem item) {
63-
ContextWrapper cw = new ContextWrapper(context);
64-
File directory = cw.getDir("widget_list_images", Context.MODE_PRIVATE);
65-
File imagePath = new File(directory, getImageName(widgetId, collectionId, position));
66-
67-
FileOutputStream fos = null;
68-
try {
69-
fos = new FileOutputStream(imagePath);
70-
item.getBitmap().compress(Bitmap.CompressFormat.PNG, 100, fos);
71-
} catch (Exception e) {
72-
e.printStackTrace();
73-
} finally {
74-
try {
75-
if (fos != null) {
76-
fos.close();
77-
}
78-
} catch (IOException e) {
79-
e.printStackTrace();
80-
}
30+
RNWidgetImageProvider.writeImage(context, getImageName(widgetId, collectionId, i), item.getBitmap());
8131
}
8232
}
8333

@@ -92,9 +42,7 @@ public RemoteViewsFactory onGetViewFactory(Intent intent) {
9242
}
9343

9444
class ListRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
95-
private final Map<String, Bitmap> mCache = new HashMap<>();
9645
private final int mCount;
97-
private final int mCollectionId;
9846
private final List<Bundle> mCollectionItems;
9947

10048
private final Context mContext;
@@ -105,7 +53,6 @@ public ListRemoteViewsFactory(Context context, Intent intent) {
10553
mContext = context;
10654
mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
10755
mCount = intent.getIntExtra("collectionSize", 0);
108-
mCollectionId = intent.getIntExtra("collectionId", 0);
10956
mCollectionItems = intent.getParcelableArrayListExtra("collectionItems");
11057
widgetName = intent.getStringExtra("widgetName");
11158
}
@@ -114,20 +61,19 @@ public void onCreate() {
11461
}
11562

11663
public void onDestroy() {
117-
mCache.clear();
11864
}
11965

12066
public int getCount() {
12167
return mCount;
12268
}
12369

12470
public RemoteViews getViewAt(int position) {
71+
Bundle bundle = mCollectionItems.get(position);
72+
12573
RemoteViews listItemView = new RemoteViews(mContext.getPackageName(), R.layout.rn_widget_list_item);
12674
listItemView.removeAllViews(R.id.rn_widget_list_item_clickable_container);
127-
Bitmap bitmapAt = getBitmapAt(position);
128-
listItemView.setImageViewBitmap(R.id.rn_widget_list_item, bitmapAt);
129-
130-
Bundle bundle = mCollectionItems.get(position);
75+
Uri imageUri = RNWidgetImageProvider.getImageUri(mContext, bundle.getString("imageName"));
76+
listItemView.setImageViewUri(R.id.rn_widget_list_item, imageUri);
13177

13278
// Set a fill-intent which will be used to fill-in the pending intent template
13379
// which is set on the collection view in RNWidget.
@@ -136,15 +82,11 @@ public RemoteViews getViewAt(int position) {
13682
listItemView.setOnClickFillInIntent(R.id.rn_widget_list_item, fillInIntent);
13783
}
13884

139-
if (bitmapAt == null) {
140-
return listItemView;
141-
}
142-
14385
ArrayList<Bundle> clickableAreas = bundle.getParcelableArrayList("clickableAreas");
14486

14587
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
14688
for (Bundle clickableArea : clickableAreas) {
147-
addClickableArea(listItemView, clickableArea, bitmapAt.getWidth());
89+
addClickableArea(listItemView, clickableArea, bundle.getInt("imageWidth"));
14890
}
14991
}
15092

@@ -220,33 +162,5 @@ public boolean hasStableIds() {
220162
}
221163

222164
public void onDataSetChanged() {
223-
this.mCache.clear();
224-
}
225-
226-
private Bitmap getBitmapAt(int position) {
227-
if (!mCache.containsKey(getImageName(position))) {
228-
mCache.put(getImageName(position), loadImage(position));
229-
}
230-
231-
return mCache.get(getImageName(position));
232-
}
233-
234-
private Bitmap loadImage(int position) {
235-
try {
236-
ContextWrapper cw = new ContextWrapper(mContext);
237-
File directory = cw.getDir("widget_list_images", Context.MODE_PRIVATE);
238-
File imagePath = new File(directory, getImageName(position));
239-
FileInputStream fileInputStream = new FileInputStream(imagePath);
240-
Bitmap bitmap = BitmapFactory.decodeStream(fileInputStream);
241-
fileInputStream.close();
242-
return bitmap;
243-
} catch (IOException e) {
244-
e.printStackTrace();
245-
return null;
246-
}
247-
}
248-
249-
private String getImageName(int position) {
250-
return RNWidgetCollectionService.getImageName(mAppWidgetId, mCollectionId, position);
251165
}
252166
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package com.reactnativeandroidwidget;
2+
3+
import android.content.ContentProvider;
4+
import android.content.ContentValues;
5+
import android.content.Context;
6+
import android.database.Cursor;
7+
import android.graphics.Bitmap;
8+
import android.net.Uri;
9+
import android.os.ParcelFileDescriptor;
10+
import androidx.annotation.NonNull;
11+
import androidx.annotation.Nullable;
12+
13+
import java.io.File;
14+
import java.io.FileNotFoundException;
15+
import java.io.FileOutputStream;
16+
import java.io.IOException;
17+
import java.util.Objects;
18+
19+
public class RNWidgetImageProvider extends ContentProvider {
20+
public static final String AUTHORITY_SUFFIX = ".rnwidget.imageprovider";
21+
22+
@Override
23+
public boolean onCreate() {
24+
return true;
25+
}
26+
27+
@Override
28+
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
29+
if (!"r".equals(mode)) {
30+
throw new FileNotFoundException("Write access denied. This provider is Read-Only.");
31+
}
32+
33+
String fileName = uri.getLastPathSegment();
34+
if (fileName == null) {
35+
throw new FileNotFoundException("Invalid URI: " + uri);
36+
}
37+
38+
File folder = getFolderWithImages(getContext());
39+
File file = new File(folder, fileName.replaceAll("\\?.*", ""));
40+
41+
// Verify the file exists and is actually inside our folder (Path Traversal check)
42+
try {
43+
if (!file.getCanonicalPath().startsWith(folder.getCanonicalPath())) {
44+
throw new FileNotFoundException("Access denied: File is outside widget directory.");
45+
}
46+
} catch (Exception e) {
47+
throw new FileNotFoundException("Path security check failed.");
48+
}
49+
50+
if (!file.exists()) {
51+
throw new FileNotFoundException("File not found: " + file.getPath());
52+
}
53+
54+
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
55+
}
56+
57+
// --- Boilerplate required for ContentProvider ---
58+
@Nullable @Override
59+
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { return null; }
60+
@Nullable @Override
61+
public String getType(@NonNull Uri uri) { return "image/png"; } // Assume PNGs
62+
@Nullable @Override
63+
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { return null; }
64+
@Override
65+
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { return 0; }
66+
@Override
67+
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) { return 0; }
68+
69+
static void deleteWidgetImages(Context context, int widgetId) {
70+
deleteImages(context, "widget_" + widgetId);
71+
}
72+
73+
static void deleteCollectionImages(Context context, int widgetId, int collectionId) {
74+
deleteImages(context, "widget_" + widgetId + "_collection_" + collectionId);
75+
}
76+
77+
private static void deleteImages(Context context, String prefix) {
78+
File folder = getFolderWithImages(context);
79+
File[] files = folder.listFiles(pathname -> pathname.getName().startsWith(prefix));
80+
81+
for (File f : Objects.requireNonNull(files)) {
82+
f.delete();
83+
}
84+
}
85+
86+
static void writeImage(Context context, String imageName, Bitmap bitmap) {
87+
File folder = getFolderWithImages(context);
88+
if (!folder.exists()) folder.mkdirs();
89+
File file = new File(folder, imageName);
90+
91+
FileOutputStream fos = null;
92+
try {
93+
fos = new FileOutputStream(file);
94+
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
95+
} catch (Exception e) {
96+
e.printStackTrace();
97+
} finally {
98+
try {
99+
if (fos != null) {
100+
fos.close();
101+
}
102+
} catch (IOException e) {
103+
e.printStackTrace();
104+
}
105+
}
106+
}
107+
108+
static Uri getImageUri(Context context, String fileName) {
109+
String authority = context.getPackageName() + AUTHORITY_SUFFIX;
110+
111+
return new Uri.Builder()
112+
.scheme("content")
113+
.authority(authority)
114+
// Add cacheKey since the uri is cached and returns old images
115+
.appendPath(fileName + "?cacheKey=" + System.currentTimeMillis())
116+
.build();
117+
}
118+
119+
private static File getFolderWithImages(Context context) {
120+
return new File(context.getFilesDir(), "widget_images");
121+
}
122+
}

android/src/main/java/com/reactnativeandroidwidget/RNWidgetProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public void onDeleted(Context context, int[] appWidgetIds) {
6868

6969
for (int widgetId : appWidgetIds) {
7070
removeWidgetSize(context, widgetId);
71-
RNWidgetCollectionService.deleteWidgetImages(context, widgetId);
71+
RNWidgetImageProvider.deleteWidgetImages(context, widgetId);
7272

7373
Data data = RNWidgetJsCommunication.buildData(context, getClass().getSimpleName(), widgetId, "WIDGET_DELETED");
7474
RNWidgetJsCommunication.startBackgroundTask(context, data);

0 commit comments

Comments
 (0)