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