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 = Config.X509_VERIFICATION
741 && (trust == XmppAxolotlSession.Trust.TRUSTED_X509 || trust == XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509);
742 switch (trust) {
743 case UNTRUSTED:
744 case TRUSTED:
745 case TRUSTED_X509:
746 trustToggle.setChecked(trust.trusted(), false);
747 trustToggle.setEnabled(!Config.X509_VERIFICATION || trust != XmppAxolotlSession.Trust.TRUSTED_X509);
748 if (Config.X509_VERIFICATION && trust == XmppAxolotlSession.Trust.TRUSTED_X509) {
749 trustToggle.setOnClickListener(null);
750 }
751 key.setTextColor(getPrimaryTextColor());
752 keyType.setTextColor(getSecondaryTextColor());
753 break;
754 case UNDECIDED:
755 trustToggle.setChecked(false, false);
756 trustToggle.setEnabled(false);
757 key.setTextColor(getPrimaryTextColor());
758 keyType.setTextColor(getSecondaryTextColor());
759 break;
760 case INACTIVE_UNTRUSTED:
761 case INACTIVE_UNDECIDED:
762 trustToggle.setOnClickListener(null);
763 trustToggle.setChecked(false, false);
764 trustToggle.setEnabled(false);
765 key.setTextColor(getTertiaryTextColor());
766 keyType.setTextColor(getTertiaryTextColor());
767 break;
768 case INACTIVE_TRUSTED:
769 case INACTIVE_TRUSTED_X509:
770 trustToggle.setOnClickListener(null);
771 trustToggle.setChecked(true, false);
772 trustToggle.setEnabled(false);
773 key.setTextColor(getTertiaryTextColor());
774 keyType.setTextColor(getTertiaryTextColor());
775 break;
776 }
777
778 if (showTag) {
779 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
780 } else {
781 keyType.setVisibility(View.GONE);
782 }
783 if (highlight) {
784 keyType.setTextColor(getResources().getColor(R.color.accent));
785 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message));
786 } else {
787 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
788 }
789
790 key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)));
791 keys.addView(view);
792 return true;
793 }
794
795 public void showPurgeKeyDialog(final Account account, final String fingerprint) {
796 Builder builder = new Builder(this);
797 builder.setTitle(getString(R.string.purge_key));
798 builder.setIconAttribute(android.R.attr.alertDialogIcon);
799 builder.setMessage(getString(R.string.purge_key_desc_part1)
800 + "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint.substring(2))
801 + "\n\n" + getString(R.string.purge_key_desc_part2));
802 builder.setNegativeButton(getString(R.string.cancel), null);
803 builder.setPositiveButton(getString(R.string.purge_key),
804 new DialogInterface.OnClickListener() {
805 @Override
806 public void onClick(DialogInterface dialog, int which) {
807 account.getAxolotlService().purgeKey(fingerprint);
808 refreshUi();
809 }
810 });
811 builder.create().show();
812 }
813
814 public boolean hasStoragePermission(int requestCode) {
815 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
816 if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
817 requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
818 return false;
819 } else {
820 return true;
821 }
822 } else {
823 return true;
824 }
825 }
826
827 public void selectPresence(final Conversation conversation,
828 final OnPresenceSelected listener) {
829 final Contact contact = conversation.getContact();
830 if (conversation.hasValidOtrSession()) {
831 SessionID id = conversation.getOtrSession().getSessionID();
832 Jid jid;
833 try {
834 jid = Jid.fromString(id.getAccountID() + "/" + id.getUserID());
835 } catch (InvalidJidException e) {
836 jid = null;
837 }
838 conversation.setNextCounterpart(jid);
839 listener.onPresenceSelected();
840 } else if (!contact.showInRoster()) {
841 showAddToRosterDialog(conversation);
842 } else {
843 Presences presences = contact.getPresences();
844 if (presences.size() == 0) {
845 if (!contact.getOption(Contact.Options.TO)
846 && !contact.getOption(Contact.Options.ASKING)
847 && contact.getAccount().getStatus() == Account.State.ONLINE) {
848 showAskForPresenceDialog(contact);
849 } else if (!contact.getOption(Contact.Options.TO)
850 || !contact.getOption(Contact.Options.FROM)) {
851 warnMutalPresenceSubscription(conversation, listener);
852 } else {
853 conversation.setNextCounterpart(null);
854 listener.onPresenceSelected();
855 }
856 } else if (presences.size() == 1) {
857 String presence = presences.asStringArray()[0];
858 try {
859 conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence));
860 } catch (InvalidJidException e) {
861 conversation.setNextCounterpart(null);
862 }
863 listener.onPresenceSelected();
864 } else {
865 final StringBuilder presence = new StringBuilder();
866 AlertDialog.Builder builder = new AlertDialog.Builder(this);
867 builder.setTitle(getString(R.string.choose_presence));
868 final String[] presencesArray = presences.asStringArray();
869 int preselectedPresence = 0;
870 for (int i = 0; i < presencesArray.length; ++i) {
871 if (presencesArray[i].equals(contact.lastseen.presence)) {
872 preselectedPresence = i;
873 break;
874 }
875 }
876 presence.append(presencesArray[preselectedPresence]);
877 builder.setSingleChoiceItems(presencesArray,
878 preselectedPresence,
879 new DialogInterface.OnClickListener() {
880
881 @Override
882 public void onClick(DialogInterface dialog,
883 int which) {
884 presence.delete(0, presence.length());
885 presence.append(presencesArray[which]);
886 }
887 });
888 builder.setNegativeButton(R.string.cancel, null);
889 builder.setPositiveButton(R.string.ok, new OnClickListener() {
890
891 @Override
892 public void onClick(DialogInterface dialog, int which) {
893 try {
894 conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence.toString()));
895 } catch (InvalidJidException e) {
896 conversation.setNextCounterpart(null);
897 }
898 listener.onPresenceSelected();
899 }
900 });
901 builder.create().show();
902 }
903 }
904 }
905
906 protected void onActivityResult(int requestCode, int resultCode,
907 final Intent data) {
908 super.onActivityResult(requestCode, resultCode, data);
909 if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
910 mPendingConferenceInvite = ConferenceInvite.parse(data);
911 if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
912 mPendingConferenceInvite.execute(this);
913 mPendingConferenceInvite = null;
914 }
915 }
916 }
917
918 private UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() {
919 @Override
920 public void success(final Conversation conversation) {
921 switchToConversation(conversation);
922 runOnUiThread(new Runnable() {
923 @Override
924 public void run() {
925 Toast.makeText(XmppActivity.this,R.string.conference_created,Toast.LENGTH_LONG).show();
926 }
927 });
928 }
929
930 @Override
931 public void error(final int errorCode, Conversation object) {
932 runOnUiThread(new Runnable() {
933 @Override
934 public void run() {
935 Toast.makeText(XmppActivity.this,errorCode,Toast.LENGTH_LONG).show();
936 }
937 });
938 }
939
940 @Override
941 public void userInputRequried(PendingIntent pi, Conversation object) {
942
943 }
944 };
945
946 public int getTertiaryTextColor() {
947 return this.mTertiaryTextColor;
948 }
949
950 public int getSecondaryTextColor() {
951 return this.mSecondaryTextColor;
952 }
953
954 public int getPrimaryTextColor() {
955 return this.mPrimaryTextColor;
956 }
957
958 public int getWarningTextColor() {
959 return this.mColorRed;
960 }
961
962 public int getOnlineColor() {
963 return this.mColorGreen;
964 }
965
966 public int getPrimaryBackgroundColor() {
967 return this.mPrimaryBackgroundColor;
968 }
969
970 public int getSecondaryBackgroundColor() {
971 return this.mSecondaryBackgroundColor;
972 }
973
974 public int getPixel(int dp) {
975 DisplayMetrics metrics = getResources().getDisplayMetrics();
976 return ((int) (dp * metrics.density));
977 }
978
979 public boolean copyTextToClipboard(String text, int labelResId) {
980 ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
981 String label = getResources().getString(labelResId);
982 if (mClipBoardManager != null) {
983 ClipData mClipData = ClipData.newPlainText(label, text);
984 mClipBoardManager.setPrimaryClip(mClipData);
985 return true;
986 }
987 return false;
988 }
989
990 protected void registerNdefPushMessageCallback() {
991 NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
992 if (nfcAdapter != null && nfcAdapter.isEnabled()) {
993 nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
994 @Override
995 public NdefMessage createNdefMessage(NfcEvent nfcEvent) {
996 return new NdefMessage(new NdefRecord[]{
997 NdefRecord.createUri(getShareableUri()),
998 NdefRecord.createApplicationRecord("eu.siacs.conversations")
999 });
1000 }
1001 }, this);
1002 }
1003 }
1004
1005 protected boolean neverCompressPictures() {
1006 return getPreferences().getString("picture_compression", "auto").equals("never");
1007 }
1008
1009 protected void unregisterNdefPushMessageCallback() {
1010 NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
1011 if (nfcAdapter != null && nfcAdapter.isEnabled()) {
1012 nfcAdapter.setNdefPushMessageCallback(null,this);
1013 }
1014 }
1015
1016 protected String getShareableUri() {
1017 return null;
1018 }
1019
1020 @Override
1021 public void onResume() {
1022 super.onResume();
1023 if (this.getShareableUri()!=null) {
1024 this.registerNdefPushMessageCallback();
1025 }
1026 }
1027
1028 protected int findTheme() {
1029 if (getPreferences().getBoolean("use_larger_font", false)) {
1030 return R.style.ConversationsTheme_LargerText;
1031 } else {
1032 return R.style.ConversationsTheme;
1033 }
1034 }
1035
1036 @Override
1037 public void onPause() {
1038 super.onPause();
1039 this.unregisterNdefPushMessageCallback();
1040 }
1041
1042 protected void showQrCode() {
1043 String uri = getShareableUri();
1044 if (uri!=null) {
1045 Point size = new Point();
1046 getWindowManager().getDefaultDisplay().getSize(size);
1047 final int width = (size.x < size.y ? size.x : size.y);
1048 Bitmap bitmap = createQrCodeBitmap(uri, width);
1049 ImageView view = new ImageView(this);
1050 view.setImageBitmap(bitmap);
1051 AlertDialog.Builder builder = new AlertDialog.Builder(this);
1052 builder.setView(view);
1053 builder.create().show();
1054 }
1055 }
1056
1057 protected Bitmap createQrCodeBitmap(String input, int size) {
1058 Log.d(Config.LOGTAG,"qr code requested size: "+size);
1059 try {
1060 final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter();
1061 final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
1062 hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
1063 final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, size, size, hints);
1064 final int width = result.getWidth();
1065 final int height = result.getHeight();
1066 final int[] pixels = new int[width * height];
1067 for (int y = 0; y < height; y++) {
1068 final int offset = y * width;
1069 for (int x = 0; x < width; x++) {
1070 pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT;
1071 }
1072 }
1073 final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
1074 Log.d(Config.LOGTAG,"output size: "+width+"x"+height);
1075 bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
1076 return bitmap;
1077 } catch (final WriterException e) {
1078 return null;
1079 }
1080 }
1081
1082 protected Account extractAccount(Intent intent) {
1083 String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null;
1084 try {
1085 return jid != null ? xmppConnectionService.findAccountByJid(Jid.fromString(jid)) : null;
1086 } catch (InvalidJidException e) {
1087 return null;
1088 }
1089 }
1090
1091 public static class ConferenceInvite {
1092 private String uuid;
1093 private List<Jid> jids = new ArrayList<>();
1094
1095 public static ConferenceInvite parse(Intent data) {
1096 ConferenceInvite invite = new ConferenceInvite();
1097 invite.uuid = data.getStringExtra("conversation");
1098 if (invite.uuid == null) {
1099 return null;
1100 }
1101 try {
1102 if (data.getBooleanExtra("multiple", false)) {
1103 String[] toAdd = data.getStringArrayExtra("contacts");
1104 for (String item : toAdd) {
1105 invite.jids.add(Jid.fromString(item));
1106 }
1107 } else {
1108 invite.jids.add(Jid.fromString(data.getStringExtra("contact")));
1109 }
1110 } catch (final InvalidJidException ignored) {
1111 return null;
1112 }
1113 return invite;
1114 }
1115
1116 public void execute(XmppActivity activity) {
1117 XmppConnectionService service = activity.xmppConnectionService;
1118 Conversation conversation = service.findConversationByUuid(this.uuid);
1119 if (conversation == null) {
1120 return;
1121 }
1122 if (conversation.getMode() == Conversation.MODE_MULTI) {
1123 for (Jid jid : jids) {
1124 service.invite(conversation, jid);
1125 }
1126 } else {
1127 jids.add(conversation.getJid().toBareJid());
1128 service.createAdhocConference(conversation.getAccount(), jids, activity.adhocCallback);
1129 }
1130 }
1131 }
1132
1133 public AvatarService avatarService() {
1134 return xmppConnectionService.getAvatarService();
1135 }
1136
1137 class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
1138 private final WeakReference<ImageView> imageViewReference;
1139 private Message message = null;
1140
1141 public BitmapWorkerTask(ImageView imageView) {
1142 imageViewReference = new WeakReference<>(imageView);
1143 }
1144
1145 @Override
1146 protected Bitmap doInBackground(Message... params) {
1147 message = params[0];
1148 try {
1149 return xmppConnectionService.getFileBackend().getThumbnail(
1150 message, (int) (metrics.density * 288), false);
1151 } catch (FileNotFoundException e) {
1152 return null;
1153 }
1154 }
1155
1156 @Override
1157 protected void onPostExecute(Bitmap bitmap) {
1158 if (bitmap != null) {
1159 final ImageView imageView = imageViewReference.get();
1160 if (imageView != null) {
1161 imageView.setImageBitmap(bitmap);
1162 imageView.setBackgroundColor(0x00000000);
1163 }
1164 }
1165 }
1166 }
1167
1168 public void loadBitmap(Message message, ImageView imageView) {
1169 Bitmap bm;
1170 try {
1171 bm = xmppConnectionService.getFileBackend().getThumbnail(message,
1172 (int) (metrics.density * 288), true);
1173 } catch (FileNotFoundException e) {
1174 bm = null;
1175 }
1176 if (bm != null) {
1177 imageView.setImageBitmap(bm);
1178 imageView.setBackgroundColor(0x00000000);
1179 } else {
1180 if (cancelPotentialWork(message, imageView)) {
1181 imageView.setBackgroundColor(0xff333333);
1182 imageView.setImageDrawable(null);
1183 final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
1184 final AsyncDrawable asyncDrawable = new AsyncDrawable(
1185 getResources(), null, task);
1186 imageView.setImageDrawable(asyncDrawable);
1187 try {
1188 task.execute(message);
1189 } catch (final RejectedExecutionException ignored) {
1190 }
1191 }
1192 }
1193 }
1194
1195 public static boolean cancelPotentialWork(Message message,
1196 ImageView imageView) {
1197 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
1198
1199 if (bitmapWorkerTask != null) {
1200 final Message oldMessage = bitmapWorkerTask.message;
1201 if (oldMessage == null || message != oldMessage) {
1202 bitmapWorkerTask.cancel(true);
1203 } else {
1204 return false;
1205 }
1206 }
1207 return true;
1208 }
1209
1210 private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
1211 if (imageView != null) {
1212 final Drawable drawable = imageView.getDrawable();
1213 if (drawable instanceof AsyncDrawable) {
1214 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
1215 return asyncDrawable.getBitmapWorkerTask();
1216 }
1217 }
1218 return null;
1219 }
1220
1221 static class AsyncDrawable extends BitmapDrawable {
1222 private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
1223
1224 public AsyncDrawable(Resources res, Bitmap bitmap,
1225 BitmapWorkerTask bitmapWorkerTask) {
1226 super(res, bitmap);
1227 bitmapWorkerTaskReference = new WeakReference<>(
1228 bitmapWorkerTask);
1229 }
1230
1231 public BitmapWorkerTask getBitmapWorkerTask() {
1232 return bitmapWorkerTaskReference.get();
1233 }
1234 }
1235}