1package eu.siacs.conversations.ui;
2
3import android.annotation.SuppressLint;
4import android.app.ActionBar;
5import android.app.AlertDialog;
6import android.app.FragmentTransaction;
7import android.app.PendingIntent;
8import android.content.ActivityNotFoundException;
9import android.content.ClipData;
10import android.content.DialogInterface;
11import android.content.DialogInterface.OnClickListener;
12import android.content.Intent;
13import android.content.IntentSender.SendIntentException;
14import android.content.pm.PackageManager;
15import android.net.Uri;
16import android.os.Build;
17import android.os.Bundle;
18import android.os.Handler;
19import android.provider.MediaStore;
20import android.provider.Settings;
21import android.support.v4.widget.SlidingPaneLayout;
22import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener;
23import android.util.Log;
24import android.util.Pair;
25import android.view.Gravity;
26import android.view.KeyEvent;
27import android.view.Menu;
28import android.view.MenuItem;
29import android.view.Surface;
30import android.view.View;
31import android.widget.AdapterView;
32import android.widget.AdapterView.OnItemClickListener;
33import android.widget.ArrayAdapter;
34import android.widget.CheckBox;
35import android.widget.PopupMenu;
36import android.widget.PopupMenu.OnMenuItemClickListener;
37import android.widget.Toast;
38
39import net.java.otr4j.session.SessionStatus;
40
41import org.openintents.openpgp.util.OpenPgpApi;
42
43import java.util.ArrayList;
44import java.util.Iterator;
45import java.util.List;
46import java.util.concurrent.atomic.AtomicBoolean;
47
48import de.timroes.android.listview.EnhancedListView;
49import eu.siacs.conversations.Config;
50import eu.siacs.conversations.R;
51import eu.siacs.conversations.crypto.axolotl.AxolotlService;
52import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
53import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
54import eu.siacs.conversations.entities.Account;
55import eu.siacs.conversations.entities.Blockable;
56import eu.siacs.conversations.entities.Contact;
57import eu.siacs.conversations.entities.Conversation;
58import eu.siacs.conversations.entities.Message;
59import eu.siacs.conversations.entities.Transferable;
60import eu.siacs.conversations.persistance.FileBackend;
61import eu.siacs.conversations.services.XmppConnectionService;
62import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
63import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
64import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
65import eu.siacs.conversations.ui.adapter.ConversationAdapter;
66import eu.siacs.conversations.utils.ExceptionHelper;
67import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
68import eu.siacs.conversations.xmpp.XmppConnection;
69import eu.siacs.conversations.xmpp.jid.InvalidJidException;
70import eu.siacs.conversations.xmpp.jid.Jid;
71
72public class ConversationActivity extends XmppActivity
73 implements OnAccountUpdate, OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast {
74
75 public static final String ACTION_VIEW_CONVERSATION = "eu.siacs.conversations.action.VIEW";
76 public static final String CONVERSATION = "conversationUuid";
77 public static final String EXTRA_DOWNLOAD_UUID = "eu.siacs.conversations.download_uuid";
78 public static final String TEXT = "text";
79 public static final String NICK = "nick";
80 public static final String PRIVATE_MESSAGE = "pm";
81
82 public static final int REQUEST_SEND_MESSAGE = 0x0201;
83 public static final int REQUEST_DECRYPT_PGP = 0x0202;
84 public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207;
85 public static final int REQUEST_TRUST_KEYS_TEXT = 0x0208;
86 public static final int REQUEST_TRUST_KEYS_MENU = 0x0209;
87 public static final int REQUEST_START_DOWNLOAD = 0x0210;
88 public static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301;
89 public static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302;
90 public static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303;
91 public static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304;
92 public static final int ATTACHMENT_CHOICE_LOCATION = 0x0305;
93 public static final int ATTACHMENT_CHOICE_INVALID = 0x0306;
94 private static final String STATE_OPEN_CONVERSATION = "state_open_conversation";
95 private static final String STATE_PANEL_OPEN = "state_panel_open";
96 private static final String STATE_PENDING_URI = "state_pending_uri";
97 private static final String STATE_FIRST_VISIBLE = "first_visible";
98 private static final String STATE_OFFSET_FROM_TOP = "offset_from_top";
99
100 private String mOpenConversation = null;
101 private boolean mPanelOpen = true;
102 private AtomicBoolean mShouldPanelBeOpen = new AtomicBoolean(false);
103 private Pair<Integer,Integer> mScrollPosition = null;
104 final private List<Uri> mPendingImageUris = new ArrayList<>();
105 final private List<Uri> mPendingFileUris = new ArrayList<>();
106 private Uri mPendingGeoUri = null;
107 private boolean forbidProcessingPendings = false;
108 private Message mPendingDownloadableMessage = null;
109
110 private boolean conversationWasSelectedByKeyboard = false;
111
112 private View mContentView;
113
114 private List<Conversation> conversationList = new ArrayList<>();
115 private Conversation swipedConversation = null;
116 private Conversation mSelectedConversation = null;
117 private EnhancedListView listView;
118 private ConversationFragment mConversationFragment;
119
120 private ArrayAdapter<Conversation> listAdapter;
121
122 private boolean mActivityPaused = false;
123 private AtomicBoolean mRedirected = new AtomicBoolean(false);
124 private Pair<Integer, Intent> mPostponedActivityResult;
125 private boolean mUnprocessedNewIntent = false;
126
127 public Conversation getSelectedConversation() {
128 return this.mSelectedConversation;
129 }
130
131 public void setSelectedConversation(Conversation conversation) {
132 this.mSelectedConversation = conversation;
133 }
134
135 public void showConversationsOverview() {
136 if (mContentView instanceof SlidingPaneLayout) {
137 SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
138 mShouldPanelBeOpen.set(true);
139 mSlidingPaneLayout.openPane();
140 }
141 }
142
143 @Override
144 protected String getShareableUri() {
145 Conversation conversation = getSelectedConversation();
146 if (conversation != null) {
147 return conversation.getAccount().getShareableUri();
148 } else {
149 return "";
150 }
151 }
152
153 public void hideConversationsOverview() {
154 if (mContentView instanceof SlidingPaneLayout) {
155 SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
156 mShouldPanelBeOpen.set(false);
157 mSlidingPaneLayout.closePane();
158 }
159 }
160
161 public boolean isConversationsOverviewHideable() {
162 return mContentView instanceof SlidingPaneLayout;
163 }
164
165 public boolean isConversationsOverviewVisable() {
166 if (mContentView instanceof SlidingPaneLayout) {
167 return mShouldPanelBeOpen.get();
168 } else {
169 return true;
170 }
171 }
172
173 @Override
174 protected void onCreate(final Bundle savedInstanceState) {
175 super.onCreate(savedInstanceState);
176 if (savedInstanceState != null) {
177 mOpenConversation = savedInstanceState.getString(STATE_OPEN_CONVERSATION, null);
178 mPanelOpen = savedInstanceState.getBoolean(STATE_PANEL_OPEN, true);
179 int pos = savedInstanceState.getInt(STATE_FIRST_VISIBLE, -1);
180 int offset = savedInstanceState.getInt(STATE_OFFSET_FROM_TOP, 1);
181 if (pos >= 0 && offset <= 0) {
182 Log.d(Config.LOGTAG,"retrieved scroll position from instanceState "+pos+":"+offset);
183 mScrollPosition = new Pair<>(pos,offset);
184 } else {
185 mScrollPosition = null;
186 }
187 String pending = savedInstanceState.getString(STATE_PENDING_URI, null);
188 if (pending != null) {
189 Log.d(Config.LOGTAG,"ConversationsActivity.onCreate() - restoring pending image uri");
190 mPendingImageUris.clear();
191 mPendingImageUris.add(Uri.parse(pending));
192 }
193 }
194
195 setContentView(R.layout.fragment_conversations_overview);
196
197 this.mConversationFragment = new ConversationFragment();
198 FragmentTransaction transaction = getFragmentManager().beginTransaction();
199 transaction.replace(R.id.selected_conversation, this.mConversationFragment, "conversation");
200 transaction.commit();
201
202 listView = (EnhancedListView) findViewById(R.id.list);
203 this.listAdapter = new ConversationAdapter(this, conversationList);
204 listView.setAdapter(this.listAdapter);
205
206 if (getActionBar() != null) {
207 getActionBar().setDisplayHomeAsUpEnabled(false);
208 getActionBar().setHomeButtonEnabled(false);
209 }
210
211 listView.setOnItemClickListener(new OnItemClickListener() {
212
213 @Override
214 public void onItemClick(AdapterView<?> arg0, View clickedView,
215 int position, long arg3) {
216 if (getSelectedConversation() != conversationList.get(position)) {
217 setSelectedConversation(conversationList.get(position));
218 ConversationActivity.this.mConversationFragment.reInit(getSelectedConversation());
219 conversationWasSelectedByKeyboard = false;
220 }
221 hideConversationsOverview();
222 openConversation();
223 }
224 });
225
226 listView.setDismissCallback(new EnhancedListView.OnDismissCallback() {
227
228 @Override
229 public EnhancedListView.Undoable onDismiss(final EnhancedListView enhancedListView, final int position) {
230
231 final int index = listView.getFirstVisiblePosition();
232 View v = listView.getChildAt(0);
233 final int top = (v == null) ? 0 : (v.getTop() - listView.getPaddingTop());
234
235 try {
236 swipedConversation = listAdapter.getItem(position);
237 } catch (IndexOutOfBoundsException e) {
238 return null;
239 }
240 listAdapter.remove(swipedConversation);
241 xmppConnectionService.markRead(swipedConversation);
242
243 final boolean formerlySelected = (getSelectedConversation() == swipedConversation);
244 if (position == 0 && listAdapter.getCount() == 0) {
245 endConversation(swipedConversation, false, true);
246 return null;
247 } else if (formerlySelected) {
248 setSelectedConversation(listAdapter.getItem(0));
249 ConversationActivity.this.mConversationFragment
250 .reInit(getSelectedConversation());
251 }
252
253 return new EnhancedListView.Undoable() {
254
255 @Override
256 public void undo() {
257 listAdapter.insert(swipedConversation, position);
258 if (formerlySelected) {
259 setSelectedConversation(swipedConversation);
260 ConversationActivity.this.mConversationFragment
261 .reInit(getSelectedConversation());
262 }
263 swipedConversation = null;
264 listView.setSelectionFromTop(index + (listView.getChildCount() < position ? 1 : 0), top);
265 }
266
267 @Override
268 public void discard() {
269 if (!swipedConversation.isRead()
270 && swipedConversation.getMode() == Conversation.MODE_SINGLE) {
271 swipedConversation = null;
272 return;
273 }
274 endConversation(swipedConversation, false, false);
275 swipedConversation = null;
276 }
277
278 @Override
279 public String getTitle() {
280 if (swipedConversation.getMode() == Conversation.MODE_MULTI) {
281 return getResources().getString(R.string.title_undo_swipe_out_muc);
282 } else {
283 return getResources().getString(R.string.title_undo_swipe_out_conversation);
284 }
285 }
286 };
287 }
288 });
289 listView.enableSwipeToDismiss();
290 listView.setSwipingLayout(R.id.swipeable_item);
291 listView.setUndoStyle(EnhancedListView.UndoStyle.SINGLE_POPUP);
292 listView.setUndoHideDelay(5000);
293 listView.setRequireTouchBeforeDismiss(false);
294
295 mContentView = findViewById(R.id.content_view_spl);
296 if (mContentView == null) {
297 mContentView = findViewById(R.id.content_view_ll);
298 }
299 if (mContentView instanceof SlidingPaneLayout) {
300 SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
301 mSlidingPaneLayout.setShadowResource(R.drawable.es_slidingpane_shadow);
302 mSlidingPaneLayout.setSliderFadeColor(0);
303 mSlidingPaneLayout.setPanelSlideListener(new PanelSlideListener() {
304
305 @Override
306 public void onPanelOpened(View arg0) {
307 mShouldPanelBeOpen.set(true);
308 updateActionBarTitle();
309 invalidateOptionsMenu();
310 hideKeyboard();
311 if (xmppConnectionServiceBound) {
312 xmppConnectionService.getNotificationService().setOpenConversation(null);
313 }
314 closeContextMenu();
315 }
316
317 @Override
318 public void onPanelClosed(View arg0) {
319 mShouldPanelBeOpen.set(false);
320 listView.discardUndo();
321 openConversation();
322 }
323
324 @Override
325 public void onPanelSlide(View arg0, float arg1) {
326 // TODO Auto-generated method stub
327
328 }
329 });
330 }
331 }
332
333 @Override
334 public void switchToConversation(Conversation conversation) {
335 setSelectedConversation(conversation);
336 runOnUiThread(new Runnable() {
337 @Override
338 public void run() {
339 ConversationActivity.this.mConversationFragment.reInit(getSelectedConversation());
340 openConversation();
341 }
342 });
343 }
344
345 private void updateActionBarTitle() {
346 updateActionBarTitle(isConversationsOverviewHideable() && !isConversationsOverviewVisable());
347 }
348
349 private void updateActionBarTitle(boolean titleShouldBeName) {
350 final ActionBar ab = getActionBar();
351 final Conversation conversation = getSelectedConversation();
352 if (ab != null) {
353 if (titleShouldBeName && conversation != null) {
354 ab.setDisplayHomeAsUpEnabled(true);
355 ab.setHomeButtonEnabled(true);
356 if (conversation.getMode() == Conversation.MODE_SINGLE || useSubjectToIdentifyConference()) {
357 ab.setTitle(conversation.getName());
358 } else {
359 ab.setTitle(conversation.getJid().toBareJid().toString());
360 }
361 } else {
362 ab.setDisplayHomeAsUpEnabled(false);
363 ab.setHomeButtonEnabled(false);
364 ab.setTitle(R.string.app_name);
365 }
366 }
367 }
368
369 private void openConversation() {
370 this.updateActionBarTitle();
371 this.invalidateOptionsMenu();
372 if (xmppConnectionServiceBound) {
373 final Conversation conversation = getSelectedConversation();
374 xmppConnectionService.getNotificationService().setOpenConversation(conversation);
375 sendReadMarkerIfNecessary(conversation);
376 }
377 listAdapter.notifyDataSetChanged();
378 }
379
380 public void sendReadMarkerIfNecessary(final Conversation conversation) {
381 if (!mActivityPaused && !mUnprocessedNewIntent && conversation != null) {
382 xmppConnectionService.sendReadMarker(conversation);
383 }
384 }
385
386 @Override
387 public boolean onCreateOptionsMenu(Menu menu) {
388 getMenuInflater().inflate(R.menu.conversations, menu);
389 final MenuItem menuSecure = menu.findItem(R.id.action_security);
390 final MenuItem menuArchive = menu.findItem(R.id.action_archive);
391 final MenuItem menuMucDetails = menu.findItem(R.id.action_muc_details);
392 final MenuItem menuContactDetails = menu.findItem(R.id.action_contact_details);
393 final MenuItem menuAttach = menu.findItem(R.id.action_attach_file);
394 final MenuItem menuClearHistory = menu.findItem(R.id.action_clear_history);
395 final MenuItem menuAdd = menu.findItem(R.id.action_add);
396 final MenuItem menuInviteContact = menu.findItem(R.id.action_invite);
397 final MenuItem menuMute = menu.findItem(R.id.action_mute);
398 final MenuItem menuUnmute = menu.findItem(R.id.action_unmute);
399
400 if (isConversationsOverviewVisable() && isConversationsOverviewHideable()) {
401 menuArchive.setVisible(false);
402 menuMucDetails.setVisible(false);
403 menuContactDetails.setVisible(false);
404 menuSecure.setVisible(false);
405 menuInviteContact.setVisible(false);
406 menuAttach.setVisible(false);
407 menuClearHistory.setVisible(false);
408 menuMute.setVisible(false);
409 menuUnmute.setVisible(false);
410 } else {
411 menuAdd.setVisible(!isConversationsOverviewHideable());
412 if (this.getSelectedConversation() != null) {
413 if (this.getSelectedConversation().getNextEncryption() != Message.ENCRYPTION_NONE) {
414 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
415 menuSecure.setIcon(R.drawable.ic_lock_white_24dp);
416 } else {
417 menuSecure.setIcon(R.drawable.ic_action_secure);
418 }
419 }
420 if (this.getSelectedConversation().getMode() == Conversation.MODE_MULTI) {
421 menuContactDetails.setVisible(false);
422 menuAttach.setVisible(getSelectedConversation().getAccount().httpUploadAvailable() && getSelectedConversation().getMucOptions().participating());
423 menuInviteContact.setVisible(getSelectedConversation().getMucOptions().canInvite());
424 menuSecure.setVisible((Config.supportOpenPgp() || Config.supportOmemo()) && Config.multipleEncryptionChoices()); //only if pgp is supported we have a choice
425 } else {
426 menuContactDetails.setVisible(!this.getSelectedConversation().withSelf());
427 menuMucDetails.setVisible(false);
428 menuSecure.setVisible(Config.multipleEncryptionChoices());
429 menuInviteContact.setVisible(xmppConnectionService != null && xmppConnectionService.findConferenceServer(getSelectedConversation().getAccount()) != null);
430 }
431 if (this.getSelectedConversation().isMuted()) {
432 menuMute.setVisible(false);
433 } else {
434 menuUnmute.setVisible(false);
435 }
436 }
437 }
438 if (Config.supportOmemo()) {
439 new Handler().post(new Runnable() {
440 @Override
441 public void run() {
442 View view = findViewById(R.id.action_security);
443 if (view != null) {
444 view.setOnLongClickListener(new View.OnLongClickListener() {
445 @Override
446 public boolean onLongClick(View v) {
447 return quickOmemoDebugger(getSelectedConversation());
448 }
449 });
450 }
451 }
452 });
453 }
454 return super.onCreateOptionsMenu(menu);
455 }
456
457 private boolean quickOmemoDebugger(Conversation c) {
458 if (c != null) {
459 boolean single = c.getMode() == Conversation.MODE_SINGLE;
460 AxolotlService axolotlService = c.getAccount().getAxolotlService();
461 Pair<AxolotlService.AxolotlCapability,Jid> capabilityJidPair = axolotlService.isConversationAxolotlCapableDetailed(c);
462 switch (capabilityJidPair.first) {
463 case MISSING_PRESENCE:
464 Toast.makeText(ConversationActivity.this,single ? getString(R.string.missing_presence_subscription) : getString(R.string.missing_presence_subscription_with_x,capabilityJidPair.second.toBareJid().toString()),Toast.LENGTH_SHORT).show();
465 return true;
466 case MISSING_KEYS:
467 Toast.makeText(ConversationActivity.this,single ? getString(R.string.missing_omemo_keys) : getString(R.string.missing_keys_from_x,capabilityJidPair.second.toBareJid().toString()),Toast.LENGTH_SHORT).show();
468 return true;
469 case WRONG_CONFIGURATION:
470 Toast.makeText(ConversationActivity.this,R.string.wrong_conference_configuration, Toast.LENGTH_SHORT).show();
471 return true;
472 case NO_MEMBERS:
473 Toast.makeText(ConversationActivity.this,R.string.this_conference_has_no_members, Toast.LENGTH_SHORT).show();
474 return true;
475 }
476 }
477 return false;
478 }
479
480 protected void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) {
481 final Conversation conversation = getSelectedConversation();
482 final Account account = conversation.getAccount();
483 final OnPresenceSelected callback = new OnPresenceSelected() {
484
485 @Override
486 public void onPresenceSelected() {
487 Intent intent = new Intent();
488 boolean chooser = false;
489 String fallbackPackageId = null;
490 switch (attachmentChoice) {
491 case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
492 intent.setAction(Intent.ACTION_GET_CONTENT);
493 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
494 intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
495 }
496 intent.setType("image/*");
497 chooser = true;
498 break;
499 case ATTACHMENT_CHOICE_TAKE_PHOTO:
500 Uri uri = xmppConnectionService.getFileBackend().getTakePhotoUri();
501 intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
502 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
503 intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
504 intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
505 mPendingImageUris.clear();
506 mPendingImageUris.add(uri);
507 break;
508 case ATTACHMENT_CHOICE_CHOOSE_FILE:
509 chooser = true;
510 intent.setType("*/*");
511 intent.addCategory(Intent.CATEGORY_OPENABLE);
512 intent.setAction(Intent.ACTION_GET_CONTENT);
513 break;
514 case ATTACHMENT_CHOICE_RECORD_VOICE:
515 intent.setAction(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
516 fallbackPackageId = "eu.siacs.conversations.voicerecorder";
517 break;
518 case ATTACHMENT_CHOICE_LOCATION:
519 intent.setAction("eu.siacs.conversations.location.request");
520 fallbackPackageId = "eu.siacs.conversations.sharelocation";
521 break;
522 }
523 if (intent.resolveActivity(getPackageManager()) != null) {
524 if (chooser) {
525 startActivityForResult(
526 Intent.createChooser(intent, getString(R.string.perform_action_with)),
527 attachmentChoice);
528 } else {
529 startActivityForResult(intent, attachmentChoice);
530 }
531 } else if (fallbackPackageId != null) {
532 startActivity(getInstallApkIntent(fallbackPackageId));
533 }
534 }
535 };
536 if ((account.httpUploadAvailable() || attachmentChoice == ATTACHMENT_CHOICE_LOCATION) && encryption != Message.ENCRYPTION_OTR) {
537 conversation.setNextCounterpart(null);
538 callback.onPresenceSelected();
539 } else {
540 selectPresence(conversation, callback);
541 }
542 }
543
544 private Intent getInstallApkIntent(final String packageId) {
545 Intent intent = new Intent(Intent.ACTION_VIEW);
546 intent.setData(Uri.parse("market://details?id=" + packageId));
547 if (intent.resolveActivity(getPackageManager()) != null) {
548 return intent;
549 } else {
550 intent.setData(Uri.parse("http://play.google.com/store/apps/details?id=" + packageId));
551 return intent;
552 }
553 }
554
555 public void attachFile(final int attachmentChoice) {
556 if (attachmentChoice != ATTACHMENT_CHOICE_LOCATION) {
557 if (!Config.ONLY_INTERNAL_STORAGE && !hasStoragePermission(attachmentChoice)) {
558 return;
559 }
560 }
561 switch (attachmentChoice) {
562 case ATTACHMENT_CHOICE_LOCATION:
563 getPreferences().edit().putString("recently_used_quick_action", "location").apply();
564 break;
565 case ATTACHMENT_CHOICE_RECORD_VOICE:
566 getPreferences().edit().putString("recently_used_quick_action", "voice").apply();
567 break;
568 case ATTACHMENT_CHOICE_TAKE_PHOTO:
569 getPreferences().edit().putString("recently_used_quick_action", "photo").apply();
570 break;
571 case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
572 getPreferences().edit().putString("recently_used_quick_action", "picture").apply();
573 break;
574 }
575 final Conversation conversation = getSelectedConversation();
576 final int encryption = conversation.getNextEncryption();
577 final int mode = conversation.getMode();
578 if (encryption == Message.ENCRYPTION_PGP) {
579 if (hasPgp()) {
580 if (mode == Conversation.MODE_SINGLE && conversation.getContact().getPgpKeyId() != 0) {
581 xmppConnectionService.getPgpEngine().hasKey(
582 conversation.getContact(),
583 new UiCallback<Contact>() {
584
585 @Override
586 public void userInputRequried(PendingIntent pi, Contact contact) {
587 ConversationActivity.this.runIntent(pi, attachmentChoice);
588 }
589
590 @Override
591 public void success(Contact contact) {
592 selectPresenceToAttachFile(attachmentChoice, encryption);
593 }
594
595 @Override
596 public void error(int error, Contact contact) {
597 replaceToast(getString(error));
598 }
599 });
600 } else if (mode == Conversation.MODE_MULTI && conversation.getMucOptions().pgpKeysInUse()) {
601 if (!conversation.getMucOptions().everybodyHasKeys()) {
602 Toast warning = Toast
603 .makeText(this,
604 R.string.missing_public_keys,
605 Toast.LENGTH_LONG);
606 warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
607 warning.show();
608 }
609 selectPresenceToAttachFile(attachmentChoice, encryption);
610 } else {
611 final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
612 .findFragmentByTag("conversation");
613 if (fragment != null) {
614 fragment.showNoPGPKeyDialog(false,
615 new OnClickListener() {
616
617 @Override
618 public void onClick(DialogInterface dialog,
619 int which) {
620 conversation.setNextEncryption(Message.ENCRYPTION_NONE);
621 xmppConnectionService.updateConversation(conversation);
622 selectPresenceToAttachFile(attachmentChoice, Message.ENCRYPTION_NONE);
623 }
624 });
625 }
626 }
627 } else {
628 showInstallPgpDialog();
629 }
630 } else {
631 if (encryption != Message.ENCRYPTION_AXOLOTL || !trustKeysIfNeeded(REQUEST_TRUST_KEYS_MENU, attachmentChoice)) {
632 selectPresenceToAttachFile(attachmentChoice, encryption);
633 }
634 }
635 }
636
637 @Override
638 public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
639 if (grantResults.length > 0)
640 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
641 if (requestCode == REQUEST_START_DOWNLOAD) {
642 if (this.mPendingDownloadableMessage != null) {
643 startDownloadable(this.mPendingDownloadableMessage);
644 }
645 } else {
646 attachFile(requestCode);
647 }
648 } else {
649 Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
650 }
651 }
652
653 public void startDownloadable(Message message) {
654 if (!Config.ONLY_INTERNAL_STORAGE && !hasStoragePermission(ConversationActivity.REQUEST_START_DOWNLOAD)) {
655 this.mPendingDownloadableMessage = message;
656 return;
657 }
658 Transferable transferable = message.getTransferable();
659 if (transferable != null) {
660 if (!transferable.start()) {
661 Toast.makeText(this, R.string.not_connected_try_again, Toast.LENGTH_SHORT).show();
662 }
663 } else if (message.treatAsDownloadable() != Message.Decision.NEVER) {
664 xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message, true);
665 }
666 }
667
668 @Override
669 public boolean onOptionsItemSelected(final MenuItem item) {
670 if (item.getItemId() == android.R.id.home) {
671 showConversationsOverview();
672 return true;
673 } else if (item.getItemId() == R.id.action_add) {
674 startActivity(new Intent(this, StartConversationActivity.class));
675 return true;
676 } else if (getSelectedConversation() != null) {
677 switch (item.getItemId()) {
678 case R.id.action_attach_file:
679 attachFileDialog();
680 break;
681 case R.id.action_archive:
682 this.endConversation(getSelectedConversation());
683 break;
684 case R.id.action_contact_details:
685 switchToContactDetails(getSelectedConversation().getContact());
686 break;
687 case R.id.action_muc_details:
688 Intent intent = new Intent(this,
689 ConferenceDetailsActivity.class);
690 intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC);
691 intent.putExtra("uuid", getSelectedConversation().getUuid());
692 startActivity(intent);
693 break;
694 case R.id.action_invite:
695 inviteToConversation(getSelectedConversation());
696 break;
697 case R.id.action_security:
698 selectEncryptionDialog(getSelectedConversation());
699 break;
700 case R.id.action_clear_history:
701 clearHistoryDialog(getSelectedConversation());
702 break;
703 case R.id.action_mute:
704 muteConversationDialog(getSelectedConversation());
705 break;
706 case R.id.action_unmute:
707 unmuteConversation(getSelectedConversation());
708 break;
709 case R.id.action_block:
710 BlockContactDialog.show(this, xmppConnectionService, getSelectedConversation());
711 break;
712 case R.id.action_unblock:
713 BlockContactDialog.show(this, xmppConnectionService, getSelectedConversation());
714 break;
715 default:
716 break;
717 }
718 return super.onOptionsItemSelected(item);
719 } else {
720 return super.onOptionsItemSelected(item);
721 }
722 }
723
724 public void endConversation(Conversation conversation) {
725 endConversation(conversation, true, true);
726 }
727
728 public void endConversation(Conversation conversation, boolean showOverview, boolean reinit) {
729 if (showOverview) {
730 showConversationsOverview();
731 }
732 xmppConnectionService.archiveConversation(conversation);
733 if (reinit) {
734 if (conversationList.size() > 0) {
735 setSelectedConversation(conversationList.get(0));
736 this.mConversationFragment.reInit(getSelectedConversation());
737 } else {
738 setSelectedConversation(null);
739 if (mRedirected.compareAndSet(false, true)) {
740 Intent intent = new Intent(this, StartConversationActivity.class);
741 intent.putExtra("init", true);
742 startActivity(intent);
743 finish();
744 }
745 }
746 }
747 }
748
749 @SuppressLint("InflateParams")
750 protected void clearHistoryDialog(final Conversation conversation) {
751 AlertDialog.Builder builder = new AlertDialog.Builder(this);
752 builder.setTitle(getString(R.string.clear_conversation_history));
753 View dialogView = getLayoutInflater().inflate(
754 R.layout.dialog_clear_history, null);
755 final CheckBox endConversationCheckBox = (CheckBox) dialogView
756 .findViewById(R.id.end_conversation_checkbox);
757 builder.setView(dialogView);
758 builder.setNegativeButton(getString(R.string.cancel), null);
759 builder.setPositiveButton(getString(R.string.delete_messages),
760 new OnClickListener() {
761
762 @Override
763 public void onClick(DialogInterface dialog, int which) {
764 ConversationActivity.this.xmppConnectionService.clearConversationHistory(conversation);
765 if (endConversationCheckBox.isChecked()) {
766 endConversation(conversation);
767 } else {
768 updateConversationList();
769 ConversationActivity.this.mConversationFragment.updateMessages();
770 }
771 }
772 });
773 builder.create().show();
774 }
775
776 protected void attachFileDialog() {
777 View menuAttachFile = findViewById(R.id.action_attach_file);
778 if (menuAttachFile == null) {
779 return;
780 }
781 PopupMenu attachFilePopup = new PopupMenu(this, menuAttachFile);
782 attachFilePopup.inflate(R.menu.attachment_choices);
783 if (new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION).resolveActivity(getPackageManager()) == null) {
784 attachFilePopup.getMenu().findItem(R.id.attach_record_voice).setVisible(false);
785 }
786 if (new Intent("eu.siacs.conversations.location.request").resolveActivity(getPackageManager()) == null) {
787 attachFilePopup.getMenu().findItem(R.id.attach_location).setVisible(false);
788 }
789 attachFilePopup.setOnMenuItemClickListener(new OnMenuItemClickListener() {
790
791 @Override
792 public boolean onMenuItemClick(MenuItem item) {
793 switch (item.getItemId()) {
794 case R.id.attach_choose_picture:
795 attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE);
796 break;
797 case R.id.attach_take_picture:
798 attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO);
799 break;
800 case R.id.attach_choose_file:
801 attachFile(ATTACHMENT_CHOICE_CHOOSE_FILE);
802 break;
803 case R.id.attach_record_voice:
804 attachFile(ATTACHMENT_CHOICE_RECORD_VOICE);
805 break;
806 case R.id.attach_location:
807 attachFile(ATTACHMENT_CHOICE_LOCATION);
808 break;
809 }
810 return false;
811 }
812 });
813 attachFilePopup.show();
814 }
815
816 public void verifyOtrSessionDialog(final Conversation conversation, View view) {
817 if (!conversation.hasValidOtrSession() || conversation.getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
818 Toast.makeText(this, R.string.otr_session_not_started, Toast.LENGTH_LONG).show();
819 return;
820 }
821 if (view == null) {
822 return;
823 }
824 PopupMenu popup = new PopupMenu(this, view);
825 popup.inflate(R.menu.verification_choices);
826 popup.setOnMenuItemClickListener(new OnMenuItemClickListener() {
827 @Override
828 public boolean onMenuItemClick(MenuItem menuItem) {
829 Intent intent = new Intent(ConversationActivity.this, VerifyOTRActivity.class);
830 intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT);
831 intent.putExtra("contact", conversation.getContact().getJid().toBareJid().toString());
832 intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().toBareJid().toString());
833 switch (menuItem.getItemId()) {
834 case R.id.scan_fingerprint:
835 intent.putExtra("mode", VerifyOTRActivity.MODE_SCAN_FINGERPRINT);
836 break;
837 case R.id.ask_question:
838 intent.putExtra("mode", VerifyOTRActivity.MODE_ASK_QUESTION);
839 break;
840 case R.id.manual_verification:
841 intent.putExtra("mode", VerifyOTRActivity.MODE_MANUAL_VERIFICATION);
842 break;
843 }
844 startActivity(intent);
845 return true;
846 }
847 });
848 popup.show();
849 }
850
851 protected void selectEncryptionDialog(final Conversation conversation) {
852 View menuItemView = findViewById(R.id.action_security);
853 if (menuItemView == null) {
854 return;
855 }
856 PopupMenu popup = new PopupMenu(this, menuItemView);
857 final ConversationFragment fragment = (ConversationFragment) getFragmentManager()
858 .findFragmentByTag("conversation");
859 if (fragment != null) {
860 popup.setOnMenuItemClickListener(new OnMenuItemClickListener() {
861
862 @Override
863 public boolean onMenuItemClick(MenuItem item) {
864 switch (item.getItemId()) {
865 case R.id.encryption_choice_none:
866 conversation.setNextEncryption(Message.ENCRYPTION_NONE);
867 item.setChecked(true);
868 break;
869 case R.id.encryption_choice_otr:
870 conversation.setNextEncryption(Message.ENCRYPTION_OTR);
871 item.setChecked(true);
872 break;
873 case R.id.encryption_choice_pgp:
874 if (hasPgp()) {
875 if (conversation.getAccount().getPgpSignature() != null) {
876 conversation.setNextEncryption(Message.ENCRYPTION_PGP);
877 item.setChecked(true);
878 } else {
879 announcePgp(conversation.getAccount(), conversation, onOpenPGPKeyPublished);
880 }
881 } else {
882 showInstallPgpDialog();
883 }
884 break;
885 case R.id.encryption_choice_axolotl:
886 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(conversation.getAccount())
887 + "Enabled axolotl for Contact " + conversation.getContact().getJid());
888 conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL);
889 item.setChecked(true);
890 break;
891 default:
892 conversation.setNextEncryption(Message.ENCRYPTION_NONE);
893 break;
894 }
895 xmppConnectionService.updateConversation(conversation);
896 fragment.updateChatMsgHint();
897 invalidateOptionsMenu();
898 refreshUi();
899 return true;
900 }
901 });
902 popup.inflate(R.menu.encryption_choices);
903 MenuItem otr = popup.getMenu().findItem(R.id.encryption_choice_otr);
904 MenuItem none = popup.getMenu().findItem(R.id.encryption_choice_none);
905 MenuItem pgp = popup.getMenu().findItem(R.id.encryption_choice_pgp);
906 MenuItem axolotl = popup.getMenu().findItem(R.id.encryption_choice_axolotl);
907 pgp.setVisible(Config.supportOpenPgp());
908 none.setVisible(Config.supportUnencrypted() || conversation.getMode() == Conversation.MODE_MULTI);
909 otr.setVisible(Config.supportOtr());
910 axolotl.setVisible(Config.supportOmemo());
911 if (conversation.getMode() == Conversation.MODE_MULTI) {
912 otr.setVisible(false);
913 }
914 if (!conversation.getAccount().getAxolotlService().isConversationAxolotlCapable(conversation)) {
915 axolotl.setEnabled(false);
916 }
917 switch (conversation.getNextEncryption()) {
918 case Message.ENCRYPTION_NONE:
919 none.setChecked(true);
920 break;
921 case Message.ENCRYPTION_OTR:
922 otr.setChecked(true);
923 break;
924 case Message.ENCRYPTION_PGP:
925 pgp.setChecked(true);
926 break;
927 case Message.ENCRYPTION_AXOLOTL:
928 axolotl.setChecked(true);
929 break;
930 default:
931 none.setChecked(true);
932 break;
933 }
934 popup.show();
935 }
936 }
937
938 protected void muteConversationDialog(final Conversation conversation) {
939 AlertDialog.Builder builder = new AlertDialog.Builder(this);
940 builder.setTitle(R.string.disable_notifications);
941 final int[] durations = getResources().getIntArray(R.array.mute_options_durations);
942 builder.setItems(R.array.mute_options_descriptions,
943 new OnClickListener() {
944
945 @Override
946 public void onClick(final DialogInterface dialog, final int which) {
947 final long till;
948 if (durations[which] == -1) {
949 till = Long.MAX_VALUE;
950 } else {
951 till = System.currentTimeMillis() + (durations[which] * 1000);
952 }
953 conversation.setMutedTill(till);
954 ConversationActivity.this.xmppConnectionService.updateConversation(conversation);
955 updateConversationList();
956 ConversationActivity.this.mConversationFragment.updateMessages();
957 invalidateOptionsMenu();
958 }
959 });
960 builder.create().show();
961 }
962
963 public void unmuteConversation(final Conversation conversation) {
964 conversation.setMutedTill(0);
965 this.xmppConnectionService.updateConversation(conversation);
966 updateConversationList();
967 ConversationActivity.this.mConversationFragment.updateMessages();
968 invalidateOptionsMenu();
969 }
970
971 @Override
972 public void onBackPressed() {
973 if (!isConversationsOverviewVisable()) {
974 showConversationsOverview();
975 } else {
976 super.onBackPressed();
977 }
978 }
979
980 @Override
981 public boolean onKeyUp(int key, KeyEvent event) {
982 int rotation = getWindowManager().getDefaultDisplay().getRotation();
983 final int upKey;
984 final int downKey;
985 switch (rotation) {
986 case Surface.ROTATION_90:
987 upKey = KeyEvent.KEYCODE_DPAD_LEFT;
988 downKey = KeyEvent.KEYCODE_DPAD_RIGHT;
989 break;
990 case Surface.ROTATION_180:
991 upKey = KeyEvent.KEYCODE_DPAD_DOWN;
992 downKey = KeyEvent.KEYCODE_DPAD_UP;
993 break;
994 case Surface.ROTATION_270:
995 upKey = KeyEvent.KEYCODE_DPAD_RIGHT;
996 downKey = KeyEvent.KEYCODE_DPAD_LEFT;
997 break;
998 case Surface.ROTATION_0:
999 default:
1000 upKey = KeyEvent.KEYCODE_DPAD_UP;
1001 downKey = KeyEvent.KEYCODE_DPAD_DOWN;
1002 }
1003 final boolean modifier = event.isCtrlPressed() || (event.getMetaState() & KeyEvent.META_ALT_LEFT_ON) != 0;
1004 if (modifier && key == KeyEvent.KEYCODE_TAB && isConversationsOverviewHideable()) {
1005 toggleConversationsOverview();
1006 return true;
1007 } else if (modifier && key == KeyEvent.KEYCODE_SPACE) {
1008 startActivity(new Intent(this, StartConversationActivity.class));
1009 return true;
1010 } else if (modifier && key == downKey) {
1011 if (isConversationsOverviewHideable() && !isConversationsOverviewVisable()) {
1012 showConversationsOverview();
1013 ;
1014 }
1015 return selectDownConversation();
1016 } else if (modifier && key == upKey) {
1017 if (isConversationsOverviewHideable() && !isConversationsOverviewVisable()) {
1018 showConversationsOverview();
1019 }
1020 return selectUpConversation();
1021 } else if (modifier && key == KeyEvent.KEYCODE_1) {
1022 return openConversationByIndex(0);
1023 } else if (modifier && key == KeyEvent.KEYCODE_2) {
1024 return openConversationByIndex(1);
1025 } else if (modifier && key == KeyEvent.KEYCODE_3) {
1026 return openConversationByIndex(2);
1027 } else if (modifier && key == KeyEvent.KEYCODE_4) {
1028 return openConversationByIndex(3);
1029 } else if (modifier && key == KeyEvent.KEYCODE_5) {
1030 return openConversationByIndex(4);
1031 } else if (modifier && key == KeyEvent.KEYCODE_6) {
1032 return openConversationByIndex(5);
1033 } else if (modifier && key == KeyEvent.KEYCODE_7) {
1034 return openConversationByIndex(6);
1035 } else if (modifier && key == KeyEvent.KEYCODE_8) {
1036 return openConversationByIndex(7);
1037 } else if (modifier && key == KeyEvent.KEYCODE_9) {
1038 return openConversationByIndex(8);
1039 } else if (modifier && key == KeyEvent.KEYCODE_0) {
1040 return openConversationByIndex(9);
1041 } else {
1042 return super.onKeyUp(key, event);
1043 }
1044 }
1045
1046 private void toggleConversationsOverview() {
1047 if (isConversationsOverviewVisable()) {
1048 hideConversationsOverview();
1049 if (mConversationFragment != null) {
1050 mConversationFragment.setFocusOnInputField();
1051 }
1052 } else {
1053 showConversationsOverview();
1054 }
1055 }
1056
1057 private boolean selectUpConversation() {
1058 if (this.mSelectedConversation != null) {
1059 int index = this.conversationList.indexOf(this.mSelectedConversation);
1060 if (index > 0) {
1061 return openConversationByIndex(index - 1);
1062 }
1063 }
1064 return false;
1065 }
1066
1067 private boolean selectDownConversation() {
1068 if (this.mSelectedConversation != null) {
1069 int index = this.conversationList.indexOf(this.mSelectedConversation);
1070 if (index != -1 && index < this.conversationList.size() - 1) {
1071 return openConversationByIndex(index + 1);
1072 }
1073 }
1074 return false;
1075 }
1076
1077 private boolean openConversationByIndex(int index) {
1078 try {
1079 this.conversationWasSelectedByKeyboard = true;
1080 setSelectedConversation(this.conversationList.get(index));
1081 this.mConversationFragment.reInit(getSelectedConversation());
1082 if (index > listView.getLastVisiblePosition() - 1 || index < listView.getFirstVisiblePosition() + 1) {
1083 this.listView.setSelection(index);
1084 }
1085 openConversation();
1086 return true;
1087 } catch (IndexOutOfBoundsException e) {
1088 return false;
1089 }
1090 }
1091
1092 @Override
1093 protected void onNewIntent(final Intent intent) {
1094 if (intent != null && ACTION_VIEW_CONVERSATION.equals(intent.getAction())) {
1095 mOpenConversation = null;
1096 mUnprocessedNewIntent = true;
1097 if (xmppConnectionServiceBound) {
1098 handleViewConversationIntent(intent);
1099 intent.setAction(Intent.ACTION_MAIN);
1100 } else {
1101 setIntent(intent);
1102 }
1103 }
1104 }
1105
1106 @Override
1107 public void onStart() {
1108 super.onStart();
1109 this.mRedirected.set(false);
1110 if (this.xmppConnectionServiceBound) {
1111 this.onBackendConnected();
1112 }
1113 if (conversationList.size() >= 1) {
1114 this.onConversationUpdate();
1115 }
1116 }
1117
1118 @Override
1119 public void onPause() {
1120 listView.discardUndo();
1121 super.onPause();
1122 this.mActivityPaused = true;
1123 }
1124
1125 @Override
1126 public void onResume() {
1127 super.onResume();
1128 final int theme = findTheme();
1129 final boolean usingEnterKey = usingEnterKey();
1130 if (this.mTheme != theme || usingEnterKey != mUsingEnterKey) {
1131 recreate();
1132 }
1133 this.mActivityPaused = false;
1134
1135
1136 if (!isConversationsOverviewVisable() || !isConversationsOverviewHideable()) {
1137 sendReadMarkerIfNecessary(getSelectedConversation());
1138 }
1139
1140 }
1141
1142 @Override
1143 public void onSaveInstanceState(final Bundle savedInstanceState) {
1144 Conversation conversation = getSelectedConversation();
1145 if (conversation != null) {
1146 savedInstanceState.putString(STATE_OPEN_CONVERSATION, conversation.getUuid());
1147 Pair<Integer,Integer> scrollPosition = mConversationFragment.getScrollPosition();
1148 if (scrollPosition != null) {
1149 savedInstanceState.putInt(STATE_FIRST_VISIBLE, scrollPosition.first);
1150 savedInstanceState.putInt(STATE_OFFSET_FROM_TOP, scrollPosition.second);
1151 }
1152 } else {
1153 savedInstanceState.remove(STATE_OPEN_CONVERSATION);
1154 }
1155 savedInstanceState.putBoolean(STATE_PANEL_OPEN, isConversationsOverviewVisable());
1156 if (this.mPendingImageUris.size() >= 1) {
1157 Log.d(Config.LOGTAG,"ConversationsActivity.onSaveInstanceState() - saving pending image uri");
1158 savedInstanceState.putString(STATE_PENDING_URI, this.mPendingImageUris.get(0).toString());
1159 } else {
1160 savedInstanceState.remove(STATE_PENDING_URI);
1161 }
1162 super.onSaveInstanceState(savedInstanceState);
1163 }
1164
1165 private void clearPending() {
1166 mPendingImageUris.clear();
1167 mPendingFileUris.clear();
1168 mPendingGeoUri = null;
1169 mPostponedActivityResult = null;
1170 }
1171
1172 @Override
1173 void onBackendConnected() {
1174 this.xmppConnectionService.getNotificationService().setIsInForeground(true);
1175 updateConversationList();
1176
1177 if (mPendingConferenceInvite != null) {
1178 if (mPendingConferenceInvite.execute(this)) {
1179 mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
1180 mToast.show();
1181 }
1182 mPendingConferenceInvite = null;
1183 }
1184
1185 final Intent intent = getIntent();
1186
1187 if (xmppConnectionService.getAccounts().size() == 0) {
1188 if (mRedirected.compareAndSet(false, true)) {
1189 if (Config.X509_VERIFICATION) {
1190 startActivity(new Intent(this, ManageAccountActivity.class));
1191 } else if (Config.MAGIC_CREATE_DOMAIN != null) {
1192 startActivity(new Intent(this, WelcomeActivity.class));
1193 } else {
1194 Intent editAccount = new Intent(this, EditAccountActivity.class);
1195 editAccount.putExtra("init",true);
1196 startActivity(editAccount);
1197 }
1198 finish();
1199 }
1200 } else if (conversationList.size() <= 0) {
1201 if (mRedirected.compareAndSet(false, true)) {
1202 Account pendingAccount = xmppConnectionService.getPendingAccount();
1203 if (pendingAccount == null) {
1204 Intent startConversationActivity = new Intent(this, StartConversationActivity.class);
1205 intent.putExtra("init", true);
1206 startActivity(startConversationActivity);
1207 } else {
1208 switchToAccount(pendingAccount, true);
1209 }
1210 finish();
1211 }
1212 } else if (selectConversationByUuid(mOpenConversation)) {
1213 if (mPanelOpen) {
1214 showConversationsOverview();
1215 } else {
1216 if (isConversationsOverviewHideable()) {
1217 openConversation();
1218 updateActionBarTitle(true);
1219 }
1220 }
1221 if (this.mConversationFragment.reInit(getSelectedConversation())) {
1222 Log.d(Config.LOGTAG,"setting scroll position on fragment");
1223 this.mConversationFragment.setScrollPosition(mScrollPosition);
1224 }
1225 mOpenConversation = null;
1226 } else if (intent != null && ACTION_VIEW_CONVERSATION.equals(intent.getAction())) {
1227 clearPending();
1228 handleViewConversationIntent(intent);
1229 intent.setAction(Intent.ACTION_MAIN);
1230 } else if (getSelectedConversation() == null) {
1231 showConversationsOverview();
1232 clearPending();
1233 setSelectedConversation(conversationList.get(0));
1234 this.mConversationFragment.reInit(getSelectedConversation());
1235 } else {
1236 this.mConversationFragment.messageListAdapter.updatePreferences();
1237 this.mConversationFragment.messagesView.invalidateViews();
1238 this.mConversationFragment.setupIme();
1239 }
1240
1241 if (this.mPostponedActivityResult != null) {
1242 this.onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
1243 }
1244
1245 final boolean stopping;
1246 if (Build.VERSION.SDK_INT >= 17) {
1247 stopping = isFinishing() || isDestroyed();
1248 } else {
1249 stopping = isFinishing();
1250 }
1251
1252 if (!forbidProcessingPendings) {
1253 for (Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
1254 Uri foo = i.next();
1255 Log.d(Config.LOGTAG,"ConversationsActivity.onBackendConnected() - attaching image to conversations. stopping="+Boolean.toString(stopping));
1256 attachImageToConversation(getSelectedConversation(), foo);
1257 }
1258
1259 for (Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
1260 Log.d(Config.LOGTAG,"ConversationsActivity.onBackendConnected() - attaching file to conversations. stopping="+Boolean.toString(stopping));
1261 attachFileToConversation(getSelectedConversation(), i.next());
1262 }
1263
1264 if (mPendingGeoUri != null) {
1265 attachLocationToConversation(getSelectedConversation(), mPendingGeoUri);
1266 mPendingGeoUri = null;
1267 }
1268 }
1269 forbidProcessingPendings = false;
1270
1271 if (!ExceptionHelper.checkForCrash(this, this.xmppConnectionService)) {
1272 openBatteryOptimizationDialogIfNeeded();
1273 }
1274 if (isConversationsOverviewVisable() && isConversationsOverviewHideable()) {
1275 xmppConnectionService.getNotificationService().setOpenConversation(null);
1276 } else {
1277 xmppConnectionService.getNotificationService().setOpenConversation(getSelectedConversation());
1278 }
1279 }
1280
1281 private void handleViewConversationIntent(final Intent intent) {
1282 final String uuid = intent.getStringExtra(CONVERSATION);
1283 final String downloadUuid = intent.getStringExtra(EXTRA_DOWNLOAD_UUID);
1284 final String text = intent.getStringExtra(TEXT);
1285 final String nick = intent.getStringExtra(NICK);
1286 final boolean pm = intent.getBooleanExtra(PRIVATE_MESSAGE, false);
1287 if (selectConversationByUuid(uuid)) {
1288 this.mConversationFragment.reInit(getSelectedConversation());
1289 if (nick != null) {
1290 if (pm) {
1291 Jid jid = getSelectedConversation().getJid();
1292 try {
1293 Jid next = Jid.fromParts(jid.getLocalpart(), jid.getDomainpart(), nick);
1294 this.mConversationFragment.privateMessageWith(next);
1295 } catch (final InvalidJidException ignored) {
1296 //do nothing
1297 }
1298 } else {
1299 this.mConversationFragment.highlightInConference(nick);
1300 }
1301 } else {
1302 this.mConversationFragment.appendText(text);
1303 }
1304 hideConversationsOverview();
1305 mUnprocessedNewIntent = false;
1306 openConversation();
1307 if (mContentView instanceof SlidingPaneLayout) {
1308 updateActionBarTitle(true); //fixes bug where slp isn't properly closed yet
1309 }
1310 if (downloadUuid != null) {
1311 final Message message = mSelectedConversation.findMessageWithFileAndUuid(downloadUuid);
1312 if (message != null) {
1313 startDownloadable(message);
1314 }
1315 }
1316 } else {
1317 mUnprocessedNewIntent = false;
1318 }
1319 }
1320
1321 private boolean selectConversationByUuid(String uuid) {
1322 if (uuid == null) {
1323 return false;
1324 }
1325 for (Conversation aConversationList : conversationList) {
1326 if (aConversationList.getUuid().equals(uuid)) {
1327 setSelectedConversation(aConversationList);
1328 return true;
1329 }
1330 }
1331 return false;
1332 }
1333
1334 @Override
1335 protected void unregisterListeners() {
1336 super.unregisterListeners();
1337 xmppConnectionService.getNotificationService().setOpenConversation(null);
1338 }
1339
1340 @SuppressLint("NewApi")
1341 private static List<Uri> extractUriFromIntent(final Intent intent) {
1342 List<Uri> uris = new ArrayList<>();
1343 if (intent == null) {
1344 return uris;
1345 }
1346 Uri uri = intent.getData();
1347 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && uri == null) {
1348 final ClipData clipData = intent.getClipData();
1349 if (clipData != null) {
1350 for (int i = 0; i < clipData.getItemCount(); ++i) {
1351 uris.add(clipData.getItemAt(i).getUri());
1352 }
1353 }
1354 } else {
1355 uris.add(uri);
1356 }
1357 return uris;
1358 }
1359
1360 @Override
1361 protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
1362 super.onActivityResult(requestCode, resultCode, data);
1363 if (resultCode == RESULT_OK) {
1364 if (requestCode == REQUEST_DECRYPT_PGP) {
1365 mConversationFragment.onActivityResult(requestCode, resultCode, data);
1366 } else if (requestCode == REQUEST_CHOOSE_PGP_ID) {
1367 // the user chose OpenPGP for encryption and selected his key in the PGP provider
1368 if (xmppConnectionServiceBound) {
1369 if (data.getExtras().containsKey(OpenPgpApi.EXTRA_SIGN_KEY_ID)) {
1370 // associate selected PGP keyId with the account
1371 mSelectedConversation.getAccount().setPgpSignId(data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID));
1372 // we need to announce the key as described in XEP-027
1373 announcePgp(mSelectedConversation.getAccount(), null, onOpenPGPKeyPublished);
1374 } else {
1375 choosePgpSignId(mSelectedConversation.getAccount());
1376 }
1377 this.mPostponedActivityResult = null;
1378 } else {
1379 this.mPostponedActivityResult = new Pair<>(requestCode, data);
1380 }
1381 } else if (requestCode == REQUEST_ANNOUNCE_PGP) {
1382 if (xmppConnectionServiceBound) {
1383 announcePgp(mSelectedConversation.getAccount(), mSelectedConversation, onOpenPGPKeyPublished);
1384 this.mPostponedActivityResult = null;
1385 } else {
1386 this.mPostponedActivityResult = new Pair<>(requestCode, data);
1387 }
1388 } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) {
1389 mPendingImageUris.clear();
1390 mPendingImageUris.addAll(extractUriFromIntent(data));
1391 if (xmppConnectionServiceBound) {
1392 for (Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
1393 Log.d(Config.LOGTAG,"ConversationsActivity.onActivityResult() - attaching image to conversations. CHOOSE_IMAGE");
1394 attachImageToConversation(getSelectedConversation(), i.next());
1395 }
1396 }
1397 } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_FILE || requestCode == ATTACHMENT_CHOICE_RECORD_VOICE) {
1398 final List<Uri> uris = extractUriFromIntent(data);
1399 final Conversation c = getSelectedConversation();
1400 final OnPresenceSelected callback = new OnPresenceSelected() {
1401 @Override
1402 public void onPresenceSelected() {
1403 mPendingFileUris.clear();
1404 mPendingFileUris.addAll(uris);
1405 if (xmppConnectionServiceBound) {
1406 for (Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
1407 Log.d(Config.LOGTAG,"ConversationsActivity.onActivityResult() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE");
1408 attachFileToConversation(c, i.next());
1409 }
1410 }
1411 }
1412 };
1413 if (c == null || c.getMode() == Conversation.MODE_MULTI
1414 || FileBackend.allFilesUnderSize(this, uris, getMaxHttpUploadSize(c))
1415 || c.getNextEncryption() == Message.ENCRYPTION_OTR) {
1416 callback.onPresenceSelected();
1417 } else {
1418 selectPresence(c, callback);
1419 }
1420 } else if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) {
1421 if (mPendingImageUris.size() == 1) {
1422 Uri uri = FileBackend.getIndexableTakePhotoUri(mPendingImageUris.get(0));
1423 mPendingImageUris.set(0, uri);
1424 if (xmppConnectionServiceBound) {
1425 Log.d(Config.LOGTAG,"ConversationsActivity.onActivityResult() - attaching image to conversations. TAKE_PHOTO");
1426 attachImageToConversation(getSelectedConversation(), uri);
1427 mPendingImageUris.clear();
1428 }
1429 if (!Config.ONLY_INTERNAL_STORAGE) {
1430 Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
1431 intent.setData(uri);
1432 sendBroadcast(intent);
1433 }
1434 } else {
1435 mPendingImageUris.clear();
1436 }
1437 } else if (requestCode == ATTACHMENT_CHOICE_LOCATION) {
1438 double latitude = data.getDoubleExtra("latitude", 0);
1439 double longitude = data.getDoubleExtra("longitude", 0);
1440 this.mPendingGeoUri = Uri.parse("geo:" + String.valueOf(latitude) + "," + String.valueOf(longitude));
1441 if (xmppConnectionServiceBound) {
1442 attachLocationToConversation(getSelectedConversation(), mPendingGeoUri);
1443 this.mPendingGeoUri = null;
1444 }
1445 } else if (requestCode == REQUEST_TRUST_KEYS_TEXT || requestCode == REQUEST_TRUST_KEYS_MENU) {
1446 this.forbidProcessingPendings = !xmppConnectionServiceBound;
1447 if (xmppConnectionServiceBound) {
1448 mConversationFragment.onActivityResult(requestCode, resultCode, data);
1449 this.mPostponedActivityResult = null;
1450 } else {
1451 this.mPostponedActivityResult = new Pair<>(requestCode, data);
1452 }
1453
1454 }
1455 } else {
1456 mPendingImageUris.clear();
1457 mPendingFileUris.clear();
1458 if (requestCode == ConversationActivity.REQUEST_DECRYPT_PGP) {
1459 mConversationFragment.onActivityResult(requestCode, resultCode, data);
1460 }
1461 if (requestCode == REQUEST_BATTERY_OP) {
1462 setNeverAskForBatteryOptimizationsAgain();
1463 }
1464 }
1465 }
1466
1467 private long getMaxHttpUploadSize(Conversation conversation) {
1468 final XmppConnection connection = conversation.getAccount().getXmppConnection();
1469 return connection == null ? -1 : connection.getFeatures().getMaxHttpUploadSize();
1470 }
1471
1472 private void setNeverAskForBatteryOptimizationsAgain() {
1473 getPreferences().edit().putBoolean("show_battery_optimization", false).commit();
1474 }
1475
1476 private void openBatteryOptimizationDialogIfNeeded() {
1477 if (hasAccountWithoutPush()
1478 && isOptimizingBattery()
1479 && getPreferences().getBoolean("show_battery_optimization", true)) {
1480 AlertDialog.Builder builder = new AlertDialog.Builder(this);
1481 builder.setTitle(R.string.battery_optimizations_enabled);
1482 builder.setMessage(R.string.battery_optimizations_enabled_dialog);
1483 builder.setPositiveButton(R.string.next, new OnClickListener() {
1484 @Override
1485 public void onClick(DialogInterface dialog, int which) {
1486 Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
1487 Uri uri = Uri.parse("package:" + getPackageName());
1488 intent.setData(uri);
1489 try {
1490 startActivityForResult(intent, REQUEST_BATTERY_OP);
1491 } catch (ActivityNotFoundException e) {
1492 Toast.makeText(ConversationActivity.this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show();
1493 }
1494 }
1495 });
1496 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
1497 builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
1498 @Override
1499 public void onDismiss(DialogInterface dialog) {
1500 setNeverAskForBatteryOptimizationsAgain();
1501 }
1502 });
1503 }
1504 builder.create().show();
1505 }
1506 }
1507
1508 private boolean hasAccountWithoutPush() {
1509 for(Account account : xmppConnectionService.getAccounts()) {
1510 if (account.getStatus() != Account.State.DISABLED
1511 && !xmppConnectionService.getPushManagementService().availableAndUseful(account)) {
1512 return true;
1513 }
1514 }
1515 return false;
1516 }
1517
1518 private void attachLocationToConversation(Conversation conversation, Uri uri) {
1519 if (conversation == null) {
1520 return;
1521 }
1522 xmppConnectionService.attachLocationToConversation(conversation,uri, new UiCallback<Message>() {
1523
1524 @Override
1525 public void success(Message message) {
1526 xmppConnectionService.sendMessage(message);
1527 }
1528
1529 @Override
1530 public void error(int errorCode, Message object) {
1531
1532 }
1533
1534 @Override
1535 public void userInputRequried(PendingIntent pi, Message object) {
1536
1537 }
1538 });
1539 }
1540
1541 private void attachFileToConversation(Conversation conversation, Uri uri) {
1542 if (conversation == null) {
1543 return;
1544 }
1545 final Toast prepareFileToast = Toast.makeText(getApplicationContext(),getText(R.string.preparing_file), Toast.LENGTH_LONG);
1546 prepareFileToast.show();
1547 xmppConnectionService.attachFileToConversation(conversation, uri, new UiInformableCallback<Message>() {
1548 @Override
1549 public void inform(final String text) {
1550 hidePrepareFileToast(prepareFileToast);
1551 runOnUiThread(new Runnable() {
1552 @Override
1553 public void run() {
1554 replaceToast(text);
1555 }
1556 });
1557 }
1558
1559 @Override
1560 public void success(Message message) {
1561 runOnUiThread(new Runnable() {
1562 @Override
1563 public void run() {
1564 hideToast();
1565 }
1566 });
1567 hidePrepareFileToast(prepareFileToast);
1568 xmppConnectionService.sendMessage(message);
1569 }
1570
1571 @Override
1572 public void error(final int errorCode, Message message) {
1573 hidePrepareFileToast(prepareFileToast);
1574 runOnUiThread(new Runnable() {
1575 @Override
1576 public void run() {
1577 replaceToast(getString(errorCode));
1578 }
1579 });
1580
1581 }
1582
1583 @Override
1584 public void userInputRequried(PendingIntent pi, Message message) {
1585 hidePrepareFileToast(prepareFileToast);
1586 }
1587 });
1588 }
1589
1590 public void attachImageToConversation(Uri uri) {
1591 this.attachImageToConversation(getSelectedConversation(), uri);
1592 }
1593
1594 private void attachImageToConversation(Conversation conversation, Uri uri) {
1595 if (conversation == null) {
1596 return;
1597 }
1598 final Toast prepareFileToast = Toast.makeText(getApplicationContext(),getText(R.string.preparing_image), Toast.LENGTH_LONG);
1599 prepareFileToast.show();
1600 xmppConnectionService.attachImageToConversation(conversation, uri,
1601 new UiCallback<Message>() {
1602
1603 @Override
1604 public void userInputRequried(PendingIntent pi, Message object) {
1605 hidePrepareFileToast(prepareFileToast);
1606 }
1607
1608 @Override
1609 public void success(Message message) {
1610 hidePrepareFileToast(prepareFileToast);
1611 xmppConnectionService.sendMessage(message);
1612 }
1613
1614 @Override
1615 public void error(final int error, Message message) {
1616 hidePrepareFileToast(prepareFileToast);
1617 runOnUiThread(new Runnable() {
1618 @Override
1619 public void run() {
1620 replaceToast(getString(error));
1621 }
1622 });
1623 }
1624 });
1625 }
1626
1627 private void hidePrepareFileToast(final Toast prepareFileToast) {
1628 if (prepareFileToast != null) {
1629 runOnUiThread(new Runnable() {
1630
1631 @Override
1632 public void run() {
1633 prepareFileToast.cancel();
1634 }
1635 });
1636 }
1637 }
1638
1639 public void updateConversationList() {
1640 xmppConnectionService
1641 .populateWithOrderedConversations(conversationList);
1642 if (swipedConversation != null) {
1643 if (swipedConversation.isRead()) {
1644 conversationList.remove(swipedConversation);
1645 } else {
1646 listView.discardUndo();
1647 }
1648 }
1649 listAdapter.notifyDataSetChanged();
1650 }
1651
1652 public void runIntent(PendingIntent pi, int requestCode) {
1653 try {
1654 this.startIntentSenderForResult(pi.getIntentSender(), requestCode,
1655 null, 0, 0, 0);
1656 } catch (final SendIntentException ignored) {
1657 }
1658 }
1659
1660 public void encryptTextMessage(Message message) {
1661 xmppConnectionService.getPgpEngine().encrypt(message,
1662 new UiCallback<Message>() {
1663
1664 @Override
1665 public void userInputRequried(PendingIntent pi,Message message) {
1666 ConversationActivity.this.runIntent(pi,ConversationActivity.REQUEST_SEND_MESSAGE);
1667 }
1668
1669 @Override
1670 public void success(Message message) {
1671 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
1672 xmppConnectionService.sendMessage(message);
1673 if (mConversationFragment != null) {
1674 mConversationFragment.messageSent();
1675 }
1676 }
1677
1678 @Override
1679 public void error(final int error, Message message) {
1680 runOnUiThread(new Runnable() {
1681 @Override
1682 public void run() {
1683 Toast.makeText(ConversationActivity.this,
1684 R.string.unable_to_connect_to_keychain,
1685 Toast.LENGTH_SHORT
1686 ).show();
1687 }
1688 });
1689 }
1690 });
1691 }
1692
1693 public boolean useSendButtonToIndicateStatus() {
1694 return getPreferences().getBoolean("send_button_status", false);
1695 }
1696
1697 public boolean indicateReceived() {
1698 return getPreferences().getBoolean("indicate_received", false);
1699 }
1700
1701 public boolean useGreenBackground() {
1702 return getPreferences().getBoolean("use_green_background",true);
1703 }
1704
1705 protected boolean trustKeysIfNeeded(int requestCode) {
1706 return trustKeysIfNeeded(requestCode, ATTACHMENT_CHOICE_INVALID);
1707 }
1708
1709 protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) {
1710 AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService();
1711 final List<Jid> targets = axolotlService.getCryptoTargets(mSelectedConversation);
1712 boolean hasUnaccepted = !mSelectedConversation.getAcceptedCryptoTargets().containsAll(targets);
1713 boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided()).isEmpty();
1714 boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), targets).isEmpty();
1715 boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty();
1716 boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets);
1717 if(hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys || hasUnaccepted) {
1718 axolotlService.createSessionsIfNeeded(mSelectedConversation);
1719 Intent intent = new Intent(getApplicationContext(), TrustKeysActivity.class);
1720 String[] contacts = new String[targets.size()];
1721 for(int i = 0; i < contacts.length; ++i) {
1722 contacts[i] = targets.get(i).toString();
1723 }
1724 intent.putExtra("contacts", contacts);
1725 intent.putExtra(EXTRA_ACCOUNT, mSelectedConversation.getAccount().getJid().toBareJid().toString());
1726 intent.putExtra("choice", attachmentChoice);
1727 intent.putExtra("conversation",mSelectedConversation.getUuid());
1728 startActivityForResult(intent, requestCode);
1729 return true;
1730 } else {
1731 return false;
1732 }
1733 }
1734
1735 @Override
1736 protected void refreshUiReal() {
1737 updateConversationList();
1738 if (conversationList.size() > 0) {
1739 if (!this.mConversationFragment.isAdded()) {
1740 Log.d(Config.LOGTAG,"fragment NOT added to activity. detached="+Boolean.toString(mConversationFragment.isDetached()));
1741 }
1742 ConversationActivity.this.mConversationFragment.updateMessages();
1743 updateActionBarTitle();
1744 invalidateOptionsMenu();
1745 } else {
1746 Log.d(Config.LOGTAG,"not updating conversations fragment because conversations list size was 0");
1747 }
1748 }
1749
1750 @Override
1751 public void onAccountUpdate() {
1752 this.refreshUi();
1753 }
1754
1755 @Override
1756 public void onConversationUpdate() {
1757 this.refreshUi();
1758 }
1759
1760 @Override
1761 public void onRosterUpdate() {
1762 this.refreshUi();
1763 }
1764
1765 @Override
1766 public void OnUpdateBlocklist(Status status) {
1767 this.refreshUi();
1768 }
1769
1770 public void unblockConversation(final Blockable conversation) {
1771 xmppConnectionService.sendUnblockRequest(conversation);
1772 }
1773
1774 public boolean enterIsSend() {
1775 return getPreferences().getBoolean("enter_is_send",false);
1776 }
1777
1778 @Override
1779 public void onShowErrorToast(final int resId) {
1780 runOnUiThread(new Runnable() {
1781 @Override
1782 public void run() {
1783 Toast.makeText(ConversationActivity.this,resId,Toast.LENGTH_SHORT).show();
1784 }
1785 });
1786 }
1787
1788 public boolean highlightSelectedConversations() {
1789 return !isConversationsOverviewHideable() || this.conversationWasSelectedByKeyboard;
1790 }
1791}