expert setting to manually change presence

Daniel Gultsch created

Change summary

src/main/AndroidManifest.xml                                             |   6 
src/main/java/eu/siacs/conversations/entities/Account.java               |  35 
src/main/java/eu/siacs/conversations/entities/Bookmark.java              |  13 
src/main/java/eu/siacs/conversations/entities/Contact.java               |  30 
src/main/java/eu/siacs/conversations/entities/ListItem.java              |   6 
src/main/java/eu/siacs/conversations/entities/Presence.java              |  43 
src/main/java/eu/siacs/conversations/entities/PresenceTemplate.java      |  55 
src/main/java/eu/siacs/conversations/parser/PresenceParser.java          |   3 
src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java    |  44 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java |  40 
src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java           |   2 
src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java       |   2 
src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java   |  11 
src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java      |   2 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java        |  12 
src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java         |  14 
src/main/java/eu/siacs/conversations/ui/SetPresenceActivity.java         | 200 
src/main/java/eu/siacs/conversations/ui/SettingsActivity.java            |   6 
src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java   |   4 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java                |  15 
src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java     |   2 
src/main/java/eu/siacs/conversations/utils/UIHelper.java                 |  17 
src/main/res/drawable-hdpi/ic_account_box_white_24dp.png                 |   0 
src/main/res/drawable-hdpi/ic_announcement_white_24dp.png                |   0 
src/main/res/drawable-mdpi/ic_account_box_white_24dp.png                 |   0 
src/main/res/drawable-mdpi/ic_announcement_white_24dp.png                |   0 
src/main/res/drawable-xhdpi/ic_account_box_white_24dp.png                |   0 
src/main/res/drawable-xhdpi/ic_announcement_white_24dp.png               |   0 
src/main/res/drawable-xxhdpi/ic_account_box_white_24dp.png               |   0 
src/main/res/drawable-xxhdpi/ic_announcement_white_24dp.png              |   0 
src/main/res/drawable-xxxhdpi/ic_account_box_white_24dp.png              |   0 
src/main/res/drawable-xxxhdpi/ic_announcement_white_24dp.png             |   0 
src/main/res/layout/activity_set_presence.xml                            |  72 
src/main/res/layout/presence_template.xml                                |  49 
src/main/res/layout/simple_list_item.xml                                 |  26 
src/main/res/menu/change_presence.xml                                    |  17 
src/main/res/menu/editaccount.xml                                        |   6 
src/main/res/values/arrays.xml                                           |   8 
src/main/res/values/strings.xml                                          |  10 
src/main/res/xml/preferences.xml                                         |  12 
40 files changed, 683 insertions(+), 79 deletions(-)

Detailed changes

src/main/AndroidManifest.xml 🔗

@@ -87,6 +87,12 @@
             android:label="@string/create_account"
             android:screenOrientation="portrait"
             android:launchMode="singleTask"/>
+        <activity
+            android:name=".ui.SetPresenceActivity"
+            android:label="@string/change_presence"
+            android:configChanges="orientation|screenSize"
+            android:windowSoftInputMode="stateHidden|adjustResize"
+            android:launchMode="singleTask"/>
         <activity
             android:name=".ui.SettingsActivity"
             android:label="@string/title_activity_settings"/>

src/main/java/eu/siacs/conversations/entities/Account.java 🔗

