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