1package eu.siacs.conversations.ui;
2
3import java.io.FileNotFoundException;
4import java.lang.ref.WeakReference;
5import java.util.List;
6import java.util.concurrent.RejectedExecutionException;
7
8import eu.siacs.conversations.Config;
9import eu.siacs.conversations.R;
10import eu.siacs.conversations.entities.Account;
11import eu.siacs.conversations.entities.Contact;
12import eu.siacs.conversations.entities.Conversation;
13import eu.siacs.conversations.entities.Message;
14import eu.siacs.conversations.entities.Presences;
15import eu.siacs.conversations.services.AvatarService;
16import eu.siacs.conversations.services.XmppConnectionService;
17import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
18import eu.siacs.conversations.utils.ExceptionHelper;
19import android.annotation.SuppressLint;
20import android.app.Activity;
21import android.app.AlertDialog;
22import android.app.PendingIntent;
23import android.app.AlertDialog.Builder;
24import android.content.ComponentName;
25import android.content.Context;
26import android.content.DialogInterface;
27import android.content.SharedPreferences;
28import android.content.DialogInterface.OnClickListener;
29import android.content.IntentSender.SendIntentException;
30import android.content.pm.PackageManager;
31import android.content.pm.ResolveInfo;
32import android.content.res.Resources;
33import android.content.Intent;
34import android.content.ServiceConnection;
35import android.graphics.Bitmap;
36import android.graphics.drawable.BitmapDrawable;
37import android.graphics.drawable.Drawable;
38import android.net.Uri;
39import android.os.AsyncTask;
40import android.os.Bundle;
41import android.os.IBinder;
42import android.preference.PreferenceManager;
43import android.text.InputType;
44import android.util.DisplayMetrics;
45import android.util.Log;
46import android.view.MenuItem;
47import android.view.View;
48import android.view.inputmethod.InputMethodManager;
49import android.widget.EditText;
50import android.widget.ImageView;
51
52public abstract class XmppActivity extends Activity {
53
54 protected static final int REQUEST_ANNOUNCE_PGP = 0x0101;
55 protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102;
56
57 public XmppConnectionService xmppConnectionService;
58 public boolean xmppConnectionServiceBound = false;
59 protected boolean handledViewIntent = false;
60
61 protected int mPrimaryTextColor;
62 protected int mSecondaryTextColor;
63 protected int mSecondaryBackgroundColor;
64 protected int mColorRed;
65 protected int mColorOrange;
66 protected int mColorGreen;
67 protected int mPrimaryColor;
68
69 protected boolean mUseSubject = true;
70
71 private DisplayMetrics metrics;
72
73 protected interface OnValueEdited {
74 public void onValueEdited(String value);
75 }
76
77 public interface OnPresenceSelected {
78 public void onPresenceSelected();
79 }
80
81 protected ServiceConnection mConnection = new ServiceConnection() {
82
83 @Override
84 public void onServiceConnected(ComponentName className, IBinder service) {
85 XmppConnectionBinder binder = (XmppConnectionBinder) service;
86 xmppConnectionService = binder.getService();
87 xmppConnectionServiceBound = true;
88 onBackendConnected();
89 }
90
91 @Override
92 public void onServiceDisconnected(ComponentName arg0) {
93 xmppConnectionServiceBound = false;
94 }
95 };
96
97 @Override
98 protected void onStart() {
99 super.onStart();
100 if (!xmppConnectionServiceBound) {
101 connectToBackend();
102 }
103 }
104
105 public void connectToBackend() {
106 Intent intent = new Intent(this, XmppConnectionService.class);
107 intent.setAction("ui");
108 startService(intent);
109 bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
110 }
111
112 @Override
113 protected void onStop() {
114 super.onStop();
115 if (xmppConnectionServiceBound) {
116 unbindService(mConnection);
117 xmppConnectionServiceBound = false;
118 }
119 }
120
121 protected void hideKeyboard() {
122 InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
123
124 View focus = getCurrentFocus();
125
126 if (focus != null) {
127
128 inputManager.hideSoftInputFromWindow(focus.getWindowToken(),
129 InputMethodManager.HIDE_NOT_ALWAYS);
130 }
131 }
132
133 public boolean hasPgp() {
134 return xmppConnectionService.getPgpEngine() != null;
135 }
136
137 public void showInstallPgpDialog() {
138 Builder builder = new AlertDialog.Builder(this);
139 builder.setTitle(getString(R.string.openkeychain_required));
140 builder.setIconAttribute(android.R.attr.alertDialogIcon);
141 builder.setMessage(getText(R.string.openkeychain_required_long));
142 builder.setNegativeButton(getString(R.string.cancel), null);
143 builder.setNeutralButton(getString(R.string.restart),
144 new OnClickListener() {
145
146 @Override
147 public void onClick(DialogInterface dialog, int which) {
148 if (xmppConnectionServiceBound) {
149 unbindService(mConnection);
150 xmppConnectionServiceBound = false;
151 }
152 stopService(new Intent(XmppActivity.this,
153 XmppConnectionService.class));
154 finish();
155 }
156 });
157 builder.setPositiveButton(getString(R.string.install),
158 new OnClickListener() {
159
160 @Override
161 public void onClick(DialogInterface dialog, int which) {
162 Uri uri = Uri
163 .parse("market://details?id=org.sufficientlysecure.keychain");
164 Intent marketIntent = new Intent(Intent.ACTION_VIEW,
165 uri);
166 PackageManager manager = getApplicationContext()
167 .getPackageManager();
168 List<ResolveInfo> infos = manager
169 .queryIntentActivities(marketIntent, 0);
170 if (infos.size() > 0) {
171 startActivity(marketIntent);
172 } else {
173 uri = Uri.parse("http://www.openkeychain.org/");
174 Intent browserIntent = new Intent(
175 Intent.ACTION_VIEW, uri);
176 startActivity(browserIntent);
177 }
178 finish();
179 }
180 });
181 builder.create().show();
182 }
183
184 abstract void onBackendConnected();
185
186 public boolean onOptionsItemSelected(MenuItem item) {
187 switch (item.getItemId()) {
188 case R.id.action_settings:
189 startActivity(new Intent(this, SettingsActivity.class));
190 break;
191 case R.id.action_accounts:
192 startActivity(new Intent(this, ManageAccountActivity.class));
193 break;
194 case android.R.id.home:
195 finish();
196 break;
197 }
198 return super.onOptionsItemSelected(item);
199 }
200
201 @Override
202 protected void onCreate(Bundle savedInstanceState) {
203 super.onCreate(savedInstanceState);
204 metrics = getResources().getDisplayMetrics();
205 ExceptionHelper.init(getApplicationContext());
206 mPrimaryTextColor = getResources().getColor(R.color.primarytext);
207 mSecondaryTextColor = getResources().getColor(R.color.secondarytext);
208 mColorRed = getResources().getColor(R.color.red);
209 mColorOrange = getResources().getColor(R.color.orange);
210 mColorGreen = getResources().getColor(R.color.green);
211 mPrimaryColor = getResources().getColor(R.color.primary);
212 mSecondaryBackgroundColor = getResources().getColor(
213 R.color.secondarybackground);
214 if (getPreferences().getBoolean("use_larger_font", false)) {
215 setTheme(R.style.ConversationsTheme_LargerText);
216 }
217 mUseSubject = getPreferences().getBoolean("use_subject", true);
218 }
219
220 protected SharedPreferences getPreferences() {
221 return PreferenceManager
222 .getDefaultSharedPreferences(getApplicationContext());
223 }
224
225 public boolean useSubjectToIdentifyConference() {
226 return mUseSubject;
227 }
228
229 public void switchToConversation(Conversation conversation) {
230 switchToConversation(conversation, null, false);
231 }
232
233 public void switchToConversation(Conversation conversation, String text,
234 boolean newTask) {
235 Intent viewConversationIntent = new Intent(this,
236 ConversationActivity.class);
237 viewConversationIntent.setAction(Intent.ACTION_VIEW);
238 viewConversationIntent.putExtra(ConversationActivity.CONVERSATION,
239 conversation.getUuid());
240 if (text != null) {
241 viewConversationIntent.putExtra(ConversationActivity.TEXT, text);
242 }
243 viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
244 if (newTask) {
245 viewConversationIntent.setFlags(viewConversationIntent.getFlags()
246 | Intent.FLAG_ACTIVITY_NEW_TASK
247 | Intent.FLAG_ACTIVITY_SINGLE_TOP);
248 } else {
249 viewConversationIntent.setFlags(viewConversationIntent.getFlags()
250 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
251 }
252 startActivity(viewConversationIntent);
253 finish();
254 }
255
256 public void switchToContactDetails(Contact contact) {
257 Intent intent = new Intent(this, ContactDetailsActivity.class);
258 intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
259 intent.putExtra("account", contact.getAccount().getJid());
260 intent.putExtra("contact", contact.getJid());
261 startActivity(intent);
262 }
263
264 public void switchToAccount(Account account) {
265 Intent intent = new Intent(this, EditAccountActivity.class);
266 intent.putExtra("jid", account.getJid());
267 startActivity(intent);
268 }
269
270 protected void inviteToConversation(Conversation conversation) {
271 Intent intent = new Intent(getApplicationContext(),
272 ChooseContactActivity.class);
273 intent.putExtra("conversation", conversation.getUuid());
274 startActivityForResult(intent, REQUEST_INVITE_TO_CONVERSATION);
275 }
276
277 protected void announcePgp(Account account, final Conversation conversation) {
278 xmppConnectionService.getPgpEngine().generateSignature(account,
279 "online", new UiCallback<Account>() {
280
281 @Override
282 public void userInputRequried(PendingIntent pi,
283 Account account) {
284 try {
285 startIntentSenderForResult(pi.getIntentSender(),
286 REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
287 } catch (SendIntentException e) {
288 }
289 }
290
291 @Override
292 public void success(Account account) {
293 xmppConnectionService.databaseBackend
294 .updateAccount(account);
295 xmppConnectionService.sendPresencePacket(account,
296 xmppConnectionService.getPresenceGenerator()
297 .sendPresence(account));
298 if (conversation != null) {
299 conversation
300 .setNextEncryption(Message.ENCRYPTION_PGP);
301 xmppConnectionService.databaseBackend
302 .updateConversation(conversation);
303 }
304 }
305
306 @Override
307 public void error(int error, Account account) {
308 displayErrorDialog(error);
309 }
310 });
311 }
312
313 protected void displayErrorDialog(final int errorCode) {
314 runOnUiThread(new Runnable() {
315
316 @Override
317 public void run() {
318 AlertDialog.Builder builder = new AlertDialog.Builder(
319 XmppActivity.this);
320 builder.setIconAttribute(android.R.attr.alertDialogIcon);
321 builder.setTitle(getString(R.string.error));
322 builder.setMessage(errorCode);
323 builder.setNeutralButton(R.string.accept, null);
324 builder.create().show();
325 }
326 });
327
328 }
329
330 protected void showAddToRosterDialog(final Conversation conversation) {
331 String jid = conversation.getContactJid();
332 AlertDialog.Builder builder = new AlertDialog.Builder(this);
333 builder.setTitle(jid);
334 builder.setMessage(getString(R.string.not_in_roster));
335 builder.setNegativeButton(getString(R.string.cancel), null);
336 builder.setPositiveButton(getString(R.string.add_contact),
337 new DialogInterface.OnClickListener() {
338
339 @Override
340 public void onClick(DialogInterface dialog, int which) {
341 String jid = conversation.getContactJid();
342 Account account = conversation.getAccount();
343 Contact contact = account.getRoster().getContact(jid);
344 xmppConnectionService.createContact(contact);
345 switchToContactDetails(contact);
346 }
347 });
348 builder.create().show();
349 }
350
351 private void showAskForPresenceDialog(final Contact contact) {
352 AlertDialog.Builder builder = new AlertDialog.Builder(this);
353 builder.setTitle(contact.getJid());
354 builder.setMessage(R.string.request_presence_updates);
355 builder.setNegativeButton(R.string.cancel, null);
356 builder.setPositiveButton(R.string.request_now,
357 new DialogInterface.OnClickListener() {
358
359 @Override
360 public void onClick(DialogInterface dialog, int which) {
361 if (xmppConnectionServiceBound) {
362 xmppConnectionService.sendPresencePacket(contact
363 .getAccount(), xmppConnectionService
364 .getPresenceGenerator()
365 .requestPresenceUpdatesFrom(contact));
366 }
367 }
368 });
369 builder.create().show();
370 }
371
372 private void warnMutalPresenceSubscription(final Conversation conversation,
373 final OnPresenceSelected listener) {
374 AlertDialog.Builder builder = new AlertDialog.Builder(this);
375 builder.setTitle(conversation.getContact().getJid());
376 builder.setMessage(R.string.without_mutual_presence_updates);
377 builder.setNegativeButton(R.string.cancel, null);
378 builder.setPositiveButton(R.string.ignore, new OnClickListener() {
379
380 @Override
381 public void onClick(DialogInterface dialog, int which) {
382 conversation.setNextPresence(null);
383 if (listener != null) {
384 listener.onPresenceSelected();
385 }
386 }
387 });
388 builder.create().show();
389 }
390
391 protected void quickEdit(String previousValue, OnValueEdited callback) {
392 quickEdit(previousValue, callback, false);
393 }
394
395 protected void quickPasswordEdit(String previousValue,
396 OnValueEdited callback) {
397 quickEdit(previousValue, callback, true);
398 }
399
400 @SuppressLint("InflateParams")
401 private void quickEdit(final String previousValue,
402 final OnValueEdited callback, boolean password) {
403 AlertDialog.Builder builder = new AlertDialog.Builder(this);
404 View view = (View) getLayoutInflater()
405 .inflate(R.layout.quickedit, null);
406 final EditText editor = (EditText) view.findViewById(R.id.editor);
407 OnClickListener mClickListener = new OnClickListener() {
408
409 @Override
410 public void onClick(DialogInterface dialog, int which) {
411 String value = editor.getText().toString();
412 if (!previousValue.equals(value) && value.trim().length() > 0) {
413 callback.onValueEdited(value);
414 }
415 }
416 };
417 if (password) {
418 editor.setInputType(InputType.TYPE_CLASS_TEXT
419 | InputType.TYPE_TEXT_VARIATION_PASSWORD);
420 editor.setHint(R.string.password);
421 builder.setPositiveButton(R.string.accept, mClickListener);
422 } else {
423 builder.setPositiveButton(R.string.edit, mClickListener);
424 }
425 editor.requestFocus();
426 editor.setText(previousValue);
427 builder.setView(view);
428 builder.setNegativeButton(R.string.cancel, null);
429 builder.create().show();
430 }
431
432 public void selectPresence(final Conversation conversation,
433 final OnPresenceSelected listener) {
434 Contact contact = conversation.getContact();
435 if (!contact.showInRoster()) {
436 showAddToRosterDialog(conversation);
437 } else {
438 Presences presences = contact.getPresences();
439 if (presences.size() == 0) {
440 if (!contact.getOption(Contact.Options.TO)
441 && !contact.getOption(Contact.Options.ASKING)
442 && contact.getAccount().getStatus() == Account.STATUS_ONLINE) {
443 showAskForPresenceDialog(contact);
444 } else if (!contact.getOption(Contact.Options.TO)
445 || !contact.getOption(Contact.Options.FROM)) {
446 warnMutalPresenceSubscription(conversation, listener);
447 } else {
448 conversation.setNextPresence(null);
449 listener.onPresenceSelected();
450 }
451 } else if (presences.size() == 1) {
452 String presence = (String) presences.asStringArray()[0];
453 conversation.setNextPresence(presence);
454 listener.onPresenceSelected();
455 } else {
456 final StringBuilder presence = new StringBuilder();
457 AlertDialog.Builder builder = new AlertDialog.Builder(this);
458 builder.setTitle(getString(R.string.choose_presence));
459 final String[] presencesArray = presences.asStringArray();
460 int preselectedPresence = 0;
461 for (int i = 0; i < presencesArray.length; ++i) {
462 if (presencesArray[i].equals(contact.lastseen.presence)) {
463 preselectedPresence = i;
464 break;
465 }
466 }
467 presence.append(presencesArray[preselectedPresence]);
468 builder.setSingleChoiceItems(presencesArray,
469 preselectedPresence,
470 new DialogInterface.OnClickListener() {
471
472 @Override
473 public void onClick(DialogInterface dialog,
474 int which) {
475 presence.delete(0, presence.length());
476 presence.append(presencesArray[which]);
477 }
478 });
479 builder.setNegativeButton(R.string.cancel, null);
480 builder.setPositiveButton(R.string.ok, new OnClickListener() {
481
482 @Override
483 public void onClick(DialogInterface dialog, int which) {
484 conversation.setNextPresence(presence.toString());
485 listener.onPresenceSelected();
486 }
487 });
488 builder.create().show();
489 }
490 }
491 }
492
493 protected void onActivityResult(int requestCode, int resultCode,
494 final Intent data) {
495 super.onActivityResult(requestCode, resultCode, data);
496 if (requestCode == REQUEST_INVITE_TO_CONVERSATION
497 && resultCode == RESULT_OK) {
498 String contactJid = data.getStringExtra("contact");
499 String conversationUuid = data.getStringExtra("conversation");
500 Conversation conversation = xmppConnectionService
501 .findConversationByUuid(conversationUuid);
502 if (conversation.getMode() == Conversation.MODE_MULTI) {
503 xmppConnectionService.invite(conversation, contactJid);
504 }
505 Log.d(Config.LOGTAG, "inviting " + contactJid + " to "
506 + conversation.getName());
507 }
508 }
509
510 public int getSecondaryTextColor() {
511 return this.mSecondaryTextColor;
512 }
513
514 public int getPrimaryTextColor() {
515 return this.mPrimaryTextColor;
516 }
517
518 public int getWarningTextColor() {
519 return this.mColorRed;
520 }
521
522 public int getPrimaryColor() {
523 return this.mPrimaryColor;
524 }
525
526 public int getSecondaryBackgroundColor() {
527 return this.mSecondaryBackgroundColor;
528 }
529
530 public int getPixel(int dp) {
531 DisplayMetrics metrics = getResources().getDisplayMetrics();
532 return ((int) (dp * metrics.density));
533 }
534
535 public AvatarService avatarService() {
536 return xmppConnectionService.getAvatarService();
537 }
538
539 class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
540 private final WeakReference<ImageView> imageViewReference;
541 private Message message = null;
542
543 public BitmapWorkerTask(ImageView imageView) {
544 imageViewReference = new WeakReference<ImageView>(imageView);
545 }
546
547 @Override
548 protected Bitmap doInBackground(Message... params) {
549 message = params[0];
550 try {
551 return xmppConnectionService.getFileBackend().getThumbnail(
552 message, (int) (metrics.density * 288), false);
553 } catch (FileNotFoundException e) {
554 return null;
555 }
556 }
557
558 @Override
559 protected void onPostExecute(Bitmap bitmap) {
560 if (imageViewReference != null && bitmap != null) {
561 final ImageView imageView = imageViewReference.get();
562 if (imageView != null) {
563 imageView.setImageBitmap(bitmap);
564 imageView.setBackgroundColor(0x00000000);
565 }
566 }
567 }
568 }
569
570 public void loadBitmap(Message message, ImageView imageView) {
571 Bitmap bm;
572 try {
573 bm = xmppConnectionService.getFileBackend().getThumbnail(message,
574 (int) (metrics.density * 288), true);
575 } catch (FileNotFoundException e) {
576 bm = null;
577 }
578 if (bm != null) {
579 imageView.setImageBitmap(bm);
580 imageView.setBackgroundColor(0x00000000);
581 } else {
582 if (cancelPotentialWork(message, imageView)) {
583 imageView.setBackgroundColor(0xff333333);
584 final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
585 final AsyncDrawable asyncDrawable = new AsyncDrawable(
586 getResources(), null, task);
587 imageView.setImageDrawable(asyncDrawable);
588 try {
589 task.execute(message);
590 } catch (RejectedExecutionException e) {
591 return;
592 }
593 }
594 }
595 }
596
597 public static boolean cancelPotentialWork(Message message,
598 ImageView imageView) {
599 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
600
601 if (bitmapWorkerTask != null) {
602 final Message oldMessage = bitmapWorkerTask.message;
603 if (oldMessage == null || message != oldMessage) {
604 bitmapWorkerTask.cancel(true);
605 } else {
606 return false;
607 }
608 }
609 return true;
610 }
611
612 private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
613 if (imageView != null) {
614 final Drawable drawable = imageView.getDrawable();
615 if (drawable instanceof AsyncDrawable) {
616 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
617 return asyncDrawable.getBitmapWorkerTask();
618 }
619 }
620 return null;
621 }
622
623 static class AsyncDrawable extends BitmapDrawable {
624 private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
625
626 public AsyncDrawable(Resources res, Bitmap bitmap,
627 BitmapWorkerTask bitmapWorkerTask) {
628 super(res, bitmap);
629 bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(
630 bitmapWorkerTask);
631 }
632
633 public BitmapWorkerTask getBitmapWorkerTask() {
634 return bitmapWorkerTaskReference.get();
635 }
636 }
637}