Detailed changes
@@ -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<WebxdcViewHolder>() {
+ final ArrayList<WebxdcPage> 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());
+ }
+ }
+}
@@ -83,14 +83,14 @@ public class WebxdcPage implements ConversationPage {
protected Message source;
protected WebxdcUpdate lastUpdate = null;
protected WeakReference<XmppActivity> activity;
+ protected WeakReference<Consumer<ConversationPage>> 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<ConversationPage> 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<String>(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<String>(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");
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:orientation="horizontal"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:paddingLeft="@dimen/avatar_item_distance"
+ android:paddingRight="@dimen/avatar_item_distance"
+ android:background="@drawable/background_selectable_list_item">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:gravity="center_vertical"
+ android:layout_marginEnd="10sp" />
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:gravity="center_vertical"
+ android:textAppearance="?textAppearanceBodyLarge" />
+
+ </LinearLayout>
+
+</layout>
@@ -0,0 +1,28 @@
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical">
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/add_extension"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="10dp"
+ android:layout_gravity="center"
+ app:icon="@drawable/ic_add_24dp"
+ app:iconTint="?colorOnPrimary"
+ android:text="Add Extension" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/extension_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
+
+ </LinearLayout>
+
+</layout>
@@ -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<String> pendingLastMessageUuid = new PendingItem<>();
private final PendingItem<Message> pendingMessage = new PendingItem<>();
public Uri mPendingEditorContent = null;
+ protected ArrayList<WebxdcPage> 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);
@@ -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;
@@ -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<Jid> newCapsJids) { }
protected void runOnUiThread(Runnable runnable) {
@@ -852,7 +852,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
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);
@@ -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
@@ -60,12 +60,14 @@
<item
android:id="@+id/attach_subject"
android:icon="@drawable/subject"
- android:title="Add Subject" />
+ android:title="Add Subject"
+ android:orderInCategory="2" />
<item
android:id="@+id/attach_schedule"
android:icon="@drawable/schedule_message"
- android:title="@string/schedule_message" />
+ android:title="@string/schedule_message"
+ android:orderInCategory="3" />
</menu>
</item>
<item
@@ -32,6 +32,11 @@
app:fragment="eu.siacs.conversations.ui.fragment.settings.ConnectionSettingsFragment"
app:summary="@string/pref_connection_summary_w_cd"
app:title="@string/pref_connection_options" />
+ <Preference
+ android:icon="@drawable/toys_and_games_24dp"
+ android:key="extensions"
+ app:summary="Add, remove, and configure extensions"
+ app:title="Extensions" />
<Preference
android:icon="@drawable/ic_archive_24dp"
android:key="backup"