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