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