diff --git a/src/cheogram/java/com/cheogram/android/ExtensionSettingsFragment.java b/src/cheogram/java/com/cheogram/android/ExtensionSettingsFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..21f3437afd3f9e5781379a1c9d8f4fcf15df630a --- /dev/null +++ b/src/cheogram/java/com/cheogram/android/ExtensionSettingsFragment.java @@ -0,0 +1,145 @@ +package com.cheogram.android; + +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; +import androidx.databinding.DataBindingUtil; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.common.base.Strings; +import com.google.common.primitives.Longs; +import com.google.common.io.Files; + +import java.io.File; +import java.util.ArrayList; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.FragmentExtensionSettingsBinding; +import eu.siacs.conversations.databinding.ExtensionItemBinding; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.StubConversation; +import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.ui.XmppActivity; +import eu.siacs.conversations.ui.util.Attachment; +import eu.siacs.conversations.worker.ExportBackupWorker; + +public class ExtensionSettingsFragment extends androidx.fragment.app.Fragment { + FragmentExtensionSettingsBinding binding; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_extension_settings, container, false); + binding.addExtension.setOnClickListener((v) -> { + final var intent = new Intent(); + intent.setAction(Intent.ACTION_GET_CONTENT); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + intent.setType("*/*"); + intent.addCategory(Intent.CATEGORY_OPENABLE); + 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)); + } + }); + + return binding.getRoot(); + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + super.onSaveInstanceState(bundle); + } + + @Override + public void onStart() { + super.onStart(); + getActivity().setTitle("Extensions"); + } + + public void addExtension(Uri uri) { + final var xmppConnectionService = ((XmppActivity) requireActivity()).xmppConnectionService; + if (xmppConnectionService == null) return; + try { + final var fileBackend = xmppConnectionService.getFileBackend(); + final var base = fileBackend.calculateCids(fileBackend.openInputStream(uri))[0].toString(); + final var target = new File(new File(xmppConnectionService.getExternalFilesDir(null), "extensions"), base + ".xdc"); + fileBackend.copyFileToPrivateStorage(target, uri); + } catch (final Exception e) { + Toast.makeText(requireActivity(), "Could not copy extension: " + e, Toast.LENGTH_SHORT).show(); + } + } + + + @Override + public void onActivityResult(int requestCode, int resultCode, final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + 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(); + } + + protected static class WebxdcViewHolder extends RecyclerView.ViewHolder { + final ExtensionItemBinding binding; + + public WebxdcViewHolder(final ExtensionItemBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(WebxdcPage xdc) { + binding.icon.setImageDrawable(xdc.getIcon()); + binding.name.setText(xdc.getName()); + } + } +} diff --git a/src/cheogram/java/com/cheogram/android/WebxdcPage.java b/src/cheogram/java/com/cheogram/android/WebxdcPage.java index d6b80f27892c387a47648731f576c19f0d50b59b..9499619c0857efe2e69c822afaf8a4cfab217415 100644 --- a/src/cheogram/java/com/cheogram/android/WebxdcPage.java +++ b/src/cheogram/java/com/cheogram/android/WebxdcPage.java @@ -83,14 +83,14 @@ public class WebxdcPage implements ConversationPage { protected Message source; protected WebxdcUpdate lastUpdate = null; protected WeakReference activity; + protected WeakReference> remover; - public WebxdcPage(final XmppActivity activity, Cid cid, Message source, XmppConnectionService xmppConnectionService) { - this.xmppConnectionService = xmppConnectionService; + public WebxdcPage(final XmppActivity activity, File f, Message source) { + this.xmppConnectionService = activity.xmppConnectionService; this.source = source; this.activity = new WeakReference(activity); - File f = xmppConnectionService.getFileForCid(cid); try { - if (f != null) zip = new ZipFile(xmppConnectionService.getFileForCid(cid)); + 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)); @@ -106,7 +106,15 @@ public class WebxdcPage implements ConversationPage { baseUrl = "https://" + source.getUuid() + ".localhost"; } + public WebxdcPage(final XmppActivity activity, Cid cid, Message source) { + this(activity, activity.xmppConnectionService.getFileForCid(cid), source); + } + public Drawable getIcon() { + return getIcon(288); + } + + 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"); @@ -120,7 +128,7 @@ public class WebxdcPage implements ConversationPage { 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 * 288)); + Rect r = FileBackend.rectForSize(w, h, (int)(metrics.density * dp)); decoder.setTargetSize(r.width(), r.height()); }); } catch (final IOException e) { @@ -212,6 +220,7 @@ public class WebxdcPage implements ConversationPage { } public View inflateUi(Context context, Consumer remover) { + this.remover = new WeakReference<>(remover); if (binding != null) { binding.webview.loadUrl("javascript:__webxdcUpdate();"); return getView(); @@ -306,7 +315,14 @@ public class WebxdcPage implements ConversationPage { binding.webview.loadUrl(baseUrl + "/index.html"); - binding.actions.setAdapter(new ArrayAdapter(context, R.layout.simple_list_item, new String[]{"Add to Home Screen", "Close"}) { + final var actions = + source.getStatus() == Message.STATUS_DUMMY ? + new String[]{"Close"} : + new String[]{"Add to Home Screen", "Close"}; + + if (source.getStatus() == Message.STATUS_DUMMY) binding.actions.setNumColumns(1); + + binding.actions.setAdapter(new ArrayAdapter(context, R.layout.simple_list_item, actions) { @Override public View getView(int position, View convertView, ViewGroup parent) { View v = super.getView(position, convertView, parent); @@ -318,7 +334,7 @@ public class WebxdcPage implements ConversationPage { } }); binding.actions.setOnItemClickListener((parent, v, pos, id) -> { - if (pos == 0) { + if (pos == 0 && actions.length > 1) { Intent intent = new Intent(xmppConnectionService, ConversationsActivity.class); intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION); intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, ((Conversation) source.getConversation()).getUuid()); @@ -349,12 +365,14 @@ public class WebxdcPage implements ConversationPage { } public void refresh() { + if (source.getStatus() == Message.STATUS_DUMMY) return; if (binding == null) return; binding.webview.post(() -> binding.webview.loadUrl("javascript:__webxdcUpdate();")); } public void realtimeData(String base64) { + if (source.getStatus() == Message.STATUS_DUMMY) return; if (binding == null) return; binding.webview.post(() -> binding.webview.loadUrl("javascript:__webxdcRealtimeData('" + base64.replace("'", "").replace("\\", "").replace("+", "%2B") + "');")); @@ -393,6 +411,8 @@ public class WebxdcPage implements ConversationPage { @JavascriptInterface public boolean sendStatusUpdate(String paramS, String descr) { + if (source.getStatus() == Message.STATUS_DUMMY) return false; + JSONObject params = new JSONObject(); try { params = new JSONObject(paramS); @@ -444,6 +464,8 @@ public class WebxdcPage implements ConversationPage { @JavascriptInterface public String getStatusUpdates(long lastKnownSerial) { + if (source.getStatus() == Message.STATUS_DUMMY) return "[]"; + StringBuilder builder = new StringBuilder("["); String sep = ""; for (WebxdcUpdate update : xmppConnectionService.findWebxdcUpdates(source, lastKnownSerial)) { @@ -483,7 +505,14 @@ public class WebxdcPage implements ConversationPage { if (mimeType == null) mimeType = "application/octet-stream"; intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("data:" + mimeType + ";base64," + data)); } - activity.get().startActivity(intent); + activity.get().runOnUiThread(() -> { + if (source.getStatus() == Message.STATUS_DUMMY) { + binding.webview.loadUrl("about:blank"); + final var remover = WebxdcPage.this.remover.get(); + if (remover != null) remover.accept(WebxdcPage.this); + } + activity.get().startActivity(intent); + }); return null; } catch (Exception e) { e.printStackTrace(); @@ -493,6 +522,8 @@ public class WebxdcPage implements ConversationPage { @JavascriptInterface public void sendRealtime(byte[] data) { + if (source.getStatus() == Message.STATUS_DUMMY) return; + Message message = new Message(source.getConversation(), null, Message.ENCRYPTION_NONE); message.addPayload(new Element("no-store", "urn:xmpp:hints")); Element webxdc = new Element("x", "urn:xmpp:webxdc:0"); diff --git a/src/cheogram/res/layout/extension_item.xml b/src/cheogram/res/layout/extension_item.xml new file mode 100644 index 0000000000000000000000000000000000000000..cf4a4857e044093cea9635730296d3ae0e3b9b08 --- /dev/null +++ b/src/cheogram/res/layout/extension_item.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + diff --git a/src/cheogram/res/layout/fragment_extension_settings.xml b/src/cheogram/res/layout/fragment_extension_settings.xml new file mode 100644 index 0000000000000000000000000000000000000000..ae171b7de547399e56ce120dc6875e37cc44935f --- /dev/null +++ b/src/cheogram/res/layout/fragment_extension_settings.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 16c8791ba8b16a39e0cbb0183304c1ec530dda1d..4ad37779881589aace2e2294e2ebc33dabdca53d 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -101,6 +101,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; +import com.google.common.io.Files; import com.otaliastudios.autocomplete.Autocomplete; import com.otaliastudios.autocomplete.AutocompleteCallback; @@ -263,6 +264,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 MessageAdapter messageListAdapter; protected CommandAdapter commandAdapter; private MediaPreviewAdapter mediaPreviewAdapter; @@ -1812,6 +1814,22 @@ public class ConversationFragment extends XmppFragment MenuItem newItem = menu.add(item.getGroupId(), item.getItemId(), item.getOrder(), item.getTitle()); newItem.setIcon(item.getIcon()); } + + extensions.clear(); + final var xmppConnectionService = activity.xmppConnectionService; + 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)); + } + } ConversationMenuConfigurator.configureAttachmentMenu(conversation, menu, TextUtils.isEmpty(binding.textinput.getText())); return; } @@ -2126,6 +2144,10 @@ public class ConversationFragment extends XmppFragment } else if (conversation == null) { return super.onOptionsItemSelected(item); } + if (item.getGroupId() == 0x1) { + conversation.startWebxdc(extensions.get(item.getItemId())); + return true; + } switch (item.getItemId()) { case R.id.encryption_choice_axolotl: case R.id.encryption_choice_pgp: @@ -3645,7 +3667,7 @@ public class ConversationFragment extends XmppFragment if (message == null) return; Cid webxdcCid = message.getFileParams().getCids().get(0); - WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message, activity.xmppConnectionService); + WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message); Conversation conversation = (Conversation) message.getConversation(); if (!conversation.switchToSession("webxdc\0" + message.getUuid())) { conversation.startWebxdc(webxdc); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java index af99dac3ae96ce2dc0725ff4a9c8489b53ae72ee..70790814dcaf980c3a0d18b440f0e2930771df7a 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java @@ -478,7 +478,7 @@ public class ConversationsOverviewFragment extends XmppFragment { } @Override - void refresh() { + protected void refresh() { if (this.binding == null || this.activity == null) { Log.d(Config.LOGTAG,"ConversationsOverviewFragment.refresh() skipped updated because view binding or activity was null"); return; diff --git a/src/main/java/eu/siacs/conversations/ui/XmppFragment.java b/src/main/java/eu/siacs/conversations/ui/XmppFragment.java index e5e5dc6dcd781473fdc0ff273d1e181d528453bd..5347a5122d913ced490984c8b610b524c62d9fe9 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppFragment.java @@ -45,7 +45,7 @@ public abstract class XmppFragment extends Fragment implements OnBackendConnecte protected LifecycleRegistry lifecycle = new LifecycleRegistry(this); - abstract void refresh(); + abstract protected void refresh(); public void refreshForNewCaps(final Set newCapsJids) { } protected void runOnUiThread(Runnable runnable) { 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 1c8c54907f708078d527cb3d019133865f3a126b..43dd8736bcfcc8e88e5d675cfa8a24ba726d4f66 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -852,7 +852,7 @@ public class MessageAdapter extends ArrayAdapter { private void displayWebxdcMessage(ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { Cid webxdcCid = message.getFileParams().getCids().get(0); - WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message, activity.xmppConnectionService); + WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message); displayTextMessage(viewHolder, message, bubbleColor, type); viewHolder.image.setVisibility(View.GONE); viewHolder.audioPlayer.setVisibility(View.GONE); diff --git a/src/main/java/eu/siacs/conversations/ui/fragment/settings/MainSettingsFragment.java b/src/main/java/eu/siacs/conversations/ui/fragment/settings/MainSettingsFragment.java index 88e9538f48869783bbe65b2bb54710cc2ee0d233..b71681b985bacaed8238cdc8db80e6942eb43df2 100644 --- a/src/main/java/eu/siacs/conversations/ui/fragment/settings/MainSettingsFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/fragment/settings/MainSettingsFragment.java @@ -36,6 +36,13 @@ public class MainSettingsFragment extends PreferenceFragmentCompat { if (ConnectionSettingsFragment.hideChannelDiscovery()) { connection.setSummary(R.string.pref_connection_summary); } + findPreference("extensions").setOnPreferenceClickListener((p) -> { + getFragmentManager().beginTransaction() + .replace(R.id.fragment_container, new com.cheogram.android.ExtensionSettingsFragment()) + .addToBackStack(null) + .commit(); + return true; + }); } @Override diff --git a/src/main/res/menu/fragment_conversation.xml b/src/main/res/menu/fragment_conversation.xml index 9f65e90b4b982b519518f5cf525719d7cbc567db..d1876cd5cf9586468ced7258b2ffd22991c16e78 100644 --- a/src/main/res/menu/fragment_conversation.xml +++ b/src/main/res/menu/fragment_conversation.xml @@ -60,12 +60,14 @@ + android:title="Add Subject" + android:orderInCategory="2" /> + android:title="@string/schedule_message" + android:orderInCategory="3" /> +