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 protected boolean isOptimizingBattery() {
382 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
383 PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
384 return !pm.isIgnoringBatteryOptimizations(getPackageName());
385 } else {
386 return false;
387 }
388 }
389
390 protected boolean usingEnterKey() {
391 return getPreferences().getBoolean("display_enter_key", false);
392 }
393
394 protected SharedPreferences getPreferences() {
395 return PreferenceManager
396 .getDefaultSharedPreferences(getApplicationContext());
397 }
398
399 public boolean useSubjectToIdentifyConference() {
400 return mUseSubject;
401 }
402
403 public void switchToConversation(Conversation conversation) {
404 switchToConversation(conversation, null, false);
405 }
406
407 public void switchToConversation(Conversation conversation, String text,
408 boolean newTask) {
409 switchToConversation(conversation,text,null,false,newTask);
410 }
411
412 public void highlightInMuc(Conversation conversation, String nick) {
413 switchToConversation(conversation, null, nick, false, false);
414 }
415
416 public void privateMsgInMuc(Conversation conversation, String nick) {
417 switchToConversation(conversation, null, nick, true, false);
418 }
419
420 private void switchToConversation(Conversation conversation, String text, String nick, boolean pm, boolean newTask) {
421 Intent viewConversationIntent = new Intent(this,
422 ConversationActivity.class);
423 viewConversationIntent.setAction(Intent.ACTION_VIEW);
424 viewConversationIntent.putExtra(ConversationActivity.CONVERSATION,
425 conversation.getUuid());
426 if (text != null) {
427 viewConversationIntent.putExtra(ConversationActivity.TEXT, text);
428 }
429 if (nick != null) {
430 viewConversationIntent.putExtra(ConversationActivity.NICK, nick);
431 viewConversationIntent.putExtra(ConversationActivity.PRIVATE_MESSAGE,pm);
432 }
433 viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
434 if (newTask) {
435 viewConversationIntent.setFlags(viewConversationIntent.getFlags()
436 | Intent.FLAG_ACTIVITY_NEW_TASK
437 | Intent.FLAG_ACTIVITY_SINGLE_TOP);
438 } else {
439 viewConversationIntent.setFlags(viewConversationIntent.getFlags()
440 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
441 }
442 startActivity(viewConversationIntent);
443 finish();
444 }
445
446 public void switchToContactDetails(Contact contact) {
447 switchToContactDetails(contact, null);
448 }
449
450 public void switchToContactDetails(Contact contact, String messageFingerprint) {
451 Intent intent = new Intent(this, ContactDetailsActivity.class);
452 intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
453 intent.putExtra(EXTRA_ACCOUNT, contact.getAccount().getJid().toBareJid().toString());
454 intent.putExtra("contact", contact.getJid().toString());
455 intent.putExtra("fingerprint", messageFingerprint);
456 startActivity(intent);
457 }
458
459 public void switchToAccount(Account account) {
460 switchToAccount(account, false);
461 }
462
463 public void switchToAccount(Account account, boolean init) {
464 Intent intent = new Intent(this, EditAccountActivity.class);
465 intent.putExtra("jid", account.getJid().toBareJid().toString());
466 intent.putExtra("init", init);
467 startActivity(intent);
468 }
469
470 protected void inviteToConversation(Conversation conversation) {
471 Intent intent = new Intent(getApplicationContext(),
472 ChooseContactActivity.class);
473 List<String> contacts = new ArrayList<>();
474 if (conversation.getMode() == Conversation.MODE_MULTI) {
475 for (MucOptions.User user : conversation.getMucOptions().getUsers()) {
476 Jid jid = user.getJid();
477 if (jid != null) {
478 contacts.add(jid.toBareJid().toString());
479 }
480 }
481 } else {
482 contacts.add(conversation.getJid().toBareJid().toString());
483 }
484 intent.putExtra("filter_contacts", contacts.toArray(new String[contacts.size()]));
485 intent.putExtra("conversation", conversation.getUuid());
486 intent.putExtra("multiple", true);
487 intent.putExtra("show_enter_jid", true);
488 intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().toBareJid().toString());
489 startActivityForResult(intent, REQUEST_INVITE_TO_CONVERSATION);
490 }
491
492 protected void announcePgp(Account account, final Conversation conversation) {
493 if (account.getPgpId() == -1) {
494 choosePgpSignId(account);
495 } else {
496 xmppConnectionService.getPgpEngine().generateSignature(account, "", new UiCallback<Account>() {
497
498 @Override
499 public void userInputRequried(PendingIntent pi,
500 Account account) {
501 try {
502 startIntentSenderForResult(pi.getIntentSender(),
503 REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
504 } catch (final SendIntentException ignored) {
505 }
506 }
507
508 @Override
509 public void success(Account account) {
510 xmppConnectionService.databaseBackend.updateAccount(account);
511 xmppConnectionService.sendPresence(account);
512 if (conversation != null) {
513 conversation.setNextEncryption(Message.ENCRYPTION_PGP);
514 xmppConnectionService.databaseBackend.updateConversation(conversation);
515 }
516 }
517
518 @Override
519 public void error(int error, Account account) {
520 displayErrorDialog(error);
521 }
522 });
523 }
524 }
525
526 @SuppressWarnings("deprecation")
527 @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
528 protected void setListItemBackgroundOnView(View view) {
529 int sdk = android.os.Build.VERSION.SDK_INT;
530 if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) {
531 view.setBackgroundDrawable(getResources().getDrawable(R.drawable.greybackground));
532 } else {
533 view.setBackground(getResources().getDrawable(R.drawable.greybackground));
534 }
535 }
536
537 protected void choosePgpSignId(Account account) {
538 xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback<Account>() {
539 @Override
540 public void success(Account account1) {
541 }
542
543 @Override
544 public void error(int errorCode, Account object) {
545
546 }
547
548 @Override
549 public void userInputRequried(PendingIntent pi, Account object) {
550 try {
551 startIntentSenderForResult(pi.getIntentSender(),
552 REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0);
553 } catch (final SendIntentException ignored) {
554 }
555 }
556 });
557 }
558
559 protected void displayErrorDialog(final int errorCode) {
560 runOnUiThread(new Runnable() {
561
562 @Override
563 public void run() {
564 AlertDialog.Builder builder = new AlertDialog.Builder(
565 XmppActivity.this);
566 builder.setIconAttribute(android.R.attr.alertDialogIcon);
567 builder.setTitle(getString(R.string.error));
568 builder.setMessage(errorCode);
569 builder.setNeutralButton(R.string.accept, null);
570 builder.create().show();
571 }
572 });
573
574 }
575
576 protected void showAddToRosterDialog(final Conversation conversation) {
577 showAddToRosterDialog(conversation.getContact());
578 }
579
580 protected void showAddToRosterDialog(final Contact contact) {
581 AlertDialog.Builder builder = new AlertDialog.Builder(this);
582 builder.setTitle(contact.getJid().toString());
583 builder.setMessage(getString(R.string.not_in_roster));
584 builder.setNegativeButton(getString(R.string.cancel), null);
585 builder.setPositiveButton(getString(R.string.add_contact),
586 new DialogInterface.OnClickListener() {
587
588 @Override
589 public void onClick(DialogInterface dialog, int which) {
590 final Jid jid = contact.getJid();
591 Account account = contact.getAccount();
592 Contact contact = account.getRoster().getContact(jid);
593 xmppConnectionService.createContact(contact);
594 }
595 });
596 builder.create().show();
597 }
598
599 private void showAskForPresenceDialog(final Contact contact) {
600 AlertDialog.Builder builder = new AlertDialog.Builder(this);
601 builder.setTitle(contact.getJid().toString());
602 builder.setMessage(R.string.request_presence_updates);
603 builder.setNegativeButton(R.string.cancel, null);
604 builder.setPositiveButton(R.string.request_now,
605 new DialogInterface.OnClickListener() {
606
607 @Override
608 public void onClick(DialogInterface dialog, int which) {
609 if (xmppConnectionServiceBound) {
610 xmppConnectionService.sendPresencePacket(contact
611 .getAccount(), xmppConnectionService
612 .getPresenceGenerator()
613 .requestPresenceUpdatesFrom(contact));
614 }
615 }
616 });
617 builder.create().show();
618 }
619
620 private void warnMutalPresenceSubscription(final Conversation conversation,
621 final OnPresenceSelected listener) {
622 AlertDialog.Builder builder = new AlertDialog.Builder(this);
623 builder.setTitle(conversation.getContact().getJid().toString());
624 builder.setMessage(R.string.without_mutual_presence_updates);
625 builder.setNegativeButton(R.string.cancel, null);
626 builder.setPositiveButton(R.string.ignore, new OnClickListener() {
627
628 @Override
629 public void onClick(DialogInterface dialog, int which) {
630 conversation.setNextCounterpart(null);
631 if (listener != null) {
632 listener.onPresenceSelected();
633 }
634 }
635 });
636 builder.create().show();
637 }
638
639 protected void quickEdit(String previousValue, OnValueEdited callback) {
640 quickEdit(previousValue, callback, false);
641 }
642
643 protected void quickPasswordEdit(String previousValue,
644 OnValueEdited callback) {
645 quickEdit(previousValue, callback, true);
646 }
647
648 @SuppressLint("InflateParams")
649 private void quickEdit(final String previousValue,
650 final OnValueEdited callback, boolean password) {
651 AlertDialog.Builder builder = new AlertDialog.Builder(this);
652 View view = getLayoutInflater().inflate(R.layout.quickedit, null);
653 final EditText editor = (EditText) view.findViewById(R.id.editor);
654 OnClickListener mClickListener = new OnClickListener() {
655
656 @Override
657 public void onClick(DialogInterface dialog, int which) {
658 String value = editor.getText().toString();
659 if (!previousValue.equals(value) && value.trim().length() > 0) {
660 callback.onValueEdited(value);
661 }
662 }
663 };
664 if (password) {
665 editor.setInputType(InputType.TYPE_CLASS_TEXT
666 | InputType.TYPE_TEXT_VARIATION_PASSWORD);
667 editor.setHint(R.string.password);
668 builder.setPositiveButton(R.string.accept, mClickListener);
669 } else {
670 builder.setPositiveButton(R.string.edit, mClickListener);
671 }
672 editor.requestFocus();
673 editor.setText(previousValue);
674 builder.setView(view);
675 builder.setNegativeButton(R.string.cancel, null);
676 builder.create().show();
677 }
678
679 protected boolean addFingerprintRow(LinearLayout keys, final Account account, final String fingerprint, boolean highlight, View.OnClickListener onKeyClickedListener) {
680 final XmppAxolotlSession.Trust trust = account.getAxolotlService()
681 .getFingerprintTrust(fingerprint);
682 if (trust == null) {
683 return false;
684 }
685 return addFingerprintRowWithListeners(keys, account, fingerprint, highlight, trust, true,
686 new CompoundButton.OnCheckedChangeListener() {
687 @Override
688 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
689 account.getAxolotlService().setFingerprintTrust(fingerprint,
690 (isChecked) ? XmppAxolotlSession.Trust.TRUSTED :
691 XmppAxolotlSession.Trust.UNTRUSTED);
692 }
693 },
694 new View.OnClickListener() {
695 @Override
696 public void onClick(View v) {
697 account.getAxolotlService().setFingerprintTrust(fingerprint,
698 XmppAxolotlSession.Trust.UNTRUSTED);
699 v.setEnabled(true);
700 }
701 },
702 onKeyClickedListener
703
704 );
705 }
706
707 protected boolean addFingerprintRowWithListeners(LinearLayout keys, final Account account,
708 final String fingerprint,
709 boolean highlight,
710 XmppAxolotlSession.Trust trust,
711 boolean showTag,
712 CompoundButton.OnCheckedChangeListener
713 onCheckedChangeListener,
714 View.OnClickListener onClickListener,
715 View.OnClickListener onKeyClickedListener) {
716 if (trust == XmppAxolotlSession.Trust.COMPROMISED) {
717 return false;
718 }
719 View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false);
720 TextView key = (TextView) view.findViewById(R.id.key);
721 key.setOnClickListener(onKeyClickedListener);
722 TextView keyType = (TextView) view.findViewById(R.id.key_type);
723 keyType.setOnClickListener(onKeyClickedListener);
724 Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust);
725 trustToggle.setVisibility(View.VISIBLE);
726 trustToggle.setOnCheckedChangeListener(onCheckedChangeListener);
727 trustToggle.setOnClickListener(onClickListener);
728 final View.OnLongClickListener purge = new View.OnLongClickListener() {
729 @Override
730 public boolean onLongClick(View v) {
731 showPurgeKeyDialog(account, fingerprint);
732 return true;
733 }
734 };
735 view.setOnLongClickListener(purge);
736 key.setOnLongClickListener(purge);
737 keyType.setOnLongClickListener(purge);
738 boolean x509 = Config.X509_VERIFICATION
739 && (trust == XmppAxolotlSession.Trust.TRUSTED_X509 || trust == XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509);
740 switch (trust) {
741 case UNTRUSTED:
742 case TRUSTED:
743 case TRUSTED_X509:
744 trustToggle.setChecked(trust.trusted(), false);
745 trustToggle.setEnabled(!Config.X509_VERIFICATION || trust != XmppAxolotlSession.Trust.TRUSTED_X509);
746 if (Config.X509_VERIFICATION && trust == XmppAxolotlSession.Trust.TRUSTED_X509) {
747 trustToggle.setOnClickListener(null);
748 }
749 key.setTextColor(getPrimaryTextColor());
750 keyType.setTextColor(getSecondaryTextColor());
751 break;
752 case UNDECIDED:
753 trustToggle.setChecked(false, false);
754 trustToggle.setEnabled(false);
755 key.setTextColor(getPrimaryTextColor());
756 keyType.setTextColor(getSecondaryTextColor());
757 break;
758 case INACTIVE_UNTRUSTED:
759 case INACTIVE_UNDECIDED:
760 trustToggle.setOnClickListener(null);
761 trustToggle.setChecked(false, false);
762 trustToggle.setEnabled(false);
763 key.setTextColor(getTertiaryTextColor());
764 keyType.setTextColor(getTertiaryTextColor());
765 break;
766 case INACTIVE_TRUSTED:
767 case INACTIVE_TRUSTED_X509:
768 trustToggle.setOnClickListener(null);
769 trustToggle.setChecked(true, false);
770 trustToggle.setEnabled(false);
771 key.setTextColor(getTertiaryTextColor());
772 keyType.setTextColor(getTertiaryTextColor());
773 break;
774 }
775
776 if (showTag) {
777 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
778 } else {
779 keyType.setVisibility(View.GONE);
780 }
781 if (highlight) {
782 keyType.setTextColor(getResources().getColor(R.color.accent));
783 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message));
784 } else {
785 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
786 }
787
788 key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)));
789 keys.addView(view);
790 return true;
791 }
792
793 public void showPurgeKeyDialog(final Account account, final String fingerprint) {
794 Builder builder = new Builder(this);
795 builder.setTitle(getString(R.string.purge_key));
796 builder.setIconAttribute(android.R.attr.alertDialogIcon);
797 builder.setMessage(getString(R.string.purge_key_desc_part1)
798 + "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint.substring(2))
799 + "\n\n" + getString(R.string.purge_key_desc_part2));
800 builder.setNegativeButton(getString(R.string.cancel), null);
801 builder.setPositiveButton(getString(R.string.purge_key),
802 new DialogInterface.OnClickListener() {
803 @Override
804 public void onClick(DialogInterface dialog, int which) {
805 account.getAxolotlService().purgeKey(fingerprint);
806 refreshUi();
807 }
808 });
809 builder.create().show();
810 }
811
812 public boolean hasStoragePermission(int requestCode) {
813 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
814 if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
815 requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
816 return false;
817 } else {
818 return true;
819 }
820 } else {
821 return true;
822 }
823 }
824
825 public void selectPresence(final Conversation conversation,
826 final OnPresenceSelected listener) {
827 final Contact contact = conversation.getContact();
828 if (conversation.hasValidOtrSession()) {
829 SessionID id = conversation.getOtrSession().getSessionID();
830 Jid jid;
831 try {
832 jid = Jid.fromString(id.getAccountID() + "/" + id.getUserID());
833 } catch (InvalidJidException e) {
834 jid = null;
835 }
836 conversation.setNextCounterpart(jid);
837 listener.onPresenceSelected();
838 } else if (!contact.showInRoster()) {
839 showAddToRosterDialog(conversation);
840 } else {
841 Presences presences = contact.getPresences();
842 if (presences.size() == 0) {
843 if (!contact.getOption(Contact.Options.TO)
844 && !contact.getOption(Contact.Options.ASKING)
845 && contact.getAccount().getStatus() == Account.State.ONLINE) {
846 showAskForPresenceDialog(contact);
847 } else if (!contact.getOption(Contact.Options.TO)
848 || !contact.getOption(Contact.Options.FROM)) {
849 warnMutalPresenceSubscription(conversation, listener);
850 } else {
851 conversation.setNextCounterpart(null);
852 listener.onPresenceSelected();
853 }
854 } else if (presences.size() == 1) {
855 String presence = presences.asStringArray()[0];
856 try {
857 conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence));
858 } catch (InvalidJidException e) {
859 conversation.setNextCounterpart(null);
860 }
861 listener.onPresenceSelected();
862 } else {
863 final StringBuilder presence = new StringBuilder();
864 AlertDialog.Builder builder = new AlertDialog.Builder(this);
865 builder.setTitle(getString(R.string.choose_presence));
866 final String[] presencesArray = presences.asStringArray();
867 int preselectedPresence = 0;
868 for (int i = 0; i < presencesArray.length; ++i) {
869 if (presencesArray[i].equals(contact.lastseen.presence)) {
870 preselectedPresence = i;
871 break;
872 }
873 }
874 presence.append(presencesArray[preselectedPresence]);
875 builder.setSingleChoiceItems(presencesArray,
876 preselectedPresence,
877 new DialogInterface.OnClickListener() {
878
879 @Override
880 public void onClick(DialogInterface dialog,
881 int which) {
882 presence.delete(0, presence.length());
883 presence.append(presencesArray[which]);
884 }
885 });
886 builder.setNegativeButton(R.string.cancel, null);
887 builder.setPositiveButton(R.string.ok, new OnClickListener() {
888
889 @Override
890 public void onClick(DialogInterface dialog, int which) {
891 try {
892 conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence.toString()));
893 } catch (InvalidJidException e) {
894 conversation.setNextCounterpart(null);
895 }
896 listener.onPresenceSelected();
897 }
898 });
899 builder.create().show();
900 }
901 }
902 }
903
904 protected void onActivityResult(int requestCode, int resultCode,
905 final Intent data) {
906 super.onActivityResult(requestCode, resultCode, data);
907 if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
908 mPendingConferenceInvite = ConferenceInvite.parse(data);
909 if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
910 mPendingConferenceInvite.execute(this);
911 mPendingConferenceInvite = null;
912 }
913 }
914 }
915
916 private UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() {
917 @Override
918 public void success(final Conversation conversation) {
919 switchToConversation(conversation);
920 runOnUiThread(new Runnable() {
921 @Override
922 public void run() {
923 Toast.makeText(XmppActivity.this,R.string.conference_created,Toast.LENGTH_LONG).show();
924 }
925 });
926 }
927
928 @Override
929 public void error(final int errorCode, Conversation object) {
930 runOnUiThread(new Runnable() {
931 @Override
932 public void run() {
933 Toast.makeText(XmppActivity.this,errorCode,Toast.LENGTH_LONG).show();
934 }
935 });
936 }
937
938 @Override
939 public void userInputRequried(PendingIntent pi, Conversation object) {
940
941 }
942 };
943
944 public int getTertiaryTextColor() {
945 return this.mTertiaryTextColor;
946 }
947
948 public int getSecondaryTextColor() {
949 return this.mSecondaryTextColor;
950 }
951
952 public int getPrimaryTextColor() {
953 return this.mPrimaryTextColor;
954 }
955
956 public int getWarningTextColor() {
957 return this.mColorRed;
958 }
959
960 public int getOnlineColor() {
961 return this.mColorGreen;
962 }
963
964 public int getPrimaryBackgroundColor() {
965 return this.mPrimaryBackgroundColor;
966 }
967
968 public int getSecondaryBackgroundColor() {
969 return this.mSecondaryBackgroundColor;
970 }
971
972 public int getPixel(int dp) {
973 DisplayMetrics metrics = getResources().getDisplayMetrics();
974 return ((int) (dp * metrics.density));
975 }
976
977 public boolean copyTextToClipboard(String text, int labelResId) {
978 ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
979 String label = getResources().getString(labelResId);
980 if (mClipBoardManager != null) {
981 ClipData mClipData = ClipData.newPlainText(label, text);
982 mClipBoardManager.setPrimaryClip(mClipData);
983 return true;
984 }
985 return false;
986 }
987
988 protected void registerNdefPushMessageCallback() {
989 NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
990 if (nfcAdapter != null && nfcAdapter.isEnabled()) {
991 nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
992 @Override
993 public NdefMessage createNdefMessage(NfcEvent nfcEvent) {
994 return new NdefMessage(new NdefRecord[]{
995 NdefRecord.createUri(getShareableUri()),
996 NdefRecord.createApplicationRecord("eu.siacs.conversations")
997 });
998 }
999 }, this);
1000 }
1001 }
1002
1003 protected boolean neverCompressPictures() {
1004 return getPreferences().getString("picture_compression", "auto").equals("never");
1005 }
1006
1007 protected boolean manuallyChangePresence() {
1008 return getPreferences().getBoolean("manually_change_presence", false);
1009 }
1010
1011 protected void unregisterNdefPushMessageCallback() {
1012 NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
1013 if (nfcAdapter != null && nfcAdapter.isEnabled()) {
1014 nfcAdapter.setNdefPushMessageCallback(null,this);
1015 }
1016 }
1017
1018 protected String getShareableUri() {
1019 return null;
1020 }
1021
1022 @Override
1023 public void onResume() {
1024 super.onResume();
1025 if (this.getShareableUri()!=null) {
1026 this.registerNdefPushMessageCallback();
1027 }
1028 }
1029
1030 protected int findTheme() {
1031 if (getPreferences().getBoolean("use_larger_font", false)) {
1032 return R.style.ConversationsTheme_LargerText;
1033 } else {
1034 return R.style.ConversationsTheme;
1035 }
1036 }
1037
1038 @Override
1039 public void onPause() {
1040 super.onPause();
1041 this.unregisterNdefPushMessageCallback();
1042 }
1043
1044 protected void showQrCode() {
1045 String uri = getShareableUri();
1046 if (uri!=null) {
1047 Point size = new Point();
1048 getWindowManager().getDefaultDisplay().getSize(size);
1049 final int width = (size.x < size.y ? size.x : size.y);
1050 Bitmap bitmap = createQrCodeBitmap(uri, width);
1051 ImageView view = new ImageView(this);
1052 view.setImageBitmap(bitmap);
1053 AlertDialog.Builder builder = new AlertDialog.Builder(this);
1054 builder.setView(view);
1055 builder.create().show();
1056 }
1057 }
1058
1059 protected Bitmap createQrCodeBitmap(String input, int size) {
1060 Log.d(Config.LOGTAG,"qr code requested size: "+size);
1061 try {
1062 final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter();
1063 final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
1064 hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
1065 final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, size, size, hints);
1066 final int width = result.getWidth();
1067 final int height = result.getHeight();
1068 final int[] pixels = new int[width * height];
1069 for (int y = 0; y < height; y++) {
1070 final int offset = y * width;
1071 for (int x = 0; x < width; x++) {
1072 pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT;
1073 }
1074 }
1075 final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
1076 Log.d(Config.LOGTAG,"output size: "+width+"x"+height);
1077 bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
1078 return bitmap;
1079 } catch (final WriterException e) {
1080 return null;
1081 }
1082 }
1083
1084 protected Account extractAccount(Intent intent) {
1085 String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null;
1086 try {
1087 return jid != null ? xmppConnectionService.findAccountByJid(Jid.fromString(jid)) : null;
1088 } catch (InvalidJidException e) {
1089 return null;
1090 }
1091 }
1092
1093 public static class ConferenceInvite {
1094 private String uuid;
1095 private List<Jid> jids = new ArrayList<>();
1096
1097 public static ConferenceInvite parse(Intent data) {
1098 ConferenceInvite invite = new ConferenceInvite();
1099 invite.uuid = data.getStringExtra("conversation");
1100 if (invite.uuid == null) {
1101 return null;
1102 }
1103 try {
1104 if (data.getBooleanExtra("multiple", false)) {
1105 String[] toAdd = data.getStringArrayExtra("contacts");
1106 for (String item : toAdd) {
1107 invite.jids.add(Jid.fromString(item));
1108 }
1109 } else {
1110 invite.jids.add(Jid.fromString(data.getStringExtra("contact")));
1111 }
1112 } catch (final InvalidJidException ignored) {
1113 return null;
1114 }
1115 return invite;
1116 }
1117
1118 public void execute(XmppActivity activity) {
1119 XmppConnectionService service = activity.xmppConnectionService;
1120 Conversation conversation = service.findConversationByUuid(this.uuid);
1121 if (conversation == null) {
1122 return;
1123 }
1124 if (conversation.getMode() == Conversation.MODE_MULTI) {
1125 for (Jid jid : jids) {
1126 service.invite(conversation, jid);
1127 }
1128 } else {
1129 jids.add(conversation.getJid().toBareJid());
1130 service.createAdhocConference(conversation.getAccount(), jids, activity.adhocCallback);
1131 }
1132 }
1133 }
1134
1135 public AvatarService avatarService() {
1136 return xmppConnectionService.getAvatarService();
1137 }
1138
1139 class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
1140 private final WeakReference<ImageView> imageViewReference;
1141 private Message message = null;
1142
1143 public BitmapWorkerTask(ImageView imageView) {
1144 imageViewReference = new WeakReference<>(imageView);
1145 }
1146
1147 @Override
1148 protected Bitmap doInBackground(Message... params) {
1149 if (isCancelled()) {
1150 return null;
1151 }
1152 message = params[0];
1153 try {
1154 return xmppConnectionService.getFileBackend().getThumbnail(
1155 message, (int) (metrics.density * 288), false);
1156 } catch (FileNotFoundException e) {
1157 return null;
1158 }
1159 }
1160
1161 @Override
1162 protected void onPostExecute(Bitmap bitmap) {
1163 if (bitmap != null && !isCancelled()) {
1164 final ImageView imageView = imageViewReference.get();
1165 if (imageView != null) {
1166 imageView.setImageBitmap(bitmap);
1167 imageView.setBackgroundColor(0x00000000);
1168 }
1169 }
1170 }
1171 }
1172
1173 public void loadBitmap(Message message, ImageView imageView) {
1174 Bitmap bm;
1175 try {
1176 bm = xmppConnectionService.getFileBackend().getThumbnail(message,
1177 (int) (metrics.density * 288), true);
1178 } catch (FileNotFoundException e) {
1179 bm = null;
1180 }
1181 if (bm != null) {
1182 cancelPotentialWork(message, imageView);
1183 imageView.setImageBitmap(bm);
1184 imageView.setBackgroundColor(0x00000000);
1185 } else {
1186 if (cancelPotentialWork(message, imageView)) {
1187 imageView.setBackgroundColor(0xff333333);
1188 imageView.setImageDrawable(null);
1189 final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
1190 final AsyncDrawable asyncDrawable = new AsyncDrawable(
1191 getResources(), null, task);
1192 imageView.setImageDrawable(asyncDrawable);
1193 try {
1194 task.execute(message);
1195 } catch (final RejectedExecutionException ignored) {
1196 ignored.printStackTrace();
1197 }
1198 }
1199 }
1200 }
1201
1202 public static boolean cancelPotentialWork(Message message, ImageView imageView) {
1203 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
1204
1205 if (bitmapWorkerTask != null) {
1206 final Message oldMessage = bitmapWorkerTask.message;
1207 if (oldMessage == null || message != oldMessage) {
1208 bitmapWorkerTask.cancel(true);
1209 } else {
1210 return false;
1211 }
1212 }
1213 return true;
1214 }
1215
1216 private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
1217 if (imageView != null) {
1218 final Drawable drawable = imageView.getDrawable();
1219 if (drawable instanceof AsyncDrawable) {
1220 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
1221 return asyncDrawable.getBitmapWorkerTask();
1222 }
1223 }
1224 return null;
1225 }
1226
1227 static class AsyncDrawable extends BitmapDrawable {
1228 private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
1229
1230 public AsyncDrawable(Resources res, Bitmap bitmap,
1231 BitmapWorkerTask bitmapWorkerTask) {
1232 super(res, bitmap);
1233 bitmapWorkerTaskReference = new WeakReference<>(
1234 bitmapWorkerTask);
1235 }
1236
1237 public BitmapWorkerTask getBitmapWorkerTask() {
1238 return bitmapWorkerTaskReference.get();
1239 }
1240 }
1241}