diff --git a/src/conversations/res/layout/activity_channel_discovery.xml b/src/conversations/res/layout/activity_channel_discovery.xml
new file mode 100644
index 000000000..679c5156e
--- /dev/null
+++ b/src/conversations/res/layout/activity_channel_discovery.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/conversations/res/layout/search_result_item.xml b/src/conversations/res/layout/search_result_item.xml
new file mode 100644
index 000000000..2d77be1fb
--- /dev/null
+++ b/src/conversations/res/layout/search_result_item.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index b6a8faac1..6b43106f9 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -283,6 +283,7 @@
+
diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java
index 427502487..4626bffc8 100644
--- a/src/main/java/eu/siacs/conversations/Config.java
+++ b/src/main/java/eu/siacs/conversations/Config.java
@@ -40,6 +40,9 @@ public final class Config {
public static final String DOMAIN_LOCK = null; //only allow account creation for this domain
public static final String MAGIC_CREATE_DOMAIN = "conversations.im";
public static final String QUICKSY_DOMAIN = "quicksy.im";
+
+ public static final Jid CHANNEL_DISCOVERY = Jid.of("rodrigo.de.mucobedo@dreckshal.de");
+
public static final boolean DISALLOW_REGISTRATION_IN_UI = false; //hide the register checkbox
public static final boolean USE_RANDOM_RESOURCE_ON_EVERY_BIND = false;
diff --git a/src/main/java/eu/siacs/conversations/entities/ChannelSearchResult.java b/src/main/java/eu/siacs/conversations/entities/ChannelSearchResult.java
new file mode 100644
index 000000000..0222f2f85
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/entities/ChannelSearchResult.java
@@ -0,0 +1,35 @@
+package eu.siacs.conversations.entities;
+
+import eu.siacs.conversations.services.AvatarService;
+import eu.siacs.conversations.utils.UIHelper;
+import rocks.xmpp.addr.Jid;
+
+public class ChannelSearchResult implements AvatarService.Avatarable {
+
+ private final String name;
+ private final String description;
+ private final Jid room;
+
+ public String getName() {
+ return name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public Jid getRoom() {
+ return room;
+ }
+
+ public ChannelSearchResult(String name, String description, Jid room) {
+ this.name = name;
+ this.description = description;
+ this.room = room;
+ }
+
+ @Override
+ public int getAvatarBackgroundColor() {
+ return UIHelper.getColorForName(room != null ? room.asBareJid().toEscapedString() : getName());
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java
index f4807a11b..055c50005 100644
--- a/src/main/java/eu/siacs/conversations/services/AvatarService.java
+++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java
@@ -33,6 +33,7 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.ChannelSearchResult;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
@@ -82,11 +83,17 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
return get((ListItem) avatarable, size, cachedOnly);
} else if (avatarable instanceof MucOptions.User) {
return get((MucOptions.User) avatarable, size, cachedOnly);
+ } else if (avatarable instanceof ChannelSearchResult) {
+ return get((ChannelSearchResult) avatarable, size, cachedOnly);
}
throw new AssertionError("AvatarService does not know how to generate avatar from "+avatarable.getClass().getName());
}
+ private Bitmap get(final ChannelSearchResult result, final int size, boolean cacheOnly) {
+ return get(result.getName(), result.getRoom().asBareJid().toEscapedString(), size, cacheOnly);
+ }
+
private Bitmap get(final Contact contact, final int size, boolean cachedOnly) {
if (contact.isSelf()) {
return get(contact.getAccount(), size, cachedOnly);
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index 41e3ff8d9..3c2164902 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -49,6 +49,7 @@ import org.openintents.openpgp.util.OpenPgpServiceConnection;
import java.io.File;
import java.net.URL;
+import java.nio.channels.Channel;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.CertificateException;
@@ -84,6 +85,7 @@ import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Blockable;
import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.ChannelSearchResult;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
@@ -112,6 +114,7 @@ import eu.siacs.conversations.ui.UiCallback;
import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable;
+import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.ConversationsFileObserver;
import eu.siacs.conversations.utils.CryptoHelper;
@@ -795,6 +798,58 @@ public class XmppConnectionService extends Service {
return pingNow;
}
+ public void discoverChannels(String query, OnChannelSearchResultsFound listener) {
+ IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
+ packet.setTo(Config.CHANNEL_DISCOVERY);
+ Element search = packet.addChild("search","https://xmlns.zombofant.net/muclumbus/search/1.0");
+ search.addChild("set","http://jabber.org/protocol/rsm").addChild("max").setContent("100");
+ Bundle bundle = new Bundle();
+ if (!TextUtils.isEmpty(query)) {
+ bundle.putString("q",query);
+ }
+ Data data = Data.create("https://xmlns.zombofant.net/muclumbus/search/1.0#params", bundle);
+ search.addChild(data);
+ final Account account = AccountUtils.getFirstEnabled(this);
+ if (account == null) {
+ return;
+ }
+ sendIqPacket(account, packet, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket response) {
+ ArrayList searchResults = new ArrayList<>();
+ if (response.getType() == IqPacket.TYPE.RESULT) {
+ Element result = response.findChild("result","https://xmlns.zombofant.net/muclumbus/search/1.0");
+ if (result != null) {
+ for(Element child : result.getChildren()) {
+ if ("item".equals(child.getName())) {
+ String name = child.findChildContent("name");
+ String description = child.findChildContent("description");
+ Jid room = child.getAttributeAsJid("address");
+ if (room != null) {
+ searchResults.add(new ChannelSearchResult(name,description,room));
+ } else {
+ Log.d(Config.LOGTAG,"skipping because room was null");
+ }
+ }
+ }
+ } else {
+ Log.d(Config.LOGTAG,"result was null");
+ }
+ } else {
+ Log.d(Config.LOGTAG,response.toString());
+ }
+ if (listener != null) {
+ listener.onChannelSearchResultsFound(searchResults);
+ }
+
+ }
+ });
+ }
+
+ public interface OnChannelSearchResultsFound {
+ void onChannelSearchResultsFound(List results);
+ }
+
public boolean isDataSaverDisabled() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
diff --git a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java
new file mode 100644
index 000000000..5b0c30ed3
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java
@@ -0,0 +1,165 @@
+package eu.siacs.conversations.ui;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.databinding.DataBindingUtil;
+import android.os.Bundle;
+import android.support.v7.widget.Toolbar;
+import android.text.Html;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.ActivityChannelDiscoveryBinding;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.ChannelSearchResult;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.adapter.ChannelSearchResultAdapter;
+import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
+import eu.siacs.conversations.utils.AccountUtils;
+import rocks.xmpp.addr.Jid;
+
+public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.OnActionExpandListener, TextView.OnEditorActionListener, XmppConnectionService.OnChannelSearchResultsFound, ChannelSearchResultAdapter.OnChannelSearchResultSelected {
+
+ private static final String CHANNEL_DISCOVERY_OPT_IN = "channel_discovery_opt_in";
+
+ private final ChannelSearchResultAdapter adapter = new ChannelSearchResultAdapter();
+
+ private EditText mSearchEditText;
+
+ private boolean optedIn = false;
+
+ @Override
+ protected void refreshUiReal() {
+
+ }
+
+ @Override
+ void onBackendConnected() {
+ if (optedIn) {
+ xmppConnectionService.discoverChannels(null, this);
+ }
+ }
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ActivityChannelDiscoveryBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_channel_discovery);
+ setSupportActionBar((Toolbar) binding.toolbar);
+ configureActionBar(getSupportActionBar(), true);
+ binding.list.setAdapter(this.adapter);
+ this.adapter.setOnChannelSearchResultSelectedListener(this);
+ optedIn = getPreferences().getBoolean(CHANNEL_DISCOVERY_OPT_IN, false);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ getMenuInflater().inflate(R.menu.muc_users_activity, menu);
+ final MenuItem menuSearchView = menu.findItem(R.id.action_search);
+ final View mSearchView = menuSearchView.getActionView();
+ mSearchEditText = mSearchView.findViewById(R.id.search_field);
+ mSearchEditText.setHint(R.string.search_channels);
+ mSearchEditText.setOnEditorActionListener(this);
+ menuSearchView.setOnActionExpandListener(this);
+ return true;
+ }
+
+ @Override
+ public boolean onMenuItemActionExpand(MenuItem item) {
+ mSearchEditText.post(() -> {
+ mSearchEditText.requestFocus();
+ final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.showSoftInput(mSearchEditText, InputMethodManager.SHOW_IMPLICIT);
+ });
+ return true;
+ }
+
+ @Override
+ public boolean onMenuItemActionCollapse(MenuItem item) {
+ final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY);
+ mSearchEditText.setText("");
+ adapter.submitList(Collections.emptyList());
+ if (optedIn) {
+ xmppConnectionService.discoverChannels(null, this);
+ }
+ return true;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ if (!optedIn) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.channel_discovery_opt_in_title);
+ builder.setMessage(Html.fromHtml(getString(R.string.channel_discover_opt_in_message)));
+ builder.setNegativeButton(R.string.cancel, (dialog, which) -> finish());
+ builder.setPositiveButton(R.string.confirm, (dialog, which) -> optIn());
+ builder.setOnCancelListener(dialog -> finish());
+ final AlertDialog dialog = builder.create();
+ dialog.setCanceledOnTouchOutside(false);
+ dialog.show();
+ }
+ }
+
+ private void optIn() {
+ SharedPreferences preferences = getPreferences();
+ preferences.edit().putBoolean(CHANNEL_DISCOVERY_OPT_IN,true).apply();
+ optedIn = true;
+ xmppConnectionService.discoverChannels(null, this);
+ }
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (optedIn) {
+ xmppConnectionService.discoverChannels(v.getText().toString(), this);
+ }
+ adapter.submitList(Collections.emptyList());
+ SoftKeyboardUtils.hideSoftKeyboard(this);
+ return true;
+ }
+
+ @Override
+ public void onChannelSearchResultsFound(List results) {
+ runOnUiThread(() -> adapter.submitList(results));
+
+ }
+
+ @Override
+ public void onChannelSearchResult(final ChannelSearchResult result) {
+ List accounts = AccountUtils.getEnabledAccounts(xmppConnectionService);
+ if (accounts.size() == 1) {
+ joinChannelSearchResult(accounts.get(0),result);
+ } else if (accounts.size() > 0){
+ final AtomicReference account = new AtomicReference<>(accounts.get(0));
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.choose_account);
+ builder.setSingleChoiceItems(accounts.toArray(new CharSequence[0]), 0, (dialog, which) -> account.set(accounts.get(which)));
+ builder.setPositiveButton(R.string.join, (dialog, which) -> joinChannelSearchResult(account.get(), result));
+ builder.setNegativeButton(R.string.cancel, null);
+ builder.create().show();
+ }
+
+ }
+
+ public void joinChannelSearchResult(String accountJid, ChannelSearchResult result) {
+ Account account = xmppConnectionService.findAccountByJid(Jid.of(accountJid));
+ final Conversation conversation = xmppConnectionService.findOrCreateConversation(account, result.getRoom(), true, true, true);
+ switchToConversation(conversation);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
index 2a0066e1b..d776d0ff0 100644
--- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
@@ -313,6 +313,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
prefilled = null;
}
switch (actionItem.getId()) {
+ case R.id.discover_public_channels:
+ startActivity(new Intent(this, ChannelDiscoveryActivity.class));
+ break;
case R.id.join_public_channel:
showJoinConferenceDialog(prefilled);
break;
@@ -781,15 +784,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
this.mPostponedActivityResult = null;
}
this.mActivatedAccounts.clear();
- for (Account account : xmppConnectionService.getAccounts()) {
- if (account.getStatus() != Account.State.DISABLED) {
- if (Config.DOMAIN_LOCK != null) {
- this.mActivatedAccounts.add(account.getJid().getLocal());
- } else {
- this.mActivatedAccounts.add(account.getJid().asBareJid().toString());
- }
- }
- }
+ this.mActivatedAccounts.addAll(AccountUtils.getEnabledAccounts(xmppConnectionService));
configureHomeButton();
Intent intent = pendingViewIntent.pop();
if (intent != null && processViewIntent(intent)) {
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ChannelSearchResultAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ChannelSearchResultAdapter.java
new file mode 100644
index 000000000..ae1c70c1e
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/ChannelSearchResultAdapter.java
@@ -0,0 +1,78 @@
+package eu.siacs.conversations.ui.adapter;
+
+import android.databinding.DataBindingUtil;
+import android.support.annotation.NonNull;
+import android.support.v7.recyclerview.extensions.ListAdapter;
+import android.support.v7.util.DiffUtil;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.SearchResultItemBinding;
+import eu.siacs.conversations.entities.ChannelSearchResult;
+import eu.siacs.conversations.ui.util.AvatarWorkerTask;
+
+public class ChannelSearchResultAdapter extends ListAdapter {
+
+ private OnChannelSearchResultSelected listener;
+
+ private static final DiffUtil.ItemCallback DIFF = new DiffUtil.ItemCallback() {
+ @Override
+ public boolean areItemsTheSame(@NonNull ChannelSearchResult a, @NonNull ChannelSearchResult b) {
+ return false;
+ }
+
+ @Override
+ public boolean areContentsTheSame(@NonNull ChannelSearchResult a, @NonNull ChannelSearchResult b) {
+ return a.equals(b);
+ }
+ };
+
+ public ChannelSearchResultAdapter() {
+ super(DIFF);
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
+ return new ViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.search_result_item,viewGroup,false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
+ final ChannelSearchResult searchResult = getItem(position);
+ viewHolder.binding.name.setText(searchResult.getName());
+ final String description = searchResult.getDescription();
+ if (TextUtils.isEmpty(description)) {
+ viewHolder.binding.description.setVisibility(View.GONE);
+ } else {
+ viewHolder.binding.description.setText(description);
+ viewHolder.binding.description.setVisibility(View.VISIBLE);
+ }
+ viewHolder.binding.room.setText(searchResult.getRoom().asBareJid().toString());
+ AvatarWorkerTask.loadAvatar(searchResult, viewHolder.binding.avatar, R.dimen.avatar);
+ viewHolder.binding.getRoot().setOnClickListener(v -> listener.onChannelSearchResult(searchResult));
+ }
+
+ public void setOnChannelSearchResultSelectedListener(OnChannelSearchResultSelected listener) {
+ this.listener = listener;
+ }
+
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+
+ private final SearchResultItemBinding binding;
+
+ private ViewHolder(SearchResultItemBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+ }
+
+ public interface OnChannelSearchResultSelected {
+ void onChannelSearchResult(ChannelSearchResult result);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java b/src/main/java/eu/siacs/conversations/utils/AccountUtils.java
index f546a69e4..df8d128d3 100644
--- a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java
+++ b/src/main/java/eu/siacs/conversations/utils/AccountUtils.java
@@ -6,8 +6,10 @@ import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
+import java.util.ArrayList;
import java.util.List;
+import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.XmppConnectionService;
@@ -22,6 +24,20 @@ public class AccountUtils {
}
+ public static List getEnabledAccounts(final XmppConnectionService service) {
+ ArrayList accounts = new ArrayList<>();
+ for (Account account : service.getAccounts()) {
+ if (account.getStatus() != Account.State.DISABLED) {
+ if (Config.DOMAIN_LOCK != null) {
+ accounts.add(account.getJid().getLocal());
+ } else {
+ accounts.add(account.getJid().asBareJid().toString());
+ }
+ }
+ }
+ return accounts;
+ }
+
public static Account getFirstEnabled(XmppConnectionService service) {
final List accounts = service.getAccounts();
for(Account account : accounts) {
diff --git a/src/main/res/menu/channel_discovery_activity.xml b/src/main/res/menu/channel_discovery_activity.xml
new file mode 100644
index 000000000..209bb27e0
--- /dev/null
+++ b/src/main/res/menu/channel_discovery_activity.xml
@@ -0,0 +1,11 @@
+
+
diff --git a/src/main/res/menu/start_conversation_fab_submenu.xml b/src/main/res/menu/start_conversation_fab_submenu.xml
index 76a576d6a..bfaca0727 100644
--- a/src/main/res/menu/start_conversation_fab_submenu.xml
+++ b/src/main/res/menu/start_conversation_fab_submenu.xml
@@ -1,5 +1,9 @@
File too large
Attach
+ Discover channels
+ Search channels
+ Possible privacy violation!
+ search.jabber.network.
Using this feature will transmit your Jabber ID and search terms to that service. See their Privacy Policy for more information.]]>