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