diff --git a/src/cheogram/java/com/cheogram/android/ConversationPage.java b/src/cheogram/java/com/cheogram/android/ConversationPage.java index 79a21d7e5c5397e4bef92224220563535d628368..7fa637daf295cb300b2efadf4c617b9d19eee813 100644 --- a/src/cheogram/java/com/cheogram/android/ConversationPage.java +++ b/src/cheogram/java/com/cheogram/android/ConversationPage.java @@ -11,4 +11,5 @@ public interface ConversationPage { public View inflateUi(Context context, Consumer remover); public View getView(); public void refresh(); + public default void close() { } } diff --git a/src/cheogram/java/com/cheogram/android/ExtensionSettingsFragment.java b/src/cheogram/java/com/cheogram/android/ExtensionSettingsFragment.java index 21f3437afd3f9e5781379a1c9d8f4fcf15df630a..924b981f27bf2c107e8b182a459ea928f02d1ae8 100644 --- a/src/cheogram/java/com/cheogram/android/ExtensionSettingsFragment.java +++ b/src/cheogram/java/com/cheogram/android/ExtensionSettingsFragment.java @@ -41,6 +41,7 @@ import eu.siacs.conversations.worker.ExportBackupWorker; public class ExtensionSettingsFragment extends androidx.fragment.app.Fragment { FragmentExtensionSettingsBinding binding; + ExtensionAdapter extensionAdapter; @Override public void onCreate(Bundle savedInstanceState) { @@ -59,38 +60,9 @@ public class ExtensionSettingsFragment extends androidx.fragment.app.Fragment { startActivityForResult(Intent.createChooser(intent, getString(R.string.perform_action_with)), 0x1); }); - binding.extensionList.setAdapter(new RecyclerView.Adapter() { - final ArrayList xdcs = new ArrayList<>(); - - @Override - public int getItemCount() { - xdcs.clear(); - final var activity = (XmppActivity) requireActivity(); - final var xmppConnectionService = activity.xmppConnectionService; - if (xmppConnectionService == null) return xdcs.size(); - final var dir = new File(xmppConnectionService.getExternalFilesDir(null), "extensions"); - for (File file : Files.fileTraverser().breadthFirst(dir)) { - if (file.isFile() && file.canRead()) { - final var dummy = new Message(new StubConversation(null, "", null, 0), null, Message.ENCRYPTION_NONE); - dummy.setStatus(Message.STATUS_DUMMY); - dummy.setUuid(file.getName()); - xdcs.add(new WebxdcPage(activity, file, dummy)); - } - } - return xdcs.size(); - } - - @Override - public WebxdcViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - final ExtensionItemBinding binding = DataBindingUtil.inflate(inflater, R.layout.extension_item, container, false); - return new WebxdcViewHolder(binding); - } - - @Override - public void onBindViewHolder(WebxdcViewHolder holder, int position) { - holder.bind(xdcs.get(position)); - } - }); + extensionAdapter = new ExtensionAdapter(inflater); + binding.extensionList.setAdapter(extensionAdapter); + extensionAdapter.refresh(); return binding.getRoot(); } @@ -104,6 +76,17 @@ public class ExtensionSettingsFragment extends androidx.fragment.app.Fragment { public void onStart() { super.onStart(); getActivity().setTitle("Extensions"); + if (extensionAdapter != null) { + extensionAdapter.refresh(); + extensionAdapter.notifyDataSetChanged(); + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + extensionAdapter = null; + binding = null; } public void addExtension(Uri uri) { @@ -126,7 +109,71 @@ public class ExtensionSettingsFragment extends androidx.fragment.app.Fragment { for (final var attachment : Attachment.extractAttachments(requireActivity(), data, Attachment.Type.FILE)) { if ("application/webxdc+zip".equals(attachment.getMime())) addExtension(attachment.getUri()); } - binding.extensionList.getAdapter().notifyDataSetChanged(); + if (extensionAdapter != null) { + extensionAdapter.refresh(); + extensionAdapter.notifyDataSetChanged(); + } + } + + protected class ExtensionAdapter extends RecyclerView.Adapter { + final LayoutInflater inflater; + final ArrayList xdcs = new ArrayList<>(); + + ExtensionAdapter(final LayoutInflater inflater) { + this.inflater = inflater; + } + + public void refresh() { + xdcs.clear(); + final var activity = (XmppActivity) requireActivity(); + final var xmppConnectionService = activity.xmppConnectionService; + if (xmppConnectionService == null) return; + final var dir = new File(xmppConnectionService.getExternalFilesDir(null), "extensions"); + for (File file : Files.fileTraverser().breadthFirst(dir)) { + if (file.isFile() && file.canRead()) { + final var xdc = new WebxdcPage(activity, file, createExtensionPreviewSource(file)); + try { + xdcs.add(new ExtensionPreview(file, xdc.getName())); + } finally { + xdc.close(); + } + } + } + } + + @Override + public int getItemCount() { + return xdcs.size(); + } + + @Override + public WebxdcViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + final ExtensionItemBinding binding = DataBindingUtil.inflate(inflater, R.layout.extension_item, parent, false); + return new WebxdcViewHolder(binding); + } + + @Override + public void onBindViewHolder(WebxdcViewHolder holder, int position) { + holder.bind((XmppActivity) requireActivity(), xdcs.get(position)); + } + } + + private static Message createExtensionPreviewSource(final File file) { + final var source = new Message(new StubConversation(null, "", null, 0), null, Message.ENCRYPTION_NONE); + source.setStatus(Message.STATUS_DUMMY); + source.setUuid(file.getName()); + return source; + } + + protected static class ExtensionPreview { + final File file; + final String name; + android.graphics.drawable.Drawable icon; + + ExtensionPreview(final File file, final String name) { + this.file = file; + this.name = name; + } } protected static class WebxdcViewHolder extends RecyclerView.ViewHolder { @@ -137,9 +184,17 @@ public class ExtensionSettingsFragment extends androidx.fragment.app.Fragment { this.binding = binding; } - public void bind(WebxdcPage xdc) { - binding.icon.setImageDrawable(xdc.getIcon()); - binding.name.setText(xdc.getName()); + public void bind(final XmppActivity activity, ExtensionPreview xdc) { + if (xdc.icon == null) { + final var page = new WebxdcPage(activity, xdc.file, createExtensionPreviewSource(xdc.file)); + try { + xdc.icon = page.getIcon(); + } finally { + page.close(); + } + } + binding.name.setText(xdc.name); + binding.icon.setImageDrawable(xdc.icon); } } } diff --git a/src/cheogram/java/com/cheogram/android/WebxdcPage.java b/src/cheogram/java/com/cheogram/android/WebxdcPage.java index f1df4728b2e758ffa3b004dc1fa9fe3ad54f52a4..2eafad423b3071dfb58223857c8b56ee8021ede1 100644 --- a/src/cheogram/java/com/cheogram/android/WebxdcPage.java +++ b/src/cheogram/java/com/cheogram/android/WebxdcPage.java @@ -93,7 +93,9 @@ public class WebxdcPage implements ConversationPage { if (f != null) zip = new ZipFile(f); final ZipEntry manifestEntry = zip == null ? null : zip.getEntry("manifest.toml"); if (manifestEntry != null) { - manifest = Toml.parse(zip.getInputStream(manifestEntry)); + try (final InputStream is = zip.getInputStream(manifestEntry)) { + manifest = Toml.parse(is); + } } } catch (final IOException e) { Log.w(Config.LOGTAG, "WebxdcPage: " + e); @@ -116,22 +118,27 @@ public class WebxdcPage implements ConversationPage { public Drawable getIcon(int dp) { if (android.os.Build.VERSION.SDK_INT < 28) return null; - if (zip == null) return null; - ZipEntry entry = zip.getEntry("icon.webp"); - if (entry == null) entry = zip.getEntry("icon.png"); - if (entry == null) entry = zip.getEntry("icon.jpg"); - if (entry == null) return null; + final ZipFile localZip = zip; + if (localZip == null) return null; try { + ZipEntry entry = localZip.getEntry("icon.webp"); + if (entry == null) entry = localZip.getEntry("icon.png"); + if (entry == null) entry = localZip.getEntry("icon.jpg"); + if (entry == null) return null; DisplayMetrics metrics = xmppConnectionService.getResources().getDisplayMetrics(); - ImageDecoder.Source source = ImageDecoder.createSource(ByteBuffer.wrap(ByteStreams.toByteArray(zip.getInputStream(entry)))); + final byte[] data; + try (final InputStream is = localZip.getInputStream(entry)) { + data = ByteStreams.toByteArray(is); + } + ImageDecoder.Source source = ImageDecoder.createSource(ByteBuffer.wrap(data)); return ImageDecoder.decodeDrawable(source, (decoder, info, src) -> { int w = info.getSize().getWidth(); int h = info.getSize().getHeight(); Rect r = FileBackend.rectForSize(w, h, (int)(metrics.density * dp)); decoder.setTargetSize(r.width(), r.height()); }); - } catch (final IOException e) { + } catch (final IOException | IllegalStateException e) { Log.w(Config.LOGTAG, "WebxdcPage.getIcon: " + e); return null; } @@ -142,6 +149,19 @@ public class WebxdcPage implements ConversationPage { return title == null ? "Widget" : title; } + @Override + public void close() { + final ZipFile localZip = zip; + if (localZip == null) return; + try { + localZip.close(); + } catch (final IOException e) { + Log.w(Config.LOGTAG, "WebxdcPage.close: " + e); + } finally { + zip = null; + } + } + public String getTitle() { String title = manifest == null ? null : manifest.getString("name"); if (lastUpdate != null && lastUpdate.getDocument() != null) { @@ -176,7 +196,8 @@ public class WebxdcPage implements ConversationPage { Log.i(Config.LOGTAG, "interceptRequest: " + rawUrl); WebResourceResponse res = null; try { - if (zip == null) { + final ZipFile localZip = zip; + if (localZip == null) { throw new Exception("no zip found"); } if (rawUrl == null) { @@ -187,13 +208,13 @@ public class WebxdcPage implements ConversationPage { InputStream targetStream = xmppConnectionService.getResources().openRawResource(R.raw.webxdc); res = new WebResourceResponse("text/javascript", "UTF-8", targetStream); } else { - ZipEntry entry = zip.getEntry(path.substring(1)); + ZipEntry entry = localZip.getEntry(path.substring(1)); if (entry == null) { throw new Exception("\"" + path + "\" not found"); } String mimeType = MimeUtils.guessFromPath(path); String encoding = mimeType.startsWith("text/") ? "UTF-8" : null; - res = new WebResourceResponse(mimeType, encoding, zip.getInputStream(entry)); + res = new WebResourceResponse(mimeType, encoding, localZip.getInputStream(entry)); } } catch (Exception e) { e.printStackTrace(); diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index c19fc2e632125f414be7fc8c2d19ac4520e91f63..9ffc4a63f78309af87508b4a819415c698882f3a 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -2057,9 +2057,11 @@ public class Conversation extends AbstractEntity } public void removeSession(ConversationPage session) { - sessions.remove(session); + if (sessions == null) return; + if (!sessions.remove(session)) return; + session.close(); notifyDataSetChanged(); - if (session instanceof WebxdcPage) mPager.get().setCurrentItem(0); + if (session instanceof WebxdcPage && mPager.get() != null) mPager.get().setCurrentItem(0); } public boolean switchToSession(final String node) { diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index caceddb2e0f24e802fc90c9bfbf56171a8ca2bdd..ba7711827698aa78fd40f505ba523c38e4b2af38 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -1881,14 +1881,15 @@ public class FileBackend { fileParams.runtime = getMediaRuntime(file); } if ("application/webxdc+zip".equals(mime)) { - try { - final var zip = new ZipFile(file); - final ZipEntry manifestEntry = zip == null ? null : zip.getEntry("manifest.toml"); + try (final var zip = new ZipFile(file)) { + final ZipEntry manifestEntry = zip.getEntry("manifest.toml"); if (manifestEntry != null) { - final var manifest = Toml.parse(zip.getInputStream(manifestEntry)); - if (manifest != null) { - final var name = manifest.getString("name"); - if (name != null) fileParams.setName(name); + try (final var is = zip.getInputStream(manifestEntry)) { + final var manifest = Toml.parse(is); + if (manifest != null) { + final var name = manifest.getString("name"); + if (name != null) fileParams.setName(name); + } } } } catch (final IOException e2) { } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 1a21bb6e5a41e5c105fb7e1db39e9e5bb8fe4c60..e8b0534565cab70ce4fe48c8b6bdcba29cfb5792 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -285,7 +285,7 @@ public class ConversationFragment extends XmppFragment private final PendingItem pendingLastMessageUuid = new PendingItem<>(); private final PendingItem pendingMessage = new PendingItem<>(); public Uri mPendingEditorContent = null; - protected ArrayList extensions = new ArrayList<>(); + protected ArrayList extensions = new ArrayList<>(); protected MessageAdapter messageListAdapter; protected CommandAdapter commandAdapter; private MediaPreviewAdapter mediaPreviewAdapter; @@ -1818,6 +1818,7 @@ public class ConversationFragment extends XmppFragment public void onDestroyView() { super.onDestroyView(); Log.d(Config.LOGTAG, "ConversationFragment.onDestroyView()"); + extensions.clear(); messageListAdapter.setOnContactPictureClicked(null); messageListAdapter.setOnContactPictureLongClicked(null); messageListAdapter.setOnInlineImageLongClicked(null); @@ -2020,14 +2021,14 @@ public class ConversationFragment extends XmppFragment final var dir = new File(xmppConnectionService.getExternalFilesDir(null), "extensions"); for (File file : Files.fileTraverser().breadthFirst(dir)) { if (file.isFile() && file.canRead()) { - final var dummy = new Message(conversation, null, conversation.getNextEncryption()); - dummy.setStatus(Message.STATUS_DUMMY); - dummy.setThread(conversation.getThread()); - dummy.setUuid(file.getName()); - final var xdc = new WebxdcPage(activity, file, dummy); - extensions.add(xdc); - final var item = menu.add(0x1, extensions.size() - 1, 0, xdc.getName()); - item.setIcon(xdc.getIcon(24)); + final var xdc = new WebxdcPage(activity, file, createExtensionSourceMessage(file)); + try { + extensions.add(file); + final var item = menu.add(0x1, extensions.size() - 1, 0, xdc.getName()); + item.setIcon(xdc.getIcon(24)); + } finally { + xdc.close(); + } } } ConversationMenuConfigurator.configureAttachmentMenu(conversation, menu, TextUtils.isEmpty(binding.textinput.getText())); @@ -2378,7 +2379,8 @@ public class ConversationFragment extends XmppFragment return super.onOptionsItemSelected(item); } if (item.getGroupId() == 0x1) { - conversation.startWebxdc(extensions.get(item.getItemId())); + final File file = extensions.get(item.getItemId()); + conversation.startWebxdc(new WebxdcPage(activity, file, createExtensionSourceMessage(file))); return true; } switch (item.getItemId()) { @@ -2472,6 +2474,14 @@ public class ConversationFragment extends XmppFragment return super.onOptionsItemSelected(item); } + private Message createExtensionSourceMessage(final File file) { + final var source = new Message(conversation, null, conversation.getNextEncryption()); + source.setStatus(Message.STATUS_DUMMY); + source.setThread(conversation.getThread()); + source.setUuid(file.getName()); + return source; + } + public boolean onBackPressed() { boolean wasLocked = conversation.getLockThread(); conversation.setLockThread(false); @@ -3946,11 +3956,10 @@ public class ConversationFragment extends XmppFragment } if (message == null) return; - Cid webxdcCid = message.getFileParams().getCids().get(0); - WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message); Conversation conversation = (Conversation) message.getConversation(); if (!conversation.switchToSession("webxdc\0" + message.getUuid())) { - conversation.startWebxdc(webxdc); + Cid webxdcCid = message.getFileParams().getCids().get(0); + conversation.startWebxdc(new WebxdcPage(activity, webxdcCid, message)); } } if (message != null) { diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 6f88f6720617e738687688d0489aca5ec1ca13a9..fcdb2d3e377b513cdf02d87e6f357793815d0caf 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -180,6 +180,7 @@ public class MessageAdapter extends ArrayAdapter { private BubbleDesign bubbleDesign = new BubbleDesign(false, false, false, true); private final boolean mForceNames; private final Map lastWebxdcUpdate = new HashMap<>(); + private final Map webxdcNames = new HashMap<>(); private String selectionUuid = null; private final AppSettings appSettings; @@ -847,25 +848,15 @@ public class MessageAdapter extends ArrayAdapter { private void displayWebxdcMessage(BubbleMessageItemViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) { Cid webxdcCid = message.getFileParams().getCids().get(0); - WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message); + final String webxdcName = getWebxdcName(webxdcCid, message); displayTextMessage(viewHolder, message, bubbleColor); viewHolder.image().setVisibility(View.GONE); viewHolder.audioPlayer().setVisibility(View.GONE); viewHolder.downloadButton().setVisibility(View.VISIBLE); viewHolder.downloadButton().setIconResource(0); - viewHolder.downloadButton().setText("Open " + webxdc.getName()); - viewHolder.downloadButton().setOnClickListener(v -> { - Conversation conversation = (Conversation) message.getConversation(); - if (!conversation.switchToSession("webxdc\0" + message.getUuid())) { - conversation.startWebxdc(webxdc); - } - }); - viewHolder.image().setOnClickListener(v -> { - Conversation conversation = (Conversation) message.getConversation(); - if (!conversation.switchToSession("webxdc\0" + message.getUuid())) { - conversation.startWebxdc(webxdc); - } - }); + viewHolder.downloadButton().setText("Open " + webxdcName); + viewHolder.downloadButton().setOnClickListener(v -> openWebxdcMessage(webxdcCid, message)); + viewHolder.image().setOnClickListener(v -> openWebxdcMessage(webxdcCid, message)); final WebxdcUpdate lastUpdate; synchronized(lastWebxdcUpdate) { lastUpdate = lastWebxdcUpdate.get(message.getUuid()); } @@ -890,13 +881,19 @@ public class MessageAdapter extends ArrayAdapter { final LruCache cache = activity.xmppConnectionService.getDrawableCache(); final Drawable d = cache.get("webxdc:icon:" + webxdcCid); if (d == null) { - new Thread(() -> { - Drawable icon = webxdc.getIcon(); + XmppConnectionService.FILE_ATTACHMENT_EXECUTOR.execute(() -> { + final WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message); + final Drawable icon; + try { + icon = webxdc.getIcon(); + } finally { + webxdc.close(); + } if (icon != null) { cache.put("webxdc:icon:" + webxdcCid, icon); activity.xmppConnectionService.updateConversationUi(); } - }).start(); + }); } else { viewHolder.image().setVisibility(View.VISIBLE); viewHolder.image().setImageDrawable(d); @@ -904,6 +901,38 @@ public class MessageAdapter extends ArrayAdapter { } } + private String getWebxdcName(final Cid webxdcCid, final Message message) { + final String key = message.getUuid() == null ? webxdcCid.toString() : message.getUuid(); + final String fallbackName = message.getFileParams().getName(); + final String fallback = fallbackName == null ? "Widget" : fallbackName; + synchronized (webxdcNames) { + final String cached = webxdcNames.get(key); + if (cached != null) return cached; + webxdcNames.put(key, fallback); + } + XmppConnectionService.FILE_ATTACHMENT_EXECUTOR.execute(() -> { + final WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message); + final String name; + try { + name = webxdc.getName(); + } finally { + webxdc.close(); + } + synchronized (webxdcNames) { + webxdcNames.put(key, name); + } + activity.xmppConnectionService.updateConversationUi(); + }); + return fallback; + } + + private void openWebxdcMessage(final Cid webxdcCid, final Message message) { + final Conversation conversation = (Conversation) message.getConversation(); + if (!conversation.switchToSession("webxdc\0" + message.getUuid())) { + conversation.startWebxdc(new WebxdcPage(activity, webxdcCid, message)); + } + } + private void displayOpenableMessage( final BubbleMessageItemViewHolder viewHolder, final Message message,