1package eu.siacs.conversations.ui;
2
3import android.content.Context;
4import android.content.Intent;
5import android.content.SharedPreferences;
6import android.net.Uri;
7import android.os.Bundle;
8import android.preference.PreferenceManager;
9import android.text.Html;
10import android.text.method.LinkMovementMethod;
11import android.view.KeyEvent;
12import android.view.Menu;
13import android.view.MenuItem;
14import android.view.View;
15import android.view.inputmethod.InputMethodManager;
16import android.widget.EditText;
17import android.widget.TextView;
18import android.widget.Toast;
19import androidx.annotation.NonNull;
20import androidx.core.content.ContextCompat;
21import androidx.databinding.DataBindingUtil;
22import com.google.android.material.color.MaterialColors;
23import com.google.android.material.dialog.MaterialAlertDialogBuilder;
24import com.google.common.base.Strings;
25
26import java.util.Collections;
27import java.util.HashMap;
28import java.util.List;
29import java.util.concurrent.atomic.AtomicReference;
30
31import eu.siacs.conversations.Config;
32import eu.siacs.conversations.R;
33import eu.siacs.conversations.databinding.ActivityChannelDiscoveryBinding;
34import eu.siacs.conversations.entities.Account;
35import eu.siacs.conversations.entities.Conversation;
36import eu.siacs.conversations.entities.Room;
37import eu.siacs.conversations.services.ChannelDiscoveryService;
38import eu.siacs.conversations.services.QuickConversationsService;
39import eu.siacs.conversations.ui.adapter.ChannelSearchResultAdapter;
40import eu.siacs.conversations.ui.util.PendingItem;
41import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
42import eu.siacs.conversations.utils.AccountUtils;
43import eu.siacs.conversations.xmpp.Jid;
44import eu.siacs.conversations.xmpp.XmppConnection;
45import eu.siacs.conversations.xmpp.manager.BookmarkManager;
46import java.util.Collections;
47import java.util.List;
48import java.util.concurrent.atomic.AtomicReference;
49
50public class ChannelDiscoveryActivity extends XmppActivity
51 implements MenuItem.OnActionExpandListener,
52 TextView.OnEditorActionListener,
53 ChannelDiscoveryService.OnChannelSearchResultsFound,
54 ChannelSearchResultAdapter.OnChannelSearchResultSelected {
55
56 private static final String CHANNEL_DISCOVERY_OPT_IN = "channel_discovery_opt_in";
57
58 private final ChannelSearchResultAdapter adapter = new ChannelSearchResultAdapter();
59 private final PendingItem<String> mInitialSearchValue = new PendingItem<>();
60 private ActivityChannelDiscoveryBinding binding;
61 private MenuItem mMenuSearchView;
62 private EditText mSearchEditText;
63
64 private String[] pendingServices = null;
65 private ChannelDiscoveryService.Method method = ChannelDiscoveryService.Method.LOCAL_SERVER;
66 private HashMap<Jid, XmppConnection> mucServices = null;
67
68 private boolean optedIn = false;
69
70 @Override
71 protected void refreshUiReal() {}
72
73 @Override
74 protected void onBackendConnected() {
75 if (pendingServices != null) {
76 mucServices = new HashMap<>();
77 for (int i = 0; i < pendingServices.length; i += 2) {
78 mucServices.put(Jid.of(pendingServices[i]), xmppConnectionService.findAccountByJid(Jid.of(pendingServices[i+1])).getXmppConnection());
79 }
80 }
81
82 this.method = getMethod(this);
83
84 if (optedIn || method == ChannelDiscoveryService.Method.LOCAL_SERVER) {
85 final String query;
86 if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) {
87 query = mSearchEditText.getText().toString();
88 } else {
89 query = mInitialSearchValue.peek();
90 }
91 toggleLoadingScreen();
92 xmppConnectionService.discoverChannels(query, this.method, this.mucServices, this);
93 }
94 }
95
96 @Override
97 protected void onCreate(final Bundle savedInstanceState) {
98 super.onCreate(savedInstanceState);
99 binding = DataBindingUtil.setContentView(this, R.layout.activity_channel_discovery);
100 setSupportActionBar(binding.toolbar);
101 Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
102 configureActionBar(getSupportActionBar(), true);
103 binding.list.setAdapter(this.adapter);
104 this.adapter.setOnChannelSearchResultSelectedListener(this);
105 this.optedIn = getPreferences().getBoolean(CHANNEL_DISCOVERY_OPT_IN, false);
106
107 final String search =
108 savedInstanceState == null ? null : savedInstanceState.getString("search");
109 if (search != null) {
110 mInitialSearchValue.push(search);
111 }
112
113 pendingServices = getIntent().getStringArrayExtra("services");
114 }
115
116 private ChannelDiscoveryService.Method getMethod(final Context c) {
117 if (this.mucServices != null) return ChannelDiscoveryService.Method.LOCAL_SERVER;
118 if (Strings.isNullOrEmpty(Config.CHANNEL_DISCOVERY)) {
119 return ChannelDiscoveryService.Method.LOCAL_SERVER;
120 }
121 if (QuickConversationsService.isQuicksy()) {
122 return ChannelDiscoveryService.Method.JABBER_NETWORK;
123 }
124 final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(c);
125 final String m =
126 p.getString(
127 "channel_discovery_method",
128 c.getString(R.string.default_channel_discovery));
129 try {
130 return ChannelDiscoveryService.Method.valueOf(m);
131 } catch (IllegalArgumentException e) {
132 return ChannelDiscoveryService.Method.JABBER_NETWORK;
133 }
134 }
135
136 @Override
137 public boolean onCreateOptionsMenu(final Menu menu) {
138 getMenuInflater().inflate(R.menu.channel_discovery_activity, menu);
139 AccountUtils.showHideMenuItems(menu);
140 mMenuSearchView = menu.findItem(R.id.action_search);
141 final View mSearchView = mMenuSearchView.getActionView();
142 mSearchEditText = mSearchView.findViewById(R.id.search_field);
143 mSearchEditText.setHint(R.string.search_channels);
144 final String initialSearchValue = mInitialSearchValue.pop();
145 if (initialSearchValue != null) {
146 mMenuSearchView.expandActionView();
147 mSearchEditText.append(initialSearchValue);
148 mSearchEditText.requestFocus();
149 if ((optedIn || method == ChannelDiscoveryService.Method.LOCAL_SERVER)
150 && xmppConnectionService != null) {
151 xmppConnectionService.discoverChannels(initialSearchValue, this.method, this.mucServices, this);
152 }
153 }
154 mSearchEditText.setOnEditorActionListener(this);
155 mMenuSearchView.setOnActionExpandListener(this);
156 return true;
157 }
158
159 @Override
160 public boolean onMenuItemActionExpand(@NonNull MenuItem item) {
161 mSearchEditText.post(
162 () -> {
163 mSearchEditText.requestFocus();
164 final InputMethodManager imm =
165 (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
166 imm.showSoftInput(mSearchEditText, InputMethodManager.SHOW_IMPLICIT);
167 });
168 return true;
169 }
170
171 @Override
172 public boolean onMenuItemActionCollapse(@NonNull MenuItem item) {
173 final InputMethodManager imm =
174 (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
175 imm.hideSoftInputFromWindow(
176 mSearchEditText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY);
177 mSearchEditText.setText("");
178 toggleLoadingScreen();
179 if (optedIn || method == ChannelDiscoveryService.Method.LOCAL_SERVER) {
180 xmppConnectionService.discoverChannels(null, this.method, this.mucServices, this);
181 }
182 return true;
183 }
184
185 private void toggleLoadingScreen() {
186 adapter.submitList(Collections.emptyList());
187 binding.progressBar.setVisibility(View.VISIBLE);
188 binding.list.setBackgroundColor(
189 MaterialColors.getColor(
190 binding.list, com.google.android.material.R.attr.colorSurface));
191 }
192
193 @Override
194 public void onStart() {
195 super.onStart();
196 this.method = getMethod(this);
197 if (pendingServices == null && !optedIn && method == ChannelDiscoveryService.Method.JABBER_NETWORK) {
198 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
199 builder.setTitle(R.string.channel_discovery_opt_in_title);
200 builder.setMessage(Html.fromHtml(getString(R.string.channel_discover_opt_in_message)));
201 builder.setNegativeButton(R.string.cancel, (dialog, which) -> finish());
202 builder.setPositiveButton(R.string.confirm, (dialog, which) -> optIn());
203 builder.setOnCancelListener(dialog -> finish());
204 final androidx.appcompat.app.AlertDialog dialog = builder.create();
205 dialog.setOnShowListener(
206 d -> {
207 final TextView textView = dialog.findViewById(android.R.id.message);
208 if (textView == null) {
209 return;
210 }
211 textView.setMovementMethod(LinkMovementMethod.getInstance());
212 });
213 dialog.setCanceledOnTouchOutside(false);
214 dialog.show();
215 holdLoading();
216 }
217 }
218
219 private void holdLoading() {
220 adapter.submitList(Collections.emptyList());
221 binding.progressBar.setVisibility(View.GONE);
222 binding.list.setBackgroundColor(
223 MaterialColors.getColor(
224 binding.list, com.google.android.material.R.attr.colorSurface));
225 }
226
227 @Override
228 public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
229 if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) {
230 savedInstanceState.putString(
231 "search",
232 mSearchEditText != null ? mSearchEditText.getText().toString() : null);
233 }
234 super.onSaveInstanceState(savedInstanceState);
235 }
236
237 private void optIn() {
238 SharedPreferences preferences = getPreferences();
239 preferences.edit().putBoolean(CHANNEL_DISCOVERY_OPT_IN, true).apply();
240 optedIn = true;
241 toggleLoadingScreen();
242 xmppConnectionService.discoverChannels(null, this.method, this.mucServices, this);
243 }
244
245 @Override
246 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
247 if (optedIn || method == ChannelDiscoveryService.Method.LOCAL_SERVER) {
248 toggleLoadingScreen();
249 SoftKeyboardUtils.hideSoftKeyboard(this);
250 xmppConnectionService.discoverChannels(v.getText().toString(), this.method, this.mucServices, this);
251 }
252 return true;
253 }
254
255 @Override
256 public void onChannelSearchResultsFound(final List<Room> results) {
257 runOnUiThread(
258 () -> {
259 adapter.submitList(results);
260 binding.progressBar.setVisibility(View.GONE);
261 if (results.isEmpty()) {
262 binding.list.setBackground(
263 ContextCompat.getDrawable(this, R.drawable.background_no_results));
264 } else {
265 binding.list.setBackgroundColor(
266 MaterialColors.getColor(
267 binding.list,
268 com.google.android.material.R.attr.colorSurface));
269 }
270 });
271 }
272
273 @Override
274 public void onChannelSearchResult(final Room result) {
275 final List<String> accounts = AccountUtils.getEnabledAccounts(xmppConnectionService);
276 if (accounts.size() == 1) {
277 joinChannelSearchResult(accounts.get(0), result);
278 } else if (accounts.isEmpty()) {
279 Toast.makeText(this, R.string.please_enable_an_account, Toast.LENGTH_LONG).show();
280 } else {
281 final AtomicReference<String> account = new AtomicReference<>(accounts.get(0));
282 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
283 builder.setTitle(R.string.choose_account);
284 builder.setSingleChoiceItems(
285 accounts.toArray(new CharSequence[0]),
286 0,
287 (dialog, which) -> account.set(accounts.get(which)));
288 builder.setPositiveButton(
289 R.string.join,
290 (dialog, which) -> joinChannelSearchResult(account.get(), result));
291 builder.setNegativeButton(R.string.cancel, null);
292 builder.create().show();
293 }
294 }
295
296 @Override
297 public boolean onContextItemSelected(@NonNull MenuItem item) {
298 final Room room = adapter.getCurrent();
299 if (room == null) {
300 return false;
301 }
302 final int itemId = item.getItemId();
303 if (itemId == R.id.share_with) {
304 StartConversationActivity.shareAsChannel(this, room.address);
305 return true;
306 } else if (itemId == R.id.open_join_dialog) {
307 final Intent intent = new Intent(this, StartConversationActivity.class);
308 intent.setAction(Intent.ACTION_VIEW);
309 intent.putExtra("force_dialog", true);
310 intent.setData(Uri.parse(String.format("xmpp:%s?join", room.address)));
311 startActivity(intent);
312 return true;
313 } else {
314 return false;
315 }
316 }
317
318 public void joinChannelSearchResult(final String selectedAccount, final Room result) {
319 final Jid jid = Jid.of(selectedAccount);
320 final Account account = xmppConnectionService.findAccountByJid(jid);
321 final Conversation conversation =
322 xmppConnectionService.findOrCreateConversation(
323 account, result.getRoom(), true, true, true);
324 account.getXmppConnection()
325 .getManager(BookmarkManager.class)
326 .ensureBookmarkIsAutoJoin(conversation);
327 switchToConversation(conversation);
328 }
329}