XmppActivity.java

  1package eu.siacs.conversations.ui;
  2
  3import android.annotation.SuppressLint;
  4import android.app.Activity;
  5import android.app.AlertDialog;
  6import android.app.AlertDialog.Builder;
  7import android.app.PendingIntent;
  8import android.content.ClipData;
  9import android.content.ClipboardManager;
 10import android.content.ComponentName;
 11import android.content.Context;
 12import android.content.DialogInterface;
 13import android.content.DialogInterface.OnClickListener;
 14import android.content.Intent;
 15import android.content.IntentSender.SendIntentException;
 16import android.content.ServiceConnection;
 17import android.content.SharedPreferences;
 18import android.content.pm.PackageManager;
 19import android.content.pm.ResolveInfo;
 20import android.content.res.Resources;
 21import android.graphics.Bitmap;
 22import android.graphics.Color;
 23import android.graphics.Point;
 24import android.graphics.drawable.BitmapDrawable;
 25import android.graphics.drawable.Drawable;
 26import android.net.Uri;
 27import android.nfc.NdefMessage;
 28import android.nfc.NdefRecord;
 29import android.nfc.NfcAdapter;
 30import android.nfc.NfcEvent;
 31import android.os.AsyncTask;
 32import android.os.Bundle;
 33import android.os.IBinder;
 34import android.preference.PreferenceManager;
 35import android.text.InputType;
 36import android.util.DisplayMetrics;
 37import android.util.Log;
 38import android.view.MenuItem;
 39import android.view.View;
 40import android.view.inputmethod.InputMethodManager;
 41import android.widget.EditText;
 42import android.widget.ImageView;
 43
 44import com.google.zxing.BarcodeFormat;
 45import com.google.zxing.EncodeHintType;
 46import com.google.zxing.WriterException;
 47import com.google.zxing.common.BitMatrix;
 48import com.google.zxing.qrcode.QRCodeWriter;
 49import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
 50
 51import net.java.otr4j.session.SessionID;
 52
 53import java.io.FileNotFoundException;
 54import java.lang.ref.WeakReference;
 55import java.util.Hashtable;
 56import java.util.List;
 57import java.util.concurrent.RejectedExecutionException;
 58
 59import eu.siacs.conversations.Config;
 60import eu.siacs.conversations.R;
 61import eu.siacs.conversations.entities.Account;
 62import eu.siacs.conversations.entities.Contact;
 63import eu.siacs.conversations.entities.Conversation;
 64import eu.siacs.conversations.entities.Message;
 65import eu.siacs.conversations.entities.MucOptions;
 66import eu.siacs.conversations.entities.Presences;
 67import eu.siacs.conversations.services.AvatarService;
 68import eu.siacs.conversations.services.XmppConnectionService;
 69import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
 70import eu.siacs.conversations.utils.ExceptionHelper;
 71import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 72import eu.siacs.conversations.xmpp.jid.Jid;
 73
 74public abstract class XmppActivity extends Activity {
 75
 76	protected static final int REQUEST_ANNOUNCE_PGP = 0x0101;
 77	protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102;
 78
 79	public XmppConnectionService xmppConnectionService;
 80	public boolean xmppConnectionServiceBound = false;
 81	protected boolean registeredListeners = false;
 82
 83	protected int mPrimaryTextColor;
 84	protected int mSecondaryTextColor;
 85	protected int mSecondaryBackgroundColor;
 86	protected int mColorRed;
 87	protected int mColorOrange;
 88	protected int mColorGreen;
 89	protected int mPrimaryColor;
 90
 91	protected boolean mUseSubject = true;
 92
 93	private DisplayMetrics metrics;
 94
 95	protected interface OnValueEdited {
 96		public void onValueEdited(String value);
 97	}
 98
 99	public interface OnPresenceSelected {
100		public void onPresenceSelected();
101	}
102
103	protected ServiceConnection mConnection = new ServiceConnection() {
104
105		@Override
106		public void onServiceConnected(ComponentName className, IBinder service) {
107			XmppConnectionBinder binder = (XmppConnectionBinder) service;
108			xmppConnectionService = binder.getService();
109			xmppConnectionServiceBound = true;
110			if (!registeredListeners) {
111				registerListeners();
112				registeredListeners = true;
113			}
114			onBackendConnected();
115		}
116
117		@Override
118		public void onServiceDisconnected(ComponentName arg0) {
119			xmppConnectionServiceBound = false;
120		}
121	};
122
123	@Override
124	protected void onStart() {
125		super.onStart();
126		if (!xmppConnectionServiceBound) {
127			connectToBackend();
128		} else {
129			if (!registeredListeners) {
130				this.registerListeners();
131				this.registeredListeners = true;
132			}
133			this.onBackendConnected();
134		}
135	}
136
137	public void connectToBackend() {
138		Intent intent = new Intent(this, XmppConnectionService.class);
139		intent.setAction("ui");
140		startService(intent);
141		bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
142	}
143
144	@Override
145	protected void onStop() {
146		super.onStop();
147		if (xmppConnectionServiceBound) {
148			if (registeredListeners) {
149				this.unregisterListeners();
150				this.registeredListeners = false;
151			}
152			unbindService(mConnection);
153			xmppConnectionServiceBound = false;
154		}
155	}
156
157	protected void hideKeyboard() {
158		InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
159
160		View focus = getCurrentFocus();
161
162		if (focus != null) {
163
164			inputManager.hideSoftInputFromWindow(focus.getWindowToken(),
165					InputMethodManager.HIDE_NOT_ALWAYS);
166		}
167	}
168
169	public boolean hasPgp() {
170		return xmppConnectionService.getPgpEngine() != null;
171	}
172
173	public void showInstallPgpDialog() {
174		Builder builder = new AlertDialog.Builder(this);
175		builder.setTitle(getString(R.string.openkeychain_required));
176		builder.setIconAttribute(android.R.attr.alertDialogIcon);
177		builder.setMessage(getText(R.string.openkeychain_required_long));
178		builder.setNegativeButton(getString(R.string.cancel), null);
179		builder.setNeutralButton(getString(R.string.restart),
180				new OnClickListener() {
181
182					@Override
183					public void onClick(DialogInterface dialog, int which) {
184						if (xmppConnectionServiceBound) {
185							unbindService(mConnection);
186							xmppConnectionServiceBound = false;
187						}
188						stopService(new Intent(XmppActivity.this,
189								XmppConnectionService.class));
190						finish();
191					}
192				});
193		builder.setPositiveButton(getString(R.string.install),
194				new OnClickListener() {
195
196					@Override
197					public void onClick(DialogInterface dialog, int which) {
198						Uri uri = Uri
199								.parse("market://details?id=org.sufficientlysecure.keychain");
200						Intent marketIntent = new Intent(Intent.ACTION_VIEW,
201								uri);
202						PackageManager manager = getApplicationContext()
203								.getPackageManager();
204						List<ResolveInfo> infos = manager
205								.queryIntentActivities(marketIntent, 0);
206						if (infos.size() > 0) {
207							startActivity(marketIntent);
208						} else {
209							uri = Uri.parse("http://www.openkeychain.org/");
210							Intent browserIntent = new Intent(
211									Intent.ACTION_VIEW, uri);
212							startActivity(browserIntent);
213						}
214						finish();
215					}
216				});
217		builder.create().show();
218	}
219
220	abstract void onBackendConnected();
221
222	protected void registerListeners() {
223		if (this instanceof XmppConnectionService.OnConversationUpdate) {
224			this.xmppConnectionService.setOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this);
225		}
226		if (this instanceof XmppConnectionService.OnAccountUpdate) {
227			this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this);
228		}
229		if (this instanceof XmppConnectionService.OnRosterUpdate) {
230			this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this);
231		}
232		if (this instanceof MucOptions.OnRenameListener) {
233			this.xmppConnectionService.setOnRenameListener((MucOptions.OnRenameListener) this);
234		}
235	}
236
237	protected void unregisterListeners() {
238		if (this instanceof XmppConnectionService.OnConversationUpdate) {
239			this.xmppConnectionService.removeOnConversationListChangedListener();
240		}
241		if (this instanceof XmppConnectionService.OnAccountUpdate) {
242			this.xmppConnectionService.removeOnAccountListChangedListener();
243		}
244		if (this instanceof XmppConnectionService.OnRosterUpdate) {
245			this.xmppConnectionService.removeOnRosterUpdateListener();
246		}
247		if (this instanceof MucOptions.OnRenameListener) {
248			this.xmppConnectionService.setOnRenameListener(null);
249		}
250	}
251
252	public boolean onOptionsItemSelected(MenuItem item) {
253		switch (item.getItemId()) {
254			case R.id.action_settings:
255				startActivity(new Intent(this, SettingsActivity.class));
256				break;
257			case R.id.action_accounts:
258				startActivity(new Intent(this, ManageAccountActivity.class));
259				break;
260			case android.R.id.home:
261				finish();
262				break;
263			case R.id.action_show_qr_code:
264				showQrCode();
265				break;
266		}
267		return super.onOptionsItemSelected(item);
268	}
269
270	@Override
271	protected void onCreate(Bundle savedInstanceState) {
272		super.onCreate(savedInstanceState);
273		metrics = getResources().getDisplayMetrics();
274		ExceptionHelper.init(getApplicationContext());
275		mPrimaryTextColor = getResources().getColor(R.color.primarytext);
276		mSecondaryTextColor = getResources().getColor(R.color.secondarytext);
277		mColorRed = getResources().getColor(R.color.red);
278		mColorOrange = getResources().getColor(R.color.orange);
279		mColorGreen = getResources().getColor(R.color.green);
280		mPrimaryColor = getResources().getColor(R.color.primary);
281		mSecondaryBackgroundColor = getResources().getColor(
282				R.color.secondarybackground);
283		if (getPreferences().getBoolean("use_larger_font", false)) {
284			setTheme(R.style.ConversationsTheme_LargerText);
285		}
286		mUseSubject = getPreferences().getBoolean("use_subject", true);
287	}
288
289	protected SharedPreferences getPreferences() {
290		return PreferenceManager
291				.getDefaultSharedPreferences(getApplicationContext());
292	}
293
294	public boolean useSubjectToIdentifyConference() {
295		return mUseSubject;
296	}
297
298	public void switchToConversation(Conversation conversation) {
299		switchToConversation(conversation, null, false);
300	}
301
302	public void switchToConversation(Conversation conversation, String text,
303									 boolean newTask) {
304		switchToConversation(conversation,text,null,newTask);
305	}
306
307	public void highlightInMuc(Conversation conversation, String nick) {
308		switchToConversation(conversation,null,nick,false);
309	}
310
311	private void switchToConversation(Conversation conversation, String text, String nick, boolean newTask) {
312		Intent viewConversationIntent = new Intent(this,
313				ConversationActivity.class);
314		viewConversationIntent.setAction(Intent.ACTION_VIEW);
315		viewConversationIntent.putExtra(ConversationActivity.CONVERSATION,
316				conversation.getUuid());
317		if (text != null) {
318			viewConversationIntent.putExtra(ConversationActivity.TEXT, text);
319		}
320		if (nick != null) {
321			viewConversationIntent.putExtra(ConversationActivity.NICK, nick);
322		}
323		viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
324		if (newTask) {
325			viewConversationIntent.setFlags(viewConversationIntent.getFlags()
326					| Intent.FLAG_ACTIVITY_NEW_TASK
327					| Intent.FLAG_ACTIVITY_SINGLE_TOP);
328		} else {
329			viewConversationIntent.setFlags(viewConversationIntent.getFlags()
330					| Intent.FLAG_ACTIVITY_CLEAR_TOP);
331		}
332		startActivity(viewConversationIntent);
333		finish();
334	}
335
336	public void switchToContactDetails(Contact contact) {
337		Intent intent = new Intent(this, ContactDetailsActivity.class);
338		intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
339		intent.putExtra("account", contact.getAccount().getJid().toBareJid().toString());
340		intent.putExtra("contact", contact.getJid().toString());
341		startActivity(intent);
342	}
343
344	public void switchToAccount(Account account) {
345		Intent intent = new Intent(this, EditAccountActivity.class);
346		intent.putExtra("jid", account.getJid().toBareJid().toString());
347		startActivity(intent);
348	}
349
350	protected void inviteToConversation(Conversation conversation) {
351		Intent intent = new Intent(getApplicationContext(),
352				ChooseContactActivity.class);
353		intent.putExtra("conversation", conversation.getUuid());
354		startActivityForResult(intent, REQUEST_INVITE_TO_CONVERSATION);
355	}
356
357	protected void announcePgp(Account account, final Conversation conversation) {
358		xmppConnectionService.getPgpEngine().generateSignature(account,
359				"online", new UiCallback<Account>() {
360
361					@Override
362					public void userInputRequried(PendingIntent pi,
363												  Account account) {
364						try {
365							startIntentSenderForResult(pi.getIntentSender(),
366									REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
367						} catch (final SendIntentException ignored) {
368						}
369					}
370
371					@Override
372					public void success(Account account) {
373						xmppConnectionService.databaseBackend
374								.updateAccount(account);
375						xmppConnectionService.sendPresencePacket(account,
376								xmppConnectionService.getPresenceGenerator()
377										.sendPresence(account));
378						if (conversation != null) {
379							conversation
380									.setNextEncryption(Message.ENCRYPTION_PGP);
381							xmppConnectionService.databaseBackend
382									.updateConversation(conversation);
383						}
384					}
385
386					@Override
387					public void error(int error, Account account) {
388						displayErrorDialog(error);
389					}
390				});
391	}
392
393	protected void displayErrorDialog(final int errorCode) {
394		runOnUiThread(new Runnable() {
395
396			@Override
397			public void run() {
398				AlertDialog.Builder builder = new AlertDialog.Builder(
399						XmppActivity.this);
400				builder.setIconAttribute(android.R.attr.alertDialogIcon);
401				builder.setTitle(getString(R.string.error));
402				builder.setMessage(errorCode);
403				builder.setNeutralButton(R.string.accept, null);
404				builder.create().show();
405			}
406		});
407
408	}
409
410	protected void showAddToRosterDialog(final Conversation conversation) {
411		final Jid jid = conversation.getContactJid();
412		AlertDialog.Builder builder = new AlertDialog.Builder(this);
413		builder.setTitle(jid.toString());
414		builder.setMessage(getString(R.string.not_in_roster));
415		builder.setNegativeButton(getString(R.string.cancel), null);
416		builder.setPositiveButton(getString(R.string.add_contact),
417				new DialogInterface.OnClickListener() {
418
419					@Override
420					public void onClick(DialogInterface dialog, int which) {
421						final Jid jid = conversation.getContactJid();
422						Account account = conversation.getAccount();
423						Contact contact = account.getRoster().getContact(jid);
424						xmppConnectionService.createContact(contact);
425						switchToContactDetails(contact);
426					}
427				});
428		builder.create().show();
429	}
430
431	private void showAskForPresenceDialog(final Contact contact) {
432		AlertDialog.Builder builder = new AlertDialog.Builder(this);
433		builder.setTitle(contact.getJid().toString());
434		builder.setMessage(R.string.request_presence_updates);
435		builder.setNegativeButton(R.string.cancel, null);
436		builder.setPositiveButton(R.string.request_now,
437				new DialogInterface.OnClickListener() {
438
439					@Override
440					public void onClick(DialogInterface dialog, int which) {
441						if (xmppConnectionServiceBound) {
442							xmppConnectionService.sendPresencePacket(contact
443									.getAccount(), xmppConnectionService
444									.getPresenceGenerator()
445									.requestPresenceUpdatesFrom(contact));
446						}
447					}
448				});
449		builder.create().show();
450	}
451
452	private void warnMutalPresenceSubscription(final Conversation conversation,
453											   final OnPresenceSelected listener) {
454		AlertDialog.Builder builder = new AlertDialog.Builder(this);
455		builder.setTitle(conversation.getContact().getJid().toString());
456		builder.setMessage(R.string.without_mutual_presence_updates);
457		builder.setNegativeButton(R.string.cancel, null);
458		builder.setPositiveButton(R.string.ignore, new OnClickListener() {
459
460			@Override
461			public void onClick(DialogInterface dialog, int which) {
462				conversation.setNextCounterpart(null);
463				if (listener != null) {
464					listener.onPresenceSelected();
465				}
466			}
467		});
468		builder.create().show();
469	}
470
471	protected void quickEdit(String previousValue, OnValueEdited callback) {
472		quickEdit(previousValue, callback, false);
473	}
474
475	protected void quickPasswordEdit(String previousValue,
476									 OnValueEdited callback) {
477		quickEdit(previousValue, callback, true);
478	}
479
480	@SuppressLint("InflateParams")
481	private void quickEdit(final String previousValue,
482						   final OnValueEdited callback, boolean password) {
483		AlertDialog.Builder builder = new AlertDialog.Builder(this);
484		View view = getLayoutInflater().inflate(R.layout.quickedit, null);
485		final EditText editor = (EditText) view.findViewById(R.id.editor);
486		OnClickListener mClickListener = new OnClickListener() {
487
488			@Override
489			public void onClick(DialogInterface dialog, int which) {
490				String value = editor.getText().toString();
491				if (!previousValue.equals(value) && value.trim().length() > 0) {
492					callback.onValueEdited(value);
493				}
494			}
495		};
496		if (password) {
497			editor.setInputType(InputType.TYPE_CLASS_TEXT
498					| InputType.TYPE_TEXT_VARIATION_PASSWORD);
499			editor.setHint(R.string.password);
500			builder.setPositiveButton(R.string.accept, mClickListener);
501		} else {
502			builder.setPositiveButton(R.string.edit, mClickListener);
503		}
504		editor.requestFocus();
505		editor.setText(previousValue);
506		builder.setView(view);
507		builder.setNegativeButton(R.string.cancel, null);
508		builder.create().show();
509	}
510
511	public void selectPresence(final Conversation conversation,
512							   final OnPresenceSelected listener) {
513		final Contact contact = conversation.getContact();
514		if (conversation.hasValidOtrSession()) {
515			SessionID id = conversation.getOtrSession().getSessionID();
516			Jid jid;
517			try {
518				jid = Jid.fromString(id.getAccountID() + "/" + id.getUserID());
519			} catch (InvalidJidException e) {
520				jid = null;
521			}
522			conversation.setNextCounterpart(jid);
523			listener.onPresenceSelected();
524		} else 	if (!contact.showInRoster()) {
525			showAddToRosterDialog(conversation);
526		} else {
527			Presences presences = contact.getPresences();
528			if (presences.size() == 0) {
529				if (!contact.getOption(Contact.Options.TO)
530						&& !contact.getOption(Contact.Options.ASKING)
531						&& contact.getAccount().getStatus() == Account.State.ONLINE) {
532					showAskForPresenceDialog(contact);
533				} else if (!contact.getOption(Contact.Options.TO)
534						|| !contact.getOption(Contact.Options.FROM)) {
535					warnMutalPresenceSubscription(conversation, listener);
536				} else {
537					conversation.setNextCounterpart(null);
538					listener.onPresenceSelected();
539				}
540			} else if (presences.size() == 1) {
541				String presence = presences.asStringArray()[0];
542				try {
543					conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence));
544				} catch (InvalidJidException e) {
545					conversation.setNextCounterpart(null);
546				}
547				listener.onPresenceSelected();
548			} else {
549				final StringBuilder presence = new StringBuilder();
550				AlertDialog.Builder builder = new AlertDialog.Builder(this);
551				builder.setTitle(getString(R.string.choose_presence));
552				final String[] presencesArray = presences.asStringArray();
553				int preselectedPresence = 0;
554				for (int i = 0; i < presencesArray.length; ++i) {
555					if (presencesArray[i].equals(contact.lastseen.presence)) {
556						preselectedPresence = i;
557						break;
558					}
559				}
560				presence.append(presencesArray[preselectedPresence]);
561				builder.setSingleChoiceItems(presencesArray,
562						preselectedPresence,
563						new DialogInterface.OnClickListener() {
564
565							@Override
566							public void onClick(DialogInterface dialog,
567												int which) {
568								presence.delete(0, presence.length());
569								presence.append(presencesArray[which]);
570							}
571						});
572				builder.setNegativeButton(R.string.cancel, null);
573				builder.setPositiveButton(R.string.ok, new OnClickListener() {
574
575					@Override
576					public void onClick(DialogInterface dialog, int which) {
577						try {
578							conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence.toString()));
579						} catch (InvalidJidException e) {
580							conversation.setNextCounterpart(null);
581						}
582						listener.onPresenceSelected();
583					}
584				});
585				builder.create().show();
586			}
587		}
588	}
589
590	protected void onActivityResult(int requestCode, int resultCode,
591									final Intent data) {
592		super.onActivityResult(requestCode, resultCode, data);
593		if (requestCode == REQUEST_INVITE_TO_CONVERSATION
594				&& resultCode == RESULT_OK) {
595			String contactJid = data.getStringExtra("contact");
596			String conversationUuid = data.getStringExtra("conversation");
597			Conversation conversation = xmppConnectionService
598					.findConversationByUuid(conversationUuid);
599			if (conversation.getMode() == Conversation.MODE_MULTI) {
600				xmppConnectionService.invite(conversation, contactJid);
601			}
602			Log.d(Config.LOGTAG, "inviting " + contactJid + " to "
603					+ conversation.getName());
604		}
605	}
606
607	public int getSecondaryTextColor() {
608		return this.mSecondaryTextColor;
609	}
610
611	public int getPrimaryTextColor() {
612		return this.mPrimaryTextColor;
613	}
614
615	public int getWarningTextColor() {
616		return this.mColorRed;
617	}
618
619	public int getPrimaryColor() {
620		return this.mPrimaryColor;
621	}
622
623	public int getSecondaryBackgroundColor() {
624		return this.mSecondaryBackgroundColor;
625	}
626
627	public int getPixel(int dp) {
628		DisplayMetrics metrics = getResources().getDisplayMetrics();
629		return ((int) (dp * metrics.density));
630	}
631
632	public boolean copyTextToClipboard(String text, int labelResId) {
633		ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
634		String label = getResources().getString(labelResId);
635		if (mClipBoardManager != null) {
636			ClipData mClipData = ClipData.newPlainText(label, text);
637			mClipBoardManager.setPrimaryClip(mClipData);
638			return true;
639		}
640		return false;
641	}
642
643	protected void registerNdefPushMessageCallback() {
644			NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
645			if (nfcAdapter != null && nfcAdapter.isEnabled()) {
646				nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
647					@Override
648					public NdefMessage createNdefMessage(NfcEvent nfcEvent) {
649                        return new NdefMessage(new NdefRecord[]{
650                                NdefRecord.createUri(getShareableUri()),
651                                NdefRecord.createApplicationRecord("eu.siacs.conversations")
652                        });
653					}
654				}, this);
655			}
656	}
657
658	protected void unregisterNdefPushMessageCallback() {
659		NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
660		if (nfcAdapter != null && nfcAdapter.isEnabled()) {
661			nfcAdapter.setNdefPushMessageCallback(null,this);
662		}
663	}
664
665	protected String getShareableUri() {
666		return null;
667	}
668
669	@Override
670	public void onResume() {
671		super.onResume();
672		if (this.getShareableUri()!=null) {
673			this.registerNdefPushMessageCallback();
674		}
675	}
676
677	@Override
678	public void onPause() {
679		super.onPause();
680		this.unregisterNdefPushMessageCallback();
681	}
682
683	protected void showQrCode() {
684		String uri = getShareableUri();
685		if (uri!=null) {
686			Point size = new Point();
687			getWindowManager().getDefaultDisplay().getSize(size);
688			final int width = (size.x < size.y ? size.x : size.y);
689			Bitmap bitmap = createQrCodeBitmap(uri, width);
690			ImageView view = new ImageView(this);
691			view.setImageBitmap(bitmap);
692			AlertDialog.Builder builder = new AlertDialog.Builder(this);
693			builder.setView(view);
694			builder.create().show();
695		}
696	}
697
698	protected Bitmap createQrCodeBitmap(String input, int size) {
699		Log.d(Config.LOGTAG,"qr code requested size: "+size);
700		try {
701			final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter();
702			final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
703			hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
704			final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, size, size, hints);
705			final int width = result.getWidth();
706			final int height = result.getHeight();
707			final int[] pixels = new int[width * height];
708			for (int y = 0; y < height; y++) {
709				final int offset = y * width;
710				for (int x = 0; x < width; x++) {
711					pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT;
712				}
713			}
714			final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
715			Log.d(Config.LOGTAG,"output size: "+width+"x"+height);
716			bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
717			return bitmap;
718		} catch (final WriterException e) {
719			return null;
720		}
721	}
722
723	public AvatarService avatarService() {
724		return xmppConnectionService.getAvatarService();
725	}
726
727	class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
728		private final WeakReference<ImageView> imageViewReference;
729		private Message message = null;
730
731		public BitmapWorkerTask(ImageView imageView) {
732			imageViewReference = new WeakReference<>(imageView);
733		}
734
735		@Override
736		protected Bitmap doInBackground(Message... params) {
737			message = params[0];
738			try {
739				return xmppConnectionService.getFileBackend().getThumbnail(
740						message, (int) (metrics.density * 288), false);
741			} catch (FileNotFoundException e) {
742				return null;
743			}
744		}
745
746		@Override
747		protected void onPostExecute(Bitmap bitmap) {
748			if (bitmap != null) {
749				final ImageView imageView = imageViewReference.get();
750				if (imageView != null) {
751					imageView.setImageBitmap(bitmap);
752					imageView.setBackgroundColor(0x00000000);
753				}
754			}
755		}
756	}
757
758	public void loadBitmap(Message message, ImageView imageView) {
759		Bitmap bm;
760		try {
761			bm = xmppConnectionService.getFileBackend().getThumbnail(message,
762					(int) (metrics.density * 288), true);
763		} catch (FileNotFoundException e) {
764			bm = null;
765		}
766		if (bm != null) {
767			imageView.setImageBitmap(bm);
768			imageView.setBackgroundColor(0x00000000);
769		} else {
770			if (cancelPotentialWork(message, imageView)) {
771				imageView.setBackgroundColor(0xff333333);
772				final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
773				final AsyncDrawable asyncDrawable = new AsyncDrawable(
774						getResources(), null, task);
775				imageView.setImageDrawable(asyncDrawable);
776				try {
777					task.execute(message);
778				} catch (final RejectedExecutionException ignored) {
779                }
780			}
781		}
782	}
783
784	public static boolean cancelPotentialWork(Message message,
785											  ImageView imageView) {
786		final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
787
788		if (bitmapWorkerTask != null) {
789			final Message oldMessage = bitmapWorkerTask.message;
790			if (oldMessage == null || message != oldMessage) {
791				bitmapWorkerTask.cancel(true);
792			} else {
793				return false;
794			}
795		}
796		return true;
797	}
798
799	private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
800		if (imageView != null) {
801			final Drawable drawable = imageView.getDrawable();
802			if (drawable instanceof AsyncDrawable) {
803				final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
804				return asyncDrawable.getBitmapWorkerTask();
805			}
806		}
807		return null;
808	}
809
810	static class AsyncDrawable extends BitmapDrawable {
811		private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
812
813		public AsyncDrawable(Resources res, Bitmap bitmap,
814							 BitmapWorkerTask bitmapWorkerTask) {
815			super(res, bitmap);
816			bitmapWorkerTaskReference = new WeakReference<>(
817					bitmapWorkerTask);
818		}
819
820		public BitmapWorkerTask getBitmapWorkerTask() {
821			return bitmapWorkerTaskReference.get();
822		}
823	}
824}