1package eu.siacs.conversations.ui;
2
3import android.Manifest;
4import android.annotation.SuppressLint;
5import android.annotation.TargetApi;
6import android.app.ActionBar;
7import android.app.Activity;
8import android.app.AlertDialog;
9import android.app.AlertDialog.Builder;
10import android.app.PendingIntent;
11import android.content.ClipData;
12import android.content.ClipboardManager;
13import android.content.ComponentName;
14import android.content.Context;
15import android.content.DialogInterface;
16import android.content.DialogInterface.OnClickListener;
17import android.content.Intent;
18import android.content.IntentSender.SendIntentException;
19import android.content.ServiceConnection;
20import android.content.SharedPreferences;
21import android.content.pm.PackageManager;
22import android.content.pm.ResolveInfo;
23import android.content.res.Resources;
24import android.graphics.Bitmap;
25import android.graphics.Color;
26import android.graphics.Point;
27import android.graphics.drawable.BitmapDrawable;
28import android.graphics.drawable.Drawable;
29import android.net.Uri;
30import android.nfc.NdefMessage;
31import android.nfc.NdefRecord;
32import android.nfc.NfcAdapter;
33import android.nfc.NfcEvent;
34import android.os.AsyncTask;
35import android.os.Build;
36import android.os.Bundle;
37import android.os.Handler;
38import android.os.IBinder;
39import android.os.PowerManager;
40import android.os.SystemClock;
41import android.preference.PreferenceManager;
42import android.text.InputType;
43import android.util.DisplayMetrics;
44import android.util.Log;
45import android.view.MenuItem;
46import android.view.View;
47import android.view.inputmethod.InputMethodManager;
48import android.widget.CompoundButton;
49import android.widget.EditText;
50import android.widget.ImageView;
51import android.widget.LinearLayout;
52import android.widget.TextView;
53import android.widget.Toast;
54
55import com.google.zxing.BarcodeFormat;
56import com.google.zxing.EncodeHintType;
57import com.google.zxing.WriterException;
58import com.google.zxing.common.BitMatrix;
59import com.google.zxing.qrcode.QRCodeWriter;
60import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
61
62import net.java.otr4j.session.SessionID;
63
64import java.io.FileNotFoundException;
65import java.lang.ref.WeakReference;
66import java.util.ArrayList;
67import java.util.Hashtable;
68import java.util.List;
69import java.util.concurrent.RejectedExecutionException;
70
71import eu.siacs.conversations.Config;
72import eu.siacs.conversations.R;
73import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
74import eu.siacs.conversations.entities.Account;
75import eu.siacs.conversations.entities.Contact;
76import eu.siacs.conversations.entities.Conversation;
77import eu.siacs.conversations.entities.Message;
78import eu.siacs.conversations.entities.MucOptions;
79import eu.siacs.conversations.entities.Presences;
80import eu.siacs.conversations.services.AvatarService;
81import eu.siacs.conversations.services.XmppConnectionService;
82import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
83import eu.siacs.conversations.ui.widget.Switch;
84import eu.siacs.conversations.utils.CryptoHelper;
85import eu.siacs.conversations.utils.ExceptionHelper;
86import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
87import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
88import eu.siacs.conversations.xmpp.jid.InvalidJidException;
89import eu.siacs.conversations.xmpp.jid.Jid;
90
91public abstract class XmppActivity extends Activity {
92
93 protected static final int REQUEST_ANNOUNCE_PGP = 0x0101;
94 protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102;
95 protected static final int REQUEST_CHOOSE_PGP_ID = 0x0103;
96 protected static final int REQUEST_BATTERY_OP = 0x13849ff;
97
98 public static final String EXTRA_ACCOUNT = "account";
99
100 public XmppConnectionService xmppConnectionService;
101 public boolean xmppConnectionServiceBound = false;
102 protected boolean registeredListeners = false;
103
104 protected int mPrimaryTextColor;
105 protected int mSecondaryTextColor;
106 protected int mTertiaryTextColor;
107 protected int mPrimaryBackgroundColor;
108 protected int mSecondaryBackgroundColor;
109 protected int mColorRed;
110 protected int mColorOrange;
111 protected int mColorGreen;
112 protected int mPrimaryColor;
113
114 protected boolean mUseSubject = true;
115
116 private DisplayMetrics metrics;
117 protected int mTheme;
118 protected boolean mUsingEnterKey = false;
119
120 protected Toast mToast;
121
122 protected void hideToast() {
123 if (mToast != null) {
124 mToast.cancel();
125 }
126 }
127
128 protected void replaceToast(String msg) {
129 hideToast();
130 mToast = Toast.makeText(this, msg ,Toast.LENGTH_LONG);
131 mToast.show();
132 }
133
134 protected Runnable onOpenPGPKeyPublished = new Runnable() {
135 @Override
136 public void run() {
137 Toast.makeText(XmppActivity.this,R.string.openpgp_has_been_published, Toast.LENGTH_SHORT).show();
138 }
139 };
140
141 private long mLastUiRefresh = 0;
142 private Handler mRefreshUiHandler = new Handler();
143 private Runnable mRefreshUiRunnable = new Runnable() {
144 @Override
145 public void run() {
146 mLastUiRefresh = SystemClock.elapsedRealtime();
147 refreshUiReal();
148 }
149 };
150
151 protected ConferenceInvite mPendingConferenceInvite = null;
152
153
154 protected final void refreshUi() {
155 final long diff = SystemClock.elapsedRealtime() - mLastUiRefresh;
156 if (diff > Config.REFRESH_UI_INTERVAL) {
157 mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
158 runOnUiThread(mRefreshUiRunnable);
159 } else {
160 final long next = Config.REFRESH_UI_INTERVAL - diff;
161 mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
162 mRefreshUiHandler.postDelayed(mRefreshUiRunnable,next);
163 }
164 }
165
166 abstract protected void refreshUiReal();
167
168 protected interface OnValueEdited {
169 public void onValueEdited(String value);
170 }
171
172 public interface OnPresenceSelected {
173 public void onPresenceSelected();
174 }
175
176 protected ServiceConnection mConnection = new ServiceConnection() {
177
178 @Override
179 public void onServiceConnected(ComponentName className, IBinder service) {
180 XmppConnectionBinder binder = (XmppConnectionBinder) service;
181 xmppConnectionService = binder.getService();
182 xmppConnectionServiceBound = true;
183 if (!registeredListeners && shouldRegisterListeners()) {
184 registerListeners();
185 registeredListeners = true;
186 }
187 onBackendConnected();
188 }
189
190 @Override
191 public void onServiceDisconnected(ComponentName arg0) {
192 xmppConnectionServiceBound = false;
193 }
194 };
195
196 @Override
197 protected void onStart() {
198 super.onStart();
199 if (!xmppConnectionServiceBound) {
200 connectToBackend();
201 } else {
202 if (!registeredListeners) {
203 this.registerListeners();
204 this.registeredListeners = true;
205 }
206 this.onBackendConnected();
207 }
208 }
209
210 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
211 protected boolean shouldRegisterListeners() {
212 if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
213 return !isDestroyed() && !isFinishing();
214 } else {
215 return !isFinishing();
216 }
217 }
218
219 public void connectToBackend() {
220 Intent intent = new Intent(this, XmppConnectionService.class);
221 intent.setAction("ui");
222 startService(intent);
223 bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
224 }
225
226 @Override
227 protected void onStop() {
228 super.onStop();
229 if (xmppConnectionServiceBound) {
230 if (registeredListeners) {
231 this.unregisterListeners();
232 this.registeredListeners = false;
233 }
234 unbindService(mConnection);
235 xmppConnectionServiceBound = false;
236 }
237 }
238
239 protected void hideKeyboard() {
240 InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
241
242 View focus = getCurrentFocus();
243
244 if (focus != null) {
245
246 inputManager.hideSoftInputFromWindow(focus.getWindowToken(),
247 InputMethodManager.HIDE_NOT_ALWAYS);
248 }
249 }
250
251 public boolean hasPgp() {
252 return xmppConnectionService.getPgpEngine() != null;
253 }
254
255 public void showInstallPgpDialog() {
256 Builder builder = new AlertDialog.Builder(this);
257 builder.setTitle(getString(R.string.openkeychain_required));
258 builder.setIconAttribute(android.R.attr.alertDialogIcon);
259 builder.setMessage(getText(R.string.openkeychain_required_long));
260 builder.setNegativeButton(getString(R.string.cancel), null);
261 builder.setNeutralButton(getString(R.string.restart),
262 new OnClickListener() {
263
264 @Override
265 public void onClick(DialogInterface dialog, int which) {
266 if (xmppConnectionServiceBound) {
267 unbindService(mConnection);
268 xmppConnectionServiceBound = false;
269 }
270 stopService(new Intent(XmppActivity.this,
271 XmppConnectionService.class));
272 finish();
273 }
274 });
275 builder.setPositiveButton(getString(R.string.install),
276 new OnClickListener() {
277
278 @Override
279 public void onClick(DialogInterface dialog, int which) {
280 Uri uri = Uri
281 .parse("market://details?id=org.sufficientlysecure.keychain");
282 Intent marketIntent = new Intent(Intent.ACTION_VIEW,
283 uri);
284 PackageManager manager = getApplicationContext()
285 .getPackageManager();
286 List<ResolveInfo> infos = manager
287 .queryIntentActivities(marketIntent, 0);
288 if (infos.size() > 0) {
289 startActivity(marketIntent);
290 } else {
291 uri = Uri.parse("http://www.openkeychain.org/");
292 Intent browserIntent = new Intent(
293 Intent.ACTION_VIEW, uri);
294 startActivity(browserIntent);
295 }
296 finish();
297 }
298 });
299 builder.create().show();
300 }
301
302 abstract void onBackendConnected();
303
304 protected void registerListeners() {
305 if (this instanceof XmppConnectionService.OnConversationUpdate) {
306 this.xmppConnectionService.setOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this);
307 }
308 if (this instanceof XmppConnectionService.OnAccountUpdate) {
309 this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this);
310 }
311 if (this instanceof XmppConnectionService.OnCaptchaRequested) {
312 this.xmppConnectionService.setOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this);
313 }
314 if (this instanceof XmppConnectionService.OnRosterUpdate) {
315 this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this);
316 }
317 if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
318 this.xmppConnectionService.setOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this);
319 }
320 if (this instanceof OnUpdateBlocklist) {
321 this.xmppConnectionService.setOnUpdateBlocklistListener((OnUpdateBlocklist) this);
322 }
323 if (this instanceof XmppConnectionService.OnShowErrorToast) {
324 this.xmppConnectionService.setOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this);
325 }
326 if (this instanceof OnKeyStatusUpdated) {
327 this.xmppConnectionService.setOnKeyStatusUpdatedListener((OnKeyStatusUpdated) this);
328 }
329 }
330
331 protected void unregisterListeners() {
332 if (this instanceof XmppConnectionService.OnConversationUpdate) {
333 this.xmppConnectionService.removeOnConversationListChangedListener();
334 }
335 if (this instanceof XmppConnectionService.OnAccountUpdate) {
336 this.xmppConnectionService.removeOnAccountListChangedListener();
337 }
338 if (this instanceof XmppConnectionService.OnCaptchaRequested) {
339 this.xmppConnectionService.removeOnCaptchaRequestedListener();
340 }
341 if (this instanceof XmppConnectionService.OnRosterUpdate) {
342 this.xmppConnectionService.removeOnRosterUpdateListener();
343 }
344 if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
345 this.xmppConnectionService.removeOnMucRosterUpdateListener();
346 }
347 if (this instanceof OnUpdateBlocklist) {
348 this.xmppConnectionService.removeOnUpdateBlocklistListener();
349 }
350 if (this instanceof XmppConnectionService.OnShowErrorToast) {
351 this.xmppConnectionService.removeOnShowErrorToastListener();
352 }
353 if (this instanceof OnKeyStatusUpdated) {
354 this.xmppConnectionService.removeOnNewKeysAvailableListener();
355 }
356 }
357
358 @Override
359 public boolean onOptionsItemSelected(final MenuItem item) {
360 switch (item.getItemId()) {
361 case R.id.action_settings:
362 startActivity(new Intent(this, SettingsActivity.class));
363 break;
364 case R.id.action_accounts:
365 startActivity(new Intent(this, ManageAccountActivity.class));
366 break;
367 case android.R.id.home:
368 finish();
369 break;
370 case R.id.action_show_qr_code:
371 showQrCode();
372 break;
373 }
374 return super.onOptionsItemSelected(item);
375 }
376
377 @Override
378 protected void onCreate(Bundle savedInstanceState) {
379 super.onCreate(savedInstanceState);
380 metrics = getResources().getDisplayMetrics();
381 ExceptionHelper.init(getApplicationContext());
382 mPrimaryTextColor = getResources().getColor(R.color.black87);
383 mSecondaryTextColor = getResources().getColor(R.color.black54);
384 mTertiaryTextColor = getResources().getColor(R.color.black12);
385 mColorRed = getResources().getColor(R.color.red800);
386 mColorOrange = getResources().getColor(R.color.orange500);
387 mColorGreen = getResources().getColor(R.color.green500);
388 mPrimaryColor = getResources().getColor(R.color.primary);
389 mPrimaryBackgroundColor = getResources().getColor(R.color.grey50);
390 mSecondaryBackgroundColor = getResources().getColor(R.color.grey200);
391 this.mTheme = findTheme();
392 setTheme(this.mTheme);
393 this.mUsingEnterKey = usingEnterKey();
394 mUseSubject = getPreferences().getBoolean("use_subject", true);
395 final ActionBar ab = getActionBar();
396 if (ab!=null) {
397 ab.setDisplayHomeAsUpEnabled(true);
398 }
399 }
400
401 protected boolean isOptimizingBattery() {
402 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
403 PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
404 return !pm.isIgnoringBatteryOptimizations(getPackageName());
405 } else {
406 return false;
407 }
408 }
409
410 protected boolean usingEnterKey() {
411 return getPreferences().getBoolean("display_enter_key", false);
412 }
413
414 protected SharedPreferences getPreferences() {
415 return PreferenceManager
416 .getDefaultSharedPreferences(getApplicationContext());
417 }
418
419 public boolean useSubjectToIdentifyConference() {
420 return mUseSubject;
421 }
422
423 public void switchToConversation(Conversation conversation) {
424 switchToConversation(conversation, null, false);
425 }
426
427 public void switchToConversation(Conversation conversation, String text,
428 boolean newTask) {
429 switchToConversation(conversation,text,null,false,newTask);
430 }
431
432 public void highlightInMuc(Conversation conversation, String nick) {
433 switchToConversation(conversation, null, nick, false, false);
434 }
435
436 public void privateMsgInMuc(Conversation conversation, String nick) {
437 switchToConversation(conversation, null, nick, true, false);
438 }
439
440 private void switchToConversation(Conversation conversation, String text, String nick, boolean pm, boolean newTask) {
441 Intent viewConversationIntent = new Intent(this,
442 ConversationActivity.class);
443 viewConversationIntent.setAction(Intent.ACTION_VIEW);
444 viewConversationIntent.putExtra(ConversationActivity.CONVERSATION,
445 conversation.getUuid());
446 if (text != null) {
447 viewConversationIntent.putExtra(ConversationActivity.TEXT, text);
448 }
449 if (nick != null) {
450 viewConversationIntent.putExtra(ConversationActivity.NICK, nick);
451 viewConversationIntent.putExtra(ConversationActivity.PRIVATE_MESSAGE,pm);
452 }
453 viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
454 if (newTask) {
455 viewConversationIntent.setFlags(viewConversationIntent.getFlags()
456 | Intent.FLAG_ACTIVITY_NEW_TASK
457 | Intent.FLAG_ACTIVITY_SINGLE_TOP);
458 } else {
459 viewConversationIntent.setFlags(viewConversationIntent.getFlags()
460 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
461 }
462 startActivity(viewConversationIntent);
463 finish();
464 }
465
466 public void switchToContactDetails(Contact contact) {
467 switchToContactDetails(contact, null);
468 }
469
470 public void switchToContactDetails(Contact contact, String messageFingerprint) {
471 Intent intent = new Intent(this, ContactDetailsActivity.class);
472 intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
473 intent.putExtra(EXTRA_ACCOUNT, contact.getAccount().getJid().toBareJid().toString());
474 intent.putExtra("contact", contact.getJid().toString());
475 intent.putExtra("fingerprint", messageFingerprint);
476 startActivity(intent);
477 }
478
479 public void switchToAccount(Account account) {
480 switchToAccount(account, false);
481 }
482
483 public void switchToAccount(Account account, boolean init) {
484 Intent intent = new Intent(this, EditAccountActivity.class);
485 intent.putExtra("jid", account.getJid().toBareJid().toString());
486 intent.putExtra("init", init);
487 startActivity(intent);
488 }
489
490 protected void inviteToConversation(Conversation conversation) {
491 Intent intent = new Intent(getApplicationContext(),
492 ChooseContactActivity.class);
493 List<String> contacts = new ArrayList<>();
494 if (conversation.getMode() == Conversation.MODE_MULTI) {
495 for (MucOptions.User user : conversation.getMucOptions().getUsers(false)) {
496 Jid jid = user.getRealJid();
497 if (jid != null) {
498 contacts.add(jid.toBareJid().toString());
499 }
500 }
501 } else {
502 contacts.add(conversation.getJid().toBareJid().toString());
503 }
504 intent.putExtra("filter_contacts", contacts.toArray(new String[contacts.size()]));
505 intent.putExtra("conversation", conversation.getUuid());
506 intent.putExtra("multiple", true);
507 intent.putExtra("show_enter_jid", true);
508 intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().toBareJid().toString());
509 startActivityForResult(intent, REQUEST_INVITE_TO_CONVERSATION);
510 }
511
512 protected void announcePgp(Account account, final Conversation conversation, final Runnable onSuccess) {
513 if (account.getPgpId() == 0) {
514 choosePgpSignId(account);
515 } else {
516 String status = null;
517 if (manuallyChangePresence()) {
518 status = account.getPresenceStatusMessage();
519 }
520 if (status == null) {
521 status = "";
522 }
523 xmppConnectionService.getPgpEngine().generateSignature(account, status, new UiCallback<Account>() {
524
525 @Override
526 public void userInputRequried(PendingIntent pi, Account account) {
527 try {
528 startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
529 } catch (final SendIntentException ignored) {
530 }
531 }
532
533 @Override
534 public void success(Account account) {
535 xmppConnectionService.databaseBackend.updateAccount(account);
536 xmppConnectionService.sendPresence(account);
537 if (conversation != null) {
538 conversation.setNextEncryption(Message.ENCRYPTION_PGP);
539 xmppConnectionService.databaseBackend.updateConversation(conversation);
540 }
541 if (onSuccess != null) {
542 runOnUiThread(onSuccess);
543 }
544 }
545
546 @Override
547 public void error(int error, Account account) {
548 displayErrorDialog(error);
549 }
550 });
551 }
552 }
553
554 protected boolean noAccountUsesPgp() {
555 if (!hasPgp()) {
556 return true;
557 }
558 for(Account account : xmppConnectionService.getAccounts()) {
559 if (account.getPgpId() != 0) {
560 return false;
561 }
562 }
563 return true;
564 }
565
566 @SuppressWarnings("deprecation")
567 @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
568 protected void setListItemBackgroundOnView(View view) {
569 int sdk = android.os.Build.VERSION.SDK_INT;
570 if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) {
571 view.setBackgroundDrawable(getResources().getDrawable(R.drawable.greybackground));
572 } else {
573 view.setBackground(getResources().getDrawable(R.drawable.greybackground));
574 }
575 }
576
577 protected void choosePgpSignId(Account account) {
578 xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback<Account>() {
579 @Override
580 public void success(Account account1) {
581 }
582
583 @Override
584 public void error(int errorCode, Account object) {
585
586 }
587
588 @Override
589 public void userInputRequried(PendingIntent pi, Account object) {
590 try {
591 startIntentSenderForResult(pi.getIntentSender(),
592 REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0);
593 } catch (final SendIntentException ignored) {
594 }
595 }
596 });
597 }
598
599 protected void displayErrorDialog(final int errorCode) {
600 runOnUiThread(new Runnable() {
601
602 @Override
603 public void run() {
604 AlertDialog.Builder builder = new AlertDialog.Builder(
605 XmppActivity.this);
606 builder.setIconAttribute(android.R.attr.alertDialogIcon);
607 builder.setTitle(getString(R.string.error));
608 builder.setMessage(errorCode);
609 builder.setNeutralButton(R.string.accept, null);
610 builder.create().show();
611 }
612 });
613
614 }
615
616 protected void showAddToRosterDialog(final Conversation conversation) {
617 showAddToRosterDialog(conversation.getContact());
618 }
619
620 protected void showAddToRosterDialog(final Contact contact) {
621 AlertDialog.Builder builder = new AlertDialog.Builder(this);
622 builder.setTitle(contact.getJid().toString());
623 builder.setMessage(getString(R.string.not_in_roster));
624 builder.setNegativeButton(getString(R.string.cancel), null);
625 builder.setPositiveButton(getString(R.string.add_contact),
626 new DialogInterface.OnClickListener() {
627
628 @Override
629 public void onClick(DialogInterface dialog, int which) {
630 final Jid jid = contact.getJid();
631 Account account = contact.getAccount();
632 Contact contact = account.getRoster().getContact(jid);
633 xmppConnectionService.createContact(contact);
634 }
635 });
636 builder.create().show();
637 }
638
639 private void showAskForPresenceDialog(final Contact contact) {
640 AlertDialog.Builder builder = new AlertDialog.Builder(this);
641 builder.setTitle(contact.getJid().toString());
642 builder.setMessage(R.string.request_presence_updates);
643 builder.setNegativeButton(R.string.cancel, null);
644 builder.setPositiveButton(R.string.request_now,
645 new DialogInterface.OnClickListener() {
646
647 @Override
648 public void onClick(DialogInterface dialog, int which) {
649 if (xmppConnectionServiceBound) {
650 xmppConnectionService.sendPresencePacket(contact
651 .getAccount(), xmppConnectionService
652 .getPresenceGenerator()
653 .requestPresenceUpdatesFrom(contact));
654 }
655 }
656 });
657 builder.create().show();
658 }
659
660 private void warnMutalPresenceSubscription(final Conversation conversation,
661 final OnPresenceSelected listener) {
662 AlertDialog.Builder builder = new AlertDialog.Builder(this);
663 builder.setTitle(conversation.getContact().getJid().toString());
664 builder.setMessage(R.string.without_mutual_presence_updates);
665 builder.setNegativeButton(R.string.cancel, null);
666 builder.setPositiveButton(R.string.ignore, new OnClickListener() {
667
668 @Override
669 public void onClick(DialogInterface dialog, int which) {
670 conversation.setNextCounterpart(null);
671 if (listener != null) {
672 listener.onPresenceSelected();
673 }
674 }
675 });
676 builder.create().show();
677 }
678
679 protected void quickEdit(String previousValue, int hint, OnValueEdited callback) {
680 quickEdit(previousValue, callback, hint, false);
681 }
682
683 protected void quickPasswordEdit(String previousValue, OnValueEdited callback) {
684 quickEdit(previousValue, callback, R.string.password, true);
685 }
686
687 @SuppressLint("InflateParams")
688 private void quickEdit(final String previousValue,
689 final OnValueEdited callback,
690 final int hint,
691 boolean password) {
692 AlertDialog.Builder builder = new AlertDialog.Builder(this);
693 View view = getLayoutInflater().inflate(R.layout.quickedit, null);
694 final EditText editor = (EditText) view.findViewById(R.id.editor);
695 OnClickListener mClickListener = new OnClickListener() {
696
697 @Override
698 public void onClick(DialogInterface dialog, int which) {
699 String value = editor.getText().toString();
700 if (!value.equals(previousValue) && value.trim().length() > 0) {
701 callback.onValueEdited(value);
702 }
703 }
704 };
705 if (password) {
706 editor.setInputType(InputType.TYPE_CLASS_TEXT
707 | InputType.TYPE_TEXT_VARIATION_PASSWORD);
708 builder.setPositiveButton(R.string.accept, mClickListener);
709 } else {
710 builder.setPositiveButton(R.string.edit, mClickListener);
711 }
712 if (hint != 0) {
713 editor.setHint(hint);
714 }
715 editor.requestFocus();
716 editor.setText("");
717 if (previousValue != null) {
718 editor.getText().append(previousValue);
719 }
720 builder.setView(view);
721 builder.setNegativeButton(R.string.cancel, null);
722 builder.create().show();
723 }
724
725 protected boolean addFingerprintRow(LinearLayout keys, final Account account, final String fingerprint, boolean highlight, View.OnClickListener onKeyClickedListener) {
726 final XmppAxolotlSession.Trust trust = account.getAxolotlService()
727 .getFingerprintTrust(fingerprint);
728 if (trust == null) {
729 return false;
730 }
731 return addFingerprintRowWithListeners(keys, account, fingerprint, highlight, trust, true,
732 new CompoundButton.OnCheckedChangeListener() {
733 @Override
734 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
735 account.getAxolotlService().setFingerprintTrust(fingerprint,
736 (isChecked) ? XmppAxolotlSession.Trust.TRUSTED :
737 XmppAxolotlSession.Trust.UNTRUSTED);
738 }
739 },
740 new View.OnClickListener() {
741 @Override
742 public void onClick(View v) {
743 account.getAxolotlService().setFingerprintTrust(fingerprint,
744 XmppAxolotlSession.Trust.UNTRUSTED);
745 v.setEnabled(true);
746 }
747 },
748 onKeyClickedListener
749
750 );
751 }
752
753 protected boolean addFingerprintRowWithListeners(LinearLayout keys, final Account account,
754 final String fingerprint,
755 boolean highlight,
756 XmppAxolotlSession.Trust trust,
757 boolean showTag,
758 CompoundButton.OnCheckedChangeListener
759 onCheckedChangeListener,
760 View.OnClickListener onClickListener,
761 View.OnClickListener onKeyClickedListener) {
762 if (trust == XmppAxolotlSession.Trust.COMPROMISED) {
763 return false;
764 }
765 View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false);
766 TextView key = (TextView) view.findViewById(R.id.key);
767 key.setOnClickListener(onKeyClickedListener);
768 TextView keyType = (TextView) view.findViewById(R.id.key_type);
769 keyType.setOnClickListener(onKeyClickedListener);
770 Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust);
771 trustToggle.setVisibility(View.VISIBLE);
772 trustToggle.setOnCheckedChangeListener(onCheckedChangeListener);
773 trustToggle.setOnClickListener(onClickListener);
774 final View.OnLongClickListener purge = new View.OnLongClickListener() {
775 @Override
776 public boolean onLongClick(View v) {
777 showPurgeKeyDialog(account, fingerprint);
778 return true;
779 }
780 };
781 view.setOnLongClickListener(purge);
782 key.setOnLongClickListener(purge);
783 keyType.setOnLongClickListener(purge);
784 boolean x509 = Config.X509_VERIFICATION
785 && (trust == XmppAxolotlSession.Trust.TRUSTED_X509 || trust == XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509);
786 switch (trust) {
787 case UNTRUSTED:
788 case TRUSTED:
789 case TRUSTED_X509:
790 trustToggle.setChecked(trust.trusted(), false);
791 trustToggle.setEnabled(!Config.X509_VERIFICATION || trust != XmppAxolotlSession.Trust.TRUSTED_X509);
792 if (Config.X509_VERIFICATION && trust == XmppAxolotlSession.Trust.TRUSTED_X509) {
793 trustToggle.setOnClickListener(null);
794 }
795 key.setTextColor(getPrimaryTextColor());
796 keyType.setTextColor(getSecondaryTextColor());
797 break;
798 case UNDECIDED:
799 trustToggle.setChecked(false, false);
800 trustToggle.setEnabled(false);
801 key.setTextColor(getPrimaryTextColor());
802 keyType.setTextColor(getSecondaryTextColor());
803 break;
804 case INACTIVE_UNTRUSTED:
805 case INACTIVE_UNDECIDED:
806 trustToggle.setOnClickListener(null);
807 trustToggle.setChecked(false, false);
808 trustToggle.setEnabled(false);
809 key.setTextColor(getTertiaryTextColor());
810 keyType.setTextColor(getTertiaryTextColor());
811 break;
812 case INACTIVE_TRUSTED:
813 case INACTIVE_TRUSTED_X509:
814 trustToggle.setOnClickListener(null);
815 trustToggle.setChecked(true, false);
816 trustToggle.setEnabled(false);
817 key.setTextColor(getTertiaryTextColor());
818 keyType.setTextColor(getTertiaryTextColor());
819 break;
820 }
821
822 if (showTag) {
823 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
824 } else {
825 keyType.setVisibility(View.GONE);
826 }
827 if (highlight) {
828 keyType.setTextColor(getResources().getColor(R.color.accent));
829 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message));
830 } else {
831 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
832 }
833
834 key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)));
835 keys.addView(view);
836 return true;
837 }
838
839 public void showPurgeKeyDialog(final Account account, final String fingerprint) {
840 Builder builder = new Builder(this);
841 builder.setTitle(getString(R.string.purge_key));
842 builder.setIconAttribute(android.R.attr.alertDialogIcon);
843 builder.setMessage(getString(R.string.purge_key_desc_part1)
844 + "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint.substring(2))
845 + "\n\n" + getString(R.string.purge_key_desc_part2));
846 builder.setNegativeButton(getString(R.string.cancel), null);
847 builder.setPositiveButton(getString(R.string.purge_key),
848 new DialogInterface.OnClickListener() {
849 @Override
850 public void onClick(DialogInterface dialog, int which) {
851 account.getAxolotlService().purgeKey(fingerprint);
852 refreshUi();
853 }
854 });
855 builder.create().show();
856 }
857
858 public boolean hasStoragePermission(int requestCode) {
859 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
860 if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
861 requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
862 return false;
863 } else {
864 return true;
865 }
866 } else {
867 return true;
868 }
869 }
870
871 public void selectPresence(final Conversation conversation,
872 final OnPresenceSelected listener) {
873 final Contact contact = conversation.getContact();
874 if (conversation.hasValidOtrSession()) {
875 SessionID id = conversation.getOtrSession().getSessionID();
876 Jid jid;
877 try {
878 jid = Jid.fromString(id.getAccountID() + "/" + id.getUserID());
879 } catch (InvalidJidException e) {
880 jid = null;
881 }
882 conversation.setNextCounterpart(jid);
883 listener.onPresenceSelected();
884 } else if (!contact.showInRoster()) {
885 showAddToRosterDialog(conversation);
886 } else {
887 Presences presences = contact.getPresences();
888 if (presences.size() == 0) {
889 if (!contact.getOption(Contact.Options.TO)
890 && !contact.getOption(Contact.Options.ASKING)
891 && contact.getAccount().getStatus() == Account.State.ONLINE) {
892 showAskForPresenceDialog(contact);
893 } else if (!contact.getOption(Contact.Options.TO)
894 || !contact.getOption(Contact.Options.FROM)) {
895 warnMutalPresenceSubscription(conversation, listener);
896 } else {
897 conversation.setNextCounterpart(null);
898 listener.onPresenceSelected();
899 }
900 } else if (presences.size() == 1) {
901 String presence = presences.asStringArray()[0];
902 try {
903 conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence));
904 } catch (InvalidJidException e) {
905 conversation.setNextCounterpart(null);
906 }
907 listener.onPresenceSelected();
908 } else {
909 final StringBuilder presence = new StringBuilder();
910 AlertDialog.Builder builder = new AlertDialog.Builder(this);
911 builder.setTitle(getString(R.string.choose_presence));
912 final String[] presencesArray = presences.asStringArray();
913 int preselectedPresence = 0;
914 for (int i = 0; i < presencesArray.length; ++i) {
915 if (presencesArray[i].equals(contact.getLastPresence())) {
916 preselectedPresence = i;
917 break;
918 }
919 }
920 presence.append(presencesArray[preselectedPresence]);
921 builder.setSingleChoiceItems(presencesArray,
922 preselectedPresence,
923 new DialogInterface.OnClickListener() {
924
925 @Override
926 public void onClick(DialogInterface dialog,
927 int which) {
928 presence.delete(0, presence.length());
929 presence.append(presencesArray[which]);
930 }
931 });
932 builder.setNegativeButton(R.string.cancel, null);
933 builder.setPositiveButton(R.string.ok, new OnClickListener() {
934
935 @Override
936 public void onClick(DialogInterface dialog, int which) {
937 try {
938 conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence.toString()));
939 } catch (InvalidJidException e) {
940 conversation.setNextCounterpart(null);
941 }
942 listener.onPresenceSelected();
943 }
944 });
945 builder.create().show();
946 }
947 }
948 }
949
950 protected void onActivityResult(int requestCode, int resultCode,
951 final Intent data) {
952 super.onActivityResult(requestCode, resultCode, data);
953 if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
954 mPendingConferenceInvite = ConferenceInvite.parse(data);
955 if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
956 mPendingConferenceInvite.execute(this);
957 mToast = Toast.makeText(this, R.string.creating_conference,Toast.LENGTH_LONG);
958 mToast.show();
959 mPendingConferenceInvite = null;
960 }
961 }
962 }
963
964
965 private UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() {
966 @Override
967 public void success(final Conversation conversation) {
968 runOnUiThread(new Runnable() {
969 @Override
970 public void run() {
971 switchToConversation(conversation);
972 hideToast();
973 }
974 });
975 }
976
977 @Override
978 public void error(final int errorCode, Conversation object) {
979 runOnUiThread(new Runnable() {
980 @Override
981 public void run() {
982 replaceToast(getString(errorCode));
983 }
984 });
985 }
986
987 @Override
988 public void userInputRequried(PendingIntent pi, Conversation object) {
989
990 }
991 };
992
993 public int getTertiaryTextColor() {
994 return this.mTertiaryTextColor;
995 }
996
997 public int getSecondaryTextColor() {
998 return this.mSecondaryTextColor;
999 }
1000
1001 public int getPrimaryTextColor() {
1002 return this.mPrimaryTextColor;
1003 }
1004
1005 public int getWarningTextColor() {
1006 return this.mColorRed;
1007 }
1008
1009 public int getOnlineColor() {
1010 return this.mColorGreen;
1011 }
1012
1013 public int getPrimaryBackgroundColor() {
1014 return this.mPrimaryBackgroundColor;
1015 }
1016
1017 public int getSecondaryBackgroundColor() {
1018 return this.mSecondaryBackgroundColor;
1019 }
1020
1021 public int getPixel(int dp) {
1022 DisplayMetrics metrics = getResources().getDisplayMetrics();
1023 return ((int) (dp * metrics.density));
1024 }
1025
1026 public boolean copyTextToClipboard(String text, int labelResId) {
1027 ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
1028 String label = getResources().getString(labelResId);
1029 if (mClipBoardManager != null) {
1030 ClipData mClipData = ClipData.newPlainText(label, text);
1031 mClipBoardManager.setPrimaryClip(mClipData);
1032 return true;
1033 }
1034 return false;
1035 }
1036
1037 protected void registerNdefPushMessageCallback() {
1038 NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
1039 if (nfcAdapter != null && nfcAdapter.isEnabled()) {
1040 nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
1041 @Override
1042 public NdefMessage createNdefMessage(NfcEvent nfcEvent) {
1043 return new NdefMessage(new NdefRecord[]{
1044 NdefRecord.createUri(getShareableUri()),
1045 NdefRecord.createApplicationRecord("eu.siacs.conversations")
1046 });
1047 }
1048 }, this);
1049 }
1050 }
1051
1052 protected boolean neverCompressPictures() {
1053 return getPreferences().getString("picture_compression", "auto").equals("never");
1054 }
1055
1056 protected boolean manuallyChangePresence() {
1057 return getPreferences().getBoolean("manually_change_presence", false);
1058 }
1059
1060 protected void unregisterNdefPushMessageCallback() {
1061 NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
1062 if (nfcAdapter != null && nfcAdapter.isEnabled()) {
1063 nfcAdapter.setNdefPushMessageCallback(null,this);
1064 }
1065 }
1066
1067 protected String getShareableUri() {
1068 return null;
1069 }
1070
1071 @Override
1072 public void onResume() {
1073 super.onResume();
1074 if (this.getShareableUri()!=null) {
1075 this.registerNdefPushMessageCallback();
1076 }
1077 }
1078
1079 protected int findTheme() {
1080 if (getPreferences().getBoolean("use_larger_font", false)) {
1081 return R.style.ConversationsTheme_LargerText;
1082 } else {
1083 return R.style.ConversationsTheme;
1084 }
1085 }
1086
1087 @Override
1088 public void onPause() {
1089 super.onPause();
1090 this.unregisterNdefPushMessageCallback();
1091 }
1092
1093 protected void showQrCode() {
1094 String uri = getShareableUri();
1095 if (uri!=null) {
1096 Point size = new Point();
1097 getWindowManager().getDefaultDisplay().getSize(size);
1098 final int width = (size.x < size.y ? size.x : size.y);
1099 Bitmap bitmap = createQrCodeBitmap(uri, width);
1100 ImageView view = new ImageView(this);
1101 view.setImageBitmap(bitmap);
1102 AlertDialog.Builder builder = new AlertDialog.Builder(this);
1103 builder.setView(view);
1104 builder.create().show();
1105 }
1106 }
1107
1108 protected Bitmap createQrCodeBitmap(String input, int size) {
1109 Log.d(Config.LOGTAG,"qr code requested size: "+size);
1110 try {
1111 final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter();
1112 final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
1113 hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
1114 final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, size, size, hints);
1115 final int width = result.getWidth();
1116 final int height = result.getHeight();
1117 final int[] pixels = new int[width * height];
1118 for (int y = 0; y < height; y++) {
1119 final int offset = y * width;
1120 for (int x = 0; x < width; x++) {
1121 pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT;
1122 }
1123 }
1124 final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
1125 Log.d(Config.LOGTAG,"output size: "+width+"x"+height);
1126 bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
1127 return bitmap;
1128 } catch (final WriterException e) {
1129 return null;
1130 }
1131 }
1132
1133 protected Account extractAccount(Intent intent) {
1134 String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null;
1135 try {
1136 return jid != null ? xmppConnectionService.findAccountByJid(Jid.fromString(jid)) : null;
1137 } catch (InvalidJidException e) {
1138 return null;
1139 }
1140 }
1141
1142 public static class ConferenceInvite {
1143 private String uuid;
1144 private List<Jid> jids = new ArrayList<>();
1145
1146 public static ConferenceInvite parse(Intent data) {
1147 ConferenceInvite invite = new ConferenceInvite();
1148 invite.uuid = data.getStringExtra("conversation");
1149 if (invite.uuid == null) {
1150 return null;
1151 }
1152 try {
1153 if (data.getBooleanExtra("multiple", false)) {
1154 String[] toAdd = data.getStringArrayExtra("contacts");
1155 for (String item : toAdd) {
1156 invite.jids.add(Jid.fromString(item));
1157 }
1158 } else {
1159 invite.jids.add(Jid.fromString(data.getStringExtra("contact")));
1160 }
1161 } catch (final InvalidJidException ignored) {
1162 return null;
1163 }
1164 return invite;
1165 }
1166
1167 public void execute(XmppActivity activity) {
1168 XmppConnectionService service = activity.xmppConnectionService;
1169 Conversation conversation = service.findConversationByUuid(this.uuid);
1170 if (conversation == null) {
1171 return;
1172 }
1173 if (conversation.getMode() == Conversation.MODE_MULTI) {
1174 for (Jid jid : jids) {
1175 service.invite(conversation, jid);
1176 }
1177 } else {
1178 jids.add(conversation.getJid().toBareJid());
1179 service.createAdhocConference(conversation.getAccount(), null, jids, activity.adhocCallback);
1180 }
1181 }
1182 }
1183
1184 public AvatarService avatarService() {
1185 return xmppConnectionService.getAvatarService();
1186 }
1187
1188 class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
1189 private final WeakReference<ImageView> imageViewReference;
1190 private Message message = null;
1191
1192 public BitmapWorkerTask(ImageView imageView) {
1193 imageViewReference = new WeakReference<>(imageView);
1194 }
1195
1196 @Override
1197 protected Bitmap doInBackground(Message... params) {
1198 if (isCancelled()) {
1199 return null;
1200 }
1201 message = params[0];
1202 try {
1203 return xmppConnectionService.getFileBackend().getThumbnail(
1204 message, (int) (metrics.density * 288), false);
1205 } catch (FileNotFoundException e) {
1206 return null;
1207 }
1208 }
1209
1210 @Override
1211 protected void onPostExecute(Bitmap bitmap) {
1212 if (bitmap != null && !isCancelled()) {
1213 final ImageView imageView = imageViewReference.get();
1214 if (imageView != null) {
1215 imageView.setImageBitmap(bitmap);
1216 imageView.setBackgroundColor(0x00000000);
1217 }
1218 }
1219 }
1220 }
1221
1222 public void loadBitmap(Message message, ImageView imageView) {
1223 Bitmap bm;
1224 try {
1225 bm = xmppConnectionService.getFileBackend().getThumbnail(message,
1226 (int) (metrics.density * 288), true);
1227 } catch (FileNotFoundException e) {
1228 bm = null;
1229 }
1230 if (bm != null) {
1231 cancelPotentialWork(message, imageView);
1232 imageView.setImageBitmap(bm);
1233 imageView.setBackgroundColor(0x00000000);
1234 } else {
1235 if (cancelPotentialWork(message, imageView)) {
1236 imageView.setBackgroundColor(0xff333333);
1237 imageView.setImageDrawable(null);
1238 final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
1239 final AsyncDrawable asyncDrawable = new AsyncDrawable(
1240 getResources(), null, task);
1241 imageView.setImageDrawable(asyncDrawable);
1242 try {
1243 task.execute(message);
1244 } catch (final RejectedExecutionException ignored) {
1245 ignored.printStackTrace();
1246 }
1247 }
1248 }
1249 }
1250
1251 public static boolean cancelPotentialWork(Message message, ImageView imageView) {
1252 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
1253
1254 if (bitmapWorkerTask != null) {
1255 final Message oldMessage = bitmapWorkerTask.message;
1256 if (oldMessage == null || message != oldMessage) {
1257 bitmapWorkerTask.cancel(true);
1258 } else {
1259 return false;
1260 }
1261 }
1262 return true;
1263 }
1264
1265 private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
1266 if (imageView != null) {
1267 final Drawable drawable = imageView.getDrawable();
1268 if (drawable instanceof AsyncDrawable) {
1269 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
1270 return asyncDrawable.getBitmapWorkerTask();
1271 }
1272 }
1273 return null;
1274 }
1275
1276 static class AsyncDrawable extends BitmapDrawable {
1277 private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
1278
1279 public AsyncDrawable(Resources res, Bitmap bitmap,
1280 BitmapWorkerTask bitmapWorkerTask) {
1281 super(res, bitmap);
1282 bitmapWorkerTaskReference = new WeakReference<>(
1283 bitmapWorkerTask);
1284 }
1285
1286 public BitmapWorkerTask getBitmapWorkerTask() {
1287 return bitmapWorkerTaskReference.get();
1288 }
1289 }
1290}