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