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