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