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