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