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