XmppActivity.java

  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}