forked from mirror/monocles_chat_clean
Experimental WebXDC extensions functionality
(cherry picked from commit 3de2b965c8725320cae1fd6b3c3a116b8e03097d)
This commit is contained in:
parent
0e4d3ea9ac
commit
aeac25dd54
11 changed files with 286 additions and 13 deletions
|
@ -122,6 +122,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;
|
||||
|
@ -311,6 +312,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;
|
||||
|
@ -2127,6 +2129,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;
|
||||
}
|
||||
|
@ -2446,6 +2464,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:
|
||||
|
@ -4075,7 +4097,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);
|
||||
|
|
|
@ -499,7 +499,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) {
|
||||
|
|
|
@ -855,7 +855,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
|
||||
private void displayWebxdcMessage(BubbleMessageItemViewHolder 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);
|
||||
|
|
|
@ -38,6 +38,13 @@ public class MainSettingsFragment extends PreferenceFragmentCompat {
|
|||
connection.setSummary(R.string.pref_connection_summary);
|
||||
}
|
||||
up.setVisible(!Strings.isNullOrEmpty(getString(R.string.default_push_server)));
|
||||
findPreference("extensions").setOnPreferenceClickListener((p) -> {
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.fragment_container, new com.cheogram.android.ExtensionSettingsFragment())
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -84,10 +84,13 @@
|
|||
android:id="@+id/attach_subject"
|
||||
android:icon="@drawable/subject"
|
||||
android:title="@string/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="@string/pref_extensions_summary"
|
||||
app:title="@string/pref_extensions_title" />
|
||||
<Preference
|
||||
android:icon="@drawable/ic_archive_24dp"
|
||||
android:key="backup"
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
package de.monocles.chat;
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -84,14 +84,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));
|
||||
|
@ -107,7 +107,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");
|
||||
|
@ -121,7 +129,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) {
|
||||
|
@ -213,6 +221,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();
|
||||
|
@ -309,7 +318,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[]{activity.get().getString(R.string.add_to_home_screen), activity.get().getString(R.string.action_close)}) {
|
||||
final var actions =
|
||||
source.getStatus() == Message.STATUS_DUMMY ?
|
||||
new String[]{activity.get().getString(R.string.action_close)} :
|
||||
new String[]{activity.get().getString(R.string.add_to_home_screen), activity.get().getString(R.string.action_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);
|
||||
|
@ -321,7 +337,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());
|
||||
|
@ -352,6 +368,7 @@ public class WebxdcPage implements ConversationPage {
|
|||
}
|
||||
|
||||
public void refresh() {
|
||||
if (source.getStatus() == Message.STATUS_DUMMY) return;
|
||||
if (binding == null) return;
|
||||
if (activity != null) {
|
||||
activity.get().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
|
||||
|
@ -360,6 +377,7 @@ public class WebxdcPage implements ConversationPage {
|
|||
}
|
||||
|
||||
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") + "');"));
|
||||
|
@ -398,6 +416,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);
|
||||
|
@ -449,6 +469,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)) {
|
||||
|
@ -488,7 +510,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();
|
||||
|
@ -498,6 +527,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");
|
||||
|
|
32
src/monocleschat/res/layout/extension_item.xml
Normal file
32
src/monocleschat/res/layout/extension_item.xml
Normal file
|
@ -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>
|
28
src/monocleschat/res/layout/fragment_extension_settings.xml
Normal file
28
src/monocleschat/res/layout/fragment_extension_settings.xml
Normal file
|
@ -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="@string/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>
|
Loading…
Reference in a new issue