XmppActivity.java

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