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