@@ -43,6 +43,8 @@ public class Account extends AbstractEntity {
 	public static final String DISPLAY_NAME = "display_name";
 	public static final String HOSTNAME = "hostname";
 	public static final String PORT = "port";
+	public static final String STATUS = "status";
+	public static final String STATUS_MESSAGE = "status_message";
 
 	public static final String PINNED_MECHANISM_KEY = "pinned_mechanism";
 
@@ -168,15 +170,18 @@ public class Account extends AbstractEntity {
 	private final Roster roster = new Roster(this);
 	private List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
 	private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
+	private Presence.Status presenceStatus = Presence.Status.ONLINE;
+	private String presenceStatusMessage = null;
 
 	public Account(final Jid jid, final String password) {
 		this(java.util.UUID.randomUUID().toString(), jid,
-				password, 0, null, "", null, null, null, 5222);
+				password, 0, null, "", null, null, null, 5222, Presence.Status.ONLINE, null);
 	}
 
 	private Account(final String uuid, final Jid jid,
-			final String password, final int options, final String rosterVersion, final String keys,
-			final String avatar, String displayName, String hostname, int port) {
+					final String password, final int options, final String rosterVersion, final String keys,
+					final String avatar, String displayName, String hostname, int port,
+					final Presence.Status status, String statusMessage) {
 		this.uuid = uuid;
 		this.jid = jid;
 		if (jid.isBareJid()) {
@@ -194,6 +199,8 @@ public class Account extends AbstractEntity {
 		this.displayName = displayName;
 		this.hostname = hostname;
 		this.port = port;
+		this.presenceStatus = status;
+		this.presenceStatusMessage = statusMessage;
 	}
 
 	public static Account fromCursor(final Cursor cursor) {
@@ -212,7 +219,9 @@ public class Account extends AbstractEntity {
 				cursor.getString(cursor.getColumnIndex(AVATAR)),
 				cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)),
 				cursor.getString(cursor.getColumnIndex(HOSTNAME)),
-				cursor.getInt(cursor.getColumnIndex(PORT)));
+				cursor.getInt(cursor.getColumnIndex(PORT)),
+				Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS))),
+				cursor.getString(cursor.getColumnIndex(STATUS_MESSAGE)));
 	}
 
 	public boolean isOptionSet(final int option) {
@@ -287,6 +296,22 @@ public class Account extends AbstractEntity {
 		return getXmppConnection() != null && getStatus().isError() && getXmppConnection().getAttempt() >= 3;
 	}
 
+	public void setPresenceStatus(Presence.Status status) {
+		this.presenceStatus = status;
+	}
+
+	public Presence.Status getPresenceStatus() {
+		return this.presenceStatus;
+	}
+
+	public void setPresenceStatusMessage(String message) {
+		this.presenceStatusMessage = message;
+	}
+
+	public String getPresenceStatusMessage() {
+		return this.presenceStatusMessage;
+	}
+
 	public String getResource() {
 		return jid.getResourcepart();
 	}
@@ -347,6 +372,8 @@ public class Account extends AbstractEntity {
 		values.put(DISPLAY_NAME, displayName);
 		values.put(HOSTNAME, hostname);
 		values.put(PORT, port);
+		values.put(STATUS, presenceStatus.toShowString());
+		values.put(STATUS_MESSAGE, presenceStatusMessage);
 		return values;
 	}
 

src/main/java/eu/siacs/conversations/entities/Bookmark.java 🔗

@@ -1,5 +1,7 @@
 package eu.siacs.conversations.entities;
 
+import android.content.Context;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
@@ -76,7 +78,7 @@ public class Bookmark extends Element implements ListItem {
 	}
 
 	@Override
-	public List<Tag> getTags() {
+	public List<Tag> getTags(Context context) {
 		ArrayList<Tag> tags = new ArrayList<Tag>();
 		for (Element element : getChildren()) {
 			if (element.getName().equals("group") && element.getContent() != null) {
@@ -114,7 +116,8 @@ public class Bookmark extends Element implements ListItem {
 		}
 	}
 
-	public boolean match(String needle) {
+	@Override
+	public boolean match(Context context, String needle) {
 		if (needle == null) {
 			return true;
 		}
@@ -122,12 +125,12 @@ public class Bookmark extends Element implements ListItem {
 		final Jid jid = getJid();
 		return (jid != null && jid.toString().contains(needle)) ||
 			getDisplayName().toLowerCase(Locale.US).contains(needle) ||
-			matchInTag(needle);
+			matchInTag(context, needle);
 	}
 
-	private boolean matchInTag(String needle) {
+	private boolean matchInTag(Context context, String needle) {
 		needle = needle.toLowerCase(Locale.US);
-		for (Tag tag : getTags()) {
+		for (Tag tag : getTags(context)) {
 			if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
 				return true;
 			}

src/main/java/eu/siacs/conversations/entities/Contact.java 🔗

@@ -1,6 +1,7 @@
 package eu.siacs.conversations.entities;
 
 import android.content.ContentValues;
+import android.content.Context;
 import android.database.Cursor;
 
 import org.json.JSONArray;
@@ -141,25 +142,14 @@ public class Contact implements ListItem, Blockable {
 	}
 
 	@Override
-	public List<Tag> getTags() {
+	public List<Tag> getTags(Context context) {
 		final ArrayList<Tag> tags = new ArrayList<>();
 		for (final String group : getGroups()) {
 			tags.add(new Tag(group, UIHelper.getColorForName(group)));
 		}
-		switch (getMostAvailableStatus()) {
-			case CHAT:
-			case ONLINE:
-				tags.add(new Tag("online", 0xff259b24));
-				break;
-			case AWAY:
-				tags.add(new Tag("away", 0xffff9800));
-				break;
-			case XA:
-				tags.add(new Tag("not available", 0xfff44336));
-				break;
-			case DND:
-				tags.add(new Tag("dnd", 0xfff44336));
-				break;
+		Presence.Status status = getMostAvailableStatus();
+		if (status != Presence.Status.OFFLINE) {
+			tags.add(UIHelper.getTagForStatus(context, status));
 		}
 		if (isBlocked()) {
 			tags.add(new Tag("blocked", 0xff2e2f3b));
@@ -167,7 +157,7 @@ public class Contact implements ListItem, Blockable {
 		return tags;
 	}
 
-	public boolean match(String needle) {
+	public boolean match(Context context, String needle) {
 		if (needle == null || needle.isEmpty()) {
 			return true;
 		}
@@ -175,7 +165,7 @@ public class Contact implements ListItem, Blockable {
 		String[] parts = needle.split("\\s+");
 		if (parts.length > 1) {
 			for(int i = 0; i < parts.length; ++i) {
-				if (!match(parts[i])) {
+				if (!match(context, parts[i])) {
 					return false;
 				}
 			}
@@ -183,13 +173,13 @@ public class Contact implements ListItem, Blockable {
 		} else {
 			return jid.toString().contains(needle) ||
 				getDisplayName().toLowerCase(Locale.US).contains(needle) ||
-				matchInTag(needle);
+				matchInTag(context, needle);
 		}
 	}
 
-	private boolean matchInTag(String needle) {
+	private boolean matchInTag(Context context, String needle) {
 		needle = needle.toLowerCase(Locale.US);
-		for (Tag tag : getTags()) {
+		for (Tag tag : getTags(context)) {
 			if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
 				return true;
 			}

src/main/java/eu/siacs/conversations/entities/ListItem.java 🔗

@@ -1,5 +1,7 @@
 package eu.siacs.conversations.entities;
 
+import android.content.Context;
+
 import java.util.List;
 
 import eu.siacs.conversations.xmpp.jid.Jid;
@@ -11,7 +13,7 @@ public interface ListItem extends Comparable<ListItem> {
 
 	Jid getJid();
 
-	List<Tag> getTags();
+	List<Tag> getTags(Context context);
 
 	final class Tag {
 		private final String name;
@@ -31,5 +33,5 @@ public interface ListItem extends Comparable<ListItem> {
 		}
 	}
 
-	boolean match(final String needle);
+	boolean match(Context context, final String needle);
 }

src/main/java/eu/siacs/conversations/entities/Presence.java 🔗

@@ -17,41 +17,46 @@ public class Presence implements Comparable {
 				case XA:   return "xa";
 				case DND:  return "dnd";
 			}
-
 			return null;
 		}
+
+		public static Status fromShowString(String show) {
+			if (show == null) {
+				return ONLINE;
+			} else {
+				switch (show.toLowerCase(Locale.US)) {
+					case "away":
+						return AWAY;
+					case "xa":
+						return XA;
+					case "dnd":
+						return DND;
+					case "chat":
+						return CHAT;
+					default:
+						return ONLINE;
+				}
+			}
+		}
 	}
 
 	protected final Status status;
 	protected ServiceDiscoveryResult disco;
 	protected final String ver;
 	protected final String hash;
+	protected final String message;
 
-	private Presence(Status status, String ver, String hash) {
+	private Presence(Status status, String ver, String hash, String message) {
 		this.status = status;
 		this.ver = ver;
 		this.hash = hash;
+		this.message = message;
 	}
 
-	public static Presence parse(String show, Element caps) {
+	public static Presence parse(String show, Element caps, String message) {
 		final String hash = caps == null ? null : caps.getAttribute("hash");
 		final String ver = caps == null ? null : caps.getAttribute("ver");
-		if (show == null) {
-			return new Presence(Status.ONLINE, ver, hash);
-		} else {
-			switch (show.toLowerCase(Locale.US)) {
-				case "away":
-					return new Presence(Status.AWAY, ver, hash);
-				case "xa":
-					return new Presence(Status.XA, ver, hash);
-				case "dnd":
-					return new Presence(Status.DND, ver, hash);
-				case "chat":
-					return new Presence(Status.CHAT, ver, hash);
-				default:
-					return new Presence(Status.ONLINE, ver, hash);
-			}
-		}
+		return new Presence(Status.fromShowString(show), ver, hash, message);
 	}
 
 	public int compareTo(Object other) {

src/main/java/eu/siacs/conversations/entities/PresenceTemplate.java 🔗

@@ -0,0 +1,55 @@
+package eu.siacs.conversations.entities;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+
+public class PresenceTemplate extends AbstractEntity {
+
+	public static final String TABELNAME = "presence_templates";
+	public static final String LAST_USED = "last_used";
+	public static final String MESSAGE = "message";
+	public static final String STATUS = "status";
+
+	private long lastUsed = 0;
+	private String statusMessage;
+	private Presence.Status status = Presence.Status.ONLINE;
+
+	public PresenceTemplate(Presence.Status status, String statusMessage) {
+		this.status = status;
+		this.statusMessage = statusMessage;
+		this.lastUsed = System.currentTimeMillis();
+		this.uuid = java.util.UUID.randomUUID().toString();
+	}
+
+	private PresenceTemplate() {
+
+	}
+
+	@Override
+	public ContentValues getContentValues() {
+		ContentValues values = new ContentValues();
+		values.put(LAST_USED, lastUsed);
+		values.put(MESSAGE, statusMessage);
+		values.put(STATUS, status.toShowString());
+		values.put(UUID, uuid);
+		return values;
+	}
+
+	public static PresenceTemplate fromCursor(Cursor cursor) {
+		PresenceTemplate template = new PresenceTemplate();
+		template.uuid = cursor.getString(cursor.getColumnIndex(UUID));
+		template.lastUsed = cursor.getLong(cursor.getColumnIndex(LAST_USED));
+		template.statusMessage = cursor.getString(cursor.getColumnIndex(MESSAGE));
+		template.status = Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS)));
+		return template;
+	}
+
+	public Presence.Status getStatus() {
+		return status;
+	}
+
+	public String getStatusMessage() {
+		return statusMessage;
+	}
+}

src/main/java/eu/siacs/conversations/parser/PresenceParser.java 🔗

@@ -196,7 +196,8 @@ public class PresenceParser extends AbstractParser implements
 
 			final String show = packet.findChildContent("show");
 			final Element caps = packet.findChild("c", "http://jabber.org/protocol/caps");
-			final Presence presence = Presence.parse(show, caps);
+			final String message = packet.findChildContent("status");
+			final Presence presence = Presence.parse(show, caps, message);
 			contact.updatePresence(resource, presence);
 			if (presence.hasCaps() && Config.REQUEST_DISCO) {
 				mXmppConnectionService.fetchCaps(account, from, presence);

src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java 🔗

@@ -41,6 +41,7 @@ import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.PresenceTemplate;
 import eu.siacs.conversations.entities.Roster;
 import eu.siacs.conversations.entities.ServiceDiscoveryResult;
 import eu.siacs.conversations.xmpp.jid.InvalidJidException;
@@ -51,7 +52,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 	private static DatabaseBackend instance = null;
 
 	private static final String DATABASE_NAME = "history";
-	private static final int DATABASE_VERSION = 25;
+	private static final int DATABASE_VERSION = 26;
 
 	private static String CREATE_CONTATCS_STATEMENT = "create table "
 			+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@@ -73,6 +74,14 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 			+ "UNIQUE(" + ServiceDiscoveryResult.HASH + ", "
 			+ ServiceDiscoveryResult.VER + ") ON CONFLICT REPLACE);";
 
+	private static String CREATE_PRESENCE_TEMPLATES_STATEMENT = "CREATE TABLE "
+			+ PresenceTemplate.TABELNAME + "("
+			+ PresenceTemplate.UUID + " TEXT, "
+			+ PresenceTemplate.LAST_USED + " NUMBER,"
+			+ PresenceTemplate.MESSAGE + " TEXT,"
+			+ PresenceTemplate.STATUS + " TEXT,"
+			+ "UNIQUE("+PresenceTemplate.MESSAGE + "," +PresenceTemplate.STATUS+") ON CONFLICT REPLACE);";
+
 	private static String CREATE_PREKEYS_STATEMENT = "CREATE TABLE "
 			+ SQLiteAxolotlStore.PREKEY_TABLENAME + "("
 			+ SQLiteAxolotlStore.ACCOUNT + " TEXT,  "
@@ -175,6 +184,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 		db.execSQL(CREATE_PREKEYS_STATEMENT);
 		db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
 		db.execSQL(CREATE_IDENTITIES_STATEMENT);
+		db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT);
 	}
 
 	@Override
@@ -331,6 +341,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 			db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.HOSTNAME + " TEXT");
 			db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PORT + " NUMBER DEFAULT 5222");
 		}
+		if (oldVersion < 26 && newVersion >= 26) {
+			db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS + " TEXT");
+			db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS_MESSAGE + " TEXT");
+		}
 		/* Any migrations that alter the Account table need to happen BEFORE this migration, as it
 		 * depends on account de-serialization.
 		 */
@@ -380,6 +394,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 		if (oldVersion < 25 && newVersion >= 25) {
 			db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.OOB + " INTEGER");
 		}
+
+		if (oldVersion <  26 && newVersion >= 26) {
+			db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT);
+		}
 	}
 
 	public static synchronized DatabaseBackend getInstance(Context context) {
@@ -430,6 +448,30 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 		return result;
 	}
 
+	public void insertPresenceTemplate(PresenceTemplate template) {
+		SQLiteDatabase db = this.getWritableDatabase();
+		db.insert(PresenceTemplate.TABELNAME, null, template.getContentValues());
+	}
+
+	public List<PresenceTemplate> getPresenceTemplates() {
+		ArrayList<PresenceTemplate> templates = new ArrayList<>();
+		SQLiteDatabase db = this.getReadableDatabase();
+		Cursor cursor = db.query(PresenceTemplate.TABELNAME,null,null,null,null,null,PresenceTemplate.LAST_USED+" desc");
+		while (cursor.moveToNext()) {
+			templates.add(PresenceTemplate.fromCursor(cursor));
+		}
+		cursor.close();
+		return templates;
+	}
+
+	public void deletePresenceTemplate(PresenceTemplate template) {
+		Log.d(Config.LOGTAG,"deleting presence template with uuid "+template.getUuid());
+		SQLiteDatabase db = this.getWritableDatabase();
+		String where = PresenceTemplate.UUID+"=?";
+		String[] whereArgs = {template.getUuid()};
+		db.delete(PresenceTemplate.TABELNAME,where,whereArgs);
+	}
+
 	public CopyOnWriteArrayList<Conversation> getConversations(int status) {
 		CopyOnWriteArrayList<Conversation> list = new CopyOnWriteArrayList<>();
 		SQLiteDatabase db = this.getReadableDatabase();

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -73,6 +73,7 @@ import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.MucOptions;
 import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
 import eu.siacs.conversations.entities.Presence;
+import eu.siacs.conversations.entities.PresenceTemplate;
 import eu.siacs.conversations.entities.Roster;
 import eu.siacs.conversations.entities.ServiceDiscoveryResult;
 import eu.siacs.conversations.entities.Transferable;
@@ -629,6 +630,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
 		return getPreferences().getBoolean("xa_on_silent_mode", false);
 	}
 
+	private boolean manuallyChangePresence() {
+		return getPreferences().getBoolean("manually_change_presence", false);
+	}
+
 	private boolean treatVibrateAsSilent() {
 		return getPreferences().getBoolean("treat_vibrate_as_silent", false);
 	}
@@ -762,7 +767,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
 	}
 
 	public void toggleScreenEventReceiver() {
-		if (awayWhenScreenOff()) {
+		if (awayWhenScreenOff() && !manuallyChangePresence()) {
 			final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
 			filter.addAction(Intent.ACTION_SCREEN_OFF);
 			registerReceiver(this.mEventReceiver, filter);
@@ -2998,7 +3003,17 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
 	}
 
 	public void sendPresence(final Account account) {
-		sendPresencePacket(account, mPresenceGenerator.selfPresence(account, getTargetPresence()));
+		PresencePacket packet;
+		if (manuallyChangePresence()) {
+			packet =  mPresenceGenerator.selfPresence(account, account.getPresenceStatus());
+			String message = account.getPresenceStatusMessage();
+			if (message != null && !message.isEmpty()) {
+				packet.addChild(new Element("status").setContent(message));
+			}
+		} else {
+			packet = mPresenceGenerator.selfPresence(account, getTargetPresence());
+		}
+		sendPresencePacket(account, packet);
 	}
 
 	public void refreshAllPresences() {
@@ -3229,6 +3244,27 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
 		return pending;
 	}
 
+	public void changeStatus(Account account, Presence.Status status, String statusMessage) {
+		databaseBackend.insertPresenceTemplate(new PresenceTemplate(status, statusMessage));
+		changeStatusReal(account, status, statusMessage);
+	}
+
+	private void changeStatusReal(Account account, Presence.Status status, String statusMessage) {
+		account.setPresenceStatus(status);
+		account.setPresenceStatusMessage(statusMessage);
+		databaseBackend.updateAccount(account);
+		if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+			sendPresence(account);
+		}
+	}
+
+	public void changeStatus(Presence.Status status, String statusMessage) {
+		databaseBackend.insertPresenceTemplate(new PresenceTemplate(status, statusMessage));
+		for(Account account : getAccounts()) {
+			changeStatusReal(account, status, statusMessage);
+		}
+	}
+
 	public interface OnMamPreferencesFetched {
 		void onPreferencesFetched(Element prefs);
 		void onPreferencesFetchFailed();

src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java 🔗

@@ -49,7 +49,7 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem
 		if (account != null) {
 			for (final Jid jid : account.getBlocklist()) {
 				final Contact contact = account.getRoster().getContact(jid);
-				if (contact.match(needle) && contact.isBlocked()) {
+				if (contact.match(this, needle) && contact.isBlocked()) {
 					getListItems().add(contact);
 				}
 			}

src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java 🔗

@@ -144,7 +144,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity {
 				for (final Contact contact : account.getRoster().getContacts()) {
 					if (contact.showInRoster() &&
 							!filterContacts.contains(contact.getJid().toBareJid().toString())
-							&& contact.match(needle)) {
+							&& contact.match(this, needle)) {
 						getListItems().add(contact);
 					}
 				}

src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java 🔗

@@ -643,17 +643,6 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
 		}
 	}
 
-	@SuppressWarnings("deprecation")
-	@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
-	private void setListItemBackgroundOnView(View view) {
-		int sdk = android.os.Build.VERSION.SDK_INT;
-		if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) {
-			view.setBackgroundDrawable(getResources().getDrawable(R.drawable.greybackground));
-		} else {
-			view.setBackground(getResources().getDrawable(R.drawable.greybackground));
-		}
-	}
-
 	private void viewPgpKey(User user) {
 		PgpEngine pgp = xmppConnectionService.getPgpEngine();
 		if (pgp != null) {

src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java 🔗

@@ -439,7 +439,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
 			keys.setVisibility(View.GONE);
 		}
 
-		List<ListItem.Tag> tagList = contact.getTags();
+		List<ListItem.Tag> tagList = contact.getTags(this);
 		if (tagList.size() == 0 || !this.showDynamicTags) {
 			tags.setVisibility(View.GONE);
 		} else {

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java 🔗

@@ -474,9 +474,15 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 					}
 				} else {
 					Account account = message.getConversation().getAccount();
-					Intent intent = new Intent(activity, EditAccountActivity.class);
-					intent.putExtra("jid", account.getJid().toBareJid().toString());
-					intent.putExtra("fingerprint", message.getFingerprint());
+					Intent intent;
+					if (activity.manuallyChangePresence()) {
+						intent = new Intent(activity, SetPresenceActivity.class);
+						intent.putExtra(SetPresenceActivity.EXTRA_ACCOUNT, account.getJid().toBareJid().toString());
+					} else {
+						intent = new Intent(activity, EditAccountActivity.class);
+						intent.putExtra("jid", account.getJid().toBareJid().toString());
+						intent.putExtra("fingerprint", message.getFingerprint());
+					}
 					startActivity(intent);
 				}
 			}

src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java 🔗

@@ -476,6 +476,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 		final MenuItem clearDevices = menu.findItem(R.id.action_clear_devices);
 		final MenuItem renewCertificate = menu.findItem(R.id.action_renew_certificate);
 		final MenuItem mamPrefs = menu.findItem(R.id.action_mam_prefs);
+		final MenuItem changePresence = menu.findItem(R.id.action_change_presence);
+
+		changePresence.setVisible(manuallyChangePresence());
 
 		renewCertificate.setVisible(mAccount != null && mAccount.getPrivateKeyAlias() != null);
 
@@ -536,8 +539,8 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 	protected void onBackendConnected() {
 		if (this.jidToEdit != null) {
 			this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit);
-			this.mInitMode |= this.mAccount.isOptionSet(Account.OPTION_REGISTER);
 			if (this.mAccount != null) {
+				this.mInitMode |= this.mAccount.isOptionSet(Account.OPTION_REGISTER);
 				if (this.mAccount.getPrivateKeyAlias() != null) {
 					this.mPassword.setHint(R.string.authenticate_with_certificate);
 					if (this.mInitMode) {
@@ -593,6 +596,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 			case R.id.action_renew_certificate:
 				renewCertificate();
 				break;
+			case R.id.action_change_presence:
+				changePresence();
+				break;
 		}
 		return super.onOptionsItemSelected(item);
 	}
@@ -601,6 +607,12 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 		KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
 	}
 
+	private void changePresence() {
+		Intent intent = new Intent(this, SetPresenceActivity.class);
+		intent.putExtra(SetPresenceActivity.EXTRA_ACCOUNT,mAccount.getJid().toBareJid().toString());
+		startActivity(intent);
+	}
+
 	@Override
 	public void alias(String alias) {
 		if (alias != null) {

src/main/java/eu/siacs/conversations/ui/SetPresenceActivity.java 🔗

@@ -0,0 +1,200 @@
+package eu.siacs.conversations.ui;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import android.util.Log;
+
+import java.util.List;
+import java.util.concurrent.RunnableFuture;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.ListItem;
+import eu.siacs.conversations.entities.Presence;
+import eu.siacs.conversations.entities.PresenceTemplate;
+import eu.siacs.conversations.utils.UIHelper;
+
+public class SetPresenceActivity extends XmppActivity implements View.OnClickListener {
+
+	//data
+	protected Account mAccount;
+	private List<PresenceTemplate> mTemplates;
+
+	//UI Elements
+	protected ScrollView mScrollView;
+	protected EditText mStatusMessage;
+	protected Spinner mShowSpinner;
+	protected CheckBox mAllAccounts;
+	protected LinearLayout mTemplatesView;
+
+	protected void onCreate(final Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		setContentView(R.layout.activity_set_presence);
+		mScrollView = (ScrollView) findViewById(R.id.scroll_view);
+		mShowSpinner = (Spinner) findViewById(R.id.presence_show);
+		ArrayAdapter adapter = ArrayAdapter.createFromResource(this,
+				R.array.presence_show_options,
+				R.layout.simple_list_item);
+		mShowSpinner.setAdapter(adapter);
+		mShowSpinner.setSelection(1);
+		mStatusMessage = (EditText) findViewById(R.id.presence_status_message);
+		mAllAccounts = (CheckBox) findViewById(R.id.all_accounts);
+		mTemplatesView = (LinearLayout) findViewById(R.id.templates);
+		final Button changePresence = (Button) findViewById(R.id.change_presence);
+		changePresence.setOnClickListener(new View.OnClickListener() {
+			@Override
+			public void onClick(View v) {
+				executeChangePresence();
+			}
+		});
+	}
+
+	@Override
+	public boolean onCreateOptionsMenu(Menu menu) {
+		getMenuInflater().inflate(R.menu.change_presence, menu);
+		return super.onCreateOptionsMenu(menu);
+	}
+
+	@Override
+	public boolean onOptionsItemSelected(final MenuItem item) {
+		if (item.getItemId() == R.id.action_account_details) {
+			if (mAccount != null) {
+				switchToAccount(mAccount);
+			}
+			return true;
+		} else {
+			return super.onOptionsItemSelected(item);
+		}
+	}
+
+	private void executeChangePresence() {
+		Presence.Status status = getStatusFromSpinner();
+		boolean allAccounts = mAllAccounts.isChecked();
+		String statusMessage = mStatusMessage.getText().toString().trim();
+		if (allAccounts) {
+			xmppConnectionService.changeStatus(status, statusMessage);
+		} else if (mAccount != null) {
+			xmppConnectionService.changeStatus(mAccount, status, statusMessage);
+		}
+		finish();
+	}
+
+	private Presence.Status getStatusFromSpinner() {
+		switch (mShowSpinner.getSelectedItemPosition()) {
+			case 0:
+				return Presence.Status.CHAT;
+			case 2:
+				return Presence.Status.AWAY;
+			case 3:
+				return Presence.Status.XA;
+			case 4:
+				return Presence.Status.DND;
+			default:
+				return Presence.Status.ONLINE;
+		}
+	}
+
+	private void setStatusInSpinner(Presence.Status status) {
+		switch(status) {
+			case AWAY:
+				mShowSpinner.setSelection(2);
+				break;
+			case XA:
+				mShowSpinner.setSelection(3);
+				break;
+			case CHAT:
+				mShowSpinner.setSelection(0);
+				break;
+			case DND:
+				mShowSpinner.setSelection(4);
+				break;
+			default:
+				mShowSpinner.setSelection(1);
+				break;
+		}
+	}
+
+	@Override
+	protected void refreshUiReal() {
+
+	}
+
+	@Override
+	void onBackendConnected() {
+		mAccount = extractAccount(getIntent());
+		if (mAccount != null) {
+			setStatusInSpinner(mAccount.getPresenceStatus());
+			String message = mAccount.getPresenceStatusMessage();
+			if (mStatusMessage.getText().length() == 0 && message != null) {
+				mStatusMessage.append(message);
+			}
+			mTemplates = xmppConnectionService.databaseBackend.getPresenceTemplates();
+		}
+		redrawTemplates();
+	}
+
+	private void redrawTemplates() {
+		if (mTemplates == null || mTemplates.size() == 0) {
+			mTemplatesView.setVisibility(View.GONE);
+		} else {
+			mTemplatesView.removeAllViews();
+			mTemplatesView.setVisibility(View.VISIBLE);
+			LayoutInflater inflater = getLayoutInflater();
+			for (PresenceTemplate template : mTemplates) {
+				View templateLayout = inflater.inflate(R.layout.presence_template, mTemplatesView, false);
+				templateLayout.setTag(template);
+				setListItemBackgroundOnView(templateLayout);
+				templateLayout.setOnClickListener(this);
+				TextView message = (TextView) templateLayout.findViewById(R.id.presence_status_message);
+				TextView status = (TextView) templateLayout.findViewById(R.id.status);
+				ImageButton button = (ImageButton) templateLayout.findViewById(R.id.delete_button);
+				button.setTag(template);
+				button.setOnClickListener(this);
+				ListItem.Tag tag = UIHelper.getTagForStatus(this, template.getStatus());
+				status.setText(tag.getName());
+				status.setBackgroundColor(tag.getColor());
+				message.setText(template.getStatusMessage());
+				mTemplatesView.addView(templateLayout);
+			}
+		}
+	}
+
+	@Override
+	public void onClick(View v) {
+		PresenceTemplate template = (PresenceTemplate) v.getTag();
+		if (template == null) {
+			return;
+		}
+		if (v.getId() == R.id.presence_template) {
+			setStatusInSpinner(template.getStatus());
+			mStatusMessage.getEditableText().clear();
+			mStatusMessage.getEditableText().append(template.getStatusMessage());
+			new Handler().post(new Runnable() {
+				@Override
+				public void run() {
+					mScrollView.smoothScrollTo(0,0);
+				}
+			});
+		} else if (v.getId() == R.id.delete_button) {
+			xmppConnectionService.databaseBackend.deletePresenceTemplate(template);
+			mTemplates.remove(template);
+			redrawTemplates();
+		}
+	}
+}

src/main/java/eu/siacs/conversations/ui/SettingsActivity.java 🔗

@@ -161,7 +161,8 @@ public class SettingsActivity extends XmppActivity implements
 				"xa_on_silent_mode",
 				"away_when_screen_off",
 				"allow_message_correction",
-				"treat_vibrate_as_silent");
+				"treat_vibrate_as_silent",
+				"manually_change_presence");
 		if (name.equals("resource")) {
 			String resource = preferences.getString("resource", "mobile")
 					.toLowerCase(Locale.US);
@@ -182,7 +183,8 @@ public class SettingsActivity extends XmppActivity implements
 			xmppConnectionService.toggleForegroundService();
 		} else if (resendPresence.contains(name)) {
 			if (xmppConnectionServiceBound) {
-				if (name.equals("away_when_screen_off")) {
+				if (name.equals("away_when_screen_off")
+						|| name.equals("manually_change_presence")) {
 					xmppConnectionService.toggleScreenEventReceiver();
 				}
 				xmppConnectionService.refreshAllPresences();

src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java 🔗

@@ -738,7 +738,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
 				for (Contact contact : account.getRoster().getContacts()) {
 					Presence p = contact.getPresences().getMostAvailablePresence();
 					Presence.Status s = p == null ? Presence.Status.OFFLINE : p.getStatus();
-					if (contact.showInRoster() && contact.match(needle)
+					if (contact.showInRoster() && contact.match(this, needle)
 							&& (!this.mHideOfflineContacts
 							|| (needle != null && !needle.trim().isEmpty())
 							|| s.compareTo(Presence.Status.OFFLINE) < 0)) {
@@ -756,7 +756,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
 		for (Account account : xmppConnectionService.getAccounts()) {
 			if (account.getStatus() != Account.State.DISABLED) {
 				for (Bookmark bookmark : account.getBookmarks()) {
-					if (bookmark.match(needle)) {
+					if (bookmark.match(this, needle)) {
 						this.conferences.add(bookmark);
 					}
 				}

src/main/java/eu/siacs/conversations/ui/XmppActivity.java 🔗

@@ -536,6 +536,17 @@ public abstract class XmppActivity extends Activity {
 		}
 	}
 
+	@SuppressWarnings("deprecation")
+	@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+	protected void setListItemBackgroundOnView(View view) {
+		int sdk = android.os.Build.VERSION.SDK_INT;
+		if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) {
+			view.setBackgroundDrawable(getResources().getDrawable(R.drawable.greybackground));
+		} else {
+			view.setBackground(getResources().getDrawable(R.drawable.greybackground));
+		}
+	}
+
 	protected void choosePgpSignId(Account account) {
 		xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback<Account>() {
 			@Override
@@ -1006,6 +1017,10 @@ public abstract class XmppActivity extends Activity {
 		return getPreferences().getString("picture_compression", "auto").equals("never");
 	}
 
+	protected boolean manuallyChangePresence() {
+		return getPreferences().getBoolean("manually_change_presence", false);
+	}
+
 	protected void unregisterNdefPushMessageCallback() {
 		NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
 		if (nfcAdapter != null && nfcAdapter.isEnabled()) {

src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java 🔗

@@ -62,7 +62,7 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> {
 		ImageView picture = (ImageView) view.findViewById(R.id.contact_photo);
 		LinearLayout tagLayout = (LinearLayout) view.findViewById(R.id.tags);
 
-		List<ListItem.Tag> tags = item.getTags();
+		List<ListItem.Tag> tags = item.getTags(activity);
 		if (tags.size() == 0 || !this.showDynamicTags) {
 			tagLayout.setVisibility(View.GONE);
 		} else {

src/main/java/eu/siacs/conversations/utils/UIHelper.java 🔗

@@ -14,7 +14,9 @@ import java.util.Locale;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.ListItem;
 import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Presence;
 import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.xmpp.jid.Jid;
 
@@ -267,4 +269,19 @@ public class UIHelper {
 		body = body.replace("?","").replace("¿","");
 		return LOCATION_QUESTIONS.contains(body);
 	}
+
+	public static ListItem.Tag getTagForStatus(Context context, Presence.Status status) {
+		switch (status) {
+			case CHAT:
+				return new ListItem.Tag(context.getString(R.string.presence_chat), 0xff259b24);
+			case AWAY:
+				return new ListItem.Tag(context.getString(R.string.presence_away), 0xffff9800);
+			case XA:
+				return new ListItem.Tag(context.getString(R.string.presence_xa), 0xfff44336);
+			case DND:
+				return new ListItem.Tag(context.getString(R.string.presence_dnd), 0xfff44336);
+			default:
+				return new ListItem.Tag(context.getString(R.string.presence_online), 0xff259b24);
+		}
+	}
 }

src/main/res/layout/activity_set_presence.xml 🔗

@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+            android:layout_width="fill_parent"
+            android:layout_height="fill_parent"
+            android:background="@color/grey200"
+            android:id="@+id/scroll_view">
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="@dimen/activity_horizontal_margin"
+            android:layout_marginRight="@dimen/activity_horizontal_margin"
+            android:layout_marginTop="@dimen/activity_vertical_margin"
+            android:layout_marginBottom="@dimen/activity_vertical_margin"
+            android:background="@drawable/infocard_border"
+            android:padding="@dimen/infocard_padding"
+            android:orientation="vertical">
+            <EditText
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:inputType="textMultiLine"
+                android:hint="@string/status_message"
+                android:id="@+id/presence_status_message"
+                android:textColor="@color/black87"
+                android:layout_marginBottom="8dp"
+                android:textSize="?attr/TextSizeBody"/>
+            <Spinner
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:id="@+id/presence_show"
+                android:layout_gravity="center_horizontal"/>
+            <CheckBox
+                android:layout_marginTop="16dp"
+                android:layout_marginBottom="16dp"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/all_accounts_on_this_device"
+                android:id="@+id/all_accounts"
+                android:textColor="@color/black87"
+                android:textSize="?attr/TextSizeBody"/>
+            <Button
+                android:id="@+id/change_presence"
+                style="?android:attr/borderlessButtonStyle"
+                android:layout_marginRight="-8dp"
+                android:layout_marginBottom="-8dp"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="right"
+                android:text="@string/change_presence"
+                android:textColor="@color/accent"/>
+        </LinearLayout>
+    <LinearLayout
+        android:id="@+id/templates"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="@dimen/activity_horizontal_margin"
+        android:layout_marginRight="@dimen/activity_horizontal_margin"
+        android:layout_marginTop="@dimen/activity_vertical_margin"
+        android:layout_marginBottom="@dimen/activity_vertical_margin"
+        android:background="@drawable/infocard_border"
+        android:padding="@dimen/infocard_padding"
+        android:orientation="vertical"
+        android:divider="?android:dividerHorizontal"
+        android:showDividers="middle">
+        </LinearLayout>
+    </LinearLayout>
+</ScrollView>

src/main/res/layout/presence_template.xml 🔗

@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="?android:attr/activatedBackgroundIndicator"
+                android:paddingTop="8dp"
+                android:paddingLeft="8dp"
+                android:paddingBottom="8dp"
+                android:id="@+id/presence_template">
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:layout_centerVertical="true"
+        android:layout_alignParentLeft="true"
+        android:layout_alignParentStart="true"
+        android:layout_toLeftOf="@+id/delete_button"
+        android:layout_toStartOf="@+id/delete_button"
+        android:layout_marginRight="8dp">
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/presence_status_message"
+        android:textColor="@color/black87"
+        android:textSize="?attr/TextSizeBody"/>
+    <TextView
+        android:id="@+id/status"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:paddingBottom="1dp"
+        android:paddingLeft="4dp"
+        android:paddingRight="4dp"
+        android:paddingTop="1dp"
+        android:textAllCaps="true"
+        android:textColor="@color/white"
+        android:textSize="?attr/TextSizeInfo"
+        android:layout_marginTop="4dp"/>
+    </LinearLayout>
+    <ImageButton
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/delete_button"
+        android:layout_centerVertical="true"
+        android:layout_alignParentRight="true"
+        android:layout_alignParentEnd="true"
+        android:background="?android:selectableItemBackground"
+        android:padding="@dimen/image_button_padding"
+        android:src="?attr/icon_remove"/>
+</RelativeLayout>

src/main/res/layout/simple_list_item.xml 🔗

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+          android:id="@android:id/text1"
+          android:layout_width="match_parent"
+          android:layout_height="wrap_content"
+          android:textColor="@color/black87"
+          android:textSize="?attr/TextSizeBody"
+          android:gravity="center_vertical"
+          android:paddingLeft="8dp"
+          android:paddingRight="8dp"
+          android:minHeight="?android:attr/listPreferredItemHeightSmall" />

src/main/res/menu/change_presence.xml 🔗

@@ -0,0 +1,17 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/action_account_details"
+        android:title="@string/account_details"
+        android:showAsAction="always"
+        android:icon="@drawable/ic_account_box_white_24dp"/>
+    <item
+        android:id="@+id/action_accounts"
+        android:orderInCategory="90"
+        android:showAsAction="never"
+        android:title="@string/action_accounts"/>
+    <item
+        android:id="@+id/action_settings"
+        android:orderInCategory="100"
+        android:showAsAction="never"
+        android:title="@string/action_settings"/>
+</menu>

src/main/res/menu/editaccount.xml 🔗

@@ -1,5 +1,11 @@
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
 
+    <item
+        android:id="@+id/action_change_presence"
+        android:showAsAction="always"
+        android:title="@string/change_presence"
+        android:icon="@drawable/ic_announcement_white_24dp"/>
+
     <item
         android:id="@+id/action_show_qr_code"
         android:showAsAction="never"

src/main/res/values/arrays.xml 🔗

@@ -75,4 +75,12 @@
 		<item>@string/contacts</item>
 		<item>@string/always</item>
 	</string-array>
+
+	<string-array name="presence_show_options">
+		<item>@string/presence_chat</item>
+		<item>@string/presence_online</item>
+		<item>@string/presence_away</item>
+		<item>@string/presence_xa</item>
+		<item>@string/presence_dnd</item>
+	</string-array>
 </resources>

src/main/res/values/strings.xml 🔗

@@ -625,4 +625,14 @@
 	<string name="create_account">Create Account</string>
 	<string name="use_own_provider">Use my own provider</string>
 	<string name="pick_your_username">Pick your username</string>
+	<string name="pref_manually_change_presence">Manually change presence</string>
+	<string name="pref_manually_change_presence_summary">Touch your avatar to change your presence</string>
+	<string name="change_presence">Change Presence</string>
+	<string name="status_message">Status message</string>
+	<string name="all_accounts_on_this_device">Set for all accounts on this device</string>
+	<string name="presence_chat">Free for Chat</string>
+	<string name="presence_online">Online</string>
+	<string name="presence_away">Away</string>
+	<string name="presence_xa">Not Available</string>
+	<string name="presence_dnd">Busy</string>
 </resources>

src/main/res/xml/preferences.xml 🔗

@@ -191,16 +191,24 @@
                     android:title="@string/pref_display_enter_key"/>
             </PreferenceCategory>
             <PreferenceCategory android:title="@string/pref_presence_settings">
+                <CheckBoxPreference
+                    android:defaultValue="false"
+                    android:key="manually_change_presence"
+                    android:title="@string/pref_manually_change_presence"
+                    android:summary="@string/pref_manually_change_presence_summary"
+                    android:disableDependentsState="true"/>
                 <CheckBoxPreference
                     android:defaultValue="false"
                     android:key="away_when_screen_off"
                     android:summary="@string/pref_away_when_screen_off_summary"
-                    android:title="@string/pref_away_when_screen_off"/>
+                    android:title="@string/pref_away_when_screen_off"
+                    android:dependency="manually_change_presence"/>
                 <CheckBoxPreference
                     android:defaultValue="false"
                     android:key="xa_on_silent_mode"
                     android:summary="@string/pref_xa_on_silent_mode_summary"
-                    android:title="@string/pref_xa_on_silent_mode"/>
+                    android:title="@string/pref_xa_on_silent_mode"
+                    android:dependency="manually_change_presence"/>
                 <CheckBoxPreference
                     android:dependency="xa_on_silent_mode"
                     android:defaultValue="false"