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