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