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