diff options
Diffstat (limited to 'src/main')
224 files changed, 3520 insertions, 1865 deletions
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index c64f89ef8..bfdb2ec3d 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="com.android.alarm.permission.SET_ALARM" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.CAMERA" /> @@ -161,7 +162,12 @@ android:launchMode="singleTask" /> <activity android:name=".ui.SettingsActivity" - android:label="@string/title_activity_settings" /> + android:label="@string/title_activity_settings"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.NOTIFICATION_PREFERENCES" /> + </intent-filter> + </activity> <activity android:name=".ui.ChooseContactActivity" android:label="@string/title_activity_choose_contact" /> @@ -208,18 +214,10 @@ android:launchMode="singleTop"> <intent-filter> <action android:name="android.intent.action.SEND" /> - <category android:name="android.intent.category.DEFAULT" /> - <data android:mimeType="text/plain" /> - </intent-filter> - <intent-filter> - <action android:name="android.intent.action.SEND" /> - <category android:name="android.intent.category.DEFAULT" /> - <data android:mimeType="*/*" /> - </intent-filter> - <intent-filter> <action android:name="android.intent.action.SEND_MULTIPLE" /> + <category android:name="android.intent.category.DEFAULT" /> - <data android:mimeType="image/*" /> + <data android:mimeType="*/*" /> </intent-filter> <meta-data @@ -235,8 +233,7 @@ android:label="@string/share_location" /> <activity android:name=".ui.ShowLocationActivity" - android:label="@string/show_location"> - </activity> + android:label="@string/show_location"></activity> <activity android:name=".ui.SearchActivity" android:label="@string/search_messages" /> @@ -278,6 +275,10 @@ android:theme="@style/Base.Theme.AppCompat" /> <activity android:name=".ui.MemorizingActivity" /> + <activity + android:name=".ui.MediaBrowserActivity" + android:label="@string/media_browser" /> + <service android:name=".services.ExportLogsService" /> <service android:name=".services.ContactChooserTargetService" diff --git a/src/main/java/de/pixart/messenger/Config.java b/src/main/java/de/pixart/messenger/Config.java index ac99b1709..e2914f9d3 100644 --- a/src/main/java/de/pixart/messenger/Config.java +++ b/src/main/java/de/pixart/messenger/Config.java @@ -77,6 +77,8 @@ public final class Config { public static final int CONNECT_DISCO_TIMEOUT = 30; public static final int MINI_GRACE_PERIOD = 750; + public static final boolean XEP_0392 = false; //enables a variant of XEP-0392 that is based on HSLUV + public static final int FILE_SIZE = 1048576; // 1 MiB public static final int AVATAR_SIZE = 480; @@ -86,8 +88,6 @@ public final class Config { public static final Bitmap.CompressFormat IMAGE_FORMAT = Bitmap.CompressFormat.JPEG; public static final int IMAGE_QUALITY = 75; - public static final boolean QUICK_SHARE_ATTACHMENT_CHOICE = true; // set to true to use attachment choice instead of quick share for send button - public static final int DEFAULT_ZOOM = 15; //for locations public final static long LOCATION_FIX_TIME_DELTA = 1000 * 10; // ms public final static float LOCATION_FIX_SPACE_DELTA = 10; // m diff --git a/src/main/java/de/pixart/messenger/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/de/pixart/messenger/crypto/axolotl/XmppAxolotlMessage.java index 0adbbe51f..f9b2539c3 100644 --- a/src/main/java/de/pixart/messenger/crypto/axolotl/XmppAxolotlMessage.java +++ b/src/main/java/de/pixart/messenger/crypto/axolotl/XmppAxolotlMessage.java @@ -21,6 +21,7 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import de.pixart.messenger.Config; +import de.pixart.messenger.utils.Compatibility; import de.pixart.messenger.xml.Element; import rocks.xmpp.addr.Jid; @@ -177,7 +178,7 @@ public class XmppAxolotlMessage { try { SecretKey secretKey = new SecretKeySpec(innerKey, KEYTYPE); IvParameterSpec ivSpec = new IvParameterSpec(iv); - Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER); + Cipher cipher = Compatibility.twentyTwo() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER); cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); this.ciphertext = cipher.doFinal(Config.OMEMO_PADDING ? getPaddedBytes(plaintext) : plaintext.getBytes()); if (Config.PUT_AUTH_TAG_INTO_KEY && this.ciphertext != null) { @@ -293,7 +294,7 @@ public class XmppAxolotlMessage { key = newKey; } - Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER); + Cipher cipher = Compatibility.twentyTwo() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER); SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE); IvParameterSpec ivSpec = new IvParameterSpec(iv); diff --git a/src/main/java/de/pixart/messenger/entities/Account.java b/src/main/java/de/pixart/messenger/entities/Account.java index a8c72cdf9..6fd4c1c05 100644 --- a/src/main/java/de/pixart/messenger/entities/Account.java +++ b/src/main/java/de/pixart/messenger/entities/Account.java @@ -139,8 +139,7 @@ public class Account extends AbstractEntity { STREAM_ERROR, POLICY_VIOLATION, PAYMENT_REQUIRED, - MISSING_INTERNET_PERMISSION(false), - NETWORK_IS_UNREACHABLE(false); + MISSING_INTERNET_PERMISSION(false); private final boolean isError; private final boolean attemptReconnect; @@ -218,8 +217,6 @@ public class Account extends AbstractEntity { return R.string.payment_required; case MISSING_INTERNET_PERMISSION: return R.string.missing_internet_permission; - case NETWORK_IS_UNREACHABLE: - return R.string.network_is_unreachable; default: return R.string.account_status_unknown; } @@ -638,8 +635,10 @@ public class Account extends AbstractEntity { return this.avatar; } - public void activateGracePeriod(long duration) { - this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration; + public void activateGracePeriod(final long duration) { + if (duration > 0) { + this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration; + } } public void deactivateGracePeriod() { diff --git a/src/main/java/de/pixart/messenger/entities/Contact.java b/src/main/java/de/pixart/messenger/entities/Contact.java index 40bd69976..6edb14ada 100644 --- a/src/main/java/de/pixart/messenger/entities/Contact.java +++ b/src/main/java/de/pixart/messenger/entities/Contact.java @@ -159,6 +159,9 @@ public class Contact implements ListItem, Blockable { if (isBlocked()) { tags.add(new Tag(context.getString(R.string.blocked), 0xff2e2f3b, 0)); } + if (showInPhoneBook()) { + tags.add(new Tag(context.getString(R.string.phone_book), 0xFF1E88E5, 0)); + } return tags; } @@ -357,13 +360,16 @@ public class Contact implements ListItem, Blockable { } } - public void setPgpKeyId(long keyId) { + public boolean setPgpKeyId(long keyId) { + final long previousKeyId = getPgpKeyId(); synchronized (this.keys) { try { this.keys.put("pgp_keyid", keyId); + return previousKeyId != keyId; } catch (final JSONException ignored) { } } + return false; } public void setOption(int option) { @@ -384,6 +390,10 @@ public class Contact implements ListItem, Blockable { || (this.getOption(Contact.Options.DIRTY_PUSH)); } + public boolean showInPhoneBook() { + return systemAccount != null && !systemAccount.trim().isEmpty(); + } + public void parseSubscriptionFromElement(Element item) { String ask = item.getAttribute("ask"); String subscription = item.getAttribute("subscription"); diff --git a/src/main/java/de/pixart/messenger/entities/Conversation.java b/src/main/java/de/pixart/messenger/entities/Conversation.java index cc995a30b..1e5224489 100644 --- a/src/main/java/de/pixart/messenger/entities/Conversation.java +++ b/src/main/java/de/pixart/messenger/entities/Conversation.java @@ -1135,6 +1135,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl && !contact.isOwnServer() && !contact.showInRoster() && !contact.isSelf() + && !contact.showInPhoneBook() && sentMessagesCount() == 0; } diff --git a/src/main/java/de/pixart/messenger/entities/MucOptions.java b/src/main/java/de/pixart/messenger/entities/MucOptions.java index 774620707..258ded842 100644 --- a/src/main/java/de/pixart/messenger/entities/MucOptions.java +++ b/src/main/java/de/pixart/messenger/entities/MucOptions.java @@ -559,6 +559,15 @@ public class MucOptions { return null; } + public User findOrCreateUserByRealJid(Jid jid) { + User user = findUserByRealJid(jid); + if (user == null) { + user = new User(this, null); + user.setRealJid(jid); + } + return user; + } + public User findUser(ReadByMarker readByMarker) { if (readByMarker.getRealJid() != null) { User user = findUserByRealJid(readByMarker.getRealJid().asBareJid()); diff --git a/src/main/java/de/pixart/messenger/generator/AbstractGenerator.java b/src/main/java/de/pixart/messenger/generator/AbstractGenerator.java index 37a098300..507c0c39a 100644 --- a/src/main/java/de/pixart/messenger/generator/AbstractGenerator.java +++ b/src/main/java/de/pixart/messenger/generator/AbstractGenerator.java @@ -36,6 +36,7 @@ public abstract class AbstractGenerator { "http://jabber.org/protocol/disco#info", "urn:xmpp:avatar:metadata+notify", "http://jabber.org/protocol/nick+notify", + Namespace.BOOKMARKS + "+notify", "urn:xmpp:ping", "jabber:iq:version", "http://jabber.org/protocol/chatstates" @@ -59,11 +60,11 @@ public abstract class AbstractGenerator { protected XmppConnectionService mXmppConnectionService; - protected AbstractGenerator(XmppConnectionService service) { + AbstractGenerator(XmppConnectionService service) { this.mXmppConnectionService = service; } - protected String getIdentityVersion() { + String getIdentityVersion() { if (mVersion == null) { this.mVersion = PhoneHelper.getVersionName(mXmppConnectionService); } diff --git a/src/main/java/de/pixart/messenger/generator/IqGenerator.java b/src/main/java/de/pixart/messenger/generator/IqGenerator.java index 918c95e6f..86f58af69 100644 --- a/src/main/java/de/pixart/messenger/generator/IqGenerator.java +++ b/src/main/java/de/pixart/messenger/generator/IqGenerator.java @@ -138,6 +138,13 @@ public class IqGenerator extends AbstractGenerator { return publish("urn:xmpp:avatar:data", item); } + public IqPacket publishElement(final String namespace, final Element element, final Bundle options) { + final Element item = new Element("item"); + item.setAttribute("id", "current"); + item.addChild(element); + return publish(namespace, item, options); + } + public IqPacket publishAvatarMetadata(final Avatar avatar) { final Element item = new Element("item"); item.setAttribute("id", avatar.sha1sum); @@ -197,6 +204,7 @@ public class IqGenerator extends AbstractGenerator { public IqPacket publishDeviceIds(final Set<Integer> ids, final Bundle publishOptions) { final Element item = new Element("item"); + item.setAttribute("id", "current"); final Element list = item.addChild("list", AxolotlService.PEP_PREFIX); for (Integer id : ids) { final Element device = new Element("device"); @@ -209,6 +217,7 @@ public class IqGenerator extends AbstractGenerator { public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey, final Set<PreKeyRecord> preKeyRecords, final int deviceId, Bundle publishOptions) { final Element item = new Element("item"); + item.setAttribute("id", "current"); final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX); final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic"); signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId()); @@ -231,6 +240,7 @@ public class IqGenerator extends AbstractGenerator { public IqPacket publishVerification(byte[] signature, X509Certificate[] certificates, final int deviceId) { final Element item = new Element("item"); + item.setAttribute("id", "current"); final Element verification = item.addChild("verification", AxolotlService.PEP_PREFIX); final Element chain = verification.addChild("chain"); for (int i = 0; i < certificates.length; ++i) { diff --git a/src/main/java/de/pixart/messenger/parser/MessageParser.java b/src/main/java/de/pixart/messenger/parser/MessageParser.java index 7cfd69e8b..df85f2f25 100644 --- a/src/main/java/de/pixart/messenger/parser/MessageParser.java +++ b/src/main/java/de/pixart/messenger/parser/MessageParser.java @@ -304,6 +304,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece AxolotlService axolotlService = account.getAxolotlService(); axolotlService.registerDevices(from, deviceIds); mXmppConnectionService.updateAccountUi(); + } else if (Namespace.BOOKMARKS.equals(node)) { + Log.d(Config.LOGTAG, "received bookmarks from " + from); + if (account.getJid().asBareJid().equals(from)) { + final Element i = items.findChild("item"); + final Element storage = i == null ? null : i.findChild("storage", Namespace.BOOKMARKS); + mXmppConnectionService.processBookmarks(account, storage); + } } } diff --git a/src/main/java/de/pixart/messenger/parser/PresenceParser.java b/src/main/java/de/pixart/messenger/parser/PresenceParser.java index 6602e8aef..39a3cfeba 100644 --- a/src/main/java/de/pixart/messenger/parser/PresenceParser.java +++ b/src/main/java/de/pixart/messenger/parser/PresenceParser.java @@ -236,7 +236,7 @@ public class PresenceParser extends AbstractParser implements return codes; } - public void parseContactPresence(final PresencePacket packet, final Account account) { + private void parseContactPresence(final PresencePacket packet, final Account account) { final PresenceGenerator mPresenceGenerator = mXmppConnectionService.getPresenceGenerator(); final Jid from = packet.getFrom(); if (from == null || from.equals(account.getJid())) { @@ -269,6 +269,10 @@ public class PresenceParser extends AbstractParser implements mXmppConnectionService.fetchAvatar(account, avatar); } } + + if (mXmppConnectionService.isMuc(account, from)) { + return; + } int sizeBefore = contact.getPresences().size(); final String show = packet.findChildContent("show"); @@ -302,7 +306,9 @@ public class PresenceParser extends AbstractParser implements if (pgp != null && x != null) { Element status = packet.findChild("status"); String msg = status != null ? status.getContent() : ""; - contact.setPgpKeyId(pgp.fetchKeyId(account, msg, x.getContent())); + if (contact.setPgpKeyId(pgp.fetchKeyId(account, msg, x.getContent()))) { + mXmppConnectionService.syncRoster(account); + } } boolean online = sizeBefore < contact.getPresences().size(); mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, online); @@ -349,6 +355,8 @@ public class PresenceParser extends AbstractParser implements this.parseConferencePresence(packet, account); } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { this.parseConferencePresence(packet, account); + } else if ("error".equals(packet.getAttribute("type")) && mXmppConnectionService.isMuc(account, packet.getFrom())) { + this.parseConferencePresence(packet, account); } else { this.parseContactPresence(packet, account); } diff --git a/src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java b/src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java index 94710b86e..365f8fa87 100644 --- a/src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java +++ b/src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java @@ -35,6 +35,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; import de.pixart.messenger.Config; @@ -787,6 +788,29 @@ public class DatabaseBackend extends SQLiteOpenHelper { }; } + public List<FilePath> getRelativeFilePaths(String account, Jid jid, int limit) { + SQLiteDatabase db = this.getReadableDatabase(); + final String SQL = "select uuid,relativeFilePath from messages where type in (1,2) and conversationUuid=(select uuid from conversations where accountUuid=? and (contactJid=? or contactJid like ?)) order by timeSent desc"; + final String[] args = {account, jid.toEscapedString(), jid.toEscapedString() + "/%"}; + Cursor cursor = db.rawQuery(SQL + (limit > 0 ? " limit " + String.valueOf(limit) : ""), args); + List<FilePath> filesPaths = new ArrayList<>(); + while (cursor.moveToNext()) { + filesPaths.add(new FilePath(cursor.getString(0), cursor.getString(1))); + } + cursor.close(); + return filesPaths; + } + + public static class FilePath { + public final UUID uuid; + public final String path; + + private FilePath(String uuid, String path) { + this.uuid = UUID.fromString(uuid); + this.path = path; + } + } + public Conversation findConversation(final Account account, final Jid contactJid) { SQLiteDatabase db = this.getReadableDatabase(); String[] selectionArgs = {account.getUuid(), diff --git a/src/main/java/de/pixart/messenger/persistance/FileBackend.java b/src/main/java/de/pixart/messenger/persistance/FileBackend.java index 3bb1e5c4e..822fa6318 100644 --- a/src/main/java/de/pixart/messenger/persistance/FileBackend.java +++ b/src/main/java/de/pixart/messenger/persistance/FileBackend.java @@ -46,6 +46,7 @@ import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; @@ -56,6 +57,7 @@ import de.pixart.messenger.R; import de.pixart.messenger.entities.DownloadableFile; import de.pixart.messenger.entities.Message; import de.pixart.messenger.services.XmppConnectionService; +import de.pixart.messenger.ui.util.Attachment; import de.pixart.messenger.utils.CryptoHelper; import de.pixart.messenger.utils.ExifHelper; import de.pixart.messenger.utils.FileUtils; @@ -80,8 +82,8 @@ public class FileBackend { } private void createNoMedia() { - final File nomedia_files = new File(getConversationsDirectory("Files", true) + ".nomedia"); - final File nomedia_audios = new File(getConversationsDirectory("Audios", true) + ".nomedia"); + final File nomedia_files = new File(getConversationsDirectory("Files") + ".nomedia"); + final File nomedia_audios = new File(getConversationsDirectory("Audios") + ".nomedia"); if (!nomedia_files.exists()) { try { nomedia_files.createNewFile(); @@ -99,8 +101,8 @@ public class FileBackend { } public void updateMediaScanner(File file) { - if (file.getAbsolutePath().startsWith(getConversationsDirectory("Images", true)) - || file.getAbsolutePath().startsWith(getConversationsDirectory("Videos", true))) { + if (file.getAbsolutePath().startsWith(getConversationsDirectory("Images")) + || file.getAbsolutePath().startsWith(getConversationsDirectory("Videos"))) { Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); intent.setData(Uri.fromFile(file)); mXmppConnectionService.sendBroadcast(intent); @@ -129,13 +131,13 @@ public class FileBackend { file = new DownloadableFile(path); } else { if (mime != null && mime.startsWith("image")) { - file = new DownloadableFile(getConversationsDirectory("Images", true) + path); + file = new DownloadableFile(getConversationsDirectory("Images") + path); } else if (mime != null && mime.startsWith("video")) { - file = new DownloadableFile(getConversationsDirectory("Videos", true) + path); + file = new DownloadableFile(getConversationsDirectory("Videos") + path); } else if (mime != null && mime.startsWith("audio")) { - file = new DownloadableFile(getConversationsDirectory("Audios", true) + path); + file = new DownloadableFile(getConversationsDirectory("Audios") + path); } else { - file = new DownloadableFile(getConversationsDirectory("Files", true) + path); + file = new DownloadableFile(getConversationsDirectory("Files") + path); } } return file; @@ -151,7 +153,7 @@ public class FileBackend { } final DownloadableFile file = getFileForPath(path, message.getMimeType()); if (encrypted) { - return new DownloadableFile(getConversationsDirectory("Files", true) + file.getName() + ".pgp"); + return new DownloadableFile(getConversationsDirectory("Files") + file.getName() + ".pgp"); } else { return file; } @@ -172,16 +174,19 @@ public class FileBackend { } } - public static boolean allFilesUnderSize(Context context, List<Uri> uris, long max) { + public static boolean allFilesUnderSize(Context context, List<Attachment> attachments, long max) { if (max <= 0) { Log.d(Config.LOGTAG, "server did not report max file size for http upload"); return true; //exception to be compatible with HTTP Upload < v0.2 } - for (Uri uri : uris) { - String mime = context.getContentResolver().getType(uri); + for (Attachment attachment : attachments) { + if (attachment.getType() != Attachment.Type.FILE) { + continue; + } + String mime = attachment.getMime(); if (mime != null && mime.startsWith("video/")) { try { - Dimensions dimensions = FileBackend.getVideoDimensions(context, uri); + Dimensions dimensions = FileBackend.getVideoDimensions(context, attachment.getUri()); if (dimensions.getMin() >= 720) { Log.d(Config.LOGTAG, "do not consider video file with min width larger or equal than 720 for size check"); continue; @@ -190,7 +195,7 @@ public class FileBackend { //ignore and fall through } } - if (FileBackend.getFileSize(context, uri) > max) { + if (FileBackend.getFileSize(context, attachment.getUri()) > max) { Log.d(Config.LOGTAG, "not all files are under " + max + " bytes. suggesting falling back to jingle"); return false; } @@ -198,31 +203,43 @@ public class FileBackend { return true; } - public static String getDirectoryName(final String type, final boolean isMedia) { - String media = ""; - if (isMedia) { - media = "Media/Pix-Art Messenger "; + public List<Attachment> convertToAttachments(List<DatabaseBackend.FilePath> relativeFilePaths) { + List<Attachment> attachments = new ArrayList<>(); + for (DatabaseBackend.FilePath relativeFilePath : relativeFilePaths) { + final String mime = MimeUtils.guessMimeTypeFromExtension(MimeUtils.extractRelevantExtension(relativeFilePath.path)); + Log.d(Config.LOGTAG, "mime=" + mime); + File file = getFileForPath(relativeFilePath.path, mime); + if (file.exists()) { + attachments.add(Attachment.of(relativeFilePath.uuid, file, mime)); + } else { + Log.d(Config.LOGTAG, "file " + file.getAbsolutePath() + " doesn't exist"); + } } - if (type == "null" || type == null) { - return "/Pix-Art Messenger/"; + return attachments; + } + + public static String getConversationsDirectory(final String type) { + if (type.equalsIgnoreCase("null") || type == null) { + return getAppMediaDirectory() + "Pix-Art Messenger" + "/"; } else { - return "/Pix-Art Messenger" + "/" + media + type + "/"; + return getAppMediaDirectory() + "Pix-Art Messenger" + " " + type + "/"; } } - public static String getConversationsDirectory(final String type, final boolean isMedia) { - String DirName = null; - if (type != "null" || type != null) { - DirName = type; - } - String path = Environment.getExternalStorageDirectory().getAbsolutePath() + getDirectoryName(DirName, isMedia); - File createFolders = new File(path); - if (!createFolders.exists()) { - Log.d(Config.LOGTAG, "creating directory " + createFolders); - createFolders.mkdirs(); - } - return path; + public static String getAppMediaDirectory() { + return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + "Pix-Art Messenger" + "/Media/"; + } + + public static String getBackupDirectory() { + return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + "Pix-Art Messenger" + "/Database/"; + } + public static String getAppLogsDirectory() { + return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + "Pix-Art Messenger" + "/Chats/"; + } + + public static String getAppUpdateDirectory() { + return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + "Pix-Art Messenger" + "/Update/"; } private Bitmap resize(final Bitmap originalBitmap, int size) throws IOException { @@ -294,7 +311,7 @@ public class FileBackend { Log.d(Config.LOGTAG, "File path = null"); return false; } - if (path.contains(getDirectoryName("null", true))) { + if (path.contains(getConversationsDirectory("null"))) { Log.d(Config.LOGTAG, "File " + path + " is in our directory"); return true; } @@ -367,7 +384,12 @@ public class FileBackend { private String getExtensionFromUri(Uri uri) { String[] projection = {MediaStore.MediaColumns.DATA}; String filename = null; - Cursor cursor = mXmppConnectionService.getContentResolver().query(uri, projection, null, null, null); + Cursor cursor; + try { + cursor = mXmppConnectionService.getContentResolver().query(uri, projection, null, null, null); + } catch (IllegalArgumentException e) { + cursor = null; + } if (cursor != null) { try { if (cursor.moveToFirst()) { @@ -549,6 +571,21 @@ public class FileBackend { return paint; } + private Bitmap cropCenterSquareVideo(Uri uri, int size) { + MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever(); + Bitmap frame; + try { + metadataRetriever.setDataSource(mXmppConnectionService, uri); + frame = metadataRetriever.getFrameAtTime(0); + metadataRetriever.release(); + return cropCenterSquare(frame, size); + } catch (Exception e) { + frame = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + frame.eraseColor(0xff000000); + return frame; + } + } + private Bitmap getVideoPreview(File file, int size) throws IOException { MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever(); Bitmap frame; @@ -641,11 +678,12 @@ public class FileBackend { bitmap = BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri)); return getPepAvatar(bitmap, Bitmap.CompressFormat.PNG, 100); } catch (Exception e) { + return null; + } finally { if (bitmap != null) { bitmap.recycle(); } } - return null; } private Avatar getPepAvatar(Bitmap bitmap, Bitmap.CompressFormat format, int quality) { @@ -679,6 +717,9 @@ public class FileBackend { avatar.width = bitmap.getWidth(); avatar.height = bitmap.getHeight(); return avatar; + } catch (OutOfMemoryError e) { + Log.d(Config.LOGTAG, "unable to convert avatar to base64 due to low memory"); + return null; } catch (Exception e) { return null; } @@ -1032,6 +1073,30 @@ public class FileBackend { return getVideoDimensions(metadataRetriever); } + public Bitmap getPreviewForUri(Attachment attachment, int size, boolean cacheOnly) { + final String key = "attachment_" + attachment.getUuid().toString() + "_" + String.valueOf(size); + final LruCache<String, Bitmap> cache = mXmppConnectionService.getBitmapCache(); + Bitmap bitmap = cache.get(key); + if (bitmap != null || cacheOnly) { + return bitmap; + } + if (attachment.getMime() != null && attachment.getMime().startsWith("video/")) { + bitmap = cropCenterSquareVideo(attachment.getUri(), size); + drawOverlay(bitmap, R.drawable.play_video, 0.75f); + } else { + bitmap = cropCenterSquare(attachment.getUri(), size); + if ("image/gif".equals(attachment.getMime())) { + Bitmap withGifOverlay = bitmap.copy(Bitmap.Config.ARGB_8888, true); + drawOverlay(withGifOverlay, R.drawable.play_gif, 1.0f); + bitmap.recycle(); + bitmap = withGifOverlay; + } + } + if (bitmap != null) { + cache.put(key, bitmap); + } + return bitmap; + } private static Dimensions getVideoDimensions(Context context, Uri uri) throws NotAVideoFile { MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); diff --git a/src/main/java/de/pixart/messenger/services/AbstractConnectionManager.java b/src/main/java/de/pixart/messenger/services/AbstractConnectionManager.java index 1c4c6ca19..65711ef42 100644 --- a/src/main/java/de/pixart/messenger/services/AbstractConnectionManager.java +++ b/src/main/java/de/pixart/messenger/services/AbstractConnectionManager.java @@ -1,85 +1,39 @@ package de.pixart.messenger.services; -import android.Manifest; import android.content.Context; -import android.content.pm.PackageManager; -import android.os.Build; import android.os.PowerManager; import android.os.SystemClock; -import android.util.Log; import android.util.Pair; -import org.bouncycastle.crypto.engines.AESEngine; -import org.bouncycastle.crypto.modes.AEADBlockCipher; -import org.bouncycastle.crypto.modes.GCMBlockCipher; -import org.bouncycastle.crypto.params.AEADParameters; -import org.bouncycastle.crypto.params.KeyParameter; - import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; import java.util.concurrent.atomic.AtomicLong; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.CipherOutputStream; -import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; -import de.pixart.messenger.Config; import de.pixart.messenger.R; import de.pixart.messenger.entities.DownloadableFile; +import de.pixart.messenger.utils.Compatibility; public class AbstractConnectionManager { - protected XmppConnectionService mXmppConnectionService; + private static final String KEYTYPE = "AES"; + private static final String CIPHERMODE = "AES/GCM/NoPadding"; + private static final String PROVIDER = "BC"; private static final int UI_REFRESH_THRESHOLD = 250; private static final AtomicLong LAST_UI_UPDATE_CALL = new AtomicLong(0); + protected XmppConnectionService mXmppConnectionService; public AbstractConnectionManager(XmppConnectionService service) { this.mXmppConnectionService = service; } - public XmppConnectionService getXmppConnectionService() { - return this.mXmppConnectionService; - } - - public long getAutoAcceptFileSize() { - long defaultValue_wifi = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_wifi); - long defaultValue_mobile = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_mobile); - long defaultValue_roaming = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_roaming); - - String config = "0"; - if (mXmppConnectionService.isWIFI()) { - config = this.mXmppConnectionService.getPreferences().getString( - "auto_accept_file_size_wifi", String.valueOf(defaultValue_wifi)); - } else if (mXmppConnectionService.isMobile()) { - config = this.mXmppConnectionService.getPreferences().getString( - "auto_accept_file_size_mobile", String.valueOf(defaultValue_mobile)); - } else if (mXmppConnectionService.isMobileRoaming()) { - config = this.mXmppConnectionService.getPreferences().getString( - "auto_accept_file_size_roaming", String.valueOf(defaultValue_roaming)); - } - try { - return Long.parseLong(config); - } catch (NumberFormatException e) { - return defaultValue_mobile; - } - } - - public boolean hasStoragePermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - return mXmppConnectionService.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; - } else { - return true; - } - } - public static Pair<InputStream, Integer> createInputStream(DownloadableFile file, boolean gcm) throws FileNotFoundException { FileInputStream is; int size; @@ -90,15 +44,15 @@ public class AbstractConnectionManager { } try { if (gcm) { - AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); - cipher.init(true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv())); - InputStream cis = new org.bouncycastle.crypto.io.CipherInputStream(is, cipher); - return new Pair<>(cis, cipher.getOutputSize(size)); + Cipher cipher = Compatibility.twentyTwo() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER); + SecretKeySpec keySpec = new SecretKeySpec(file.getKey(), KEYTYPE); + IvParameterSpec ivSpec = new IvParameterSpec(file.getIv()); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); + return new Pair<>(new CipherInputStream(is, cipher), cipher.getOutputSize(size)); } else { IvParameterSpec ips = new IvParameterSpec(file.getIv()); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(file.getKey(), "AES"), ips); - Log.d(Config.LOGTAG, "opening encrypted input stream"); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(file.getKey(), KEYTYPE), ips); return new Pair<>(new CipherInputStream(is, cipher), (size / 16 + 1) * 16); } } catch (Exception e) { @@ -126,27 +80,53 @@ public class AbstractConnectionManager { } try { if (gcm) { - AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); - cipher.init(false, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv())); - return new org.bouncycastle.crypto.io.CipherOutputStream(os, cipher); + Cipher cipher = Compatibility.twentyTwo() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER); + SecretKeySpec keySpec = new SecretKeySpec(file.getKey(), KEYTYPE); + IvParameterSpec ivSpec = new IvParameterSpec(file.getIv()); + cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); + return new CipherOutputStream(os, cipher); } else { IvParameterSpec ips = new IvParameterSpec(file.getIv()); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(file.getKey(), "AES"), ips); - Log.d(Config.LOGTAG, "opening encrypted output stream"); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(file.getKey(), KEYTYPE), ips); return new CipherOutputStream(os, cipher); } - } catch (InvalidKeyException e) { - return null; - } catch (NoSuchAlgorithmException e) { - return null; - } catch (NoSuchPaddingException e) { - return null; - } catch (InvalidAlgorithmParameterException e) { - return null; + } catch (Exception e) { + throw new AssertionError(e); } } + public XmppConnectionService getXmppConnectionService() { + return this.mXmppConnectionService; + } + + public long getAutoAcceptFileSize() { + long defaultValue_wifi = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_wifi); + long defaultValue_mobile = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_mobile); + long defaultValue_roaming = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_roaming); + + String config = "0"; + if (mXmppConnectionService.isWIFI()) { + config = this.mXmppConnectionService.getPreferences().getString( + "auto_accept_file_size_wifi", String.valueOf(defaultValue_wifi)); + } else if (mXmppConnectionService.isMobile()) { + config = this.mXmppConnectionService.getPreferences().getString( + "auto_accept_file_size_mobile", String.valueOf(defaultValue_mobile)); + } else if (mXmppConnectionService.isMobileRoaming()) { + config = this.mXmppConnectionService.getPreferences().getString( + "auto_accept_file_size_roaming", String.valueOf(defaultValue_roaming)); + } + try { + return Long.parseLong(config); + } catch (NumberFormatException e) { + return defaultValue_mobile; + } + } + + public boolean hasStoragePermission() { + return Compatibility.hasStoragePermission(mXmppConnectionService); + } + public void updateConversationUi(boolean force) { synchronized (LAST_UI_UPDATE_CALL) { if (force || SystemClock.elapsedRealtime() - LAST_UI_UPDATE_CALL.get() >= UI_REFRESH_THRESHOLD) { diff --git a/src/main/java/de/pixart/messenger/services/AudioPlayer.java b/src/main/java/de/pixart/messenger/services/AudioPlayer.java index e1f97ef1a..e3e17fdb0 100644 --- a/src/main/java/de/pixart/messenger/services/AudioPlayer.java +++ b/src/main/java/de/pixart/messenger/services/AudioPlayer.java @@ -1,13 +1,21 @@ package de.pixart.messenger.services; import android.Manifest; +import android.content.Context; import android.content.pm.PackageManager; import android.content.res.ColorStateList; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.media.AudioManager; import android.media.MediaPlayer; import android.os.Build; import android.os.Handler; +import android.os.PowerManager; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; +import android.util.Log; import android.view.View; import android.widget.ImageButton; import android.widget.RelativeLayout; @@ -17,6 +25,7 @@ import android.widget.TextView; import java.lang.ref.WeakReference; import java.util.Locale; +import de.pixart.messenger.Config; import de.pixart.messenger.R; import de.pixart.messenger.entities.Message; import de.pixart.messenger.ui.ConversationsActivity; @@ -24,24 +33,44 @@ import de.pixart.messenger.ui.adapter.MessageAdapter; import de.pixart.messenger.ui.util.PendingItem; import de.pixart.messenger.utils.WeakReferenceSet; -public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompletionListener, SeekBar.OnSeekBarChangeListener, Runnable { +public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompletionListener, SeekBar.OnSeekBarChangeListener, Runnable, SensorEventListener { private static final int REFRESH_INTERVAL = 250; private static final Object LOCK = new Object(); - public static MediaPlayer player = null; + private static MediaPlayerWrapper player = null; private static Message currentlyPlayingMessage = null; private final MessageAdapter messageAdapter; private final WeakReferenceSet<RelativeLayout> audioPlayerLayouts = new WeakReferenceSet<>(); + private final SensorManager sensorManager; + private final Sensor proximitySensor; + private static PowerManager.WakeLock wakeLock; private final PendingItem<WeakReference<ImageButton>> pendingOnClickView = new PendingItem<>(); private final Handler handler = new Handler(); public AudioPlayer(MessageAdapter adapter) { + final Context context = adapter.getContext(); this.messageAdapter = adapter; + this.sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + this.proximitySensor = this.sensorManager == null ? null : this.sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); + if (Build.VERSION.SDK_INT >= 21) { + synchronized (AudioPlayer.LOCK) { + if (AudioPlayer.wakeLock == null) { + final PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + AudioPlayer.wakeLock = powerManager == null ? null : powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, AudioPlayer.class.getSimpleName()); + AudioPlayer.wakeLock.setReferenceCounted(false); + } + } + } else { + AudioPlayer.wakeLock = null; + } synchronized (AudioPlayer.LOCK) { if (AudioPlayer.player != null) { AudioPlayer.player.setOnCompletionListener(this); + if (AudioPlayer.player.isPlaying() && sensorManager != null) { + sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL); + } } } } @@ -91,6 +120,8 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti viewHolder.progress.setProgress(0); viewHolder.progress.setEnabled(false); messageAdapter.flagScreenOff(); + releaseProximityWakeLock(); + messageAdapter.flagEnableInputs(); return false; } } @@ -126,32 +157,52 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti viewHolder.progress.setEnabled(false); player.pause(); messageAdapter.flagScreenOff(); + releaseProximityWakeLock(); + messageAdapter.flagEnableInputs(); viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp); } else { viewHolder.progress.setEnabled(true); player.start(); messageAdapter.flagScreenOn(); + acquireProximityWakeLock(); this.stopRefresher(true); viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp); } return false; } - private boolean play(ViewHolder viewHolder, Message message) { - AudioPlayer.player = new MediaPlayer(); + private void play(ViewHolder viewHolder, Message message, boolean earpiece, double progress) { + if (play(viewHolder, message, earpiece)) { + AudioPlayer.player.seekTo((int) (AudioPlayer.player.getDuration() * progress)); + } + } + + private boolean play(ViewHolder viewHolder, Message message, boolean earpiece) { + AudioPlayer.player = new MediaPlayerWrapper(); try { AudioPlayer.currentlyPlayingMessage = message; + AudioPlayer.player.setAudioStreamType(earpiece ? AudioManager.STREAM_VOICE_CALL : AudioManager.STREAM_MUSIC); AudioPlayer.player.setDataSource(messageAdapter.getFileBackend().getFile(message).getAbsolutePath()); AudioPlayer.player.setOnCompletionListener(this); AudioPlayer.player.prepare(); AudioPlayer.player.start(); messageAdapter.flagScreenOn(); + acquireProximityWakeLock(); + if (earpiece) { + messageAdapter.flagDisableInputs(); + } else { + messageAdapter.flagEnableInputs(); + } viewHolder.progress.setEnabled(true); viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp); + sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL); return true; } catch (Exception e) { messageAdapter.flagScreenOff(); + releaseProximityWakeLock(); + messageAdapter.flagEnableInputs(); AudioPlayer.currentlyPlayingMessage = null; + sensorManager.unregisterListener(this); return false; } } @@ -173,7 +224,7 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti if (AudioPlayer.player != null) { stopCurrent(); } - return play(viewHolder, message); + return play(viewHolder, message, false); } private void stopCurrent() { @@ -182,8 +233,11 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti } AudioPlayer.player.release(); messageAdapter.flagScreenOff(); + releaseProximityWakeLock(); + messageAdapter.flagEnableInputs(); AudioPlayer.player = null; resetPlayerUi(); + sensorManager.unregisterListener(this); } private void resetPlayerUi() { @@ -207,7 +261,7 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti } @Override - public void onCompletion(MediaPlayer mediaPlayer) { + public void onCompletion(android.media.MediaPlayer mediaPlayer) { synchronized (AudioPlayer.LOCK) { this.stopRefresher(false); if (AudioPlayer.player == mediaPlayer) { @@ -216,7 +270,10 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti } mediaPlayer.release(); messageAdapter.flagScreenOff(); + releaseProximityWakeLock(); + messageAdapter.flagEnableInputs(); resetPlayerUi(); + sensorManager.unregisterListener(this); } } @@ -251,6 +308,11 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti stopCurrent(); } AudioPlayer.currentlyPlayingMessage = null; + sensorManager.unregisterListener(this); + if (wakeLock != null && wakeLock.isHeld()) { + wakeLock.release(); + } + wakeLock = null; } } @@ -261,6 +323,12 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti } } + public void unregisterListener() { + if (sensorManager != null) { + sensorManager.unregisterListener(this); + } + } + @Override public void run() { synchronized (AudioPlayer.LOCK) { @@ -288,6 +356,67 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti return true; } + @Override + public void onSensorChanged(SensorEvent event) { + if (event.sensor.getType() != Sensor.TYPE_PROXIMITY) { + return; + } + if (AudioPlayer.player == null || !AudioPlayer.player.isPlaying()) { + return; + } + int streamType; + if (event.values[0] < 5f && event.values[0] != proximitySensor.getMaximumRange()) { + streamType = AudioManager.STREAM_VOICE_CALL; + } else { + streamType = AudioManager.STREAM_MUSIC; + } + double position = AudioPlayer.player.getCurrentPosition(); + double duration = AudioPlayer.player.getDuration(); + double progress = position / duration; + if (AudioPlayer.player.getAudioStreamType() != streamType) { + synchronized (AudioPlayer.LOCK) { + AudioPlayer.player.stop(); + AudioPlayer.player.release(); + AudioPlayer.player = null; + try { + ViewHolder currentViewHolder = getCurrentViewHolder(); + if (currentViewHolder != null) { + play(currentViewHolder, currentlyPlayingMessage, streamType == AudioManager.STREAM_VOICE_CALL, progress); + } + } catch (Exception e) { + Log.d(Config.LOGTAG, "AudioPlayer Exception: " + e); + } + } + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int i) { + } + private void acquireProximityWakeLock() { + synchronized (AudioPlayer.LOCK) { + if (wakeLock != null) { + wakeLock.acquire(); + } + } + } + private void releaseProximityWakeLock() { + synchronized (AudioPlayer.LOCK) { + if (wakeLock != null && wakeLock.isHeld()) { + wakeLock.release(); + } + } + } + private ViewHolder getCurrentViewHolder() { + for (WeakReference<RelativeLayout> audioPlayer : audioPlayerLayouts) { + final Message message = (Message) audioPlayer.get().getTag(); + if (message == currentlyPlayingMessage) { + return ViewHolder.get(audioPlayer.get()); + } + } + return null; + } + public static class ViewHolder { private TextView runtime; private SeekBar progress; @@ -310,4 +439,18 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti this.darkBackground = darkBackground; } } + + private static class MediaPlayerWrapper extends MediaPlayer { + private int streamType; + + private int getAudioStreamType() { + return streamType; + } + + @Override + public void setAudioStreamType(int streamType) { + this.streamType = streamType; + super.setAudioStreamType(streamType); + } + } }
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/services/BarcodeProvider.java b/src/main/java/de/pixart/messenger/services/BarcodeProvider.java index 67dc74cc6..5ac2ee016 100644 --- a/src/main/java/de/pixart/messenger/services/BarcodeProvider.java +++ b/src/main/java/de/pixart/messenger/services/BarcodeProvider.java @@ -162,7 +162,6 @@ public class BarcodeProvider extends ContentProvider implements ServiceConnectio synchronized (this) { if (mXmppConnectionService == null && !mBindingInProcess) { Log.d(Config.LOGTAG, "calling to bind service"); - context.startService(intent); context.bindService(intent, this, Context.BIND_AUTO_CREATE); this.mBindingInProcess = true; } diff --git a/src/main/java/de/pixart/messenger/services/ContactChooserTargetService.java b/src/main/java/de/pixart/messenger/services/ContactChooserTargetService.java index 5d6469bae..4c0aec3ca 100644 --- a/src/main/java/de/pixart/messenger/services/ContactChooserTargetService.java +++ b/src/main/java/de/pixart/messenger/services/ContactChooserTargetService.java @@ -17,7 +17,7 @@ import java.util.ArrayList; import java.util.List; import de.pixart.messenger.entities.Conversation; -import de.pixart.messenger.ui.ShareWithActivity; +import de.pixart.messenger.ui.ConversationsActivity; @TargetApi(Build.VERSION_CODES.M) public class ContactChooserTargetService extends ChooserTargetService implements ServiceConnection { @@ -32,7 +32,6 @@ public class ContactChooserTargetService extends ChooserTargetService implements public List<ChooserTarget> onGetChooserTargets(ComponentName targetActivityName, IntentFilter matchedFilter) { Intent intent = new Intent(this, XmppConnectionService.class); intent.setAction("contact_chooser"); - startService(intent); bindService(intent, this, Context.BIND_AUTO_CREATE); ArrayList<ChooserTarget> chooserTargets = new ArrayList<>(); try { @@ -42,7 +41,7 @@ public class ContactChooserTargetService extends ChooserTargetService implements return chooserTargets; } mXmppConnectionService.populateWithOrderedConversations(conversations, false); - final ComponentName componentName = new ComponentName(this, ShareWithActivity.class); + final ComponentName componentName = new ComponentName(this, ConversationsActivity.class); final int pixel = (int) (48 * getResources().getDisplayMetrics().density); for (Conversation conversation : conversations) { if (conversation.sentMessagesCount() == 0) { @@ -52,7 +51,7 @@ public class ContactChooserTargetService extends ChooserTargetService implements final Icon icon = Icon.createWithBitmap(mXmppConnectionService.getAvatarService().get(conversation, pixel)); final float score = 1 - (1.0f / MAX_TARGETS) * chooserTargets.size(); final Bundle extras = new Bundle(); - extras.putString("uuid", conversation.getUuid()); + extras.putString(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid()); chooserTargets.add(new ChooserTarget(name, icon, score, componentName, extras)); if (chooserTargets.size() >= MAX_TARGETS) { break; diff --git a/src/main/java/de/pixart/messenger/services/EventReceiver.java b/src/main/java/de/pixart/messenger/services/EventReceiver.java index 6200a40ce..529741ead 100644 --- a/src/main/java/de/pixart/messenger/services/EventReceiver.java +++ b/src/main/java/de/pixart/messenger/services/EventReceiver.java @@ -4,6 +4,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.preference.PreferenceManager; +import android.support.v4.content.ContextCompat; import android.util.Log; import de.pixart.messenger.Config; @@ -13,26 +14,26 @@ public class EventReceiver extends BroadcastReceiver { public static final String SETTING_ENABLED_ACCOUNTS = "enabled_accounts"; @Override - public void onReceive(Context context, Intent intent) { - Intent mIntentForService = new Intent(context, XmppConnectionService.class); - if (intent.getAction() != null) { - mIntentForService.setAction(intent.getAction()); + public void onReceive(final Context context, final Intent originalIntent) { + final Intent intentForService = new Intent(context, XmppConnectionService.class); + if (originalIntent.getAction() != null) { + intentForService.setAction(originalIntent.getAction()); } else { - mIntentForService.setAction("other"); + intentForService.setAction("other"); } - final String action = intent.getAction(); + final String action = originalIntent.getAction(); if (action.equals("ui") || hasEnabledAccounts(context)) { try { - context.startService(mIntentForService); + ContextCompat.startForegroundService(context, intentForService); } catch (RuntimeException e) { Log.d(Config.LOGTAG, "EventReceiver was unable to start service"); } } else { - Log.d(Config.LOGTAG, "EventReceiver ignored action " + mIntentForService.getAction()); + Log.d(Config.LOGTAG, "EventReceiver ignored action " + intentForService.getAction()); } } - public static boolean hasEnabledAccounts(Context context) { + public static boolean hasEnabledAccounts(final Context context) { return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTING_ENABLED_ACCOUNTS, true); } diff --git a/src/main/java/de/pixart/messenger/services/ExportLogsService.java b/src/main/java/de/pixart/messenger/services/ExportLogsService.java index 33725e211..e66873acc 100644 --- a/src/main/java/de/pixart/messenger/services/ExportLogsService.java +++ b/src/main/java/de/pixart/messenger/services/ExportLogsService.java @@ -1,7 +1,5 @@ package de.pixart.messenger.services; -import android.app.NotificationManager; -import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -10,7 +8,6 @@ import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.preference.PreferenceManager; import android.support.annotation.BoolRes; -import android.support.v4.app.NotificationCompat; import android.util.Log; import java.io.BufferedWriter; @@ -41,19 +38,17 @@ import rocks.xmpp.addr.Jid; import static de.pixart.messenger.ui.SettingsActivity.USE_MULTI_ACCOUNTS; -public class ExportLogsService extends Service { +public class ExportLogsService extends XmppConnectionService { private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - private static final String DIRECTORY_STRING_FORMAT = FileBackend.getConversationsDirectory("Chats", false) + "%s"; + private static final String DIRECTORY_STRING_FORMAT = FileBackend.getAppLogsDirectory() + "%s"; private static final String MESSAGE_STRING_FORMAT = "(%s) %s: %s\n"; - private static final int NOTIFICATION_ID = 1; private static AtomicBoolean running = new AtomicBoolean(false); + boolean ReadableLogsEnabled = false; private DatabaseBackend mDatabaseBackend; private List<Account> mAccounts; - boolean ReadableLogsEnabled = false; private WakeLock wakeLock; private PowerManager pm; - XmppConnectionService mXmppConnectionService; @Override public void onCreate() { @@ -63,20 +58,19 @@ public class ExportLogsService extends Service { ReadableLogsEnabled = ReadableLogs.getBoolean("export_plain_text_logs", getResources().getBoolean(R.bool.plain_text_logs)); pm = (PowerManager) getSystemService(Context.POWER_SERVICE); wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ExportLogsService"); + this.startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, getNotificationService().exportLogsNotification()); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (running.compareAndSet(false, true)) { - new Thread(new Runnable() { - @Override - public void run() { - export(); - stopForeground(true); - WakeLockHelper.release(wakeLock); - running.set(false); - stopSelf(); - } + new Thread(() -> { + startForcingForegroundNotification(); + export(); + stopForcingForegroundNotification(); + WakeLockHelper.release(wakeLock); + running.set(false); + stopSelf(); }).start(); } return START_NOT_STICKY; @@ -84,27 +78,22 @@ public class ExportLogsService extends Service { private void export() { wakeLock.acquire(); + getNotificationService().exportLogsServiceNotification(getNotificationService().exportLogsNotification()); List<Conversation> conversations = mDatabaseBackend.getConversations(Conversation.STATUS_AVAILABLE); conversations.addAll(mDatabaseBackend.getConversations(Conversation.STATUS_ARCHIVED)); - NotificationManager mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext()); - mBuilder.setContentTitle(getString(R.string.app_name)) - .setContentText(getString(R.string.notification_export_logs_title)) - .setSmallIcon(R.drawable.ic_import_export_white_24dp) - .setProgress(0, 0, true); - startForeground(NOTIFICATION_ID, mBuilder.build()); - mNotifyManager.notify(NOTIFICATION_ID, mBuilder.build()); - if (ReadableLogsEnabled) { - for (Conversation conversation : conversations) { - writeToFile(conversation); + if (mAccounts.size() >= 1) { + if (ReadableLogsEnabled) { + for (Conversation conversation : conversations) { + writeToFile(conversation); + } } - } - if (mAccounts.size() == 1) { try { ExportDatabase(); } catch (IOException e) { e.printStackTrace(); } + } else { + Log.d(Config.LOGTAG, "ExportLogsService: no accounts, aborting export"); } } @@ -176,21 +165,24 @@ public class ExportLogsService extends Service { } public void ExportDatabase() throws IOException { + Log.d(Config.LOGTAG, "ExportLogsService: start creating backup"); Account mAccount = mAccounts.get(0); String EncryptionKey = null; // Get hold of the db: FileInputStream InputFile = new FileInputStream(this.getDatabasePath(DatabaseBackend.DATABASE_NAME)); // Set the output folder on the SDcard - File directory = new File(FileBackend.getConversationsDirectory("Database", false)); + File directory = new File(FileBackend.getBackupDirectory()); // Create the folder if it doesn't exist: if (!directory.exists()) { - directory.mkdirs(); + boolean directory_created = directory.mkdirs(); + Log.d(Config.LOGTAG, "ExportLogsService: backup directory created " + directory_created); } //Delete old database export file File temp_db_file = new File(directory + "/database.bak"); if (temp_db_file.exists()) { - Log.d(Config.LOGTAG, "Delete temp database backup file from " + temp_db_file.toString()); - temp_db_file.delete(); + Log.d(Config.LOGTAG, "ExportLogsService: Delete temp database backup file from " + temp_db_file.toString()); + boolean temp_db_file_deleted = temp_db_file.delete(); + Log.d(Config.LOGTAG, "ExportLogsService: old backup file deleted " + temp_db_file_deleted); } // Set the output file stream up: FileOutputStream OutputFile = new FileOutputStream(directory.getPath() + "/database.db.crypt"); @@ -199,26 +191,28 @@ public class ExportLogsService extends Service { EncryptionKey = mAccount.getPassword(); //get account password } else { SharedPreferences multiaccount_prefs = getApplicationContext().getSharedPreferences(USE_MULTI_ACCOUNTS, Context.MODE_PRIVATE); - String password = multiaccount_prefs.getString("BackupPW", null); - if (password == null) { - Log.d(Config.LOGTAG, "Database exporter: failed to write encryted backup to sdcard because of missing password"); - return; - } - EncryptionKey = password; //get previously set backup password + EncryptionKey = multiaccount_prefs.getString("BackupPW", null); + } + if (EncryptionKey == null) { + Log.d(Config.LOGTAG, "ExportLogsService: Database exporter: failed to write encryted backup to sdcard because of missing password"); + return; } // encrypt database from the input file to the output file try { EncryptDecryptFile.encrypt(InputFile, OutputFile, EncryptionKey); + Log.d(Config.LOGTAG, "ExportLogsService: starting encrypted output to " + OutputFile.toString()); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { - Log.d(Config.LOGTAG, "Database exporter: encryption failed with " + e); + Log.d(Config.LOGTAG, "ExportLogsService: Database exporter: encryption failed with " + e); e.printStackTrace(); } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, "Database exporter: encryption failed (invalid key) with " + e); + Log.d(Config.LOGTAG, "ExportLogsService: Database exporter: encryption failed (invalid key) with " + e); e.printStackTrace(); } catch (IOException e) { - Log.d(Config.LOGTAG, "Database exporter: encryption failed (IO) with " + e); + Log.d(Config.LOGTAG, "ExportLogsService: Database exporter: encryption failed (IO) with " + e); e.printStackTrace(); + } finally { + Log.d(Config.LOGTAG, "ExportLogsService: backup job finished"); } } @@ -236,6 +230,6 @@ public class ExportLogsService extends Service { } public boolean multipleAccounts() { - return getBooleanPreference("enable_multi_accounts", R.bool.confirm_messages); + return getBooleanPreference("enable_multi_accounts", R.bool.enable_multi_accounts); } } diff --git a/src/main/java/de/pixart/messenger/services/MediaPlayer.java b/src/main/java/de/pixart/messenger/services/MediaPlayer.java new file mode 100644 index 000000000..59e8bfecb --- /dev/null +++ b/src/main/java/de/pixart/messenger/services/MediaPlayer.java @@ -0,0 +1,15 @@ +package de.pixart.messenger.services; + +public class MediaPlayer extends android.media.MediaPlayer { + private int streamType; + + public int getAudioStreamType() { + return streamType; + } + + @Override + public void setAudioStreamType(int streamType) { + this.streamType = streamType; + super.setAudioStreamType(streamType); + } +}
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/services/NotificationService.java b/src/main/java/de/pixart/messenger/services/NotificationService.java index 7fa091478..8f82207ac 100644 --- a/src/main/java/de/pixart/messenger/services/NotificationService.java +++ b/src/main/java/de/pixart/messenger/services/NotificationService.java @@ -1,16 +1,23 @@ package de.pixart.messenger.services; import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationChannelGroup; +import android.app.NotificationManager; import android.app.PendingIntent; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Typeface; +import android.media.AudioAttributes; +import android.media.RingtoneManager; import android.net.Uri; import android.os.Build; import android.os.SystemClock; import android.preference.PreferenceManager; +import android.support.annotation.RequiresApi; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.BigPictureStyle; import android.support.v4.app.NotificationCompat.Builder; @@ -28,6 +35,7 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; @@ -46,9 +54,9 @@ import de.pixart.messenger.entities.Conversational; import de.pixart.messenger.entities.Message; import de.pixart.messenger.persistance.FileBackend; import de.pixart.messenger.ui.ConversationsActivity; -import de.pixart.messenger.ui.EditAccountActivity; import de.pixart.messenger.ui.ManageAccountActivity; import de.pixart.messenger.ui.TimePreference; +import de.pixart.messenger.utils.Compatibility; import de.pixart.messenger.utils.GeoHelper; import de.pixart.messenger.utils.UIHelper; import de.pixart.messenger.xmpp.XmppConnection; @@ -56,47 +64,135 @@ import de.pixart.messenger.xmpp.XmppConnection; public class NotificationService { public static final Object CATCHUP_LOCK = new Object(); + private static final String CONVERSATIONS_GROUP = "de.pixart.messenger"; - private final XmppConnectionService mXmppConnectionService; - private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>(); private static final int NOTIFICATION_ID_MULTIPLIER = 1024 * 1024; public static final int NOTIFICATION_ID = 2 * NOTIFICATION_ID_MULTIPLIER; public static final int FOREGROUND_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 4; public static final int ERROR_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 6; - public static final int UPDATE_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 8; + public static final String MESSAGES_CHANNEL_ID = "messages"; + public static final String FOREGROUND_CHANNEL_ID = "foreground"; + public static final String BACKUP_CHANNEL_ID = "backup"; + public static final String UPDATE_CHANNEL_ID = "appupdate"; + public static final String VIDEOCOMPRESSION_CHANNEL_ID = "compression"; + public static final String ERROR_CHANNEL_ID = "error"; + + private final XmppConnectionService mXmppConnectionService; + private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>(); + private final HashMap<Conversation, AtomicInteger> mBacklogMessageCounter = new HashMap<>(); private Conversation mOpenConversation; private boolean mIsInForeground; private long mLastNotification; - private final HashMap<Conversation, AtomicInteger> mBacklogMessageCounter = new HashMap<>(); - - public NotificationService(final XmppConnectionService service) { + NotificationService(final XmppConnectionService service) { this.mXmppConnectionService = service; } + private static boolean displaySnoozeAction(List<Message> messages) { + int numberOfMessagesWithoutReply = 0; + for (Message message : messages) { + if (message.getStatus() == Message.STATUS_RECEIVED) { + ++numberOfMessagesWithoutReply; + } else { + return false; + } + } + return numberOfMessagesWithoutReply >= 3; + } + public static Pattern generateNickHighlightPattern(final String nick) { return Pattern.compile("(?<=(^|\\s))" + Pattern.quote(nick) + "\\b"); } + @RequiresApi(api = Build.VERSION_CODES.O) + public void initializeChannels() { + final Context c = mXmppConnectionService; + NotificationManager notificationManager = c.getSystemService(NotificationManager.class); + if (notificationManager == null) { + return; + } + notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("status", c.getString(R.string.notification_group_status_information))); + notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("chats", c.getString(R.string.notification_group_messages))); + + final NotificationChannel foregroundServiceChannel = new NotificationChannel(FOREGROUND_CHANNEL_ID, + c.getString(R.string.foreground_service_channel_name), + NotificationManager.IMPORTANCE_MIN); + foregroundServiceChannel.setDescription(c.getString(R.string.foreground_service_channel_description)); + foregroundServiceChannel.setShowBadge(false); + foregroundServiceChannel.setGroup("status"); + notificationManager.createNotificationChannel(foregroundServiceChannel); + + final NotificationChannel backupChannel = new NotificationChannel(BACKUP_CHANNEL_ID, + c.getString(R.string.backup_channel_name), + NotificationManager.IMPORTANCE_LOW); + backupChannel.setShowBadge(false); + backupChannel.setGroup("status"); + notificationManager.createNotificationChannel(backupChannel); + + final NotificationChannel videoCompressionChannel = new NotificationChannel(VIDEOCOMPRESSION_CHANNEL_ID, + c.getString(R.string.video_compression_channel_name), + NotificationManager.IMPORTANCE_LOW); + videoCompressionChannel.setShowBadge(false); + videoCompressionChannel.setGroup("status"); + notificationManager.createNotificationChannel(videoCompressionChannel); + + final NotificationChannel AppUpdateChannel = new NotificationChannel(UPDATE_CHANNEL_ID, + c.getString(R.string.app_update_channel_name), + NotificationManager.IMPORTANCE_LOW); + AppUpdateChannel.setShowBadge(false); + AppUpdateChannel.setGroup("status"); + notificationManager.createNotificationChannel(AppUpdateChannel); + + final NotificationChannel errorChannel = new NotificationChannel(ERROR_CHANNEL_ID, + c.getString(R.string.error_channel_name), + NotificationManager.IMPORTANCE_LOW); + errorChannel.setDescription(c.getString(R.string.error_channel_description)); + errorChannel.setShowBadge(false); + errorChannel.setGroup("status"); + notificationManager.createNotificationChannel(errorChannel); + + final NotificationChannel messagesChannel = new NotificationChannel(MESSAGES_CHANNEL_ID, + c.getString(R.string.messages_channel_name), + NotificationManager.IMPORTANCE_HIGH); + messagesChannel.setShowBadge(true); + messagesChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) + .build()); + messagesChannel.setLightColor(0xff0080FF); + final int dat = 70; + final long[] pattern = {0, 3 * dat, dat, dat}; + messagesChannel.setVibrationPattern(pattern); + messagesChannel.enableVibration(true); + messagesChannel.enableLights(true); + messagesChannel.setGroup("chats"); + notificationManager.createNotificationChannel(messagesChannel); + + final NotificationChannel silentMessagesChannel = new NotificationChannel("silent_messages", + c.getString(R.string.silent_messages_channel_name), + NotificationManager.IMPORTANCE_LOW); + silentMessagesChannel.setDescription(c.getString(R.string.silent_messages_channel_description)); + silentMessagesChannel.setShowBadge(true); + silentMessagesChannel.setLightColor(0xff0080FF); + silentMessagesChannel.enableLights(true); + silentMessagesChannel.setGroup("chats"); + notificationManager.createNotificationChannel(silentMessagesChannel); + } + public boolean notify(final Message message) { final Conversation conversation = (Conversation) message.getConversation(); return message.getStatus() == Message.STATUS_RECEIVED - && notificationsEnabled() && !conversation.isMuted() && (conversation.alwaysNotify() || wasHighlightedOrPrivate(message)) - && (!conversation.isWithStranger() || notificationsFromStrangers() - ); - } - - public boolean notificationsEnabled() { - return mXmppConnectionService.getBooleanPreference("show_notification", R.bool.show_notification); + && (!conversation.isWithStranger() || notificationsFromStrangers()) + ; } private boolean notificationsFromStrangers() { return mXmppConnectionService.getBooleanPreference("notifications_from_strangers", R.bool.notifications_from_strangers); } - public boolean isQuietHours() { + private boolean isQuietHours() { if (!mXmppConnectionService.getBooleanPreference("enable_quiet_hours", R.bool.enable_quiet_hours)) { return false; } @@ -104,7 +200,6 @@ public class NotificationService { final long startTime = preferences.getLong("quiet_hours_start", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY; final long endTime = preferences.getLong("quiet_hours_end", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY; final long nowTime = Calendar.getInstance().getTimeInMillis() % Config.MILLISECONDS_IN_DAY; - if (endTime < startTime) { return nowTime > startTime || nowTime < endTime; } else { @@ -143,23 +238,37 @@ public class NotificationService { if (account == null || !notify) { updateNotification(notify); } else { - updateNotification(getBacklogMessageCount(account) > 0); + final int count; + final List<String> conversations; + synchronized (this.mBacklogMessageCounter) { + conversations = getBacklogConversations(account); + count = getBacklogMessageCount(account); + } + updateNotification(count > 0, conversations); + } + } + } + private List<String> getBacklogConversations(Account account) { + final List<String> conversations = new ArrayList<>(); + for (Iterator<Map.Entry<Conversation, AtomicInteger>> it = mBacklogMessageCounter.entrySet().iterator(); it.hasNext(); ) { + Map.Entry<Conversation, AtomicInteger> entry = it.next(); + if (entry.getKey().getAccount() == account) { + conversations.add(entry.getKey().getUuid()); } } + return conversations; } private int getBacklogMessageCount(Account account) { int count = 0; - synchronized (this.mBacklogMessageCounter) { - for (Iterator<Map.Entry<Conversation, AtomicInteger>> it = mBacklogMessageCounter.entrySet().iterator(); it.hasNext(); ) { - Map.Entry<Conversation, AtomicInteger> entry = it.next(); - if (entry.getKey().getAccount() == account) { - count += entry.getValue().get(); - it.remove(); - } + for (Iterator<Map.Entry<Conversation, AtomicInteger>> it = mBacklogMessageCounter.entrySet().iterator(); it.hasNext(); ) { + Map.Entry<Conversation, AtomicInteger> entry = it.next(); + if (entry.getKey().getAccount() == account) { + count += entry.getValue().get(); + it.remove(); } } - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": backlog message count="+count); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": backlog message count=" + count); return count; } @@ -206,11 +315,12 @@ public class NotificationService { } synchronized (notifications) { pushToStack(message); - final Account account = message.getConversation().getAccount(); + final Conversational conversation = message.getConversation(); + final Account account = conversation.getAccount(); final boolean doNotify = (!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn) && !account.inGracePeriod() && !this.inMiniGracePeriod(account); - updateNotification(doNotify); + updateNotification(doNotify, Collections.singletonList(conversation.getUuid())); } } @@ -232,7 +342,7 @@ public class NotificationService { markAsReadIfHasDirectReply(conversation); if (notifications.remove(conversation.getUuid()) != null) { cancel(conversation.getUuid(), NOTIFICATION_ID); - updateNotification(false, true); + updateNotification(false, null, true); } } } @@ -257,12 +367,16 @@ public class NotificationService { } public void updateNotification(final boolean notify) { - updateNotification(notify, false); + updateNotification(notify, null, false); } - public void updateNotification(final boolean notify, boolean summaryOnly) { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); + public void updateNotification(final boolean notify, final List<String> conversations) { + updateNotification(notify, conversations, false); + } + private void updateNotification(final boolean notify, final List<String> conversations, final boolean summaryOnly) { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); + final boolean notifyOnlyOneChild = notify && conversations != null && conversations.size() == 1; //if this check is changed to > 0 catchup messages will create one notification per conversation if (notifications.size() == 0) { cancel(NOTIFICATION_ID); } else { @@ -271,15 +385,22 @@ public class NotificationService { } final Builder mBuilder; if (notifications.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - mBuilder = buildSingleConversations(notifications.values().iterator().next()); + mBuilder = buildSingleConversations(notifications.values().iterator().next(), notify); modifyForSoundVibrationAndLight(mBuilder, notify, preferences); notify(NOTIFICATION_ID, mBuilder.build()); } else { - mBuilder = buildMultipleConversation(); + mBuilder = buildMultipleConversation(notify); + if (notifyOnlyOneChild) { + mBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); + } modifyForSoundVibrationAndLight(mBuilder, notify, preferences); if (!summaryOnly) { for (Map.Entry<String, ArrayList<Message>> entry : notifications.entrySet()) { - Builder singleBuilder = buildSingleConversations(entry.getValue()); + String uuid = entry.getKey(); + Builder singleBuilder = buildSingleConversations(entry.getValue(), notifyOnlyOneChild ? conversations.contains(uuid) : notify); + if (!notifyOnlyOneChild) { + singleBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY); + } singleBuilder.setGroup(CONVERSATIONS_GROUP); setNotificationColor(singleBuilder); notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build()); @@ -330,9 +451,8 @@ public class NotificationService { } } - private Builder buildMultipleConversation() { - final Builder mBuilder = new NotificationCompat.Builder( - mXmppConnectionService); + private Builder buildMultipleConversation(final boolean notify) { + final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService, notify ? "messages" : "silent_messages"); final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); style.setBigContentTitle(notifications.size() + " " @@ -378,8 +498,8 @@ public class NotificationService { return mBuilder; } - private Builder buildSingleConversations(final ArrayList<Message> messages) { - final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService); + private Builder buildSingleConversations(final ArrayList<Message> messages, final boolean notify) { + final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService, notify ? "messages" : "silent_messages"); if (messages.size() >= 1) { final Conversation conversation = (Conversation) messages.get(0).getConversation(); final UnreadConversation.Builder mUnreadBuilder = new UnreadConversation.Builder(conversation.getName().toString()); @@ -407,8 +527,7 @@ public class NotificationService { R.drawable.ic_reply_white_24dp, replyLabel, createReplyIntent(conversation, false)).addRemoteInput(remoteInput).build(); - NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder( - R.drawable.ic_wear_reply, + NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply, replyLabel, createReplyIntent(conversation, true)).addRemoteInput(remoteInput).build(); mBuilder.extend(new NotificationCompat.WearableExtender().addAction(wearReplyAction)); @@ -473,18 +592,6 @@ public class NotificationService { return mBuilder; } - private static boolean displaySnoozeAction(List<Message> messages) { - int numberOfMessagesWithoutReply = 0; - for (Message message : messages) { - if (message.getStatus() == Message.STATUS_RECEIVED) { - ++numberOfMessagesWithoutReply; - } else { - return false; - } - } - return numberOfMessagesWithoutReply >= 3; - } - private void modifyForImage(final Builder builder, final UnreadConversation.Builder uBuilder, final Message message, final ArrayList<Message> messages) { try { @@ -545,7 +652,7 @@ public class NotificationService { styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0); builder.setContentText(styledString); } else { - builder.setContentText(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages,count,count)); + builder.setContentText(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count)); } } } @@ -596,7 +703,7 @@ public class NotificationService { private CharSequence getMergedBodies(final ArrayList<Message> messages) { final StringBuilder text = new StringBuilder(); - for(Message message : messages) { + for (Message message : messages) { if (text.length() != 0) { text.append("\n"); } @@ -606,7 +713,7 @@ public class NotificationService { } private PendingIntent createShowLocationIntent(final Message message) { - Iterable<Intent> intents = GeoHelper.createGeoIntentsFromMessage(message, this.mXmppConnectionService.getApplicationContext()); + Iterable<Intent> intents = GeoHelper.createGeoIntentsFromMessage(message, mXmppConnectionService); for (Intent intent : intents) { if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) { return PendingIntent.getActivity(mXmppConnectionService, generateRequestCode(message.getConversation(), 18), intent, PendingIntent.FLAG_UPDATE_CURRENT); @@ -676,7 +783,7 @@ public class NotificationService { return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 16), intent, PendingIntent.FLAG_UPDATE_CURRENT); } - public PendingIntent createSnoozeIntent(Conversation conversation) { + private PendingIntent createSnoozeIntent(Conversation conversation) { final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); intent.setAction(XmppConnectionService.ACTION_SNOOZE); intent.putExtra("uuid", conversation.getUuid()); @@ -736,52 +843,61 @@ public class NotificationService { } public Notification createForegroundNotification() { - final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService); - List<Account> accounts = mXmppConnectionService.getAccounts(); - String status; - Account mAccount = null; - Log.d(Config.LOGTAG, "Accounts size " + accounts.size()); - if (accounts.size() == 1) { - mAccount = accounts.get(0); - if (mAccount.getStatus() == Account.State.ONLINE) { - status = "(" + mXmppConnectionService.getString(R.string.account_status_online) + ")"; - } else if (mAccount.getStatus() == Account.State.CONNECTING) { - status = "(" + mXmppConnectionService.getString(R.string.account_status_connecting) + ")"; - } else { - status = "(" + mXmppConnectionService.getString(R.string.account_status_offline) + ")"; - } - } else if (accounts.size() > 1) { - status = ""; // todo: status for multiple accounts??? - } else { - status = "(" + mXmppConnectionService.getString(R.string.account_status_offline) + ")"; - } - status = " " + status; - Log.d(Config.LOGTAG, "Status: " + status); - mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service) + status); - if (Config.SHOW_CONNECTED_ACCOUNTS) { + final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService); + mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service)); + if (Compatibility.twentySix() || Config.SHOW_CONNECTED_ACCOUNTS) { + List<Account> accounts = mXmppConnectionService.getAccounts(); int enabled = 0; int connected = 0; - for (Account account : accounts) { - if (account.isOnlineAndConnected()) { - connected++; - enabled++; - } else if (account.isEnabled()) { - enabled++; + String status; + Account mAccount = null; + if (accounts != null) { + for (Account account : accounts) { + if (account.isOnlineAndConnected()) { + connected++; + enabled++; + } else if (account.isEnabled()) { + enabled++; + } } + if (accounts.size() == 1) { + mAccount = accounts.get(0); + if (mAccount.getStatus() == Account.State.ONLINE) { + status = "(" + mXmppConnectionService.getString(R.string.account_status_online) + ")"; + status = " " + status; + Log.d(Config.LOGTAG, "Status: " + status); + mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service) + status); + } else if (mAccount.getStatus() == Account.State.CONNECTING) { + status = "(" + mXmppConnectionService.getString(R.string.account_status_connecting) + ")"; + status = " " + status; + Log.d(Config.LOGTAG, "Status: " + status); + mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service) + status); + } else { + status = "(" + mXmppConnectionService.getString(R.string.account_status_offline) + ")"; + status = " " + status; + Log.d(Config.LOGTAG, "Status: " + status); + mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service) + status); + } + } else if (accounts.size() > 1) { + mBuilder.setContentText(mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled)); + } else { + status = "(" + mXmppConnectionService.getString(R.string.account_status_offline) + ")"; + status = " " + status; + Log.d(Config.LOGTAG, "Status: " + status); + mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service) + status); + } + } else { + mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_open_conversations)); } - mBuilder.setContentText(mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled)); } else { mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_open_conversations)); } mBuilder.setContentIntent(createOpenConversationsIntent()); mBuilder.setWhen(0); - mBuilder.setPriority(Config.SHOW_CONNECTED_ACCOUNTS ? NotificationCompat.PRIORITY_DEFAULT : NotificationCompat.PRIORITY_MIN); - if (accounts.size() == 1 && accounts.get(0).getStatus() == Account.State.ONLINE) { - mBuilder.setSmallIcon(R.drawable.ic_link_white_24dp); - } else if (accounts.size() > 1) { - mBuilder.setSmallIcon(R.drawable.ic_link_white_24dp); // todo: status for multiple accounts??? - } else { - mBuilder.setSmallIcon(R.drawable.ic_unlink_white_24dp); + mBuilder.setPriority(Notification.PRIORITY_LOW); + mBuilder.setSmallIcon(R.drawable.ic_link_white_24dp); + if (Compatibility.twentySix()) { + mBuilder.setChannelId(FOREGROUND_CHANNEL_ID); } return mBuilder.build(); } @@ -801,10 +917,10 @@ public class NotificationService { errors.add(account); } } - if (mXmppConnectionService.showForegroundService()) { + if (Compatibility.keepForegroundService(mXmppConnectionService)) { notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification()); } - final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService); + final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService); if (errors.size() == 0) { cancel(ERROR_NOTIFICATION_ID); return; @@ -819,38 +935,77 @@ public class NotificationService { mXmppConnectionService.getString(R.string.try_again), createTryAgainIntent()); mBuilder.setDeleteIntent(createDismissErrorIntent()); - mBuilder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE); - mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp); - mBuilder.setLocalOnly(true); - mBuilder.setPriority(NotificationCompat.PRIORITY_LOW); - if (errors.size() == 1) { - Intent intent = new Intent(mXmppConnectionService, EditAccountActivity.class); - Account mAccount = mXmppConnectionService.getAccounts().get(0); - intent.putExtra("jid", mAccount.getJid().asBareJid().toString()); - intent.putExtra("init", false); - mBuilder.setContentIntent(PendingIntent.getActivity(mXmppConnectionService, - 145, - intent, - PendingIntent.FLAG_UPDATE_CURRENT)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mBuilder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE); + mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp); } else { - mBuilder.setContentIntent(PendingIntent.getActivity(mXmppConnectionService, - 145, - new Intent(mXmppConnectionService, ManageAccountActivity.class), - PendingIntent.FLAG_UPDATE_CURRENT)); + mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { + mBuilder.setLocalOnly(true); + } + mBuilder.setPriority(Notification.PRIORITY_LOW); + mBuilder.setContentIntent(PendingIntent.getActivity(mXmppConnectionService, + 145, + new Intent(mXmppConnectionService, ManageAccountActivity.class), + PendingIntent.FLAG_UPDATE_CURRENT)); + if (Compatibility.twentySix()) { + mBuilder.setChannelId(ERROR_CHANNEL_ID); } notify(ERROR_NOTIFICATION_ID, mBuilder.build()); } - public Notification updateFileAddingNotification(int current, Message message) { - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService); - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService); + public void updateFileAddingNotification(int current, Message message) { + Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService); mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.transcoding_video)); mBuilder.setProgress(100, current, false); mBuilder.setSmallIcon(R.drawable.ic_hourglass_empty_white_24dp); mBuilder.setContentIntent(createContentIntent(message.getConversation())); + mBuilder.setOngoing(true); + if (Compatibility.twentySix()) { + mBuilder.setChannelId(VIDEOCOMPRESSION_CHANNEL_ID); + } Notification notification = mBuilder.build(); notify(FOREGROUND_NOTIFICATION_ID, notification); - return notification; + } + + public void dismissForcedForegroundNotification() { + cancel(FOREGROUND_NOTIFICATION_ID); + } + + public Notification exportLogsNotification() { + Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService); + mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name)); + mBuilder.setContentText(mXmppConnectionService.getString(R.string.notification_export_logs_title)); + mBuilder.setProgress(0, 0, true); + mBuilder.setSmallIcon(R.drawable.ic_import_export_white_24dp); + mBuilder.setOngoing(true); + if (Compatibility.twentySix()) { + mBuilder.setChannelId(BACKUP_CHANNEL_ID); + } + return mBuilder.build(); + } + + public void exportLogsServiceNotification(Notification notification) { + notify(FOREGROUND_NOTIFICATION_ID, notification); + } + + public Notification AppUpdateNotification(PendingIntent intent, String version, String filesize) { + Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService); + mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name)); + mBuilder.setContentText(mXmppConnectionService.getString(R.string.notification_export_logs_title)); + mBuilder.setContentText(String.format(mXmppConnectionService.getString(R.string.update_available), version, filesize)); + mBuilder.setSmallIcon(R.drawable.ic_update_notification); + mBuilder.setContentIntent(intent); + mBuilder.setOngoing(true); + if (Compatibility.twentySix()) { + mBuilder.setChannelId(UPDATE_CHANNEL_ID); + } + return mBuilder.build(); + } + + public void AppUpdateServiceNotification(Notification notification) { + notify(FOREGROUND_NOTIFICATION_ID, notification); } private void notify(String tag, int id, Notification notification) { diff --git a/src/main/java/de/pixart/messenger/services/UpdateService.java b/src/main/java/de/pixart/messenger/services/UpdateService.java index 3b3829290..bf5242939 100644 --- a/src/main/java/de/pixart/messenger/services/UpdateService.java +++ b/src/main/java/de/pixart/messenger/services/UpdateService.java @@ -1,14 +1,11 @@ package de.pixart.messenger.services; -import android.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; -import android.support.v4.app.NotificationCompat; -import android.support.v4.app.NotificationManagerCompat; import android.util.Log; import android.widget.Toast; @@ -27,12 +24,12 @@ import de.pixart.messenger.R; import de.pixart.messenger.ui.UpdaterActivity; import static de.pixart.messenger.http.HttpConnectionManager.getProxy; -import static de.pixart.messenger.services.NotificationService.UPDATE_NOTIFICATION_ID; public class UpdateService extends AsyncTask<String, Object, UpdateService.Wrapper> { private boolean mUseTor; private Context context; private String store; + private NotificationService getNotificationService; public UpdateService() { } @@ -40,6 +37,7 @@ public class UpdateService extends AsyncTask<String, Object, UpdateService.Wrapp this.context = context; this.store = Store; this.mUseTor = mXmppConnectionService.useTorToConnect(); + this.getNotificationService = mXmppConnectionService.getNotificationService(); } @Override @@ -145,14 +143,7 @@ public class UpdateService extends AsyncTask<String, Object, UpdateService.Wrapp intent.putExtra("changelog", changelog); intent.putExtra("store", store); PendingIntent pi = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context); - mBuilder.setContentTitle(context.getString(R.string.update_service)); - mBuilder.setContentText(String.format(context.getString(R.string.update_available), version, filesize)); - mBuilder.setSmallIcon(R.drawable.ic_update_notification); - mBuilder.setContentIntent(pi); - Notification notification = mBuilder.build(); - notificationManager.notify(UPDATE_NOTIFICATION_ID, notification); + getNotificationService.AppUpdateServiceNotification(getNotificationService.AppUpdateNotification(pi, version, filesize)); } private int checkVersion(String remoteVersion, String installedVersion) { diff --git a/src/main/java/de/pixart/messenger/services/XmppConnectionService.java b/src/main/java/de/pixart/messenger/services/XmppConnectionService.java index e50f0d9e2..525354957 100644 --- a/src/main/java/de/pixart/messenger/services/XmppConnectionService.java +++ b/src/main/java/de/pixart/messenger/services/XmppConnectionService.java @@ -1,10 +1,13 @@ package de.pixart.messenger.services; +import android.Manifest; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.AlarmManager; +import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -113,7 +116,9 @@ import de.pixart.messenger.persistance.FileBackend; import de.pixart.messenger.ui.SettingsActivity; import de.pixart.messenger.ui.UiCallback; import de.pixart.messenger.ui.interfaces.OnAvatarPublication; +import de.pixart.messenger.ui.interfaces.OnMediaLoaded; import de.pixart.messenger.ui.interfaces.OnSearchResultsAvailable; +import de.pixart.messenger.utils.Compatibility; import de.pixart.messenger.utils.ConversationsFileObserver; import de.pixart.messenger.utils.CryptoHelper; import de.pixart.messenger.utils.ExceptionHelper; @@ -150,6 +155,7 @@ import de.pixart.messenger.xmpp.jingle.OnJinglePacketReceived; import de.pixart.messenger.xmpp.jingle.stanzas.JinglePacket; import de.pixart.messenger.xmpp.mam.MamReference; import de.pixart.messenger.xmpp.pep.Avatar; +import de.pixart.messenger.xmpp.pep.PublishOptions; import de.pixart.messenger.xmpp.stanzas.IqPacket; import de.pixart.messenger.xmpp.stanzas.MessagePacket; import de.pixart.messenger.xmpp.stanzas.PresencePacket; @@ -167,11 +173,9 @@ public class XmppConnectionService extends Service { public static final String ACTION_IDLE_PING = "idle_ping"; public static final String ACTION_FCM_TOKEN_REFRESH = "fcm_token_refresh"; public static final String ACTION_FCM_MESSAGE_RECEIVED = "fcm_message_received"; - private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts"; - private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp"; public static final String FDroid = "org.fdroid.fdroid"; public static final String PlayStore = "com.android.vending"; - public static final String Yalp = "com.github.yeriomin.yalpstore"; + private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp"; static { URL.setURLStreamHandlerFactory(new CustomURLStreamHandlerFactory()); @@ -198,29 +202,60 @@ public class XmppConnectionService extends Service { } } }; + //Ui callback listeners + private final Set<OnConversationUpdate> mOnConversationUpdates = Collections.newSetFromMap(new WeakHashMap<OnConversationUpdate, Boolean>()); + private final Set<OnShowErrorToast> mOnShowErrorToasts = Collections.newSetFromMap(new WeakHashMap<OnShowErrorToast, Boolean>()); + private final Set<OnAccountUpdate> mOnAccountUpdates = Collections.newSetFromMap(new WeakHashMap<OnAccountUpdate, Boolean>()); + private final Set<OnCaptchaRequested> mOnCaptchaRequested = Collections.newSetFromMap(new WeakHashMap<OnCaptchaRequested, Boolean>()); + private final Set<OnRosterUpdate> mOnRosterUpdates = Collections.newSetFromMap(new WeakHashMap<OnRosterUpdate, Boolean>()); + private final Set<OnUpdateBlocklist> mOnUpdateBlocklist = Collections.newSetFromMap(new WeakHashMap<OnUpdateBlocklist, Boolean>()); + private final Set<OnMucRosterUpdate> mOnMucRosterUpdate = Collections.newSetFromMap(new WeakHashMap<OnMucRosterUpdate, Boolean>()); + private final Set<OnKeyStatusUpdated> mOnKeyStatusUpdated = Collections.newSetFromMap(new WeakHashMap<OnKeyStatusUpdated, Boolean>()); + private final Object LISTENER_LOCK = new Object(); public DatabaseBackend databaseBackend; - private ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor(true); - private long mLastActivity = 0; - private ContentObserver contactObserver = new ContentObserver(null) { + private final OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() { + @Override - public void onChange(boolean selfChange) { - super.onChange(selfChange); - Intent intent = new Intent(getApplicationContext(), - XmppConnectionService.class); - intent.setAction(ACTION_MERGE_PHONE_CONTACTS); - startService(intent); + public boolean onMessageAcknowledged(Account account, String uuid) { + for (final Conversation conversation : getConversations()) { + if (conversation.getAccount() == account) { + Message message = conversation.findUnsentMessageWithUuid(uuid); + if (message != null) { + message.setStatus(Message.STATUS_SEND); + message.setErrorMessage(null); + databaseBackend.updateMessage(message, false); + return true; + } + } + } + return false; } }; public FileBackend fileBackend = new FileBackend(this); + private final ConversationsFileObserver fileObserver = new ConversationsFileObserver( + Environment.getExternalStorageDirectory().getAbsolutePath() + ) { + @Override + public void onEvent(int event, String path) { + Log.d(Config.LOGTAG, "event " + event + " path=" + path); + markFileDeleted(path); + } + }; + public HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(this); + private ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor(true); + private long mLastActivity = 0; private MemorizingTrustManager mMemorizingTrustManager; private NotificationService mNotificationService = new NotificationService(this); private ShortcutService mShortcutService = new ShortcutService(this); private AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false); - private AtomicBoolean mForceForegroundService = new AtomicBoolean(false); + protected AtomicBoolean mForceForegroundService = new AtomicBoolean(false); private OnMessagePacketReceived mMessageParser = new MessageParser(this); private OnPresencePacketReceived mPresenceParser = new PresenceParser(this); private IqParser mIqParser = new IqParser(this); private MessageGenerator mMessageGenerator = new MessageGenerator(this); + private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this); + private List<Account> accounts; + private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(this); public OnContactStatusChanged onContactStatusChanged = (contact, online) -> { Conversation conversation = find(getConversations(), contact); if (conversation != null) { @@ -240,9 +275,6 @@ public class XmppConnectionService extends Service { } } }; - private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this); - private List<Account> accounts; - private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(this); private final OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() { @Override @@ -250,51 +282,9 @@ public class XmppConnectionService extends Service { mJingleConnectionManager.deliverPacket(account, packet); } }; - public HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(this); private AvatarService mAvatarService = new AvatarService(this); private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this); private PushManagementService mPushManagementService = new PushManagementService(this); - private final ConversationsFileObserver fileObserver = new ConversationsFileObserver( - Environment.getExternalStorageDirectory().getAbsolutePath() - ) { - @Override - public void onEvent(int event, String path) { - markFileDeleted(path); - } - }; - private final OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() { - - @Override - public boolean onMessageAcknowledged(Account account, String uuid) { - for (final Conversation conversation : getConversations()) { - if (conversation.getAccount() == account) { - Message message = conversation.findUnsentMessageWithUuid(uuid); - if (message != null) { - message.setStatus(Message.STATUS_SEND); - message.setErrorMessage(null); - databaseBackend.updateMessage(message, false); - return true; - } - } - } - return false; - } - }; - - private int unreadCount = -1; - - //Ui callback listeners - private final Set<OnConversationUpdate> mOnConversationUpdates = Collections.newSetFromMap(new WeakHashMap<OnConversationUpdate, Boolean>()); - private final Set<OnShowErrorToast> mOnShowErrorToasts = Collections.newSetFromMap(new WeakHashMap<OnShowErrorToast, Boolean>()); - private final Set<OnAccountUpdate> mOnAccountUpdates = Collections.newSetFromMap(new WeakHashMap<OnAccountUpdate, Boolean>()); - private final Set<OnCaptchaRequested> mOnCaptchaRequested = Collections.newSetFromMap(new WeakHashMap<OnCaptchaRequested, Boolean>()); - private final Set<OnRosterUpdate> mOnRosterUpdates = Collections.newSetFromMap(new WeakHashMap<OnRosterUpdate, Boolean>()); - private final Set<OnUpdateBlocklist> mOnUpdateBlocklist = Collections.newSetFromMap(new WeakHashMap<OnUpdateBlocklist, Boolean>()); - private final Set<OnMucRosterUpdate> mOnMucRosterUpdate = Collections.newSetFromMap(new WeakHashMap<OnMucRosterUpdate, Boolean>()); - private final Set<OnKeyStatusUpdated> mOnKeyStatusUpdated = Collections.newSetFromMap(new WeakHashMap<OnKeyStatusUpdated, Boolean>()); - - private final Object LISTENER_LOCK = new Object(); - private final OnBindListener mOnBindListener = new OnBindListener() { @Override @@ -315,7 +305,9 @@ public class XmppConnectionService extends Service { account.getRoster().clearPresences(); mJingleConnectionManager.cancelInTransmission(); fetchRosterFromServer(account); - fetchBookmarks(account); + if (!account.getXmppConnection().getFeatures().bookmarksConversion()) { + fetchBookmarks(account); + } final boolean flexible = account.getXmppConnection().getFeatures().flexibleOfflineMessageRetrieval(); final boolean catchup = getMessageArchiveService().inCatchup(account); if (flexible && catchup) { @@ -333,6 +325,7 @@ public class XmppConnectionService extends Service { syncDirtyContacts(account); } }; + private int unreadCount = -1; private AtomicLong mLastExpiryRun = new AtomicLong(0); private SecureRandom mRandom; private LruCache<Pair<String, String>, ServiceDiscoveryResult> discoCache = new LruCache<>(20); @@ -418,7 +411,8 @@ public class XmppConnectionService extends Service { private WakeLock wakeLock; private PowerManager pm; private LruCache<String, Bitmap> mBitmapCache; - private EventReceiver mEventReceiver = new EventReceiver(); + private BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver(); + private BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver(); private static String generateFetchKey(Account account, final Avatar avatar) { return account.getJid().asBareJid() + "_" + avatar.owner + "_" + avatar.sha1sum; @@ -569,6 +563,11 @@ public class XmppConnectionService extends Service { return find(getConversations(), account, jid); } + public boolean isMuc(final Account account, final Jid jid) { + final Conversation c = find(account, jid); + return c != null && c.getMode() == Conversational.MODE_MULTI; + } + public void search(List<String> term, OnSearchResultsAvailable onSearchResultsAvailable) { MessageSearchTask.search(this, term, onSearchResultsAvailable); } @@ -586,11 +585,6 @@ public class XmppConnectionService extends Service { resetAllAttemptCounts(true, false); } break; - case ACTION_MERGE_PHONE_CONTACTS: - if (restoredFromDatabaseLatch.getCount() == 0) { - loadPhoneContacts(); - } - return START_STICKY; case Intent.ACTION_SHUTDOWN: logoutAndSave(true); return START_NOT_STICKY; @@ -667,6 +661,7 @@ public class XmppConnectionService extends Service { updateConversation(c); }); case AudioManager.RINGER_MODE_CHANGED_ACTION: + case NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED: if (dndOnSilentMode()) { refreshAllPresences(); } @@ -732,6 +727,7 @@ public class XmppConnectionService extends Service { } private boolean processAccountState(Account account, boolean interactive, boolean isUiAction, boolean isAccountPushed, HashSet<Account> pingCandidates) { + storeNumberOfAccounts(this.getAccounts().size()); boolean pingNow = false; if (account.getStatus().isAttemptReconnect()) { if (!hasInternetConnection()) { @@ -807,6 +803,14 @@ public class XmppConnectionService extends Service { return pingNow; } + private void storeNumberOfAccounts(int accounts) { + //write No of accounts to file + final SharedPreferences.Editor editor = getPreferences().edit(); + Log.d(Config.LOGTAG, "Number of accounts is " + accounts); + editor.putInt(SettingsActivity.NUMBER_OF_ACCOUNTS, accounts); + editor.apply(); + } + public boolean isDataSaverDisabled() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); @@ -925,6 +929,10 @@ public class XmppConnectionService extends Service { } } + public boolean getAttachmentChoicePreference() { + return getBooleanPreference(SettingsActivity.QUICK_SHARE_ATTACHMENT_CHOICE, R.bool.quick_share_attachment_choice); + } + private Presence.Status getTargetPresence() { if (dndOnSilentMode() && isPhoneSilenced()) { return Presence.Status.DND; @@ -950,16 +958,25 @@ public class XmppConnectionService extends Service { } private boolean isPhoneSilenced() { - AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + final boolean notificationDnd; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + final NotificationManager notificationManager = getSystemService(NotificationManager.class); + final int filter = notificationManager == null ? NotificationManager.INTERRUPTION_FILTER_UNKNOWN : notificationManager.getCurrentInterruptionFilter(); + notificationDnd = filter >= NotificationManager.INTERRUPTION_FILTER_PRIORITY; + } else { + notificationDnd = false; + } + final AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + final int ringerMode = audioManager == null ? AudioManager.RINGER_MODE_NORMAL : audioManager.getRingerMode(); try { if (treatVibrateAsSilent()) { - return audioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL; + return notificationDnd || ringerMode != AudioManager.RINGER_MODE_NORMAL; } else { - return audioManager.getRingerMode() == AudioManager.RINGER_MODE_SILENT; + return notificationDnd || ringerMode == AudioManager.RINGER_MODE_SILENT; } } catch (Throwable throwable) { Log.d(Config.LOGTAG, "platform bug in isPhoneSilenced (" + throwable.getMessage() + ")"); - return false; + return notificationDnd; } } @@ -1018,7 +1035,7 @@ public class XmppConnectionService extends Service { final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); try { final NetworkInfo activeNetwork = cm == null ? null : cm.getActiveNetworkInfo(); - return activeNetwork != null && activeNetwork.isConnected(); + return activeNetwork != null && (activeNetwork.isConnected() || activeNetwork.getType() == ConnectivityManager.TYPE_ETHERNET); } catch (RuntimeException e) { Log.d(Config.LOGTAG, "unable to check for internet connection", e); return true; //if internet connection can not be checked it is probably best to just try @@ -1086,6 +1103,9 @@ public class XmppConnectionService extends Service { Resolver.init(this); this.mRandom = new SecureRandom(); updateMemorizingTrustmanager(); + if (Compatibility.twentySix()) { + mNotificationService.initializeChannels(); + } final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); final int cacheSize = maxMemory / 8; this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) { @@ -1112,8 +1132,10 @@ public class XmppConnectionService extends Service { editor.apply(); restoreFromDatabase(); - getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) { + startContactObserver(); + } + if (Compatibility.hasStoragePermission(this)) { Log.d(Config.LOGTAG, "starting file observer"); new Thread(fileObserver::startWatching).start(); } @@ -1143,14 +1165,29 @@ public class XmppConnectionService extends Service { toggleScreenEventReceiver(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { scheduleNextIdlePing(); + IntentFilter intentFilter = new IntentFilter(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + } + intentFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED); + registerReceiver(this.mInternalEventReceiver, intentFilter); } //start export log service every day at given time ScheduleAutomaticExport(); // cancel scheduled exporter CancelAutomaticExport(false); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - registerReceiver(this.mEventReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); - } + } + + public void startContactObserver() { + getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, new ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + if (restoredFromDatabaseLatch.getCount() == 0) { + loadPhoneContacts(); + } + } + }); } @Override @@ -1165,7 +1202,7 @@ public class XmppConnectionService extends Service { @Override public void onDestroy() { try { - unregisterReceiver(this.mEventReceiver); + unregisterReceiver(this.mInternalEventReceiver); } catch (IllegalArgumentException e) { //ignored } @@ -1182,12 +1219,13 @@ public class XmppConnectionService extends Service { public void toggleScreenEventReceiver() { if (awayWhenScreenOff() && !manuallyChangePresence()) { - final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON); + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_ON); filter.addAction(Intent.ACTION_SCREEN_OFF); - registerReceiver(this.mEventReceiver, filter); + registerReceiver(this.mInternalScreenEventReceiver, filter); } else { try { - unregisterReceiver(this.mEventReceiver); + unregisterReceiver(this.mInternalScreenEventReceiver); } catch (IllegalArgumentException e) { //ignored } @@ -1195,19 +1233,22 @@ public class XmppConnectionService extends Service { } public void toggleForegroundService() { - if (mForceForegroundService.get() || (showForegroundService())) { + final boolean status; + if (mForceForegroundService.get() || (Compatibility.keepForegroundService(this)/* && hasEnabledAccounts()*/)) { startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification()); - Log.d(Config.LOGTAG, "started foreground service"); + status = true; } else { stopForeground(true); - Log.d(Config.LOGTAG, "stopped foreground service"); + status = false; } + mNotificationService.dismissForcedForegroundNotification(); //if the channel was changed the previous call might fail + Log.d(Config.LOGTAG, "ForegroundService: " + (status ? "on" : "off")); } @Override public void onTaskRemoved(final Intent rootIntent) { super.onTaskRemoved(rootIntent); - if (showForegroundService() || mForceForegroundService.get()) { + if ((Compatibility.keepForegroundService(this) && hasEnabledAccounts()) || mForceForegroundService.get()) { Log.d(Config.LOGTAG, "ignoring onTaskRemoved because foreground service is activated"); } else { this.logoutAndSave(false); @@ -1494,7 +1535,7 @@ public class XmppConnectionService extends Service { @Override public void onMessageFound(Message message) { if (mHttpConnectionManager.getAutoAcceptFileSize() >= message.getFileParams().size) { - Log.d(Config.LOGTAG, "Resend failed message with size " + message.getFileParams().size + " bytes for " + conversation.getJid()); + Log.d(Config.LOGTAG, "Resend failed message with size " + message.getFileParams().size + " bytes for " + conversation.getJid()); resendMessage(message, true); } } @@ -1520,45 +1561,53 @@ public class XmppConnectionService extends Service { public void fetchBookmarks(final Account account) { final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET); final Element query = iqPacket.query("jabber:iq:private"); - query.addChild("storage", "storage:bookmarks"); - final OnIqPacketReceived callback = new OnIqPacketReceived() { + query.addChild("storage", Namespace.BOOKMARKS); + final OnIqPacketReceived callback = (a, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + final Element query1 = response.query(); + final Element storage = query1.findChild("storage", "storage:bookmarks"); + processBookmarks(a, storage); + } else { + Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": could not fetch bookmarks"); + } + }; + sendIqPacket(account, iqPacket, callback); + } - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - final Element query = packet.query(); - final HashMap<Jid, Bookmark> bookmarks = new HashMap<>(); - final Element storage = query.findChild("storage", "storage:bookmarks"); - final boolean autojoin = respectAutojoin(); - if (storage != null) { - for (final Element item : storage.getChildren()) { - if (item.getName().equals("conference")) { - final Bookmark bookmark = Bookmark.parse(item, account); - Bookmark old = bookmarks.put(bookmark.getJid(), bookmark); - if (old != null && old.getBookmarkName() != null && bookmark.getBookmarkName() == null) { - bookmark.setBookmarkName(old.getBookmarkName()); - } - Conversation conversation = find(bookmark); - if (conversation != null) { - bookmark.setConversation(conversation); - } else if (bookmark.autojoin() && bookmark.getJid() != null && autojoin) { - conversation = findOrCreateConversation(account, bookmark.getJid(), true, true, false); - bookmark.setConversation(conversation); - } - } - } + public void processBookmarks(Account account, Element storage) { + final HashMap<Jid, Bookmark> bookmarks = new HashMap<>(); + final boolean autojoin = respectAutojoin(); + if (storage != null) { + for (final Element item : storage.getChildren()) { + if (item.getName().equals("conference")) { + final Bookmark bookmark = Bookmark.parse(item, account); + Bookmark old = bookmarks.put(bookmark.getJid(), bookmark); + if (old != null && old.getBookmarkName() != null && bookmark.getBookmarkName() == null) { + bookmark.setBookmarkName(old.getBookmarkName()); + } + Conversation conversation = find(bookmark); + if (conversation != null) { + bookmark.setConversation(conversation); + } else if (bookmark.autojoin() && bookmark.getJid() != null && autojoin) { + conversation = findOrCreateConversation(account, bookmark.getJid(), true, true, false); + bookmark.setConversation(conversation); } - account.setBookmarks(new CopyOnWriteArrayList<>(bookmarks.values())); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not fetch bookmarks"); } } - }; - sendIqPacket(account, iqPacket, callback); + } + account.setBookmarks(new CopyOnWriteArrayList<>(bookmarks.values())); } public void pushBookmarks(Account account) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks"); + if (account.getXmppConnection().getFeatures().bookmarksConversion()) { + pushBookmarksPep(account); + } else { + pushBookmarksPrivateXml(account); + } + } + + private void pushBookmarksPrivateXml(Account account) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via private xml"); IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET); Element query = iqPacket.query("jabber:iq:private"); Element storage = query.addChild("storage", "storage:bookmarks"); @@ -1568,6 +1617,46 @@ public class XmppConnectionService extends Service { sendIqPacket(account, iqPacket, mDefaultIqHandler); } + private void pushBookmarksPep(Account account) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via pep"); + Element storage = new Element("storage", "storage:bookmarks"); + for (Bookmark bookmark : account.getBookmarks()) { + storage.addChild(bookmark); + } + pushNodeAndEnforcePublishOptions(account, Namespace.BOOKMARKS, storage, PublishOptions.persistentWhitelistAccess()); + } + + private void pushNodeAndEnforcePublishOptions(final Account account, final String node, final Element element, final Bundle options) { + pushNodeAndEnforcePublishOptions(account, node, element, options, true); + } + + private void pushNodeAndEnforcePublishOptions(final Account account, final String node, final Element element, final Bundle options, final boolean retry) { + IqPacket packet = mIqGenerator.publishElement(node, element, options); + Log.d(Config.LOGTAG, packet.toString()); + sendIqPacket(account, packet, (a, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + return; + } + final Element error = response.getType() == IqPacket.TYPE.ERROR ? response.findChild("error") : null; + final boolean preconditionNotMet = error != null && error.hasChild("precondition-not-met", Namespace.PUBSUB_ERROR); + if (retry && preconditionNotMet) { + pushNodeConfiguration(account, node, options, new OnConfigurationPushed() { + @Override + public void onPushSucceeded() { + pushNodeAndEnforcePublishOptions(account, node, element, options, false); + } + + @Override + public void onPushFailed() { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to push node configuration (" + node + ")"); + } + }); + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error publishing bookmarks (retry=" + Boolean.toString(retry) + ") " + response); + } + }); + } + private void restoreFromDatabase() { synchronized (this.conversations) { final Map<String, Account> accountLookupTable = new Hashtable<>(); @@ -1671,7 +1760,7 @@ public class XmppConnectionService extends Service { } Log.d(Config.LOGTAG, "finished merging phone contacts"); mShortcutService.refresh(mInitialAddressbookSyncCompleted.compareAndSet(false, true)); - updateAccountUi(); + updateRosterUi(); } })); } @@ -1860,7 +1949,7 @@ public class XmppConnectionService extends Service { conversation.setContactJid(jid.asBareJid()); } databaseBackend.updateConversation(conversation); - loadMessagesFromDb = conversation.messagesLoaded.compareAndSet(true,false); + loadMessagesFromDb = conversation.messagesLoaded.compareAndSet(true, false); } else { String conversationName; Contact contact = account.getRoster().getContact(jid); @@ -1934,7 +2023,7 @@ public class XmppConnectionService extends Service { conversation.setContactJid(jid.asBareJid()); } databaseBackend.updateConversation(conversation); - loadMessagesFromDb = conversation.messagesLoaded.compareAndSet(true,false); + loadMessagesFromDb = conversation.messagesLoaded.compareAndSet(true, false); } else { String conversationName; Contact contact = account.getRoster().getContact(jid); @@ -2139,6 +2228,7 @@ public class XmppConnectionService extends Service { updateAccountUi(); getNotificationService().updateErrorNotification(); syncEnabledAccountSetting(); + toggleForegroundService(); } } @@ -2147,7 +2237,7 @@ public class XmppConnectionService extends Service { synchronized (LISTENER_LOCK) { remainingListeners = checkListeners(); if (!this.mOnConversationUpdates.add(listener)) { - Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as ConversationListChangedListener"); + Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as ConversationListChangedListener"); } this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0); } @@ -2173,7 +2263,7 @@ public class XmppConnectionService extends Service { synchronized (LISTENER_LOCK) { remainingListeners = checkListeners(); if (!this.mOnShowErrorToasts.add(listener)) { - Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnShowErrorToastListener"); + Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnShowErrorToastListener"); } } if (remainingListeners) { @@ -2197,7 +2287,7 @@ public class XmppConnectionService extends Service { synchronized (LISTENER_LOCK) { remainingListeners = checkListeners(); if (!this.mOnAccountUpdates.add(listener)) { - Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnAccountListChangedtListener"); + Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnAccountListChangedtListener"); } } if (remainingListeners) { @@ -2221,7 +2311,7 @@ public class XmppConnectionService extends Service { synchronized (LISTENER_LOCK) { remainingListeners = checkListeners(); if (!this.mOnCaptchaRequested.add(listener)) { - Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnCaptchaRequestListener"); + Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnCaptchaRequestListener"); } } if (remainingListeners) { @@ -2245,7 +2335,7 @@ public class XmppConnectionService extends Service { synchronized (LISTENER_LOCK) { remainingListeners = checkListeners(); if (!this.mOnRosterUpdates.add(listener)) { - Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnRosterUpdateListener"); + Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnRosterUpdateListener"); } } if (remainingListeners) { @@ -2269,7 +2359,7 @@ public class XmppConnectionService extends Service { synchronized (LISTENER_LOCK) { remainingListeners = checkListeners(); if (!this.mOnUpdateBlocklist.add(listener)) { - Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnUpdateBlocklistListener"); + Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnUpdateBlocklistListener"); } } if (remainingListeners) { @@ -2293,7 +2383,7 @@ public class XmppConnectionService extends Service { synchronized (LISTENER_LOCK) { remainingListeners = checkListeners(); if (!this.mOnKeyStatusUpdated.add(listener)) { - Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnKeyStatusUpdateListener"); + Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnKeyStatusUpdateListener"); } } if (remainingListeners) { @@ -2317,7 +2407,7 @@ public class XmppConnectionService extends Service { synchronized (LISTENER_LOCK) { remainingListeners = checkListeners(); if (!this.mOnMucRosterUpdate.add(listener)) { - Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnMucRosterListener"); + Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnMucRosterListener"); } } if (remainingListeners) { @@ -2581,7 +2671,10 @@ public class XmppConnectionService extends Service { } private boolean hasEnabledAccounts() { - for(Account account : this.accounts) { + if (this.accounts == null) { + return false; // set to false if accounts could not be fetched - used for notifications + } + for (Account account : this.accounts) { if (account.isEnabled()) { return true; } @@ -2589,6 +2682,18 @@ public class XmppConnectionService extends Service { return false; } + public void getAttachments(final Conversation conversation, int limit, final OnMediaLoaded onMediaLoaded) { + getAttachments(conversation.getAccount(), conversation.getJid().asBareJid(), limit, onMediaLoaded); + } + + public void getAttachments(final Account account, final Jid jid, final int limit, final OnMediaLoaded onMediaLoaded) { + getAttachments(account.getUuid(), jid.asBareJid(), limit, onMediaLoaded); + } + + public void getAttachments(final String account, final Jid jid, final int limit, final OnMediaLoaded onMediaLoaded) { + new Thread(() -> onMediaLoaded.onMediaLoaded(fileBackend.convertToAttachments(databaseBackend.getRelativeFilePaths(account, jid, limit)))).start(); + } + public void persistSelfNick(MucOptions.User self) { final Conversation conversation = self.getConversation(); Jid full = self.getFullJid(); @@ -2744,13 +2849,13 @@ public class XmppConnectionService extends Service { if (callback != null) { callback.error(R.string.conference_creation_failed, null); } - return true; + return false; } } else { if (callback != null) { callback.error(R.string.not_connected_try_again, null); } - return true; + return false; } } @@ -2796,6 +2901,7 @@ public class XmppConnectionService extends Service { } public void pushNodeConfiguration(Account account, final String node, final Bundle options, final OnConfigurationPushed callback) { + Log.d(Config.LOGTAG, "pushing node configuration"); pushNodeConfiguration(account, account.getJid().asBareJid(), node, options, callback); } @@ -3172,7 +3278,7 @@ public class XmppConnectionService extends Service { getAvatarService().clear(account); databaseBackend.updateAccount(account); } - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": published avatar "+(avatar.size/1024)+"KiB"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": published avatar " + (avatar.size / 1024) + "KiB"); if (callback != null) { callback.onAvatarPublicationSucceeded(); } @@ -3185,7 +3291,7 @@ public class XmppConnectionService extends Service { }); } else { Element error = result.findChild("error"); - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": server rejected avatar "+(avatar.size/1024)+"KiB "+(error!=null?error.toString():"")); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server rejected avatar " + (avatar.size / 1024) + "KiB " + (error != null ? error.toString() : "")); if (callback != null) { callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject); } @@ -3539,7 +3645,7 @@ public class XmppConnectionService extends Service { public long getLongPreference(String name, @IntegerRes int res) { long defaultValue = getResources().getInteger(res); try { - return Long.parseLong(getPreferences().getString(name,String.valueOf(defaultValue))); + return Long.parseLong(getPreferences().getString(name, String.valueOf(defaultValue))); } catch (NumberFormatException e) { return defaultValue; } @@ -3777,7 +3883,7 @@ public class XmppConnectionService extends Service { public void updateMemorizingTrustmanager() { final MemorizingTrustManager tm; - final boolean dontTrustSystemCAs = getPreferences().getBoolean("dont_trust_system_cas", getResources().getBoolean(R.bool.dont_trust_system_cas)); + final boolean dontTrustSystemCAs = getBooleanPreference("dont_trust_system_cas", R.bool.dont_trust_system_cas); if (dontTrustSystemCAs) { tm = new MemorizingTrustManager(getApplicationContext(), null); } else { @@ -4133,7 +4239,7 @@ public class XmppConnectionService extends Service { ServiceDiscoveryResult discoveryResult = new ServiceDiscoveryResult(response); if (presence.getVer().equals(discoveryResult.getVer())) { databaseBackend.insertDiscoveryResult(discoveryResult); - injectServiceDiscorveryResult(a.getRoster(), presence.getHash(), presence.getVer(), discoveryResult); + injectServiceDiscoveryResult(a.getRoster(), presence.getHash(), presence.getVer(), discoveryResult); } else { Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + discoveryResult.getVer()); } @@ -4144,7 +4250,7 @@ public class XmppConnectionService extends Service { } } - private void injectServiceDiscorveryResult(Roster roster, String hash, String ver, ServiceDiscoveryResult disco) { + private void injectServiceDiscoveryResult(Roster roster, String hash, String ver, ServiceDiscoveryResult disco) { for (Contact contact : roster.getContacts()) { for (Presence presence : contact.getPresences().getPresences().values()) { if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) { @@ -4270,11 +4376,7 @@ public class XmppConnectionService extends Service { } public boolean blindTrustBeforeVerification() { - return getPreferences().getBoolean(SettingsActivity.BLIND_TRUST_BEFORE_VERIFICATION, getResources().getBoolean(R.bool.btbv)); - } - - public boolean showForegroundService() { - return getPreferences().getBoolean(SettingsActivity.SHOW_FOREGROUND_SERVICE, getResources().getBoolean(R.bool.show_foreground_service)); + return getBooleanPreference(SettingsActivity.BLIND_TRUST_BEFORE_VERIFICATION, R.bool.btbv); } public void ScheduleAutomaticExport() { @@ -4293,7 +4395,7 @@ public class XmppConnectionService extends Service { if (timetoexport.before(now)) { SimpleDateFormat newDate = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US); timetoexport.add(Calendar.DAY_OF_YEAR, 1); //DATE or DAY_OF_MONTH - Log.d(Config.LOGTAG, "Schedule automatic export logs, for today, the export time is in the past, scheduling first export run for the next day ("+ newDate.format(timetoexport.getTimeInMillis()) +")."); + Log.d(Config.LOGTAG, "Schedule automatic export logs, for today, the export time is in the past, scheduling first export run for the next day (" + newDate.format(timetoexport.getTimeInMillis()) + ")."); } final PendingIntent ScheduleExportIntent = PendingIntent.getBroadcast(this, AlarmReceiver.SCHEDULE_ALARM_REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT); ((AlarmManager) getSystemService(ALARM_SERVICE)).setInexactRepeating(AlarmManager.RTC_WAKEUP, timetoexport.getTimeInMillis(), AlarmManager.INTERVAL_DAY, ScheduleExportIntent); @@ -4419,4 +4521,11 @@ public class XmppConnectionService extends Service { return XmppConnectionService.this; } } + + private class InternalEventReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + onStartCommand(intent, 0, 0); + } + } } diff --git a/src/main/java/de/pixart/messenger/ui/ConferenceDetailsActivity.java b/src/main/java/de/pixart/messenger/ui/ConferenceDetailsActivity.java index db70c8147..070914bb8 100644 --- a/src/main/java/de/pixart/messenger/ui/ConferenceDetailsActivity.java +++ b/src/main/java/de/pixart/messenger/ui/ConferenceDetailsActivity.java @@ -32,6 +32,7 @@ import org.openintents.openpgp.util.OpenPgpUtils; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicInteger; @@ -49,8 +50,14 @@ import de.pixart.messenger.entities.MucOptions.User; import de.pixart.messenger.services.XmppConnectionService; import de.pixart.messenger.services.XmppConnectionService.OnConversationUpdate; import de.pixart.messenger.services.XmppConnectionService.OnMucRosterUpdate; +import de.pixart.messenger.ui.adapter.MediaAdapter; +import de.pixart.messenger.ui.interfaces.OnMediaLoaded; +import de.pixart.messenger.ui.util.Attachment; +import de.pixart.messenger.ui.util.GridManager; +import de.pixart.messenger.ui.util.MucDetailsContextMenuHelper; import de.pixart.messenger.ui.util.MyLinkify; import de.pixart.messenger.ui.util.SoftKeyboardUtils; +import de.pixart.messenger.utils.Compatibility; import de.pixart.messenger.utils.EmojiWrapper; import de.pixart.messenger.utils.MenuDoubleTabUtil; import de.pixart.messenger.utils.StringUtils; @@ -63,7 +70,7 @@ import rocks.xmpp.addr.Jid; import static de.pixart.messenger.entities.Bookmark.printableValue; import static de.pixart.messenger.utils.StringUtils.changed; -public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged, XmppConnectionService.OnConfigurationPushed, TextWatcher { +public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged, XmppConnectionService.OnConfigurationPushed, TextWatcher, OnMediaLoaded { public static final String ACTION_VIEW_MUC = "view_muc"; private static final float INACTIVE_ALPHA = 0.4684f; //compromise between dark and light theme private Conversation mConversation; @@ -95,6 +102,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers } }; private ActivityMucDetailsBinding binding; + private MediaAdapter mMediaAdapter; private String uuid = null; private User mSelectedUser = null; @@ -318,6 +326,9 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers this.binding.mucEditTitle.addTextChangedListener(this); this.binding.mucEditSubject.addTextChangedListener(this); this.binding.mucEditSubject.addTextChangedListener(new StylingHelper.MessageEditorStyler(this.binding.mucEditSubject)); + mMediaAdapter = new MediaAdapter(this, R.dimen.media_size); + this.binding.media.setAdapter(mMediaAdapter); + GridManager.setupLayoutManager(this, this.binding.media, R.dimen.media_size); } @Override @@ -327,6 +338,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers if (this.mTheme != theme) { recreate(); } + binding.mediaWrapper.setVisibility(Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE); } @Override @@ -447,14 +459,12 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers if (tag instanceof User) { getMenuInflater().inflate(R.menu.muc_details_context, menu); final User user = (User) tag; - final User self = mConversation.getMucOptions().getSelf(); - final Jid jid = user.getFullJid(); this.mSelectedUser = user; String name; final Contact contact = user.getContact(); if (contact != null && contact.showInRoster()) { name = contact.getDisplayName(); - } else if (user.getRealJid() != null){ + } else if (user.getRealJid() != null) { name = user.getRealJid().asBareJid().toString(); } else { name = user.getName(); @@ -463,129 +473,26 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers MenuItem sendPrivateMessage = menu.findItem(R.id.send_private_message); MenuItem highlightInMuc = menu.findItem(R.id.highlight_in_muc); highlightInMuc.setVisible(true); - if (user.getRealJid() != null) { - MenuItem startConversation = menu.findItem(R.id.start_conversation); - MenuItem giveMembership = menu.findItem(R.id.give_membership); - MenuItem removeMembership = menu.findItem(R.id.remove_membership); - MenuItem giveAdminPrivileges = menu.findItem(R.id.give_admin_privileges); - MenuItem removeAdminPrivileges = menu.findItem(R.id.remove_admin_privileges); - MenuItem removeFromRoom = menu.findItem(R.id.remove_from_room); - MenuItem banFromConference = menu.findItem(R.id.ban_from_conference); - MenuItem invite = menu.findItem(R.id.invite); - startConversation.setVisible(true); - if (user.getRole() == MucOptions.Role.NONE) { - invite.setVisible(true); - } - if (jid != null && !jid.isBareJid()) { - if (mConversation.getMucOptions().isUserInRoom(jid)) { - sendPrivateMessage.setVisible(true); - } else { - sendPrivateMessage.setVisible(false); - } - } - if (self.getAffiliation().ranks(MucOptions.Affiliation.ADMIN) && - self.getAffiliation().outranks(user.getAffiliation())) { - if (mAdvancedMode) { - if (user.getAffiliation() != MucOptions.Affiliation.ADMIN) { - giveAdminPrivileges.setVisible(true); - } else { - removeAdminPrivileges.setVisible(true); - } - } else { - if (!Config.DISABLE_BAN || mConversation.getMucOptions().membersOnly()) { - removeFromRoom.setVisible(true); - } - } - if (user.getAffiliation() == MucOptions.Affiliation.NONE) { - giveMembership.setVisible(true); - } else { - removeMembership.setVisible(true); - } - if (!Config.DISABLE_BAN) { - banFromConference.setVisible(true); - } - } - } else { - - sendPrivateMessage.setVisible(user.getRole().ranks(MucOptions.Role.VISITOR)); - } - + MucDetailsContextMenuHelper.configureMucDetailsContextMenu(this, menu, mConversation, user); } super.onCreateContextMenu(menu, v, menuInfo); } @Override public boolean onContextItemSelected(MenuItem item) { - Jid jid = mSelectedUser.getRealJid(); - switch (item.getItemId()) { - case R.id.highlight_in_muc: - highlightInMuc(mConversation, mSelectedUser.getName()); - return true; - case R.id.send_private_message: - if (mConversation.getMucOptions().allowPm()) { - privateMsgInMuc(mConversation, mSelectedUser.getName()); - } else { - Toast.makeText(this, R.string.private_messages_are_disabled, Toast.LENGTH_SHORT).show(); - } - return true; - case R.id.start_conversation: - startConversation(mSelectedUser); - return true; - case R.id.give_admin_privileges: - xmppConnectionService.changeAffiliationInConference(mConversation, jid, MucOptions.Affiliation.ADMIN, this); - return true; - case R.id.give_membership: - xmppConnectionService.changeAffiliationInConference(mConversation, jid, MucOptions.Affiliation.MEMBER, this); - return true; - case R.id.remove_membership: - xmppConnectionService.changeAffiliationInConference(mConversation, jid, MucOptions.Affiliation.NONE, this); - return true; - case R.id.remove_admin_privileges: - xmppConnectionService.changeAffiliationInConference(mConversation, jid, MucOptions.Affiliation.MEMBER, this); - return true; - case R.id.remove_from_room: - removeFromRoom(mSelectedUser); - return true; - case R.id.ban_from_conference: - xmppConnectionService.changeAffiliationInConference(mConversation, jid, MucOptions.Affiliation.OUTCAST, this); - if (mSelectedUser.getRole() != MucOptions.Role.NONE) { - xmppConnectionService.changeRoleInConference(mConversation, mSelectedUser.getName(), MucOptions.Role.NONE, this); - } - return true; - case R.id.invite: - xmppConnectionService.directInvite(mConversation, jid); - return true; - default: - return super.onContextItemSelected(item); - } - } - - private void removeFromRoom(final User user) { - if (mConversation.getMucOptions().membersOnly()) { - xmppConnectionService.changeAffiliationInConference(mConversation, user.getRealJid(), MucOptions.Affiliation.NONE, this); - if (user.getRole() != MucOptions.Role.NONE) { - xmppConnectionService.changeRoleInConference(mConversation, mSelectedUser.getName(), MucOptions.Role.NONE, ConferenceDetailsActivity.this); - } - } else { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.ban_from_conference); - builder.setMessage(getString(R.string.removing_from_public_conference, user.getName())); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.ban_now, (dialog, which) -> { - xmppConnectionService.changeAffiliationInConference(mConversation, user.getRealJid(), MucOptions.Affiliation.OUTCAST, ConferenceDetailsActivity.this); - if (user.getRole() != MucOptions.Role.NONE) { - xmppConnectionService.changeRoleInConference(mConversation, mSelectedUser.getName(), MucOptions.Role.NONE, ConferenceDetailsActivity.this); - } - }); - builder.create().show(); + if (!MucDetailsContextMenuHelper.onContextItemSelected(item, mSelectedUser, mConversation, this)) { + return super.onContextItemSelected(item); } + return true; } - protected void startConversation(User user) { - if (user.getRealJid() != null) { - Conversation conversation = xmppConnectionService.findOrCreateConversation(this.mConversation.getAccount(), user.getRealJid().asBareJid(), false, true); - switchToConversation(conversation); - } + @Override + public void onMediaLoaded(List<Attachment> attachments) { + runOnUiThread(() -> { + int limit = GridManager.getCurrentColumnCount(binding.media); + mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit, attachments.size()))); + binding.mediaWrapper.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE); + }); } protected void saveAsBookmark() { @@ -616,6 +523,11 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers if (uuid != null) { this.mConversation = xmppConnectionService.findConversationByUuid(uuid); if (this.mConversation != null) { + if (Compatibility.hasStoragePermission(this)) { + final int limit = GridManager.getCurrentColumnCount(this.binding.media); + xmppConnectionService.getAttachments(this.mConversation, limit, this); + this.binding.showMedia.setOnClickListener((v) -> MediaBrowserActivity.launch(this, mConversation)); + } updateView(); } } diff --git a/src/main/java/de/pixart/messenger/ui/ContactDetailsActivity.java b/src/main/java/de/pixart/messenger/ui/ContactDetailsActivity.java index 5dce3fca2..1dfc80e3c 100644 --- a/src/main/java/de/pixart/messenger/ui/ContactDetailsActivity.java +++ b/src/main/java/de/pixart/messenger/ui/ContactDetailsActivity.java @@ -30,6 +30,8 @@ import android.widget.Toast; import org.openintents.openpgp.util.OpenPgpUtils; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -45,6 +47,11 @@ import de.pixart.messenger.entities.Conversation; import de.pixart.messenger.entities.ListItem; import de.pixart.messenger.services.XmppConnectionService.OnAccountUpdate; import de.pixart.messenger.services.XmppConnectionService.OnRosterUpdate; +import de.pixart.messenger.ui.adapter.MediaAdapter; +import de.pixart.messenger.ui.interfaces.OnMediaLoaded; +import de.pixart.messenger.ui.util.Attachment; +import de.pixart.messenger.ui.util.GridManager; +import de.pixart.messenger.utils.Compatibility; import de.pixart.messenger.utils.CryptoHelper; import de.pixart.messenger.utils.IrregularUnicodeDetector; import de.pixart.messenger.utils.MenuDoubleTabUtil; @@ -57,12 +64,13 @@ import de.pixart.messenger.xmpp.OnUpdateBlocklist; import de.pixart.messenger.xmpp.XmppConnection; import rocks.xmpp.addr.Jid; -public class ContactDetailsActivity extends OmemoActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated { +public class ContactDetailsActivity extends OmemoActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated, OnMediaLoaded { public static final String ACTION_VIEW_CONTACT = "view_contact"; private Contact contact; private Conversation mConversation; ActivityContactDetailsBinding binding; + private MediaAdapter mMediaAdapter; private DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() { @Override @@ -260,6 +268,9 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp this.mNotifyStatusButton = findViewById(R.id.notification_status_button); this.mNotifyStatusButton.setOnClickListener(this.mNotifyStatusClickListener); this.mNotifyStatusText = findViewById(R.id.notification_status_text); + mMediaAdapter = new MediaAdapter(this, R.dimen.media_size); + this.binding.media.setAdapter(mMediaAdapter); + GridManager.setupLayoutManager(this, this.binding.media, R.dimen.media_size); } @Override @@ -279,6 +290,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp this.showDynamicTags = preferences.getBoolean(SettingsActivity.SHOW_DYNAMIC_TAGS, false); this.showLastSeen = preferences.getBoolean("last_activity", false); } + binding.mediaWrapper.setVisibility(Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE); + mMediaAdapter.setAttachments(Collections.emptyList()); } @Override @@ -552,12 +565,20 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp } final AxolotlService axolotlService = contact.getAccount().getAxolotlService(); if (Config.supportOmemo() && axolotlService != null) { + final Collection<XmppAxolotlSession> sessions = axolotlService.findSessionsForContact(contact); + boolean anyActive = false; + for(XmppAxolotlSession session : sessions) { + anyActive = session.getTrust().isActive(); + if (anyActive) { + break; + } + } boolean skippedInactive = false; boolean showsInactive = false; - for (final XmppAxolotlSession session : axolotlService.findSessionsForContact(contact)) { + for (final XmppAxolotlSession session : sessions) { final FingerprintStatus trust = session.getTrust(); hasKeys |= !trust.isCompromised(); - if (!trust.isActive()) { + if (!trust.isActive() && anyActive) { if (showInactiveOmemo) { showsInactive = true; } else { @@ -657,6 +678,11 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp processFingerprintVerification(mPendingFingerprintVerificationUri); mPendingFingerprintVerificationUri = null; } + if (Compatibility.hasStoragePermission(this)) { + final int limit = GridManager.getCurrentColumnCount(this.binding.media); + xmppConnectionService.getAttachments(account, contact.getJid().asBareJid(), limit, this); + this.binding.showMedia.setOnClickListener((v) -> MediaBrowserActivity.launch(this, contact)); + } populateView(); } } @@ -676,4 +702,13 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp Toast.makeText(this, R.string.invalid_barcode, Toast.LENGTH_SHORT).show(); } } + + @Override + public void onMediaLoaded(List<Attachment> attachments) { + runOnUiThread(() -> { + int limit = GridManager.getCurrentColumnCount(binding.media); + mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit, attachments.size()))); + binding.mediaWrapper.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE); + }); + } } diff --git a/src/main/java/de/pixart/messenger/ui/ConversationFragment.java b/src/main/java/de/pixart/messenger/ui/ConversationFragment.java index 0374e643c..d326058e7 100644 --- a/src/main/java/de/pixart/messenger/ui/ConversationFragment.java +++ b/src/main/java/de/pixart/messenger/ui/ConversationFragment.java @@ -6,6 +6,7 @@ import android.animation.AnimatorInflater; import android.annotation.SuppressLint; import android.app.Activity; import android.app.Fragment; +import android.app.FragmentManager; import android.app.PendingIntent; import android.content.Context; import android.content.DialogInterface; @@ -14,8 +15,6 @@ import android.content.IntentSender.SendIntentException; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.databinding.DataBindingUtil; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -26,7 +25,6 @@ import android.provider.MediaStore; import android.support.annotation.IdRes; import android.support.annotation.NonNull; import android.support.annotation.StringRes; -import android.support.media.ExifInterface; import android.support.v13.view.inputmethod.InputConnectionCompat; import android.support.v13.view.inputmethod.InputContentInfoCompat; import android.support.v7.app.AlertDialog; @@ -53,16 +51,12 @@ import android.widget.AbsListView.OnScrollListener; import android.widget.AdapterView; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.CheckBox; -import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView.OnEditorActionListener; import android.widget.Toast; import net.java.otr4j.session.SessionStatus; -import java.io.File; -import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; @@ -88,6 +82,7 @@ import de.pixart.messenger.entities.Conversational; import de.pixart.messenger.entities.DownloadableFile; import de.pixart.messenger.entities.Message; import de.pixart.messenger.entities.MucOptions; +import de.pixart.messenger.entities.MucOptions.User; import de.pixart.messenger.entities.Presence; import de.pixart.messenger.entities.ReadByMarker; import de.pixart.messenger.entities.Transferable; @@ -96,13 +91,15 @@ import de.pixart.messenger.http.HttpDownloadConnection; import de.pixart.messenger.persistance.FileBackend; import de.pixart.messenger.services.MessageArchiveService; import de.pixart.messenger.services.XmppConnectionService; +import de.pixart.messenger.ui.adapter.MediaPreviewAdapter; import de.pixart.messenger.ui.adapter.MessageAdapter; import de.pixart.messenger.ui.util.ActivityResult; -import de.pixart.messenger.ui.util.AttachmentTool; +import de.pixart.messenger.ui.util.Attachment; import de.pixart.messenger.ui.util.ConversationMenuConfigurator; import de.pixart.messenger.ui.util.DateSeparator; import de.pixart.messenger.ui.util.EditMessageActionModeCallback; import de.pixart.messenger.ui.util.ListViewUtils; +import de.pixart.messenger.ui.util.MucDetailsContextMenuHelper; import de.pixart.messenger.ui.util.PendingItem; import de.pixart.messenger.ui.util.PresenceSelector; import de.pixart.messenger.ui.util.ScrollState; @@ -110,7 +107,6 @@ import de.pixart.messenger.ui.util.SendButtonAction; import de.pixart.messenger.ui.util.SendButtonTool; import de.pixart.messenger.ui.util.ShareUtil; import de.pixart.messenger.ui.widget.EditMessage; -import de.pixart.messenger.utils.FileUtils; import de.pixart.messenger.utils.MenuDoubleTabUtil; import de.pixart.messenger.utils.MessageUtils; import de.pixart.messenger.utils.NickValidityChecker; @@ -135,55 +131,56 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke public static final int REQUEST_DECRYPT_PGP = 0x0202; public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207; public static final int REQUEST_TRUST_KEYS_TEXT = 0x0208; - public static final int REQUEST_TRUST_KEYS_MENU = 0x0209; + public static final int REQUEST_TRUST_KEYS_ATTACHMENTS = 0x0209; public static final int REQUEST_START_DOWNLOAD = 0x0210; public static final int REQUEST_ADD_EDITOR_CONTENT = 0x0211; public static final int ATTACHMENT_CHOICE = 0x0300; public static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301; - public static final int ATTACHMENT_CHOICE_TAKE_FROM_CAMERA = 0x0302; + public static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302; public static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303; public static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304; public static final int ATTACHMENT_CHOICE_LOCATION = 0x0305; public static final int ATTACHMENT_CHOICE_CHOOSE_VIDEO = 0x0306; + public static final int ATTACHMENT_CHOICE_RECORD_VIDEO = 0x0307; public static final int ATTACHMENT_CHOICE_INVALID = 0x0399; public static final String RECENTLY_USED_QUICK_ACTION = "recently_used_quick_action"; public static final String STATE_CONVERSATION_UUID = ConversationFragment.class.getName() + ".uuid"; - public static final String STATE_SCROLL_POSITION = ConversationFragment.class.getName() + ".scroll_position"; - public static final String STATE_PHOTO_URI = ConversationFragment.class.getName() + ".take_photo_uri"; - public static final String STATE_VIDEO_URI = ConversationFragment.class.getName() + ".take_video_uri"; + public static final String STATE_SCROLL_POSITION = ConversationFragment.class.getName() + ".scroll_position"; + public static final String STATE_PHOTO_URI = ConversationFragment.class.getName() + ".media_previews"; + public static final String STATE_MEDIA_PREVIEWS = ConversationFragment.class.getName() + ".take_photo_uri"; private static final String STATE_LAST_MESSAGE_UUID = "state_last_message_uuid"; private final List<Message> messageList = new ArrayList<>(); - final private List<Uri> mPendingImageUris = new ArrayList<>(); - private String lastMessageUuid = null; - public Uri mPendingEditorContent = null; private final PendingItem<ActivityResult> postponedActivityResult = new PendingItem<>(); - private final PendingItem<String> pendingConversationsUuid = new PendingItem<>(); + private final PendingItem<String> pendingConversationsUuid = new PendingItem<>(); + private final PendingItem<ArrayList<Attachment>> pendingMediaPreviews = new PendingItem<>(); private final PendingItem<Bundle> pendingExtras = new PendingItem<>(); private final PendingItem<Uri> pendingTakePhotoUri = new PendingItem<>(); private final PendingItem<Uri> pendingTakeVideoUri = new PendingItem<>(); private final PendingItem<ScrollState> pendingScrollState = new PendingItem<>(); private final PendingItem<String> pendingLastMessageUuid = new PendingItem<>(); private final PendingItem<Message> pendingMessage = new PendingItem<>(); + public Uri mPendingEditorContent = null; + public FragmentConversationBinding binding; protected MessageAdapter messageListAdapter; - private Conversation conversation; - public FragmentConversationBinding binding; protected Message lastHistoryMessage = null; SimpleDateFormat sdf = new SimpleDateFormat("EEEE, dd. MMM yyyy", Locale.getDefault()); + private String lastMessageUuid = null; + private Conversation conversation; private Toast messageLoaderToast; private ConversationsActivity activity; - private boolean reInitRequiredOnStart = true; - - private SimpleFingerGestures gesturesDetector = new SimpleFingerGestures(); - + private Menu mOptionsMenu; protected OnClickListener clickToVerify = new OnClickListener() { @Override public void onClick(View v) { activity.verifyOtrSessionDialog(conversation, v); } }; + private boolean reInitRequiredOnStart = true; + private MediaPreviewAdapter mediaPreviewAdapter; + private SimpleFingerGestures gesturesDetector = new SimpleFingerGestures(); private OnClickListener clickToMuc = new OnClickListener() { @Override @@ -462,7 +459,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke switch (action) { case CHOOSE_ATTACHMENT: choose_attachment(v); - case TAKE_FROM_CAMERA: + case TAKE_PHOTO: + case RECORD_VIDEO: case SEND_LOCATION: case RECORD_VOICE: case CHOOSE_PICTURE: @@ -490,32 +488,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } }; - - @SuppressLint("RestrictedApi") - private void choose_attachment(View v) { - SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity); - final boolean hideVoice = p.getBoolean("show_record_voice_btn", activity.getResources().getBoolean(R.bool.show_record_voice_btn)); - PopupMenu popup = new PopupMenu(activity, v); - popup.inflate(R.menu.choose_attachment); - Menu menu = popup.getMenu(); - ConversationMenuConfigurator.configureQuickShareAttachmentMenu(conversation, menu, hideVoice); - popup.setOnMenuItemClickListener(attachmentItem -> { - switch (attachmentItem.getItemId()) { - case R.id.attach_choose_picture: - case R.id.attach_take_picture: - case R.id.attach_choose_file: - case R.id.attach_record_voice: - case R.id.attach_location: - handleAttachmentSelection(attachmentItem); - default: - return false; - } - }); - MenuPopupHelper menuHelper = new MenuPopupHelper(getActivity(), (MenuBuilder) menu, v); - menuHelper.setForceShowIcon(true); - menuHelper.show(); - } - private View.OnLongClickListener mSendButtonLongListener = new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { @@ -589,6 +561,17 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } + public static ConversationFragment get(Activity activity) { + FragmentManager fragmentManager = activity.getFragmentManager(); + Fragment fragment = fragmentManager.findFragmentById(R.id.main_fragment); + if (fragment != null && fragment instanceof ConversationFragment) { + return (ConversationFragment) fragment; + } else { + fragment = fragmentManager.findFragmentById(R.id.secondary_fragment); + return fragment != null && fragment instanceof ConversationFragment ? (ConversationFragment) fragment : null; + } + } + public static Conversation getConversationReliable(Activity activity) { final Conversation conversation = getConversation(activity, R.id.secondary_fragment); if (conversation != null) { @@ -636,6 +619,32 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } + @SuppressLint("RestrictedApi") + private void choose_attachment(View v) { + SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity); + final boolean hideVoice = p.getBoolean("show_record_voice_btn", activity.getResources().getBoolean(R.bool.show_record_voice_btn)); + PopupMenu popup = new PopupMenu(activity, v); + popup.inflate(R.menu.choose_attachment); + Menu menu = popup.getMenu(); + ConversationMenuConfigurator.configureQuickShareAttachmentMenu(conversation, menu, hideVoice); + popup.setOnMenuItemClickListener(attachmentItem -> { + switch (attachmentItem.getItemId()) { + case R.id.attach_choose_picture: + case R.id.attach_take_picture: + case R.id.attach_record_video: + case R.id.attach_choose_file: + case R.id.attach_record_voice: + case R.id.attach_location: + handleAttachmentSelection(attachmentItem); + default: + return false; + } + }); + MenuPopupHelper menuHelper = new MenuPopupHelper(getActivity(), (MenuBuilder) menu, v); + menuHelper.setForceShowIcon(true); + menuHelper.show(); + } + private void toggleScrollDownButton() { toggleScrollDownButton(binding.messagesView); } @@ -836,94 +845,19 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } public void attachEditorContentToConversation(Uri uri) { - this.attachFileToConversation(conversation, uri, null); + mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uri, Attachment.Type.FILE)); + toggleInputMethod(); } - private void attachImageToConversation(Conversation conversation, Uri uri, boolean sendAsIs) { + private void attachImageToConversation(Conversation conversation, Uri uri) { if (conversation == null) { return; } - if (sendAsIs) { - sendImage(conversation, uri); - return; - } - final Conversation conversation_preview = conversation; - final Uri uri_preview = uri; - Bitmap bitmap = BitmapFactory.decodeFile(FileUtils.getPath(activity, uri)); - File file = null; - ExifInterface exif = null; - int orientation = 0; - try { - file = new File(FileUtils.getPath(activity, uri)); - } catch (Exception e) { - e.printStackTrace(); - } - if (file != null) { - try { - exif = new ExifInterface(file.toString()); - } catch (IOException e) { - e.printStackTrace(); - } - orientation = exif != null ? exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) : 0; - } - Log.d(Config.LOGTAG, "EXIF: " + orientation); - Bitmap rotated_image = null; - Log.d(Config.LOGTAG, "Rotate image"); - rotated_image = FileBackend.rotateBitmap(file, bitmap, orientation); - if (rotated_image != null) { - int scaleSize = 600; - int originalWidth = rotated_image.getWidth(); - int originalHeight = rotated_image.getHeight(); - int newWidth = -1; - int newHeight = -1; - float multFactor; - if (originalHeight > originalWidth) { - newHeight = scaleSize; - multFactor = (float) originalWidth / (float) originalHeight; - newWidth = (int) (newHeight * multFactor); - } else if (originalWidth > originalHeight) { - newWidth = scaleSize; - multFactor = (float) originalHeight / (float) originalWidth; - newHeight = (int) (newWidth * multFactor); - } else if (originalHeight == originalWidth) { - newHeight = scaleSize; - newWidth = scaleSize; - } - Log.d(Config.LOGTAG, "Scaling preview image from " + originalHeight + "px x " + originalWidth + "px to " + newHeight + "px x " + newWidth + "px"); - Bitmap preview = Bitmap.createScaledBitmap(rotated_image, newWidth, newHeight, false); - ImageView ImagePreview = new ImageView(activity); - LinearLayout.LayoutParams vp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT); - ImagePreview.setLayoutParams(vp); - ImagePreview.setMaxWidth(newWidth); - ImagePreview.setMaxHeight(newHeight); - //ImagePreview.setScaleType(ImageView.ScaleType.FIT_XY); - //ImagePreview.setAdjustViewBounds(true); - ImagePreview.setPadding(5, 5, 5, 5); - ImagePreview.setImageBitmap(preview); - getActivity().runOnUiThread(() -> { - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setView(ImagePreview); - builder.setTitle(R.string.send_image); - builder.setPositiveButton(R.string.ok, (dialog, which) -> sendImage(conversation_preview, uri_preview)); - builder.setOnCancelListener(dialog -> mPendingImageUris.clear()); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - builder.setOnDismissListener(dialog -> mPendingImageUris.clear()); - } - AlertDialog alertDialog = builder.create(); - alertDialog.show(); - }); - } else { - getActivity().runOnUiThread(() -> Toast.makeText(getActivity(), getText(R.string.error_file_not_found), Toast.LENGTH_LONG).show()); - } - } - - private void sendImage(Conversation conversation, Uri uri) { final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG); prepareFileToast.show(); activity.delegateUriPermissionsToService(uri); activity.xmppConnectionService.attachImageToConversation(conversation, uri, new UiCallback<Message>() { - @Override public void userInputRequried(PendingIntent pi, Message object) { hidePrepareFileToast(prepareFileToast); @@ -949,6 +883,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } private void sendMessage() { + if (mediaPreviewAdapter.hasAttachments()) { + commitAttachments(); + return; + } final String body = binding.textinput.getText().toString(); final Conversation conversation = this.conversation; if (body.length() == 0 || conversation == null) { @@ -989,10 +927,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } protected boolean trustKeysIfNeeded(int requestCode) { - return trustKeysIfNeeded(requestCode, ATTACHMENT_CHOICE_INVALID); - } - - protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) { AxolotlService axolotlService = conversation.getAccount().getAxolotlService(); final List<Jid> targets = axolotlService.getCryptoTargets(conversation); boolean hasUnaccepted = !conversation.getAcceptedCryptoTargets().containsAll(targets); @@ -1010,7 +944,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } intent.putExtra("contacts", contacts); intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString()); - intent.putExtra("choice", attachmentChoice); intent.putExtra("conversation", conversation.getUuid()); startActivityForResult(intent, requestCode); return true; @@ -1054,63 +987,43 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } private void handlePositiveActivityResult(int requestCode, final Intent data) { - final String type = data == null ? null : data.getType(); switch (requestCode) { case REQUEST_TRUST_KEYS_TEXT: final String body = binding.textinput.getText().toString(); Message message = new Message(conversation, body, conversation.getNextEncryption()); sendMessage(message); break; - case REQUEST_TRUST_KEYS_MENU: - int choice = data.getIntExtra("choice", ATTACHMENT_CHOICE_INVALID); - selectPresenceToAttachFile(choice); + case REQUEST_TRUST_KEYS_ATTACHMENTS: + commitAttachments(); break; case ATTACHMENT_CHOICE_CHOOSE_IMAGE: - final List<Uri> imageUris = AttachmentTool.extractUriFromIntent(data); - final int ImageUrisCount = imageUris.size(); - Log.d(Config.LOGTAG, "ConversationsActivity.onActivityResult() - attaching image - number of uris: " + ImageUrisCount); - if (ImageUrisCount == 1) { - Uri uri = imageUris.get(0); - Log.d(Config.LOGTAG, "ConversationsActivity.onActivityResult() - attaching image to conversations. CHOOSE_IMAGE"); - attachImageToConversation(conversation, uri, false); - } else { - for (Iterator<Uri> i = imageUris.iterator(); i.hasNext(); i.remove()) { - Log.d(Config.LOGTAG, "ConversationsActivity.onActivityResult() - attaching images to conversations. CHOOSE_IMAGES"); - attachImagesToConversation(conversation, i.next()); - } - } + final List<Attachment> imageUris = Attachment.extractAttachments(getActivity(), data, Attachment.Type.IMAGE); + mediaPreviewAdapter.addMediaPreviews(imageUris); + toggleInputMethod(); break; - case ATTACHMENT_CHOICE_TAKE_FROM_CAMERA: + case ATTACHMENT_CHOICE_TAKE_PHOTO: final Uri takePhotoUri = pendingTakePhotoUri.pop(); - final Uri takeVideoUri = pendingTakeVideoUri.pop(); if (takePhotoUri != null) { - attachPhotoToConversation(conversation, takePhotoUri); - } else if (takeVideoUri != null) { - attachFileToConversation(conversation, takeVideoUri, type); + mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), takePhotoUri, Attachment.Type.IMAGE)); + toggleInputMethod(); } else { - Log.d(Config.LOGTAG, "lost take uri. unable to to attach"); + Log.d(Config.LOGTAG, "lost take photo uri. unable to to attach"); } break; case ATTACHMENT_CHOICE_CHOOSE_FILE: + case ATTACHMENT_CHOICE_RECORD_VIDEO: case ATTACHMENT_CHOICE_RECORD_VOICE: - final List<Uri> fileUris = AttachmentTool.extractUriFromIntent(data); - final PresenceSelector.OnPresenceSelected callback = () -> { - for (Iterator<Uri> i = fileUris.iterator(); i.hasNext(); i.remove()) { - Log.d(Config.LOGTAG, "ConversationsActivity.onActivityResult() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE"); - attachFileToConversation(conversation, i.next(), type); - } - }; - if (conversation == null || conversation.getMode() == Conversation.MODE_MULTI || FileBackend.allFilesUnderSize(getActivity(), fileUris, getMaxHttpUploadSize(conversation))) { - callback.onPresenceSelected(); - } else { - activity.selectPresence(conversation, callback); - } + final Attachment.Type type = requestCode == ATTACHMENT_CHOICE_RECORD_VOICE ? Attachment.Type.RECORDING : Attachment.Type.FILE; + final List<Attachment> fileUris = Attachment.extractAttachments(getActivity(), data, type); + mediaPreviewAdapter.addMediaPreviews(fileUris); + toggleInputMethod(); break; case ATTACHMENT_CHOICE_LOCATION: double latitude = data.getDoubleExtra("latitude", 0); double longitude = data.getDoubleExtra("longitude", 0); Uri geo = Uri.parse("geo:" + String.valueOf(latitude) + "," + String.valueOf(longitude)); - attachLocationToConversation(conversation, geo); + mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), geo, Attachment.Type.LOCATION)); + toggleInputMethod(); break; case REQUEST_INVITE_TO_CONVERSATION: XmppActivity.ConferenceInvite invite = XmppActivity.ConferenceInvite.parse(data); @@ -1124,9 +1037,51 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } + private void commitAttachments() { + if (conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && trustKeysIfNeeded(REQUEST_TRUST_KEYS_ATTACHMENTS)) { + return; + } + final List<Attachment> attachments = mediaPreviewAdapter.getAttachments(); + final PresenceSelector.OnPresenceSelected callback = () -> { + for (Iterator<Attachment> i = attachments.iterator(); i.hasNext(); i.remove()) { + final Attachment attachment = i.next(); + if (attachment.getType() == Attachment.Type.LOCATION) { + attachLocationToConversation(conversation, attachment.getUri()); + } else if (attachment.getType() == Attachment.Type.IMAGE) { + Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE"); + attachImageToConversation(conversation, attachment.getUri()); + } else { + Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO"); + attachFileToConversation(conversation, attachment.getUri(), attachment.getMime()); + } + } + mediaPreviewAdapter.notifyDataSetChanged(); + toggleInputMethod(); + }; + if (conversation == null || conversation.getMode() == Conversation.MODE_MULTI || FileBackend.allFilesUnderSize(getActivity(), attachments, getMaxHttpUploadSize(conversation))) { + callback.onPresenceSelected(); + } else { + activity.selectPresence(conversation, callback); + } + } + + public void toggleInputMethod() { + boolean hasAttachments = mediaPreviewAdapter.hasAttachments(); + binding.textinput.setVisibility(hasAttachments ? View.GONE : View.VISIBLE); + binding.mediaPreview.setVisibility(hasAttachments ? View.VISIBLE : View.GONE); + if (mOptionsMenu != null) { + ConversationMenuConfigurator.configureAttachmentMenu(conversation, mOptionsMenu, activity.xmppConnectionService.getAttachmentChoicePreference(), hasAttachments); + } + updateSendButton(); + } + private void handleNegativeActivityResult(int requestCode) { switch (requestCode) { - //nothing to do for now + case ATTACHMENT_CHOICE_TAKE_PHOTO: + if (pendingTakePhotoUri.clear()) { + Log.d(Config.LOGTAG, "cleared pending photo uri after negative activity result"); + } + break; } } @@ -1170,27 +1125,38 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke @Override public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { + mOptionsMenu = menu; + boolean hasAttachments = mediaPreviewAdapter != null && mediaPreviewAdapter.hasAttachments(); menuInflater.inflate(R.menu.fragment_conversation, menu); final MenuItem menuInviteContact = menu.findItem(R.id.action_invite); final MenuItem menuNeedHelp = menu.findItem(R.id.action_create_issue); final MenuItem menuSearchUpdates = menu.findItem(R.id.action_check_updates); final MenuItem menuArchiveChat = menu.findItem(R.id.action_archive_chat); + final MenuItem menuGroupDetails = menu.findItem(R.id.action_group_details); + final MenuItem menuContactDetails = menu.findItem(R.id.action_contact_details); if (conversation != null) { if (conversation.getMode() == Conversation.MODE_MULTI) { menuInviteContact.setVisible(true); menuArchiveChat.setTitle(R.string.action_end_conversation_muc); + menuGroupDetails.setVisible(true); + menuContactDetails.setVisible(false); } else { menuInviteContact.setVisible(false); menuArchiveChat.setTitle(R.string.action_end_conversation); + menuGroupDetails.setVisible(false); + menuContactDetails.setVisible(true); } menuNeedHelp.setVisible(true); menuSearchUpdates.setVisible(false); - ConversationMenuConfigurator.configureAttachmentMenu(conversation, menu, Config.QUICK_SHARE_ATTACHMENT_CHOICE); + ConversationMenuConfigurator.configureAttachmentMenu(conversation, menu, activity.xmppConnectionService.getAttachmentChoicePreference(), hasAttachments); ConversationMenuConfigurator.configureEncryptionMenu(conversation, menu); } else { menuNeedHelp.setVisible(false); menuSearchUpdates.setVisible(true); + menuInviteContact.setVisible(false); + menuGroupDetails.setVisible(false); + menuContactDetails.setVisible(false); } super.onCreateOptionsMenu(menu, menuInflater); } @@ -1203,7 +1169,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke binding.textinput.addTextChangedListener(new StylingHelper.MessageEditorStyler(binding.textinput)); binding.textinput.setOnEditorActionListener(mEditorActionListener); binding.textinput.setRichContentListener(new String[]{"image/*"}, mEditorContentListener); - binding.textinput.setBackgroundResource(activity.isDarkTheme() ? R.drawable.message_bubble_sent_blue_dark : R.drawable.message_bubble_sent_blue); + binding.textinput.setBackgroundResource(messageInputBubble()); binding.textSendButton.setOnClickListener(this.mSendButtonListener); binding.textSendButton.setOnLongClickListener(this.mSendButtonLongListener); @@ -1212,6 +1178,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke binding.messagesView.setOnScrollListener(mOnScrollListener); binding.messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL); + mediaPreviewAdapter = new MediaPreviewAdapter(this); + binding.mediaPreview.setAdapter(mediaPreviewAdapter); + binding.mediaPreview.setBackgroundResource(messageInputBubble()); messageListAdapter = new MessageAdapter((XmppActivity) getActivity(), this.messageList); messageListAdapter.setOnContactPictureClicked(message -> { String fingerprint; @@ -1223,11 +1192,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke final boolean received = message.getStatus() <= Message.STATUS_RECEIVED; if (received) { if (message.getConversation() instanceof Conversation && message.getConversation().getMode() == Conversation.MODE_MULTI) { + Jid tcp = message.getTrueCounterpart(); Jid user = message.getCounterpart(); if (user != null && !user.isBareJid()) { final MucOptions mucOptions = ((Conversation) message.getConversation()).getMucOptions(); if (mucOptions.participating() || ((Conversation) message.getConversation()).getNextCounterpart() != null) { - if (!mucOptions.isUserInRoom(user)) { + if (!mucOptions.isUserInRoom(user) && mucOptions.findUserByRealJid(tcp == null ? null : tcp.asBareJid()) == null) { Toast.makeText(getActivity(), activity.getString(R.string.user_has_left_conference, user.getResource()), Toast.LENGTH_SHORT).show(); } highlightInConference(user.getResource()); @@ -1245,21 +1215,20 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } activity.switchToAccount(message.getConversation().getAccount(), fingerprint); }); - messageListAdapter.setOnContactPictureLongClicked(message -> { + messageListAdapter.setOnContactPictureLongClicked((v, message) -> { if (message.getStatus() <= Message.STATUS_RECEIVED) { if (message.getConversation().getMode() == Conversation.MODE_MULTI) { - final MucOptions mucOptions = conversation.getMucOptions(); - if (!mucOptions.allowPm()) { - Toast.makeText(getActivity(), R.string.private_messages_are_disabled, Toast.LENGTH_SHORT).show(); - return; - } - Jid user = message.getCounterpart(); - if (user != null && !user.isBareJid()) { - if (mucOptions.isUserInRoom(user)) { - privateMessageWith(user); - } else { - Toast.makeText(getActivity(), activity.getString(R.string.user_has_left_conference, user.getResource()), Toast.LENGTH_SHORT).show(); - } + Jid tcp = message.getTrueCounterpart(); + Jid cp = message.getCounterpart(); + if (cp != null && !cp.isBareJid()) { + User userByRealJid = tcp != null ? conversation.getMucOptions().findOrCreateUserByRealJid(tcp) : null; + final User user = userByRealJid != null ? userByRealJid : conversation.getMucOptions().findUserByFullJid(cp); + final PopupMenu popupMenu = new PopupMenu(getActivity(), v); + popupMenu.inflate(R.menu.muc_details_context); + final Menu menu = popupMenu.getMenu(); + MucDetailsContextMenuHelper.configureMucDetailsContextMenu(activity, menu, conversation, user); + popupMenu.setOnMenuItemClickListener(menuItem -> MucDetailsContextMenuHelper.onContextItemSelected(menuItem, user, conversation, activity)); + popupMenu.show(); } } } else { @@ -1407,7 +1376,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (m.isFileOrImage() && !deleted) { String path = m.getRelativeFilePath(); Log.d(Config.LOGTAG, "Path = " + path); - if (path == null || !path.startsWith("/") || path.contains(FileBackend.getConversationsDirectory("null", false))) { + if (path == null || !path.startsWith("/") || path.contains(FileBackend.getConversationsDirectory("null"))) { deleteFile.setVisible(true); deleteFile.setTitle(activity.getString(R.string.delete_x_file, UIHelper.getFileDescriptionString(activity, m))); } @@ -1481,6 +1450,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke break; case R.id.attach_choose_picture: case R.id.attach_take_picture: + case R.id.attach_record_video: case R.id.attach_choose_file: case R.id.attach_record_voice: case R.id.attach_location: @@ -1509,6 +1479,15 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke case R.id.action_clear_history: clearHistoryDialog(conversation); break; + case R.id.action_group_details: + Intent intent = new Intent(activity, ConferenceDetailsActivity.class); + intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); + intent.putExtra("uuid", conversation.getUuid()); + startActivity(intent); + break; + case R.id.action_contact_details: + activity.switchToContactDetails(conversation.getContact()); + break; case R.id.action_block: case R.id.action_unblock: final Activity activity = getActivity(); @@ -1528,7 +1507,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE); break; case R.id.attach_take_picture: - attachFile(ATTACHMENT_CHOICE_TAKE_FROM_CAMERA); + attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO); + break; + case R.id.attach_record_video: + attachFile(ATTACHMENT_CHOICE_RECORD_VIDEO); break; case R.id.attach_choose_file: attachFile(ATTACHMENT_CHOICE_CHOOSE_FILE); @@ -1588,7 +1570,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (!hasPermissions(attachmentChoice, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO)) { return; } - } else if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_FROM_CAMERA) { + } else if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO || attachmentChoice == ATTACHMENT_CHOICE_RECORD_VIDEO) { if (!hasPermissions(attachmentChoice, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA)) { return; } @@ -1652,9 +1634,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke activity.showInstallPgpDialog(); } } else { - if (encryption != Message.ENCRYPTION_AXOLOTL || !trustKeysIfNeeded(REQUEST_TRUST_KEYS_MENU, attachmentChoice)) { - selectPresenceToAttachFile(attachmentChoice); - } + selectPresenceToAttachFile(attachmentChoice); } } @@ -1774,7 +1754,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke final int encryption = conversation.getNextEncryption(); final Account account = conversation.getAccount(); final PresenceSelector.OnPresenceSelected callback = () -> { - final Intent intent = new Intent(); + Intent intent = new Intent(); boolean chooser = false; switch (attachmentChoice) { case ATTACHMENT_CHOICE_CHOOSE_IMAGE: @@ -1791,46 +1771,29 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setAction(Intent.ACTION_GET_CONTENT); break; - case ATTACHMENT_CHOICE_TAKE_FROM_CAMERA: - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(getString(R.string.attach_take_from_camera)); - builder.setNegativeButton(getString(R.string.action_take_photo), - (dialog, which) -> { - final Uri uri = activity.xmppConnectionService.getFileBackend().getTakePhotoUri(); - pendingTakePhotoUri.push(uri); - intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); - intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); - startActivityForResult(intent, attachmentChoice); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - }); - builder.setPositiveButton(getString(R.string.action_take_video), - (dialog, which) -> { - final Uri uri = activity.xmppConnectionService.getFileBackend().getTakeVideoUri(); - pendingTakeVideoUri.push(uri); - intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); - intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.setAction(MediaStore.ACTION_VIDEO_CAPTURE); - startActivityForResult(intent, attachmentChoice); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - }); - builder.create().show(); + case ATTACHMENT_CHOICE_RECORD_VIDEO: + intent.setAction(MediaStore.ACTION_VIDEO_CAPTURE); + break; + case ATTACHMENT_CHOICE_TAKE_PHOTO: + final Uri uri = activity.xmppConnectionService.getFileBackend().getTakePhotoUri(); + pendingTakePhotoUri.push(uri); + intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); break; case ATTACHMENT_CHOICE_CHOOSE_FILE: chooser = true; intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setAction(Intent.ACTION_GET_CONTENT); break; case ATTACHMENT_CHOICE_RECORD_VOICE: - startActivityForResult(new Intent(getActivity(), RecordingActivity.class), attachmentChoice); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); + intent = new Intent(getActivity(), RecordingActivity.class); break; case ATTACHMENT_CHOICE_LOCATION: - startActivityForResult(new Intent(getActivity(), ShareLocationActivity.class), attachmentChoice); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); + intent = new Intent(getActivity(), ShareLocationActivity.class); break; } if (intent.resolveActivity(getActivity().getPackageManager()) != null) { @@ -1839,13 +1802,15 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke startActivityForResult( Intent.createChooser(intent, getString(R.string.perform_action_with)), attachmentChoice); + activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); } else { startActivityForResult(intent, attachmentChoice); + activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); } } }; if (account.httpUploadAvailable() || attachmentChoice == ATTACHMENT_CHOICE_LOCATION) { - if ((account.httpUploadAvailable() || attachmentChoice == ATTACHMENT_CHOICE_LOCATION) && encryption != Message.ENCRYPTION_OTR) { + if ((account.httpUploadAvailable() || attachmentChoice == ATTACHMENT_CHOICE_LOCATION) && encryption != Message.ENCRYPTION_OTR) { conversation.setNextCounterpart(null); callback.onPresenceSelected(); } else { @@ -1997,7 +1962,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke conversation.getAccount().getPgpDecryptionService().decrypt(message, false); } - private void privateMessageWith(final Jid counterpart) { + public void privateMessageWith(final Jid counterpart) { if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { activity.xmppConnectionService.sendChatState(conversation); } @@ -2047,23 +2012,32 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } @Override + public void startActivityForResult(Intent intent, int requestCode) { + final Activity activity = getActivity(); + if (activity instanceof ConversationsActivity) { + ((ConversationsActivity) activity).clearPendingViewIntent(); + } + super.startActivityForResult(intent, requestCode); + } + + @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (conversation != null) { outState.putString(STATE_CONVERSATION_UUID, conversation.getUuid()); outState.putString(STATE_LAST_MESSAGE_UUID, lastMessageUuid); - final Uri PhotoUri = pendingTakePhotoUri.peek(); - final Uri VideoUri = pendingTakeVideoUri.peek(); - if (PhotoUri != null) { - outState.putString(STATE_PHOTO_URI, PhotoUri.toString()); - } - if (VideoUri != null) { - outState.putString(STATE_VIDEO_URI, VideoUri.toString()); + final Uri uri = pendingTakePhotoUri.peek(); + if (uri != null) { + outState.putString(STATE_PHOTO_URI, uri.toString()); } final ScrollState scrollState = getScrollPosition(); if (scrollState != null) { outState.putParcelable(STATE_SCROLL_POSITION, scrollState); } + final ArrayList<Attachment> attachments = mediaPreviewAdapter.getAttachments(); + if (attachments.size() > 0) { + outState.putParcelableArrayList(STATE_MEDIA_PREVIEWS, attachments); + } } } @@ -2074,18 +2048,18 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke return; } String uuid = savedInstanceState.getString(STATE_CONVERSATION_UUID); + ArrayList<Attachment> attachments = savedInstanceState.getParcelableArrayList(STATE_MEDIA_PREVIEWS); pendingLastMessageUuid.push(savedInstanceState.getString(STATE_LAST_MESSAGE_UUID, null)); if (uuid != null) { QuickLoader.set(uuid); this.pendingConversationsUuid.push(uuid); + if (attachments != null && attachments.size() > 0) { + this.pendingMediaPreviews.push(attachments); + } String takePhotoUri = savedInstanceState.getString(STATE_PHOTO_URI); if (takePhotoUri != null) { pendingTakePhotoUri.push(Uri.parse(takePhotoUri)); } - String takeVideoUri = savedInstanceState.getString(STATE_VIDEO_URI); - if (takeVideoUri != null) { - pendingTakeVideoUri.push(Uri.parse(takeVideoUri)); - } pendingScrollState.push(savedInstanceState.getParcelable(STATE_SCROLL_POSITION)); } } @@ -2112,16 +2086,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke public void onStop() { super.onStop(); final Activity activity = getActivity(); + messageListAdapter.unregisterListenerInAudioPlayer(); if (activity == null || !activity.isChangingConfigurations()) { hideSoftKeyboard(activity); messageListAdapter.stopAudioPlayer(); } if (this.conversation != null) { final String msg = this.binding.textinput.getText().toString(); - final boolean participating = conversation.getMode() == Conversational.MODE_SINGLE || conversation.getMucOptions().participating(); - if (this.conversation.getStatus() != Conversation.STATUS_ARCHIVED && participating && this.conversation.setNextMessage(msg)) { - this.activity.xmppConnectionService.updateConversation(this.conversation); - } + storeNextMessage(msg); updateChatState(this.conversation, msg); this.activity.xmppConnectionService.getNotificationService().setOpenConversation(null); } @@ -2143,17 +2115,17 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } Log.d(Config.LOGTAG, "ConversationFragment.saveMessageDraftStopAudioPlayer()"); final String msg = this.binding.textinput.getText().toString(); - final boolean participating = previousConversation.getMode() == Conversational.MODE_SINGLE || previousConversation.getMucOptions().participating(); - if (participating && previousConversation.setNextMessage(msg)) { - activity.xmppConnectionService.updateConversation(previousConversation); - } + storeNextMessage(msg); updateChatState(this.conversation, msg); messageListAdapter.stopAudioPlayer(); + mediaPreviewAdapter.clearPreviews(); + toggleInputMethod(); } public void reInit(Conversation conversation, Bundle extras) { QuickLoader.set(conversation.getUuid()); this.saveMessageDraftStopAudioPlayer(); + this.clearPending(); if (this.reInit(conversation, extras != null)) { if (extras != null) { processExtras(extras); @@ -2310,6 +2282,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke final String nick = extras.getString(ConversationsActivity.EXTRA_NICK); final boolean asQuote = extras.getBoolean(ConversationsActivity.EXTRA_AS_QUOTE); final boolean pm = extras.getBoolean(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, false); + final List<Uri> uris = extractUris(extras); + if (uris != null && uris.size() > 0) { + mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uris)); + toggleInputMethod(); + return; + } if (nick != null) { if (pm) { Jid jid = conversation.getJid(); @@ -2338,6 +2316,19 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } + private List<Uri> extractUris(Bundle extras) { + final List<Uri> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM); + if (uris != null) { + return uris; + } + final Uri uri = extras.getParcelable(Intent.EXTRA_STREAM); + if (uri != null) { + return Collections.singletonList(uri); + } else { + return null; + } + } + private boolean showBlockSubmenu(View view) { final Jid jid = conversation.getJid(); if (jid.getLocal() == null) { @@ -2411,7 +2402,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke showSnackbar(R.string.conference_kicked, R.string.join, joinMuc); break; case UNKNOWN: - showSnackbar(R.string.conference_unknown_error, R.string.leave, joinMuc); + showSnackbar(R.string.conference_unknown_error, R.string.join, joinMuc); break; case INVALID_NICK: showSnackbar(R.string.invalid_muc_nick, R.string.edit, clickToMuc); @@ -2518,10 +2509,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke this.binding.textinput.append(conversation.getDraftMessage()); conversation.setDraftMessage(null); } - final boolean participating = conversation.getMode() == Conversational.MODE_SINGLE || conversation.getMucOptions().participating(); - if (participating && conversation.setNextMessage(this.binding.textinput.getText().toString())) { - activity.xmppConnectionService.databaseBackend.updateConversation(conversation); - } + storeNextMessage(); updateChatMsgHint(); SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity); final boolean prefScrollToBottom = p.getBoolean("scroll_to_bottom", activity.getResources().getBoolean(R.bool.scroll_to_bottom)); @@ -2533,6 +2521,19 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } + private boolean storeNextMessage() { + return storeNextMessage(this.binding.textinput.getText().toString()); + } + + private boolean storeNextMessage(String msg) { + final boolean participating = conversation.getMode() == Conversational.MODE_SINGLE || conversation.getMucOptions().participating(); + if (this.conversation.getStatus() != Conversation.STATUS_ARCHIVED && participating && this.conversation.setNextMessage(msg)) { + this.activity.xmppConnectionService.updateConversation(this.conversation); + return true; + } + return false; + } + public void doneSendingPgpMessage() { mSendingPgpMessage.set(false); } @@ -2552,11 +2553,17 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } public void updateSendButton() { + boolean hasAttachments = mediaPreviewAdapter != null && mediaPreviewAdapter.hasAttachments(); boolean useSendButtonToIndicateStatus = PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("send_button_status", getResources().getBoolean(R.bool.send_button_status)); final Conversation c = this.conversation; final Presence.Status status; final String text = this.binding.textinput == null ? "" : this.binding.textinput.getText().toString(); - final SendButtonAction action = SendButtonTool.getAction(getActivity(), c, text); + final SendButtonAction action; + if (hasAttachments) { + action = SendButtonAction.TEXT; + } else { + action = SendButtonTool.getAction(getActivity(), c, text); + } if (useSendButtonToIndicateStatus && c.getAccount().getStatus() == Account.State.ONLINE) { if (activity.xmppConnectionService != null && activity.xmppConnectionService.getMessageArchiveService().isCatchingUp(c)) { status = Presence.Status.OFFLINE; @@ -2889,6 +2896,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { service.sendChatState(conversation); } + if (storeNextMessage()) { + activity.onConversationsListItemUpdated(); + } updateSendButton(); } @@ -3048,23 +3058,33 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke reInit(conversation); ScrollState scrollState = pendingScrollState.pop(); String lastMessageUuid = pendingLastMessageUuid.pop(); + List<Attachment> attachments = pendingMediaPreviews.pop(); if (scrollState != null) { setScrollPosition(scrollState, lastMessageUuid); } + if (attachments != null && attachments.size() > 0) { + Log.d(Config.LOGTAG, "had attachments on restore"); + mediaPreviewAdapter.addMediaPreviews(attachments); + toggleInputMethod(); + } return true; } private void clearPending() { - if (postponedActivityResult.pop() != null) { + if (postponedActivityResult.clear()) { Log.e(Config.LOGTAG, "cleared pending intent with unhandled result left"); } - pendingScrollState.pop(); - if (pendingTakePhotoUri.pop() != null) { + if (pendingScrollState.clear()) { + Log.e(Config.LOGTAG,"cleared scroll state"); + } + if (pendingTakePhotoUri.clear()) { Log.e(Config.LOGTAG, "cleared pending photo uri"); } } - + private int messageInputBubble() { + return activity.isDarkTheme() ? R.drawable.message_bubble_sent_blue_dark : R.drawable.message_bubble_sent_blue; + } public Conversation getConversation() { return conversation; diff --git a/src/main/java/de/pixart/messenger/ui/ConversationsActivity.java b/src/main/java/de/pixart/messenger/ui/ConversationsActivity.java index 30c54990f..8d8e5386a 100644 --- a/src/main/java/de/pixart/messenger/ui/ConversationsActivity.java +++ b/src/main/java/de/pixart/messenger/ui/ConversationsActivity.java @@ -63,6 +63,7 @@ import net.java.otr4j.session.SessionStatus; import org.openintents.openpgp.util.OpenPgpApi; +import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -92,13 +93,14 @@ import de.pixart.messenger.utils.UIHelper; import de.pixart.messenger.utils.XmppUri; import de.pixart.messenger.xmpp.OnUpdateBlocklist; import de.pixart.messenger.xmpp.chatstate.ChatState; +import rocks.xmpp.addr.Jid; import static de.pixart.messenger.services.XmppConnectionService.FDroid; import static de.pixart.messenger.services.XmppConnectionService.PlayStore; import static de.pixart.messenger.ui.ConversationFragment.REQUEST_DECRYPT_PGP; import static de.pixart.messenger.ui.SettingsActivity.USE_BUNDLED_EMOJIS; -public class ConversationsActivity extends XmppActivity implements OnConversationSelected, OnConversationArchived, OnConversationsListItemUpdated, OnConversationRead, XmppConnectionService.OnAccountUpdate, XmppConnectionService.OnConversationUpdate, XmppConnectionService.OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast { +public class ConversationsActivity extends XmppActivity implements OnConversationSelected, OnConversationArchived, OnConversationsListItemUpdated, OnConversationRead, XmppConnectionService.OnAccountUpdate, XmppConnectionService.OnConversationUpdate, XmppConnectionService.OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged { public static final String ACTION_VIEW_CONVERSATION = "de.pixart.messenger.VIEW"; public static final String EXTRA_CONVERSATION = "conversationUuid"; @@ -110,6 +112,11 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio public static final String ACTION_DESTROY_MUC = "de.pixart.messenger.DESTROY_MUC"; public static final int REQUEST_OPEN_MESSAGE = 0x9876; public static final int REQUEST_PLAY_PAUSE = 0x5432; + private static List<String> VIEW_AND_SHARE_ACTIONS = Arrays.asList( + ACTION_VIEW_CONVERSATION, + Intent.ACTION_SEND, + Intent.ACTION_SEND_MULTIPLE + ); private boolean showLastSeen = false; @@ -125,8 +132,9 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio private boolean mActivityPaused = true; private AtomicBoolean mRedirectInProcess = new AtomicBoolean(false); - private static boolean isViewIntent(Intent i) { - return i != null && ACTION_VIEW_CONVERSATION.equals(i.getAction()) && i.hasExtra(EXTRA_CONVERSATION); + private static boolean isViewOrShareIntent(Intent i) { + Log.d(Config.LOGTAG, "action: " + (i == null ? null : i.getAction())); + return i != null && VIEW_AND_SHARE_ACTIONS.contains(i.getAction()) && i.hasExtra(EXTRA_CONVERSATION); } private static Intent createLauncherIntent(Context context) { @@ -450,7 +458,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio } else { intent = savedInstanceState.getParcelable("intent"); } - if (isViewIntent(intent)) { + if (isViewOrShareIntent(intent)) { pendingViewIntent.push(intent); setIntent(createLauncherIntent(this)); } @@ -482,6 +490,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio @Override public void onConversationSelected(Conversation conversation) { + clearPendingViewIntent(); if (ConversationFragment.getConversation(this) == conversation) { Log.d(Config.LOGTAG, "ignore onConversationSelected() because conversation is already open"); return; @@ -489,6 +498,34 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio openConversation(conversation, null); } + public void clearPendingViewIntent() { + if (pendingViewIntent.clear()) { + Log.e(Config.LOGTAG, "cleared pending view intent"); + } + } + + private void displayToast(final String msg) { + runOnUiThread(() -> Toast.makeText(ConversationsActivity.this, msg, Toast.LENGTH_SHORT).show()); + } + + @Override + public void onAffiliationChangedSuccessful(Jid jid) { + } + + @Override + public void onAffiliationChangeFailed(Jid jid, int resId) { + displayToast(getString(resId, jid.asBareJid().toString())); + } + + @Override + public void onRoleChangedSuccessful(String nick) { + } + + @Override + public void onRoleChangeFailed(String nick, int resId) { + displayToast(getString(resId, nick)); + } + private void openConversation(Conversation conversation, Bundle extras) { ConversationFragment conversationFragment = (ConversationFragment) getFragmentManager().findFragmentById(R.id.secondary_fragment); final boolean mainNeedsRefresh; @@ -548,7 +585,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio if (fm.getBackStackEntryCount() > 0) { try { fm.popBackStack(); - } catch (IllegalArgumentException e) { + } catch (IllegalStateException e) { Log.w(Config.LOGTAG, "Unable to pop back stack after pressing home button"); } return true; @@ -559,7 +596,9 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio return true; case R.id.action_check_updates: if (xmppConnectionService.hasInternetConnection()) { - if (!installFromUnknownSourceAllowed() && !xmppConnectionService.installedFrom().equals(PlayStore)) { + if (!installFromUnknownSourceAllowed() && xmppConnectionService.installedFrom() == null) { + openInstallFromUnknownSourcesDialogIfNeeded(); + } else if (!installFromUnknownSourceAllowed() && (xmppConnectionService.installedFrom() != null && !xmppConnectionService.installedFrom().equalsIgnoreCase(PlayStore))) { openInstallFromUnknownSourcesDialogIfNeeded(); } else { UpdateService task = new UpdateService(this, xmppConnectionService.installedFrom(), xmppConnectionService); @@ -601,7 +640,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio @Override protected void onNewIntent(final Intent intent) { - if (isViewIntent(intent)) { + if (isViewOrShareIntent(intent)) { if (xmppConnectionService != null) { processViewIntent(intent); } else { @@ -950,7 +989,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio Log.d(Config.LOGTAG, "AppUpdater: CurrentTime: " + lastUpdateTime); if (!installFromUnknownSourceAllowed() && Store == null) { openInstallFromUnknownSourcesDialogIfNeeded(); - } else if (Store != null && (Store.equals(PlayStore) || Store.equals(FDroid))) { + } else if (Store != null && (Store.equalsIgnoreCase(PlayStore) || Store.equalsIgnoreCase(FDroid))) { Log.d(Config.LOGTAG, "AppUpdater aborted because app store is " + Store); } else { UpdateService task = new UpdateService(this, Store, xmppConnectionService); diff --git a/src/main/java/de/pixart/messenger/ui/EditAccountActivity.java b/src/main/java/de/pixart/messenger/ui/EditAccountActivity.java index e33de0eb4..be4222e78 100644 --- a/src/main/java/de/pixart/messenger/ui/EditAccountActivity.java +++ b/src/main/java/de/pixart/messenger/ui/EditAccountActivity.java @@ -22,6 +22,7 @@ import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; import android.support.v7.widget.Toolbar; import android.text.Editable; +import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; import android.view.Menu; @@ -327,6 +328,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat @Override public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { + updatePortLayout(); updateSaveButton(); updateInfoButtons(); } @@ -432,7 +434,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat protected void updateInfoButtons() { if (this.binding.accountRegisterNew.isChecked() && this.binding.accountJid.getText().length() > 0 && !this.binding.accountJid.getText().toString().contains("@")) { - if (!mUsernameMode && Jid.of(this.binding.accountJid.getText()).getDomain().toLowerCase().equals("pix-art.de")) { + final String jid = this.binding.accountJid.getText().toString(); + if (!mUsernameMode && Jid.of(jid).getDomain().toLowerCase().equals("pix-art.de")) { this.binding.showPrivacyPolicy.setVisibility(View.VISIBLE); this.binding.showTermsOfUse.setVisibility(View.VISIBLE); } @@ -442,6 +445,11 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } } + private void updatePortLayout() { + String hostname = this.binding.hostname.getText().toString(); + this.binding.portLayout.setEnabled(!TextUtils.isEmpty(hostname)); + } + protected void updateSaveButton() { boolean accountInfoEdited = accountInfoEdited(); @@ -551,6 +559,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat final OnCheckedChangeListener OnCheckedShowConfirmPassword = new OnCheckedChangeListener() { @Override public void onCheckedChanged(final CompoundButton buttonView, final boolean isChecked) { + updatePortLayout(); updateSaveButton(); updateInfoButtons(); } @@ -734,6 +743,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat this.binding.cancelButton.setEnabled(false); } if (mUsernameMode) { + this.binding.accountJidLayout.setHint(getString(R.string.username_hint)); this.binding.accountJid.setHint(R.string.username_hint); } else { final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this, diff --git a/src/main/java/de/pixart/messenger/ui/MediaBrowserActivity.java b/src/main/java/de/pixart/messenger/ui/MediaBrowserActivity.java new file mode 100644 index 000000000..4b32b42ab --- /dev/null +++ b/src/main/java/de/pixart/messenger/ui/MediaBrowserActivity.java @@ -0,0 +1,77 @@ +package de.pixart.messenger.ui; + +import android.content.Context; +import android.content.Intent; +import android.databinding.DataBindingUtil; +import android.os.Bundle; +import android.support.v7.widget.Toolbar; + +import java.util.List; + +import de.pixart.messenger.R; +import de.pixart.messenger.databinding.ActivityMediaBrowserBinding; +import de.pixart.messenger.entities.Account; +import de.pixart.messenger.entities.Contact; +import de.pixart.messenger.entities.Conversation; +import de.pixart.messenger.ui.adapter.MediaAdapter; +import de.pixart.messenger.ui.interfaces.OnMediaLoaded; +import de.pixart.messenger.ui.util.Attachment; +import de.pixart.messenger.ui.util.GridManager; +import rocks.xmpp.addr.Jid; + + +public class MediaBrowserActivity extends XmppActivity implements OnMediaLoaded { + + private ActivityMediaBrowserBinding binding; + + private MediaAdapter mMediaAdapter; + + public static void launch(Context context, Contact contact) { + launch(context, contact.getAccount(), contact.getJid().asBareJid().toEscapedString()); + } + + public static void launch(Context context, Conversation conversation) { + launch(context, conversation.getAccount(), conversation.getJid().asBareJid().toEscapedString()); + } + + private static void launch(Context context, Account account, String jid) { + final Intent intent = new Intent(context, MediaBrowserActivity.class); + intent.putExtra("account", account.getUuid()); + intent.putExtra("jid", jid); + context.startActivity(intent); + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + this.binding = DataBindingUtil.setContentView(this, R.layout.activity_media_browser); + setSupportActionBar((Toolbar) binding.toolbar); + configureActionBar(getSupportActionBar()); + mMediaAdapter = new MediaAdapter(this, R.dimen.media_size); + this.binding.media.setAdapter(mMediaAdapter); + GridManager.setupLayoutManager(this, this.binding.media, R.dimen.browser_media_size); + + } + + @Override + protected void refreshUiReal() { + + } + + @Override + void onBackendConnected() { + Intent intent = getIntent(); + String account = intent == null ? null : intent.getStringExtra("account"); + String jid = intent == null ? null : intent.getStringExtra("jid"); + if (account != null && jid != null) { + xmppConnectionService.getAttachments(account, Jid.of(jid), 0, this); + } + } + + @Override + public void onMediaLoaded(List<Attachment> attachments) { + runOnUiThread(() -> { + mMediaAdapter.setAttachments(attachments); + }); + } +}
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/ui/RecordingActivity.java b/src/main/java/de/pixart/messenger/ui/RecordingActivity.java index 15d193886..31dc1681a 100644 --- a/src/main/java/de/pixart/messenger/ui/RecordingActivity.java +++ b/src/main/java/de/pixart/messenger/ui/RecordingActivity.java @@ -65,7 +65,8 @@ public class RecordingActivity extends Activity implements View.OnClickListener super.onStart(); if (!startRecording()) { this.binding.shareButton.setEnabled(false); - Toast.makeText(this, R.string.unable_to_start_recording, Toast.LENGTH_SHORT).show(); + this.binding.timer.setTextAppearance(this, R.style.TextAppearance_Conversations_Title); + this.binding.timer.setText(R.string.unable_to_start_recording); } } @@ -108,12 +109,15 @@ public class RecordingActivity extends Activity implements View.OnClickListener mShouldFinishAfterWrite = saveFile; try { mRecorder.stop(); - } catch (RuntimeException stopException) { - //handle cleanup here + mRecorder.release(); + } catch (Exception e) { + if (saveFile) { + Toast.makeText(this,R.string.unable_to_save_recording, Toast.LENGTH_SHORT).show(); + } + } finally { + mRecorder = null; + mStartTime = 0; } - mRecorder.release(); - mRecorder = null; - mStartTime = 0; if (!saveFile && mOutputFile != null) { if (mOutputFile.delete()) { Log.d(Config.LOGTAG,"deleted canceled recording"); @@ -123,7 +127,7 @@ public class RecordingActivity extends Activity implements View.OnClickListener private static File generateOutputFilename(Context context) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); - return new File(FileBackend.getConversationsDirectory("Audios", true) + "/" + return new File(FileBackend.getConversationsDirectory("Audios") + dateFormat.format(new Date()) + ".m4a"); } diff --git a/src/main/java/de/pixart/messenger/ui/SearchActivity.java b/src/main/java/de/pixart/messenger/ui/SearchActivity.java index cc3172670..e9ba1d426 100644 --- a/src/main/java/de/pixart/messenger/ui/SearchActivity.java +++ b/src/main/java/de/pixart/messenger/ui/SearchActivity.java @@ -56,12 +56,11 @@ import de.pixart.messenger.services.MessageSearchTask; import de.pixart.messenger.ui.adapter.MessageAdapter; import de.pixart.messenger.ui.interfaces.OnSearchResultsAvailable; import de.pixart.messenger.ui.util.ChangeWatcher; -import de.pixart.messenger.ui.util.Color; import de.pixart.messenger.ui.util.DateSeparator; -import de.pixart.messenger.ui.util.Drawable; import de.pixart.messenger.ui.util.ListViewUtils; import de.pixart.messenger.ui.util.PendingItem; import de.pixart.messenger.ui.util.ShareUtil; +import de.pixart.messenger.ui.util.StyledAttributes; import de.pixart.messenger.utils.FtsUtils; import de.pixart.messenger.utils.MessageUtils; @@ -213,12 +212,12 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc private void changeBackground(boolean hasSearch, boolean hasResults) { if (hasSearch) { if (hasResults) { - binding.searchResults.setBackgroundColor(Color.get(this, R.attr.color_background_secondary)); + binding.searchResults.setBackgroundColor(StyledAttributes.getColor(this, R.attr.color_background_secondary)); } else { - binding.searchResults.setBackground(Drawable.get(this, R.attr.activity_background_no_results)); + binding.searchResults.setBackground(StyledAttributes.getDrawable(this, R.attr.activity_background_no_results)); } } else { - binding.searchResults.setBackground(Drawable.get(this, R.attr.activity_background_search)); + binding.searchResults.setBackground(StyledAttributes.getDrawable(this, R.attr.activity_background_search)); } } diff --git a/src/main/java/de/pixart/messenger/ui/SettingsActivity.java b/src/main/java/de/pixart/messenger/ui/SettingsActivity.java index 9ee11bd0b..1c0992cf1 100644 --- a/src/main/java/de/pixart/messenger/ui/SettingsActivity.java +++ b/src/main/java/de/pixart/messenger/ui/SettingsActivity.java @@ -17,6 +17,7 @@ import android.preference.PreferenceCategory; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; import android.util.Log; import android.view.LayoutInflater; @@ -37,7 +38,7 @@ import de.pixart.messenger.crypto.OmemoSetting; import de.pixart.messenger.entities.Account; import de.pixart.messenger.services.ExportLogsService; import de.pixart.messenger.services.MemorizingTrustManager; -import de.pixart.messenger.ui.util.Color; +import de.pixart.messenger.ui.util.StyledAttributes; import de.pixart.messenger.utils.TimeframeUtils; import rocks.xmpp.addr.Jid; @@ -58,12 +59,16 @@ public class SettingsActivity extends XmppActivity implements public static final String SHOW_FOREGROUND_SERVICE = "show_foreground_service"; public static final String USE_BUNDLED_EMOJIS = "use_bundled_emoji"; public static final String USE_MULTI_ACCOUNTS = "use_multi_accounts"; + public static final String QUICK_SHARE_ATTACHMENT_CHOICE = "quick_share_attachment_choice"; + public static final String NUMBER_OF_ACCOUNTS = "number_of_accounts"; public static final int REQUEST_WRITE_LOGS = 0xbf8701; Preference multiAccountPreference; Preference BundledEmojiPreference; + Preference QuickShareAttachmentChoicePreference; boolean isMultiAccountChecked = false; boolean isBundledEmojiChecked; + boolean isQuickShareAttachmentChoiceChecked = false; private SettingsFragment mSettingsFragment; @Override @@ -71,6 +76,7 @@ public class SettingsActivity extends XmppActivity implements super.onCreate(savedInstanceState); this.mTheme = findTheme(); setTheme(this.mTheme); + updateTheme(); setContentView(R.layout.activity_settings); FragmentManager fm = getFragmentManager(); mSettingsFragment = (SettingsFragment) fm.findFragmentById(R.id.settings_content); @@ -79,7 +85,7 @@ public class SettingsActivity extends XmppActivity implements fm.beginTransaction().replace(R.id.settings_content, mSettingsFragment).commit(); } mSettingsFragment.setActivityIntent(getIntent()); - getWindow().getDecorView().setBackgroundColor(Color.get(this, R.attr.color_background_secondary)); + getWindow().getDecorView().setBackgroundColor(StyledAttributes.getColor(this, R.attr.color_background_secondary)); setSupportActionBar(findViewById(R.id.toolbar)); configureActionBar(getSupportActionBar()); } @@ -92,12 +98,11 @@ public class SettingsActivity extends XmppActivity implements @Override public void onStart() { super.onStart(); - updateTheme(); - PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this); multiAccountPreference = mSettingsFragment.findPreference("enable_multi_accounts"); if (multiAccountPreference != null) { isMultiAccountChecked = ((CheckBoxPreference) multiAccountPreference).isChecked(); + handleMultiAccountChanges(); } BundledEmojiPreference = mSettingsFragment.findPreference("use_bundled_emoji"); @@ -105,6 +110,15 @@ public class SettingsActivity extends XmppActivity implements isBundledEmojiChecked = ((CheckBoxPreference) BundledEmojiPreference).isChecked(); } + QuickShareAttachmentChoicePreference = mSettingsFragment.findPreference("quick_share_attachment_choice"); + if (QuickShareAttachmentChoicePreference != null) { + QuickShareAttachmentChoicePreference.setOnPreferenceChangeListener((preference, newValue) -> { + refreshUiReal(); + return true; + }); + isQuickShareAttachmentChoiceChecked = ((CheckBoxPreference) QuickShareAttachmentChoicePreference).isChecked(); + } + changeOmemoSettingSummary(); if (Config.FORCE_ORBOT) { @@ -173,7 +187,7 @@ public class SettingsActivity extends XmppActivity implements quickAction.setEntryValues(entryValues.toArray(new CharSequence[entryValues.size()])); } - if (Config.QUICK_SHARE_ATTACHMENT_CHOICE) { + if (isQuickShareAttachmentChoiceChecked) { if (UIPreferenceScreen != null && quickAction != null) { UIPreferenceScreen.removePreference(quickAction); } @@ -274,21 +288,20 @@ public class SettingsActivity extends XmppActivity implements Log.d(Config.LOGTAG, "Multi account checkbox checked: " + isMultiAccountChecked); if (isMultiAccountChecked) { enableMultiAccountsPreference.setEnabled(false); - if (xmppConnectionService != null) { - final List<Account> accounts = xmppConnectionService.getAccounts(); - Log.d(Config.LOGTAG, "Disabled multi account: Number of accounts " + accounts.size()); - if (accounts.size() > 1) { - Log.d(Config.LOGTAG, "Disabled multi account not possible because you have more than one account"); + int accounts = getNumberOfAccounts(); + Log.d(Config.LOGTAG, "Disabled multi account: Number of accounts " + accounts); + if (accounts > 1) { + Log.d(Config.LOGTAG, "Disabling multi account not possible because you have more than one account"); enableMultiAccountsPreference.setEnabled(false); } else { - Log.d(Config.LOGTAG, "Disabled multi account possible because you have one account"); + Log.d(Config.LOGTAG, "Disabling multi account possible because you have only one account"); enableMultiAccountsPreference.setEnabled(true); + enableMultiAccountsPreference.setOnPreferenceClickListener(preference -> { + refreshUiReal(); + return true; + }); } } else { - Log.d(Config.LOGTAG, "Disabled multi account not possible because XmppConnectionService == null"); - enableMultiAccountsPreference.setEnabled(false); - } - } else { enableMultiAccountsPreference.setEnabled(true); enableMultiAccountsPreference.setOnPreferenceClickListener(preference -> { enableMultiAccounts(); @@ -301,7 +314,7 @@ public class SettingsActivity extends XmppActivity implements private void updateTheme() { final int theme = findTheme(); if (this.mTheme != theme) { - recreate(); + refreshUiReal(); } } @@ -465,12 +478,17 @@ public class SettingsActivity extends XmppActivity implements SharedPreferences multiaccount_prefs = getApplicationContext().getSharedPreferences(USE_MULTI_ACCOUNTS, Context.MODE_PRIVATE); SharedPreferences.Editor editor = multiaccount_prefs.edit(); editor.putString("BackupPW", pw1); - editor.commit(); + boolean passwordstored = editor.commit(); + Log.d(Config.LOGTAG, "saving multiaccount password " + passwordstored); + if (passwordstored) { + recreate(); + } else { + handleMultiAccountChanges(); + } } }) .setNegativeButton(R.string.cancel, null); alertDialogBuilder.create().show(); - } @@ -529,7 +547,7 @@ public class SettingsActivity extends XmppActivity implements } private void startExport() { - startService(new Intent(getApplicationContext(), ExportLogsService.class)); + ContextCompat.startForegroundService(this, new Intent(this, ExportLogsService.class)); } private void displayToast(final String msg) { @@ -545,6 +563,39 @@ public class SettingsActivity extends XmppActivity implements } public void refreshUiReal() { - //nothing to do. This Activity doesn't implement any listeners + recreate(); + handleMultiAccountChanges(); + } + + private void handleMultiAccountChanges() { + multiAccountPreference = mSettingsFragment.findPreference("enable_multi_accounts"); + if (multiAccountPreference != null) { + //check if password = null + final SharedPreferences multiaccount_prefs = getApplicationContext().getSharedPreferences(USE_MULTI_ACCOUNTS, Context.MODE_PRIVATE); + if (multiaccount_prefs != null && multiaccount_prefs.getString("BackupPW", null) == null) { + Log.d(Config.LOGTAG, "uncheck multiaccount because password = null"); + if (multiAccountPreference != null) { + ((CheckBoxPreference) multiAccountPreference).setChecked(false); + } + } + //if multiAccountDisabled reset password + final Preference enableMultiAccountsPreference = mSettingsFragment.findPreference("enable_multi_accounts"); + if (enableMultiAccountsPreference != null && !isMultiAccountChecked) { + SharedPreferences.Editor editor = multiaccount_prefs.edit(); + editor.putString("BackupPW", null); + if (editor.commit()) { + Log.d(Config.LOGTAG, "resetting multiaccount password because multiaccount = unchecked"); + } else { + Log.d(Config.LOGTAG, "resetting multiaccount password failed"); + } + } + } + } + + private int getNumberOfAccounts() { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + int NumberOfAccounts = preferences.getInt(NUMBER_OF_ACCOUNTS, 0); + Log.d(Config.LOGTAG, "Get number of accounts from file: " + NumberOfAccounts); + return NumberOfAccounts; } } diff --git a/src/main/java/de/pixart/messenger/ui/SettingsFragment.java b/src/main/java/de/pixart/messenger/ui/SettingsFragment.java index b40875f7c..ec4898f89 100644 --- a/src/main/java/de/pixart/messenger/ui/SettingsFragment.java +++ b/src/main/java/de/pixart/messenger/ui/SettingsFragment.java @@ -10,6 +10,7 @@ import android.text.TextUtils; import de.pixart.messenger.Config; import de.pixart.messenger.R; +import de.pixart.messenger.utils.Compatibility; public class SettingsFragment extends PreferenceFragment { @@ -30,6 +31,7 @@ public class SettingsFragment extends PreferenceFragment { mCategory.removePreference(cleanPrivateStorage); } } + Compatibility.removeUnusedPreferences(this); if (!TextUtils.isEmpty(page)) { openPreferenceScreen(page); diff --git a/src/main/java/de/pixart/messenger/ui/ShareWithActivity.java b/src/main/java/de/pixart/messenger/ui/ShareWithActivity.java index a820378a3..da3d2ff8f 100644 --- a/src/main/java/de/pixart/messenger/ui/ShareWithActivity.java +++ b/src/main/java/de/pixart/messenger/ui/ShareWithActivity.java @@ -1,6 +1,5 @@ package de.pixart.messenger.ui; -import android.app.PendingIntent; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; @@ -12,116 +11,32 @@ import android.view.Menu; import android.view.MenuItem; import android.widget.Toast; -import java.net.URLConnection; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; import de.pixart.messenger.Config; import de.pixart.messenger.R; import de.pixart.messenger.entities.Account; import de.pixart.messenger.entities.Conversation; -import de.pixart.messenger.entities.Message; -import de.pixart.messenger.persistance.FileBackend; import de.pixart.messenger.services.EmojiService; import de.pixart.messenger.services.XmppConnectionService; import de.pixart.messenger.ui.adapter.ConversationAdapter; -import de.pixart.messenger.ui.util.PresenceSelector; -import de.pixart.messenger.xmpp.XmppConnection; import rocks.xmpp.addr.Jid; import static de.pixart.messenger.ui.SettingsActivity.USE_BUNDLED_EMOJIS; -import static java.lang.String.format; public class ShareWithActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate { private static final int REQUEST_STORAGE_PERMISSION = 0x733f32; - private boolean mReturnToPrevious = false; + private static final int REQUEST_START_NEW_CONVERSATION = 0x0501; private Conversation mPendingConversation = null; - - @Override - public void onConversationUpdate() { - refreshUi(); - } - - private class Share { - public List<Uri> uris = new ArrayList<>(); - public boolean image; - public String account; - public String contact; - public String text; - public String uuid; - public boolean multiple = false; - public String type; - } - private Share share; - - private static final int REQUEST_START_NEW_CONVERSATION = 0x0501; - private RecyclerView mListView; private ConversationAdapter mAdapter; private List<Conversation> mConversations = new ArrayList<>(); - private Toast mToast; - private AtomicInteger attachmentCounter = new AtomicInteger(0); - - private UiInformableCallback<Message> attachFileCallback = new UiInformableCallback<Message>() { - - @Override - public void inform(final String text) { - runOnUiThread(() -> replaceToast(text)); - } - - @Override - public void userInputRequried(PendingIntent pi, Message object) { - // TODO Auto-generated method stub - - } - - @Override - public void success(final Message message) { - runOnUiThread(() -> { - if (attachmentCounter.decrementAndGet() <= 0) { - int resId; - if (share.image && share.multiple) { - resId = R.string.shared_images_with_x; - } else if (share.image) { - resId = R.string.shared_image_with_x; - } else { - resId = R.string.shared_file_with_x; - } - Conversation conversation = (Conversation) message.getConversation(); - replaceToast(getString(resId, conversation.getName())); - if (mReturnToPrevious) { - finish(); - } else { - switchToConversation(conversation); - } - } - }); - } - - @Override - public void error(final int errorCode, Message object) { - runOnUiThread(() -> { - replaceToast(getString(errorCode)); - if (attachmentCounter.decrementAndGet() <= 0) { - finish(); - } - }); - } - }; - - protected void hideToast() { - if (mToast != null) { - mToast.cancel(); - } - } - protected void replaceToast(String msg) { - hideToast(); - mToast = Toast.makeText(this, msg, Toast.LENGTH_LONG); - mToast.show(); + @Override + public void onConversationUpdate() { + refreshUi(); } protected void onActivityResult(int requestCode, int resultCode, final Intent data) { @@ -160,20 +75,16 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer super.onCreate(savedInstanceState); boolean useBundledEmoji = getPreferences().getBoolean(USE_BUNDLED_EMOJIS, getResources().getBoolean(R.bool.use_bundled_emoji)); new EmojiService(this).init(useBundledEmoji); - setContentView(R.layout.activity_share_with); - setSupportActionBar(findViewById(R.id.toolbar)); if (getSupportActionBar() != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(false); getSupportActionBar().setHomeButtonEnabled(false); } - setTitle(getString(R.string.title_activity_sharewith)); - - mListView = findViewById(R.id.choose_conversation_list); + RecyclerView mListView = findViewById(R.id.choose_conversation_list); mAdapter = new ConversationAdapter(this, this.mConversations); - mListView.setLayoutManager(new LinearLayoutManager(this,LinearLayoutManager.VERTICAL,false)); + mListView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)); mListView.setAdapter(mAdapter); mAdapter.setConversationClickListener((view, conversation) -> share(conversation)); this.share = new Share(); @@ -203,57 +114,28 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer if (intent == null) { return; } - this.mReturnToPrevious = getBooleanPreference("return_to_previous", R.bool.return_to_previous); final String type = intent.getType(); final String action = intent.getAction(); - Log.d(Config.LOGTAG, "action: " + action + ", type:" + type); - share.uuid = intent.getStringExtra("uuid"); if (Intent.ACTION_SEND.equals(action)) { - final String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT); final String text = intent.getStringExtra(Intent.EXTRA_TEXT); final Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); if (type != null && uri != null && (text == null || !type.equals("text/plain"))) { this.share.uris.clear(); this.share.uris.add(uri); - this.share.image = type.startsWith("image/") || isImage(uri); - this.share.type = type; } else { - if (subject != null) { - this.share.text = format("[%s]%n%s", subject, text); - } else { - this.share.text = text; - } + this.share.text = text; } } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) { - this.share.image = type != null && type.startsWith("image/"); - if (!this.share.image) { - return; - } this.share.uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); } if (xmppConnectionServiceBound) { - if (share.uuid != null) { - share(); - } else { - xmppConnectionService.populateWithOrderedConversations(mConversations, this.share.uris.size() == 0); - } - } - - } - - protected boolean isImage(Uri uri) { - try { - String guess = URLConnection.guessContentTypeFromName(uri.toString()); - return (guess != null && guess.startsWith("image/")); - } catch (final StringIndexOutOfBoundsException ignored) { - return false; + xmppConnectionService.populateWithOrderedConversations(mConversations, this.share.uris.size() == 0); } } @Override void onBackendConnected() { - if (xmppConnectionServiceBound && share != null - && ((share.contact != null && share.account != null) || share.uuid != null)) { + if (xmppConnectionServiceBound && share != null && ((share.contact != null && share.account != null))) { share(); return; } @@ -262,28 +144,19 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer private void share() { final Conversation conversation; - if (share.uuid != null) { - conversation = xmppConnectionService.findConversationByUuid(share.uuid); - if (conversation == null) { - return; - } - } else { - Account account; - try { - account = xmppConnectionService.findAccountByJid(Jid.of(share.account)); - } catch (final IllegalArgumentException e) { - account = null; - } - if (account == null) { - return; - } - - try { - conversation = xmppConnectionService - .findOrCreateConversation(account, Jid.of(share.contact), false, true); - } catch (final IllegalArgumentException e) { - return; - } + Account account; + try { + account = xmppConnectionService.findAccountByJid(Jid.of(share.account)); + } catch (final IllegalArgumentException e) { + account = null; + } + if (account == null) { + return; + } + try { + conversation = xmppConnectionService.findOrCreateConversation(account, Jid.of(share.contact), false, true); + } catch (final IllegalArgumentException e) { + return; } share(conversation); } @@ -293,110 +166,18 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer mPendingConversation = conversation; return; } - final Account account = conversation.getAccount(); - final XmppConnection connection = account.getXmppConnection(); - final long max = connection == null ? -1 : connection.getFeatures().getMaxHttpUploadSize(); - mListView.setEnabled(false); - if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP && !hasPgp()) { - if (share.uuid == null) { - showInstallPgpDialog(); - } else { - Toast.makeText(this, R.string.openkeychain_not_installed, Toast.LENGTH_SHORT).show(); - finish(); - } - return; + Intent intent = new Intent(this, ConversationsActivity.class); + intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid()); + if (share.uris.size() > 0) { + intent.setAction(Intent.ACTION_SEND_MULTIPLE); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, share.uris); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } else if (share.text != null) { + intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION); + intent.putExtra(ConversationsActivity.EXTRA_TEXT, share.text); } - if (share.uris.size() != 0) { - PresenceSelector.OnPresenceSelected callback = () -> { - attachmentCounter.set(share.uris.size()); - if (share.image) { - Log.d(Config.LOGTAG, "ShareWithActivity share() image " + share.uris.size() + " uri(s) " + share.uris.toString()); - share.multiple = share.uris.size() > 1; - replaceToast(getString(share.multiple ? R.string.preparing_images : R.string.preparing_image)); - for (Iterator<Uri> i = share.uris.iterator(); i.hasNext(); i.remove()) { - final Uri uri = i.next(); - delegateUriPermissionsToService(uri); - xmppConnectionService.attachImageToConversation(conversation, uri, attachFileCallback); - } - } else { - Log.d(Config.LOGTAG, "ShareWithActivity share() file " + share.uris.size() + " uri(s) " + share.uris.toString()); - replaceToast(getString(R.string.preparing_file)); - final Uri uri = share.uris.get(0); - delegateUriPermissionsToService(uri); - xmppConnectionService.attachFileToConversation(conversation, uri, share.type, attachFileCallback); - finish(); - } - }; - if (account.httpUploadAvailable() - && ((share.image && !neverCompressPictures()) - || conversation.getMode() == Conversation.MODE_MULTI - || FileBackend.allFilesUnderSize(this, share.uris, max)) - && conversation.getNextEncryption() != Message.ENCRYPTION_OTR) { - callback.onPresenceSelected(); - } else { - selectPresence(conversation, callback); - } - } else { - if (mReturnToPrevious && this.share.text != null && !this.share.text.isEmpty()) { - final PresenceSelector.OnPresenceSelected callback = new PresenceSelector.OnPresenceSelected() { - - private void finishAndSend(Message message) { - replaceToast(getString(R.string.shared_text_with_x, conversation.getName())); - finish(); - } - - private UiCallback<Message> messageEncryptionCallback = new UiCallback<Message>() { - @Override - public void success(final Message message) { - runOnUiThread(() -> finishAndSend(message)); - } - - @Override - public void error(final int errorCode, Message object) { - runOnUiThread(() -> { - replaceToast(getString(errorCode)); - finish(); - }); - } - - @Override - public void userInputRequried(PendingIntent pi, Message object) { - finish(); - } - }; - - @Override - public void onPresenceSelected() { - - final int encryption = conversation.getNextEncryption(); - - Message message = new Message(conversation,share.text, encryption); - - Log.d(Config.LOGTAG,"on presence selected encrpytion="+encryption); - - if (encryption == Message.ENCRYPTION_PGP) { - replaceToast(getString(R.string.encrypting_message)); - xmppConnectionService.getPgpEngine().encrypt(message,messageEncryptionCallback); - return; - } - - if (encryption == Message.ENCRYPTION_OTR) { - message.setCounterpart(conversation.getNextCounterpart()); - } - xmppConnectionService.sendMessage(message); - finishAndSend(message); - } - }; - if (conversation.getNextEncryption() == Message.ENCRYPTION_OTR) { - selectPresence(conversation, callback); - } else { - callback.onPresenceSelected(); - } - } else { - switchToConversation(conversation, this.share.text, true); - } - } - + startActivity(intent); + finish(); } public void refreshUiReal() { @@ -404,12 +185,10 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer mAdapter.notifyDataSetChanged(); } - @Override - public void onBackPressed() { - if (attachmentCounter.get() >= 1) { - replaceToast(getString(R.string.sharing_files_please_wait)); - } else { - super.onBackPressed(); - } + private class Share { + public String account; + public String contact; + public String text; + ArrayList<Uri> uris = new ArrayList<>(); } }
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/ui/StartConversationActivity.java b/src/main/java/de/pixart/messenger/ui/StartConversationActivity.java index a58c2e0c0..52bff613c 100644 --- a/src/main/java/de/pixart/messenger/ui/StartConversationActivity.java +++ b/src/main/java/de/pixart/messenger/ui/StartConversationActivity.java @@ -463,6 +463,12 @@ public class StartConversationActivity extends XmppActivity implements XmppConne if (prev != null) { ft.remove(prev); } + boolean multiAccount = false; + try { + multiAccount = xmppConnectionService.multipleAccounts(); + } catch (Exception e) { + e.printStackTrace(); + } ft.addToBackStack(null); EnterJidDialog dialog = EnterJidDialog.newInstance( mActivatedAccounts, @@ -471,7 +477,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne prefilledJid, null, invite == null || !invite.hasFingerprints(), - xmppConnectionService.multipleAccounts() + multiAccount ); dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid) -> { @@ -684,21 +690,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.sync_with_contacts); builder.setMessage(R.string.sync_with_contacts_long); - builder.setPositiveButton(R.string.next, (dialog, which) -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS); - } - }); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - builder.setOnDismissListener(dialog -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS); - } - }); - } + builder.setPositiveButton(R.string.next, (dialog, which) -> requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS)); + builder.setOnDismissListener(dialog -> requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS)); builder.create().show(); } else { - requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, 0); + requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS); } } } @@ -711,7 +707,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { ScanActivity.onRequestPermissionResult(this, requestCode, grantResults); if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) { - xmppConnectionService.loadPhoneContacts(); + xmppConnectionService.startContactObserver(); } } } @@ -868,11 +864,13 @@ public class StartConversationActivity extends XmppActivity implements XmppConne protected void filterContacts(String needle) { this.contacts.clear(); - for (Account account : xmppConnectionService.getAccounts()) { + final List<Account> accounts = xmppConnectionService.getAccounts(); + final boolean singleAccountActive = isSingleAccountActive(accounts); + for (Account account : accounts) { if (account.getStatus() != Account.State.DISABLED) { for (Contact contact : account.getRoster().getContacts()) { Presence.Status s = contact.getShownStatus(); - if (contact.showInRoster() && contact.match(this, needle) + if ((contact.showInRoster() || (singleAccountActive && contact.showInPhoneBook())) && contact.match(this, needle) && (!this.mHideOfflineContacts || (needle != null && !needle.trim().isEmpty()) || s.compareTo(Presence.Status.OFFLINE) < 0)) { @@ -885,6 +883,16 @@ public class StartConversationActivity extends XmppActivity implements XmppConne mContactsAdapter.notifyDataSetChanged(); } + private static boolean isSingleAccountActive(final List<Account> accounts) { + int i = 0; + for(Account account : accounts) { + if (account.getStatus() != Account.State.DISABLED) { + ++i; + } + } + return i == 1; + } + protected void filterConferences(String needle) { this.conferences.clear(); for (Account account : xmppConnectionService.getAccounts()) { @@ -1053,9 +1061,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne final Contact contact = (Contact) activity.contacts.get(acmi.position); final MenuItem blockUnblockItem = menu.findItem(R.id.context_contact_block_unblock); final MenuItem showContactDetailsItem = menu.findItem(R.id.context_contact_details); + final MenuItem deleteContactMenuItem = menu.findItem(R.id.context_delete_contact); if (contact.isSelf()) { showContactDetailsItem.setVisible(false); } + deleteContactMenuItem.setVisible(contact.showInRoster()); XmppConnection xmpp = contact.getAccount().getXmppConnection(); if (xmpp != null && xmpp.getFeatures().blocking() && !contact.isSelf()) { if (contact.isBlocked()) { diff --git a/src/main/java/de/pixart/messenger/ui/UpdaterActivity.java b/src/main/java/de/pixart/messenger/ui/UpdaterActivity.java index 0c59b06c6..9e35d7ebf 100644 --- a/src/main/java/de/pixart/messenger/ui/UpdaterActivity.java +++ b/src/main/java/de/pixart/messenger/ui/UpdaterActivity.java @@ -57,7 +57,10 @@ public class UpdaterActivity extends XmppActivity { //set activity setContentView(R.layout.activity_updater); - this.mUseTor = mXmppConnectionService.useTorToConnect(); + this.mUseTor = false; + if (mXmppConnectionService != null) { + this.mUseTor = mXmppConnectionService.useTorToConnect(); + } this.mTheme = findTheme(); setTheme(this.mTheme); @@ -111,7 +114,7 @@ public class UpdaterActivity extends XmppActivity { store = null; } //delete old downloaded localVersion files - File dir = new File(FileBackend.getConversationsDirectory("Update", false)); + File dir = new File(FileBackend.getAppUpdateDirectory()); if (dir.isDirectory()) { String[] children = dir.list(); for (String aChildren : children) { @@ -131,7 +134,7 @@ public class UpdaterActivity extends XmppActivity { //ask for permissions on devices >= SDK 23 if (isStoragePermissionGranted() && isNetworkAvailable(getApplicationContext())) { //start downloading the file using the download manager - if (store.equals(PlayStore)) { + if (store != null && store.equalsIgnoreCase(PlayStore)) { Uri uri = Uri.parse("market://details?id=de.pixart.messenger"); Intent marketIntent = new Intent(Intent.ACTION_VIEW, uri); PackageManager manager = getApplicationContext().getPackageManager(); @@ -145,7 +148,7 @@ public class UpdaterActivity extends XmppActivity { startActivity(browserIntent); overridePendingTransition(R.animator.fade_in, R.animator.fade_out); } - } else if (store.equals(FDroid)) { + } else if (store != null && store.equalsIgnoreCase(FDroid)) { Uri uri = Uri.parse("https://f-droid.org/de/packages/de.pixart.messenger/"); Intent marketIntent = new Intent(Intent.ACTION_VIEW, uri); PackageManager manager = getApplicationContext().getPackageManager(); @@ -288,7 +291,7 @@ public class UpdaterActivity extends XmppActivity { private class DownloadTask extends AsyncTask<String, Integer, String> { - File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + FileBackend.getDirectoryName("Update", false)); + File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + FileBackend.getAppUpdateDirectory()); File file = new File(dir, FileName); private Context context; private PowerManager.WakeLock mWakeLock; diff --git a/src/main/java/de/pixart/messenger/ui/WelcomeActivity.java b/src/main/java/de/pixart/messenger/ui/WelcomeActivity.java index 3c03542ee..e2bf9d8e9 100644 --- a/src/main/java/de/pixart/messenger/ui/WelcomeActivity.java +++ b/src/main/java/de/pixart/messenger/ui/WelcomeActivity.java @@ -152,6 +152,8 @@ public class WelcomeActivity extends XmppActivity { checkDatabase(password); } catch (IOException e) { e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); } pd.dismiss(); } @@ -191,7 +193,7 @@ public class WelcomeActivity extends XmppActivity { private boolean BackupAvailable() { // Set the folder on the SDcard - File filePath = new File(FileBackend.getConversationsDirectory("Database", false) + "database.db.crypt"); + File filePath = new File(FileBackend.getBackupDirectory() + "database.db.crypt"); Log.d(Config.LOGTAG, "DB Path: " + filePath.toString()); if (filePath.exists()) { Log.d(Config.LOGTAG, "DB Path existing"); @@ -204,11 +206,11 @@ public class WelcomeActivity extends XmppActivity { private void checkDatabase(String DecryptionKey) throws IOException { // Set the folder on the SDcard - File directory = new File(FileBackend.getConversationsDirectory("Database", false)); + File directory = new File(FileBackend.getBackupDirectory()); // Set the input file stream up: - FileInputStream InputFile = new FileInputStream(directory.getPath() + "/database.db.crypt"); + FileInputStream InputFile = new FileInputStream(directory.getPath() + "database.db.crypt"); // Temp output for DB checks - File TempFile = new File(directory.getPath() + "/database.bak"); + File TempFile = new File(directory.getPath() + "database.bak"); FileOutputStream OutputTemp = new FileOutputStream(TempFile); try { @@ -225,6 +227,9 @@ public class WelcomeActivity extends XmppActivity { } catch (IOException e) { Log.d(Config.LOGTAG, "Database importer: decryption failed (IO) with " + e); e.printStackTrace(); + } catch (Exception e) { + Log.d(Config.LOGTAG, "Database importer: Error " + e); + e.printStackTrace(); } SQLiteDatabase checkDB = null; @@ -236,10 +241,11 @@ public class WelcomeActivity extends XmppActivity { checkDB = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READONLY); Backup_DB_Version = checkDB.getVersion(); Log.d(Config.LOGTAG, "Backup found: " + checkDB + " Version: " + checkDB.getVersion()); - } catch (SQLiteException e) { //database does't exist yet. Log.d(Config.LOGTAG, "No backup found: " + checkDB); + } catch (Exception e) { + Log.d(Config.LOGTAG, "Error importing backup: " + e); } if (checkDB != null) { @@ -280,11 +286,11 @@ public class WelcomeActivity extends XmppActivity { // Set location for the db: final OutputStream OutputFile = new FileOutputStream(this.getDatabasePath(DatabaseBackend.DATABASE_NAME)); // Set the folder on the SDcard - File directory = new File(FileBackend.getConversationsDirectory("Database", false)); + File directory = new File(FileBackend.getBackupDirectory()); // Set the input file stream up: - final InputStream InputFile = new FileInputStream(directory.getPath() + "/database.bak"); + final InputStream InputFile = new FileInputStream(directory.getPath() + "database.bak"); //set temp file - File TempFile = new File(directory.getPath() + "/database.bak"); + File TempFile = new File(directory.getPath() + "database.bak"); // Transfer bytes from the input file to the output file byte[] buffer = new byte[1024]; diff --git a/src/main/java/de/pixart/messenger/ui/XmppActivity.java b/src/main/java/de/pixart/messenger/ui/XmppActivity.java index f52b2d72c..acb970e82 100644 --- a/src/main/java/de/pixart/messenger/ui/XmppActivity.java +++ b/src/main/java/de/pixart/messenger/ui/XmppActivity.java @@ -536,7 +536,11 @@ public abstract class XmppActivity extends ActionBarActivity { intent.setAction(Intent.ACTION_SEND); intent.setData(uri); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - startService(intent); + try { + startService(intent); + } catch (Exception e) { + Log.e(Config.LOGTAG, "unable to delegate uri permission", e); + } } protected void inviteToConversation(Conversation conversation) { @@ -943,7 +947,11 @@ public abstract class XmppActivity extends ActionBarActivity { } public void inviteUser() { - if (xmppConnectionServiceBound && !xmppConnectionService.multipleAccounts()) { + if (!xmppConnectionServiceBound) { + Toast.makeText(this, R.string.not_connected_try_again, Toast.LENGTH_SHORT).show(); + return; + } + if (!xmppConnectionService.multipleAccounts()) { Account mAccount = xmppConnectionService.getAccounts().get(0); String user = Jid.of(mAccount.getJid()).getLocal(); String domain = Jid.of(mAccount.getJid()).getDomain(); diff --git a/src/main/java/de/pixart/messenger/ui/adapter/AccountAdapter.java b/src/main/java/de/pixart/messenger/ui/adapter/AccountAdapter.java index 01b432d65..e37ea6121 100644 --- a/src/main/java/de/pixart/messenger/ui/adapter/AccountAdapter.java +++ b/src/main/java/de/pixart/messenger/ui/adapter/AccountAdapter.java @@ -24,7 +24,7 @@ import de.pixart.messenger.R; import de.pixart.messenger.entities.Account; import de.pixart.messenger.ui.ManageAccountActivity; import de.pixart.messenger.ui.XmppActivity; -import de.pixart.messenger.ui.util.Color; +import de.pixart.messenger.ui.util.StyledAttributes; import de.pixart.messenger.utils.UIHelper; public class AccountAdapter extends ArrayAdapter<Account> { @@ -65,14 +65,14 @@ public class AccountAdapter extends ArrayAdapter<Account> { statusView.setText(getContext().getString(account.getStatus().getReadableId())); switch (account.getStatus()) { case ONLINE: - statusView.setTextColor(Color.get(activity, R.attr.TextColorOnline)); + statusView.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorOnline)); break; case DISABLED: case CONNECTING: - statusView.setTextColor(Color.get(activity, android.R.attr.textColorSecondary)); + statusView.setTextColor(StyledAttributes.getColor(activity, android.R.attr.textColorSecondary)); break; default: - statusView.setTextColor(Color.get(activity, R.attr.TextColorError)); + statusView.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorError)); break; } final SwitchCompat tglAccountState = view.findViewById(R.id.tgl_account_status); diff --git a/src/main/java/de/pixart/messenger/ui/adapter/ConversationAdapter.java b/src/main/java/de/pixart/messenger/ui/adapter/ConversationAdapter.java index 5294ba7cd..55dd6d65f 100644 --- a/src/main/java/de/pixart/messenger/ui/adapter/ConversationAdapter.java +++ b/src/main/java/de/pixart/messenger/ui/adapter/ConversationAdapter.java @@ -31,7 +31,7 @@ import de.pixart.messenger.entities.MucOptions; import de.pixart.messenger.entities.Transferable; import de.pixart.messenger.ui.ConversationFragment; import de.pixart.messenger.ui.XmppActivity; -import de.pixart.messenger.ui.util.Color; +import de.pixart.messenger.ui.util.StyledAttributes; import de.pixart.messenger.ui.widget.UnreadCountCustomView; import de.pixart.messenger.utils.EmojiWrapper; import de.pixart.messenger.utils.IrregularUnicodeDetector; @@ -97,9 +97,9 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapte } if (conversation == ConversationFragment.getConversation(activity)) { - viewHolder.frame.setBackgroundColor(Color.get(activity, R.attr.color_background_tertiary)); + viewHolder.frame.setBackgroundColor(StyledAttributes.getColor(activity, R.attr.color_background_tertiary)); } else { - viewHolder.frame.setBackgroundColor(Color.get(activity,R.attr.color_background_primary)); + viewHolder.frame.setBackgroundColor(StyledAttributes.getColor(activity, R.attr.color_background_secondary)); } Message message = conversation.getLatestMessage(); @@ -250,11 +250,11 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapte viewHolder.name.setTextColor(ContextCompat.getColor(activity, R.color.notavailable)); break; default: - viewHolder.name.setTextColor(Color.get(activity, R.attr.text_Color_Main)); + viewHolder.name.setTextColor(StyledAttributes.getColor(activity, R.attr.text_Color_Main)); break; } } else { - viewHolder.name.setTextColor(Color.get(activity, R.attr.text_Color_Main)); + viewHolder.name.setTextColor(StyledAttributes.getColor(activity, R.attr.text_Color_Main)); } if (activity.xmppConnectionService.indicateReceived()) { diff --git a/src/main/java/de/pixart/messenger/ui/adapter/KnownHostsAdapter.java b/src/main/java/de/pixart/messenger/ui/adapter/KnownHostsAdapter.java index 9f4a52d95..2e0bfccc6 100644 --- a/src/main/java/de/pixart/messenger/ui/adapter/KnownHostsAdapter.java +++ b/src/main/java/de/pixart/messenger/ui/adapter/KnownHostsAdapter.java @@ -7,8 +7,6 @@ import android.widget.Filter; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; import java.util.Locale; public class KnownHostsAdapter extends ArrayAdapter<String> { @@ -17,34 +15,31 @@ public class KnownHostsAdapter extends ArrayAdapter<String> { @Override protected FilterResults performFiltering(CharSequence constraint) { - if (constraint != null) { - ArrayList<String> suggestions = new ArrayList<>(); - final String[] split = constraint.toString().split("@"); - if (split.length == 1) { - for (String domain : domains) { - suggestions.add(split[0].toLowerCase(Locale - .getDefault()) + "@" + domain); - } - } else if (split.length == 2) { - for (String domain : domains) { - if (domain.contentEquals(split[1])) { - suggestions.clear(); - break; - } else if (domain.contains(split[1])) { - suggestions.add(split[0].toLowerCase(Locale - .getDefault()) + "@" + domain); - } - } - } else { + final ArrayList<String> suggestions = new ArrayList<>(); + final String[] split = constraint == null ? new String[0] : constraint.toString().split("@"); + if (split.length == 1) { + final String local = split[0].toLowerCase(Locale.ENGLISH); + for (String domain : domains) { + suggestions.add(local + "@" + domain); + } + } else if (split.length == 2) { + final String localPart = split[0].toLowerCase(Locale.ENGLISH); + final String domainPart = split[1].toLowerCase(Locale.ENGLISH); + if (domains.contains(domainPart)) { return new FilterResults(); } - FilterResults filterResults = new FilterResults(); - filterResults.values = suggestions; - filterResults.count = suggestions.size(); - return filterResults; + for (String domain : domains) { + if (domain.contains(domainPart)) { + suggestions.add(localPart + "@" + domain); + } + } } else { return new FilterResults(); } + FilterResults filterResults = new FilterResults(); + filterResults.values = suggestions; + filterResults.count = suggestions.size(); + return filterResults; } @Override @@ -52,9 +47,7 @@ public class KnownHostsAdapter extends ArrayAdapter<String> { ArrayList filteredList = (ArrayList) results.values; if (results.count > 0) { clear(); - for (Object c : filteredList) { - add((String) c); - } + addAll(filteredList); notifyDataSetChanged(); } } @@ -62,140 +55,7 @@ public class KnownHostsAdapter extends ArrayAdapter<String> { public KnownHostsAdapter(Context context, int viewResourceId, Collection<String> mKnownHosts) { super(context, viewResourceId, new ArrayList<>()); - - if (mKnownHosts == null) { - domains = new ArrayList<>(); - } else { - domains = new ArrayList<>(mKnownHosts); - } - - HashSet<String> hashSet = new HashSet<>(); - - // get servers from https://conversations.im/compliance/ - new Thread(() -> { - domains.add("pix-art.de"); - domains.add("conversations.im"); - domains.add("jabber.cat"); - domains.add("jabjab.de"); - domains.add("im.koderoot.net"); - domains.add("riotcat.org"); - domains.add("magicbroccoli.de"); - domains.add("kode.im"); - domains.add("jabber-germany.de"); - domains.add("simplewire.de"); - domains.add("suchat.org"); - domains.add("jabber.at"); - domains.add("trashserver.net"); - domains.add("wiuwiu.de"); - domains.add("5222.de"); - domains.add("dismail.de"); - domains.add("chat.sum7.eu"); - domains.add("xmpp.zone"); - domains.add("libranet.de"); - domains.add("laborversuch.de"); - domains.add("creep.im"); - domains.add("jabber.systemausfall.org"); - domains.add("jabber.hot-chilli.net"); - domains.add("jabber.fr"); - domains.add("jabber.de"); - domains.add("draugr.de"); - domains.add("elaon.de"); - domains.add("high-way.me"); - domains.add("jabber.rwth-aachen.de"); - domains.add("deshalbfrei.org"); - domains.add("mail.de"); - domains.add("bommboo.de"); - domains.add("jabber.systemli.org"); - domains.add("jabb.im"); - domains.add("mailbox.org"); - domains.add("hot-chilli.net"); - domains.add("jabberpl.org"); - domains.add("chinwag.im"); - domains.add("tchncs.de"); - domains.add("zsim.de"); - domains.add("patchcord.be"); - domains.add("gajim.org"); - domains.add("talker.to"); - domains.add("pimux.de"); - domains.add("jabber.home.vdlinde.org"); - domains.add("im.apinc.org"); - domains.add("chatme.im"); - domains.add("fusselkater.org"); - domains.add("datenknoten.me"); - domains.add("fysh.in"); - domains.add("jabber.chaos-darmstadt.de"); - domains.add("yax.im"); - domains.add("neko.im"); - domains.add("jabberzac.org"); - domains.add("xmpp.is"); - domains.add("home.zom.im"); - domains.add("jabber.ccc.de"); - domains.add("jwchat.org"); - domains.add("kdetalk.net"); - domains.add("kde.org"); - domains.add("riseup.net"); - domains.add("ruhr-uni-bochum.de"); - domains.add("njs.netlab.cz"); - domains.add("schokokeks.org"); - domains.add("jabber.cz"); - domains.add("ubuntu-jabber.de"); - domains.add("xabber.de"); - domains.add("ubuntu-jabber.net"); - domains.add("jabber.ru"); - domains.add("darknet.nz"); - domains.add("movim.eu"); - domains.add("404.city"); - domains.add("igniterealtime.org"); - domains.add("kapsi.fi"); - domains.add("jabbel.net"); - domains.add("joindiaspora.com"); - domains.add("alpha-labs.net"); - domains.add("xmppnet.de"); - domains.add("hoth.one"); - domains.add("blah.im"); - domains.add("xmpp.jp"); - domains.add("jabber.uni-mainz.de"); - domains.add("richim.org"); - domains.add("tigase.im"); - domains.add("jappix.com"); - domains.add("member.fsf.org"); - domains.add("jabber.rueckgr.at"); - domains.add("swissjabber.ch"); - domains.add("twattle.net"); - domains.add("jabber.calyxinstitute.org"); - domains.add("sapo.pt"); - domains.add("uprod.biz"); - domains.add("krautspace.de"); - domains.add("kraut.space"); - domains.add("null.pm"); - domains.add("anonymitaet-im-inter.net"); - domains.add("0nl1ne.at"); - domains.add("linuxlovers.at"); - domains.add("jabber.org"); - domains.add("jabber.no-sense.net"); - domains.add("swissjabber.eu"); - domains.add("swissjabber.org"); - domains.add("swissjabber.de"); - domains.add("swissjabber.li"); - domains.add("jabber.no"); - domains.add("cypherpunks.it"); - domains.add("adastra.re"); - domains.add("jabber-br.org"); - domains.add("einfachjabber.de"); - domains.add("jabber.smash-net.org"); - domains.add("freifunk.im"); - domains.add("openmailbox.org"); - domains.add("jabber.otr.im"); - domains.add("evil.im"); - domains.add("xmpp.slack.com"); - domains.add("chat.hipchat.com"); - domains.add("googlemail.com"); - - hashSet.addAll(domains); - domains.clear(); - domains.addAll(hashSet); - Collections.sort(domains, String::compareToIgnoreCase); - }).start(); + domains = new ArrayList<>(mKnownHosts); } public KnownHostsAdapter(Context context, int viewResourceId) { diff --git a/src/main/java/de/pixart/messenger/ui/adapter/ListItemAdapter.java b/src/main/java/de/pixart/messenger/ui/adapter/ListItemAdapter.java index 64d432041..17479cf99 100644 --- a/src/main/java/de/pixart/messenger/ui/adapter/ListItemAdapter.java +++ b/src/main/java/de/pixart/messenger/ui/adapter/ListItemAdapter.java @@ -27,7 +27,7 @@ import de.pixart.messenger.databinding.ContactBinding; import de.pixart.messenger.entities.ListItem; import de.pixart.messenger.ui.SettingsActivity; import de.pixart.messenger.ui.XmppActivity; -import de.pixart.messenger.ui.util.Color; +import de.pixart.messenger.ui.util.StyledAttributes; import de.pixart.messenger.utils.IrregularUnicodeDetector; import de.pixart.messenger.utils.UIHelper; import rocks.xmpp.addr.Jid; @@ -105,16 +105,16 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> { } } if (offline) { - viewHolder.name.setTextColor(Color.get(activity, R.attr.text_Color_Main)); + viewHolder.name.setTextColor(StyledAttributes.getColor(activity, R.attr.text_Color_Main)); viewHolder.name.setAlpha(INACTIVE_ALPHA); viewHolder.jid.setAlpha(INACTIVE_ALPHA); viewHolder.avatar.setAlpha(INACTIVE_ALPHA); viewHolder.tags.setAlpha(INACTIVE_ALPHA); } else { if (ShowPresenceColoredNames()) { - viewHolder.name.setTextColor(color != 0 ? color : Color.get(activity, R.attr.text_Color_Main)); + viewHolder.name.setTextColor(color != 0 ? color : StyledAttributes.getColor(activity, R.attr.text_Color_Main)); } else { - viewHolder.name.setTextColor(Color.get(activity, R.attr.text_Color_Main)); + viewHolder.name.setTextColor(StyledAttributes.getColor(activity, R.attr.text_Color_Main)); } viewHolder.name.setAlpha(ACTIVE_ALPHA); viewHolder.jid.setAlpha(ACTIVE_ALPHA); diff --git a/src/main/java/de/pixart/messenger/ui/adapter/MediaAdapter.java b/src/main/java/de/pixart/messenger/ui/adapter/MediaAdapter.java new file mode 100644 index 000000000..0472c6d11 --- /dev/null +++ b/src/main/java/de/pixart/messenger/ui/adapter/MediaAdapter.java @@ -0,0 +1,226 @@ +package de.pixart.messenger.ui.adapter; + +import android.content.Context; +import android.content.res.Resources; +import android.databinding.DataBindingUtil; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.support.annotation.AttrRes; +import android.support.annotation.DimenRes; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.ImageView; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; + +import de.pixart.messenger.R; +import de.pixart.messenger.databinding.MediaBinding; +import de.pixart.messenger.ui.XmppActivity; +import de.pixart.messenger.ui.util.Attachment; +import de.pixart.messenger.ui.util.StyledAttributes; +import de.pixart.messenger.ui.util.ViewUtil; + +public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHolder> { + + private static final List<String> DOCUMENT_MIMES = Arrays.asList( + "application/pdf", + "application/vnd.oasis.opendocument.text", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "text/x-tex", + "text/plain" + ); + + private final ArrayList<Attachment> attachments = new ArrayList<>(); + + private final XmppActivity activity; + + private int mediaSize = 0; + + public MediaAdapter(XmppActivity activity, @DimenRes int mediaSize) { + this.activity = activity; + this.mediaSize = Math.round(activity.getResources().getDimension(mediaSize)); + } + + public static void setMediaSize(RecyclerView recyclerView, int mediaSize) { + RecyclerView.Adapter adapter = recyclerView.getAdapter(); + if (adapter instanceof MediaAdapter) { + ((MediaAdapter) adapter).setMediaSize(mediaSize); + } + } + + private static @AttrRes + int getImageAttr(Attachment attachment) { + final @AttrRes int attr; + if (attachment.getType() == Attachment.Type.LOCATION) { + attr = R.attr.media_preview_location; + } else if (attachment.getType() == Attachment.Type.RECORDING) { + attr = R.attr.media_preview_recording; + } else { + final String mime = attachment.getMime(); + if (mime == null) { + attr = R.attr.media_preview_unknown; + } else if (mime.startsWith("audio/")) { + attr = R.attr.media_preview_audio; + } else if (mime.equals("text/calendar") || (mime.equals("text/x-vcalendar"))) { + attr = R.attr.media_preview_calendar; + } else if (mime.equals("text/x-vcard")) { + attr = R.attr.media_preview_contact; + } else if (mime.equals("application/vnd.android.package-archive")) { + attr = R.attr.media_preview_app; + } else if (mime.equals("application/zip") || mime.equals("application/rar")) { + attr = R.attr.media_preview_archive; + } else if (DOCUMENT_MIMES.contains(mime)) { + attr = R.attr.media_preview_document; + } else { + attr = R.attr.media_preview_unknown; + } + } + return attr; + } + + public static void renderPreview(Context context, Attachment attachment, ImageView imageView) { + imageView.setBackgroundColor(StyledAttributes.getColor(context, R.attr.color_background_tertiary)); + imageView.setImageAlpha(Math.round(StyledAttributes.getFloat(context, R.attr.icon_alpha) * 255)); + imageView.setImageDrawable(StyledAttributes.getDrawable(context, getImageAttr(attachment))); + } + + private static boolean cancelPotentialWork(Attachment attachment, ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final Attachment oldAttachment = bitmapWorkerTask.attachment; + if (oldAttachment == null || !oldAttachment.equals(attachment)) { + bitmapWorkerTask.cancel(true); + } else { + return false; + } + } + return true; + } + + private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { + if (imageView != null) { + final Drawable drawable = imageView.getDrawable(); + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getBitmapWorkerTask(); + } + } + return null; + } + + @NonNull + @Override + public MediaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); + MediaBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.media, parent, false); + return new MediaViewHolder(binding); + } + + @Override + public void onBindViewHolder(@NonNull MediaViewHolder holder, int position) { + final Attachment attachment = attachments.get(position); + if (attachment.renderThumbnail()) { + holder.binding.media.setImageAlpha(255); + loadPreview(attachment, holder.binding.media); + } else { + cancelPotentialWork(attachment, holder.binding.media); + renderPreview(activity, attachment, holder.binding.media); + } + holder.binding.media.setOnClickListener(v -> ViewUtil.view(activity, attachment)); + } + + public void setAttachments(List<Attachment> attachments) { + this.attachments.clear(); + this.attachments.addAll(attachments); + notifyDataSetChanged(); + } + + private void setMediaSize(int mediaSize) { + this.mediaSize = mediaSize; + } + + private void loadPreview(Attachment attachment, ImageView imageView) { + if (cancelPotentialWork(attachment, imageView)) { + final Bitmap bm = activity.xmppConnectionService.getFileBackend().getPreviewForUri(attachment, mediaSize, true); + if (bm != null) { + cancelPotentialWork(attachment, imageView); + imageView.setImageBitmap(bm); + imageView.setBackgroundColor(0x00000000); + } else { + imageView.setBackgroundColor(0xff333333); + imageView.setImageDrawable(null); + final BitmapWorkerTask task = new BitmapWorkerTask(imageView); + final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task); + imageView.setImageDrawable(asyncDrawable); + try { + task.execute(attachment); + } catch (final RejectedExecutionException ignored) { + } + } + } + } + + @Override + public int getItemCount() { + return attachments.size(); + } + + static class AsyncDrawable extends BitmapDrawable { + private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; + + AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { + super(res, bitmap); + bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask); + } + + BitmapWorkerTask getBitmapWorkerTask() { + return bitmapWorkerTaskReference.get(); + } + } + + class MediaViewHolder extends RecyclerView.ViewHolder { + + private final MediaBinding binding; + + MediaViewHolder(MediaBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } + + class BitmapWorkerTask extends AsyncTask<Attachment, Void, Bitmap> { + private final WeakReference<ImageView> imageViewReference; + private Attachment attachment = null; + + BitmapWorkerTask(ImageView imageView) { + imageViewReference = new WeakReference<>(imageView); + } + + @Override + protected Bitmap doInBackground(Attachment... params) { + this.attachment = params[0]; + return activity.xmppConnectionService.getFileBackend().getPreviewForUri(this.attachment, mediaSize, false); + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (bitmap != null && !isCancelled()) { + final ImageView imageView = imageViewReference.get(); + if (imageView != null) { + imageView.setImageBitmap(bitmap); + imageView.setBackgroundColor(0x00000000); + } + } + } + } +}
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/ui/adapter/MediaPreviewAdapter.java b/src/main/java/de/pixart/messenger/ui/adapter/MediaPreviewAdapter.java new file mode 100644 index 000000000..ae4c42816 --- /dev/null +++ b/src/main/java/de/pixart/messenger/ui/adapter/MediaPreviewAdapter.java @@ -0,0 +1,188 @@ +package de.pixart.messenger.ui.adapter; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; +import android.databinding.DataBindingUtil; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.ImageView; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; + +import de.pixart.messenger.R; +import de.pixart.messenger.databinding.MediaPreviewBinding; +import de.pixart.messenger.ui.ConversationFragment; +import de.pixart.messenger.ui.XmppActivity; +import de.pixart.messenger.ui.util.Attachment; + +public class MediaPreviewAdapter extends RecyclerView.Adapter<MediaPreviewAdapter.MediaPreviewViewHolder> { + + private final ArrayList<Attachment> mediaPreviews = new ArrayList<>(); + + private final ConversationFragment conversationFragment; + + public MediaPreviewAdapter(ConversationFragment fragment) { + this.conversationFragment = fragment; + } + + private static boolean cancelPotentialWork(Attachment attachment, ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final Attachment oldAttachment = bitmapWorkerTask.attachment; + if (oldAttachment == null || !oldAttachment.equals(attachment)) { + bitmapWorkerTask.cancel(true); + } else { + return false; + } + } + return true; + } + + private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { + if (imageView != null) { + final Drawable drawable = imageView.getDrawable(); + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getBitmapWorkerTask(); + } + } + return null; + } + + @NonNull + @Override + public MediaPreviewViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); + MediaPreviewBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.media_preview, parent, false); + return new MediaPreviewViewHolder(binding); + } + + @Override + public void onBindViewHolder(@NonNull MediaPreviewViewHolder holder, int position) { + final Context context = conversationFragment.getActivity(); + final Attachment attachment = mediaPreviews.get(position); + if (attachment.renderThumbnail()) { + holder.binding.mediaPreview.setImageAlpha(255); + loadPreview(attachment, holder.binding.mediaPreview); + } else { + cancelPotentialWork(attachment, holder.binding.mediaPreview); + MediaAdapter.renderPreview(context, attachment, holder.binding.mediaPreview); + } + holder.binding.deleteButton.setOnClickListener(v -> { + int pos = mediaPreviews.indexOf(attachment); + mediaPreviews.remove(pos); + notifyItemRemoved(pos); + conversationFragment.toggleInputMethod(); + }); + } + + public void addMediaPreviews(List<Attachment> attachments) { + this.mediaPreviews.addAll(attachments); + notifyDataSetChanged(); + } + + private void loadPreview(Attachment attachment, ImageView imageView) { + if (cancelPotentialWork(attachment, imageView)) { + XmppActivity activity = (XmppActivity) conversationFragment.getActivity(); + final Bitmap bm = activity.xmppConnectionService.getFileBackend().getPreviewForUri(attachment, Math.round(activity.getResources().getDimension(R.dimen.media_preview_size)), true); + if (bm != null) { + cancelPotentialWork(attachment, imageView); + imageView.setImageBitmap(bm); + imageView.setBackgroundColor(0x00000000); + } else { + imageView.setBackgroundColor(0xff333333); + imageView.setImageDrawable(null); + final BitmapWorkerTask task = new BitmapWorkerTask(imageView); + final AsyncDrawable asyncDrawable = new AsyncDrawable(conversationFragment.getActivity().getResources(), null, task); + imageView.setImageDrawable(asyncDrawable); + try { + task.execute(attachment); + } catch (final RejectedExecutionException ignored) { + } + } + } + } + + @Override + public int getItemCount() { + return mediaPreviews.size(); + } + + public boolean hasAttachments() { + return mediaPreviews.size() > 0; + } + + public ArrayList<Attachment> getAttachments() { + return mediaPreviews; + } + + public void clearPreviews() { + this.mediaPreviews.clear(); + } + + static class AsyncDrawable extends BitmapDrawable { + private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; + + AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { + super(res, bitmap); + bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask); + } + + BitmapWorkerTask getBitmapWorkerTask() { + return bitmapWorkerTaskReference.get(); + } + } + + class MediaPreviewViewHolder extends RecyclerView.ViewHolder { + + private final MediaPreviewBinding binding; + + MediaPreviewViewHolder(MediaPreviewBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } + + class BitmapWorkerTask extends AsyncTask<Attachment, Void, Bitmap> { + private final WeakReference<ImageView> imageViewReference; + private Attachment attachment = null; + + BitmapWorkerTask(ImageView imageView) { + imageViewReference = new WeakReference<>(imageView); + } + + @Override + protected Bitmap doInBackground(Attachment... params) { + Activity activity = conversationFragment.getActivity(); + if (activity instanceof XmppActivity) { + final XmppActivity xmppActivity = (XmppActivity) activity; + this.attachment = params[0]; + return xmppActivity.xmppConnectionService.getFileBackend().getPreviewForUri(this.attachment, Math.round(xmppActivity.getResources().getDimension(R.dimen.media_preview_size)), false); + } else { + return null; + } + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (bitmap != null && !isCancelled()) { + final ImageView imageView = imageViewReference.get(); + if (imageView != null) { + imageView.setImageBitmap(bitmap); + imageView.setBackgroundColor(0x00000000); + } + } + } + } +}
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java b/src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java index 519b01c1a..b13ccb910 100644 --- a/src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java +++ b/src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java @@ -2,11 +2,9 @@ package de.pixart.messenger.ui.adapter; import android.Manifest; import android.app.Activity; -import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Color; @@ -29,7 +27,6 @@ import android.text.style.RelativeSizeSpan; import android.text.style.StyleSpan; import android.util.Base64; import android.util.DisplayMetrics; -import android.util.Log; import android.view.ActionMode; import android.view.Menu; import android.view.MenuItem; @@ -72,11 +69,11 @@ import de.pixart.messenger.services.MessageArchiveService; import de.pixart.messenger.services.NotificationService; import de.pixart.messenger.ui.ConversationFragment; import de.pixart.messenger.ui.ConversationsActivity; -import de.pixart.messenger.ui.ShowFullscreenMessageActivity; import de.pixart.messenger.ui.XmppActivity; import de.pixart.messenger.ui.text.DividerSpan; import de.pixart.messenger.ui.text.QuoteSpan; import de.pixart.messenger.ui.util.MyLinkify; +import de.pixart.messenger.ui.util.ViewUtil; import de.pixart.messenger.ui.widget.ClickableMovementMethod; import de.pixart.messenger.ui.widget.CopyTextView; import de.pixart.messenger.ui.widget.ListSelectionManager; @@ -112,8 +109,8 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie public MessageAdapter(XmppActivity activity, List<Message> messages) { super(activity, 0, messages); - this.audioPlayer = new AudioPlayer(this); this.activity = activity; + this.audioPlayer = new AudioPlayer(this); metrics = getContext().getResources().getDisplayMetrics(); updatePreferences(); } @@ -149,6 +146,14 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie } } + public void flagDisableInputs() { + activity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); + } + + public void flagEnableInputs() { + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); + } + public void flagScreenOn() { activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } @@ -490,7 +495,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie } else { viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1); } - viewHolder.messageBody.setHighlightColor(darkBackground ? type == SENT ? ContextCompat.getColor(activity, R.color.black26) : ContextCompat.getColor(activity, R.color.grey800) : ContextCompat.getColor(activity, R.color.grey500)); + viewHolder.messageBody.setHighlightColor(darkBackground ? type == SENT ? ContextCompat.getColor(activity, R.color.accent) : ContextCompat.getColor(activity, R.color.accent) : ContextCompat.getColor(activity, R.color.accent)); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); if (message.getBody() != null) { final String nick = UIHelper.getMessageDisplayName(message); @@ -870,7 +875,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie .setOnLongClickListener(v -> { if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) { MessageAdapter.this.mOnContactPictureLongClickedListener - .onContactPictureLongClicked(message); + .onContactPictureLongClicked(v, message); return true; } else { return false; @@ -1019,6 +1024,10 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie audioPlayer.stop(); } + public void unregisterListenerInAudioPlayer() { + audioPlayer.unregisterListener(); + } + public void startStopPending() { audioPlayer.startStopPending(); } @@ -1035,52 +1044,10 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie return; } String mime = file.getMimeType(); - if (mime.startsWith("image/")) { - Intent intent = new Intent(getContext(), ShowFullscreenMessageActivity.class); - intent.putExtra("image", Uri.fromFile(file)); - try { - activity.startActivity(intent); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - return; - } catch (ActivityNotFoundException e) { - //ignored - } - } else if (mime.startsWith("video/")) { - Intent intent = new Intent(getContext(), ShowFullscreenMessageActivity.class); - intent.putExtra("video", Uri.fromFile(file)); - try { - activity.startActivity(intent); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - return; - } catch (ActivityNotFoundException e) { - //ignored - } - } - Intent openIntent = new Intent(Intent.ACTION_VIEW); if (mime == null) { mime = "*/*"; } - Uri uri; - try { - uri = FileBackend.getUriForFile(activity, file); - } catch (SecurityException e) { - Log.d(Config.LOGTAG, "No permission to access " + file.getAbsolutePath(), e); - Toast.makeText(activity, activity.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show(); - return; - } - openIntent.setDataAndType(uri, mime); - openIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - PackageManager manager = activity.getPackageManager(); - List<ResolveInfo> info = manager.queryIntentActivities(openIntent, 0); - if (info.size() == 0) { - openIntent.setDataAndType(uri,"*/*"); - } - try { - getContext().startActivity(openIntent); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } catch (ActivityNotFoundException e) { - Toast.makeText(activity, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show(); - } + ViewUtil.view(activity, file, mime); } public void showLocation(Message message) { @@ -1139,7 +1106,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie } public interface OnContactPictureLongClicked { - void onContactPictureLongClicked(Message message); + void onContactPictureLongClicked(View v, Message message); } private static class ViewHolder { diff --git a/src/main/java/de/pixart/messenger/ui/forms/FormFieldWrapper.java b/src/main/java/de/pixart/messenger/ui/forms/FormFieldWrapper.java index adcc11809..0dfd86327 100644 --- a/src/main/java/de/pixart/messenger/ui/forms/FormFieldWrapper.java +++ b/src/main/java/de/pixart/messenger/ui/forms/FormFieldWrapper.java @@ -1,7 +1,6 @@ package de.pixart.messenger.ui.forms; import android.content.Context; -import android.support.v4.content.ContextCompat; import android.text.SpannableString; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; @@ -11,6 +10,7 @@ import android.view.View; import java.util.List; import de.pixart.messenger.R; +import de.pixart.messenger.ui.util.StyledAttributes; import de.pixart.messenger.xmpp.forms.Field; public abstract class FormFieldWrapper { @@ -18,9 +18,9 @@ public abstract class FormFieldWrapper { protected final Context context; protected final Field field; protected final View view; - protected OnFormFieldValuesEdited onFormFieldValuesEditedListener; + OnFormFieldValuesEdited onFormFieldValuesEditedListener; - protected FormFieldWrapper(Context context, Field field) { + FormFieldWrapper(Context context, Field field) { this.context = context; this.field = field; LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -58,7 +58,7 @@ public abstract class FormFieldWrapper { int start = label.length(); int end = label.length() + 2; spannableString.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), start, end, 0); - spannableString.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, R.color.accent)), start, end, 0); + spannableString.setSpan(new ForegroundColorSpan(StyledAttributes.getColor(context, R.color.accent)), start, end, 0); } return spannableString; } diff --git a/src/main/java/de/pixart/messenger/ui/interfaces/OnMediaLoaded.java b/src/main/java/de/pixart/messenger/ui/interfaces/OnMediaLoaded.java new file mode 100644 index 000000000..0441be96d --- /dev/null +++ b/src/main/java/de/pixart/messenger/ui/interfaces/OnMediaLoaded.java @@ -0,0 +1,10 @@ +package de.pixart.messenger.ui.interfaces; + +import java.util.List; + +import de.pixart.messenger.ui.util.Attachment; + +public interface OnMediaLoaded { + + void onMediaLoaded(List<Attachment> attachments); +}
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/ui/util/Attachment.java b/src/main/java/de/pixart/messenger/ui/util/Attachment.java new file mode 100644 index 000000000..96159c1bd --- /dev/null +++ b/src/main/java/de/pixart/messenger/ui/util/Attachment.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2018, Daniel Gultsch All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package de.pixart.messenger.ui.util; + +import android.content.ClipData; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import de.pixart.messenger.Config; +import de.pixart.messenger.utils.MimeUtils; + +public class Attachment implements Parcelable { + public static final Creator<Attachment> CREATOR = new Parcelable.Creator<Attachment>() { + @Override + public Attachment createFromParcel(Parcel in) { + return new Attachment(in); + } + + @Override + public Attachment[] newArray(int size) { + return new Attachment[size]; + } + }; + private final Uri uri; + private final Type type; + private final UUID uuid; + private final String mime; + + Attachment(Parcel in) { + uri = in.readParcelable(Uri.class.getClassLoader()); + mime = in.readString(); + uuid = UUID.fromString(in.readString()); + type = Type.valueOf(in.readString()); + } + + private Attachment(UUID uuid, Uri uri, Type type, String mime) { + this.uri = uri; + this.type = type; + this.mime = mime; + this.uuid = uuid; + } + + private Attachment(Uri uri, Type type, String mime) { + this.uri = uri; + this.type = type; + this.mime = mime; + this.uuid = UUID.randomUUID(); + } + + public static List<Attachment> of(final Context context, Uri uri, Type type) { + final String mime = type == Type.LOCATION ? null : MimeUtils.guessMimeTypeFromUri(context, uri); + return Collections.singletonList(new Attachment(uri, type, mime)); + } + + public static List<Attachment> of(final Context context, List<Uri> uris) { + List<Attachment> attachments = new ArrayList<>(); + for (Uri uri : uris) { + final String mime = MimeUtils.guessMimeTypeFromUri(context, uri); + attachments.add(new Attachment(uri, mime != null && mime.startsWith("image/") ? Type.IMAGE : Type.FILE, mime)); + } + return attachments; + } + + public static Attachment of(UUID uuid, final File file, String mime) { + return new Attachment(uuid, Uri.fromFile(file), mime != null && (mime.startsWith("image/") || mime.startsWith("video/")) ? Type.IMAGE : Type.FILE, mime); + } + + public static List<Attachment> extractAttachments(final Context context, final Intent intent, Type type) { + List<Attachment> uris = new ArrayList<>(); + if (intent == null) { + return uris; + } + final String contentType = intent.getType(); + final Uri data = intent.getData(); + if (data == null) { + final ClipData clipData = intent.getClipData(); + if (clipData != null) { + for (int i = 0; i < clipData.getItemCount(); ++i) { + final Uri uri = clipData.getItemAt(i).getUri(); + Log.d(Config.LOGTAG, "uri=" + uri + " contentType=" + contentType); + final String mime = contentType != null ? contentType : MimeUtils.guessMimeTypeFromUri(context, uri); + Log.d(Config.LOGTAG, "mime=" + mime); + uris.add(new Attachment(uri, type, mime)); + } + } + } else { + final String mime = contentType != null ? contentType : MimeUtils.guessMimeTypeFromUri(context, data); + uris.add(new Attachment(data, type, mime)); + } + return uris; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(uri, flags); + dest.writeString(mime); + dest.writeString(uuid.toString()); + dest.writeString(type.toString()); + } + + @Override + public int describeContents() { + return 0; + } + + public String getMime() { + return mime; + } + + public Type getType() { + return type; + } + + public boolean renderThumbnail() { + return type == Type.IMAGE || (type == Type.FILE && mime != null && (mime.startsWith("video/") || mime.startsWith("image/"))); + } + + public Uri getUri() { + return uri; + } + + public UUID getUuid() { + return uuid; + } + + public enum Type { + FILE, IMAGE, LOCATION, RECORDING + } +}
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/ui/util/AttachmentTool.java b/src/main/java/de/pixart/messenger/ui/util/AttachmentTool.java deleted file mode 100644 index c56f27ebf..000000000 --- a/src/main/java/de/pixart/messenger/ui/util/AttachmentTool.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package de.pixart.messenger.ui.util; - -import android.annotation.SuppressLint; -import android.content.ClipData; -import android.content.Intent; -import android.net.Uri; - -import java.util.ArrayList; -import java.util.List; - -public class AttachmentTool { - @SuppressLint("NewApi") - public static List<Uri> extractUriFromIntent(final Intent intent) { - List<Uri> uris = new ArrayList<>(); - if (intent == null) { - return uris; - } - final Uri uri = intent.getData(); - if (uri == null) { - final ClipData clipData = intent.getClipData(); - if (clipData != null) { - for (int i = 0; i < clipData.getItemCount(); ++i) { - uris.add(clipData.getItemAt(i).getUri()); - } - } - } else { - uris.add(uri); - } - return uris; - } -}
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/ui/util/ConversationMenuConfigurator.java b/src/main/java/de/pixart/messenger/ui/util/ConversationMenuConfigurator.java index 5c86b4a1b..edca88f78 100644 --- a/src/main/java/de/pixart/messenger/ui/util/ConversationMenuConfigurator.java +++ b/src/main/java/de/pixart/messenger/ui/util/ConversationMenuConfigurator.java @@ -65,12 +65,13 @@ public class ConversationMenuConfigurator { menu.findItem(R.id.attach_location).setVisible(locationAvailable); } - public static void configureAttachmentMenu(@NonNull Conversation conversation, Menu menu, Boolean Quick_share_attachment_choice) { + public static void configureAttachmentMenu(@NonNull Conversation conversation, Menu menu, Boolean Quick_share_attachment_choice, boolean hasAttachments) { final MenuItem menuAttach = menu.findItem(R.id.action_attach_file); - if (Quick_share_attachment_choice) { + if (Quick_share_attachment_choice && !hasAttachments) { menuAttach.setVisible(false); return; } + final boolean visible; if (conversation.getMode() == Conversation.MODE_MULTI) { visible = conversation.getAccount().httpUploadAvailable() && conversation.getMucOptions().participating(); diff --git a/src/main/java/de/pixart/messenger/ui/util/Drawable.java b/src/main/java/de/pixart/messenger/ui/util/Drawable.java deleted file mode 100644 index 5e0c770d4..000000000 --- a/src/main/java/de/pixart/messenger/ui/util/Drawable.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package de.pixart.messenger.ui.util; - -import android.content.Context; -import android.content.res.TypedArray; -import android.support.annotation.AttrRes; - -public class Drawable { - public static android.graphics.drawable.Drawable get(Context context, @AttrRes int id) { - TypedArray typedArray = context.obtainStyledAttributes(new int[]{id}); - android.graphics.drawable.Drawable drawable = typedArray.getDrawable(0); - typedArray.recycle(); - return drawable; - } -}
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/ui/util/GridManager.java b/src/main/java/de/pixart/messenger/ui/util/GridManager.java new file mode 100644 index 000000000..ae615b051 --- /dev/null +++ b/src/main/java/de/pixart/messenger/ui/util/GridManager.java @@ -0,0 +1,76 @@ +package de.pixart.messenger.ui.util; + +import android.content.Context; +import android.support.annotation.DimenRes; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.ViewTreeObserver; + +import de.pixart.messenger.Config; +import de.pixart.messenger.ui.adapter.MediaAdapter; + +public class GridManager { + + public static void setupLayoutManager(final Context context, RecyclerView recyclerView, @DimenRes int desiredSize) { + int maxWidth = context.getResources().getDisplayMetrics().widthPixels; + ColumnInfo columnInfo = calculateColumnCount(context, maxWidth, desiredSize); + Log.d(Config.LOGTAG, "preliminary count=" + columnInfo.count); + MediaAdapter.setMediaSize(recyclerView, columnInfo.width); + recyclerView.setLayoutManager(new GridLayoutManager(context, columnInfo.count)); + recyclerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + final int availableWidth = recyclerView.getMeasuredWidth(); + if (availableWidth == 0) { + Log.e(Config.LOGTAG, "GridManager: available width was 0; probably because layout was hidden"); + return; + } + final ColumnInfo columnInfo = calculateColumnCount(context, recyclerView.getMeasuredWidth(), desiredSize); + Log.d(Config.LOGTAG, "final count " + columnInfo.count); + if (recyclerView.getAdapter().getItemCount() != 0) { + Log.e(Config.LOGTAG, "adapter already has items; just go with it now"); + return; + } + setupLayoutManagerInternal(recyclerView, columnInfo); + MediaAdapter.setMediaSize(recyclerView, columnInfo.width); + } + }); + } + + private static void setupLayoutManagerInternal(RecyclerView recyclerView, final ColumnInfo columnInfo) { + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + if (layoutManager instanceof GridLayoutManager) { + ((GridLayoutManager) layoutManager).setSpanCount(columnInfo.count); + } + } + + private static ColumnInfo calculateColumnCount(Context context, int availableWidth, @DimenRes int desiredSize) { + final float desiredWidth = context.getResources().getDimension(desiredSize); + final int columns = Math.round(availableWidth / desiredWidth); + final int realWidth = availableWidth / columns; + Log.d(Config.LOGTAG, "desired=" + desiredWidth + " real=" + realWidth); + return new ColumnInfo(columns, realWidth); + } + + public static int getCurrentColumnCount(RecyclerView recyclerView) { + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + if (layoutManager instanceof GridLayoutManager) { + return ((GridLayoutManager) layoutManager).getSpanCount(); + } else { + return 0; + } + } + + public static class ColumnInfo { + private final int count; + private final int width; + + private ColumnInfo(int count, int width) { + this.count = count; + this.width = width; + } + } + +}
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/ui/util/MucDetailsContextMenuHelper.java b/src/main/java/de/pixart/messenger/ui/util/MucDetailsContextMenuHelper.java new file mode 100644 index 000000000..dc338e02c --- /dev/null +++ b/src/main/java/de/pixart/messenger/ui/util/MucDetailsContextMenuHelper.java @@ -0,0 +1,163 @@ +package de.pixart.messenger.ui.util; + +import android.app.Activity; +import android.preference.PreferenceManager; +import android.support.v7.app.AlertDialog; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.TypefaceSpan; +import android.view.Menu; +import android.view.MenuItem; + +import de.pixart.messenger.Config; +import de.pixart.messenger.R; +import de.pixart.messenger.entities.Contact; +import de.pixart.messenger.entities.Conversation; +import de.pixart.messenger.entities.MucOptions; +import de.pixart.messenger.entities.MucOptions.User; +import de.pixart.messenger.services.XmppConnectionService; +import de.pixart.messenger.ui.ConferenceDetailsActivity; +import de.pixart.messenger.ui.ConversationFragment; +import de.pixart.messenger.ui.ConversationsActivity; +import de.pixart.messenger.ui.XmppActivity; +import rocks.xmpp.addr.Jid; + +public final class MucDetailsContextMenuHelper { + public static void configureMucDetailsContextMenu(Activity activity, Menu menu, Conversation conversation, User user) { + final boolean advancedMode = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean("advanced_muc_mode", false); + if (user != null && user.getRealJid() != null) { + MenuItem showContactDetails = menu.findItem(R.id.action_contact_details); + MenuItem startConversation = menu.findItem(R.id.start_conversation); + MenuItem giveMembership = menu.findItem(R.id.give_membership); + MenuItem removeMembership = menu.findItem(R.id.remove_membership); + MenuItem giveAdminPrivileges = menu.findItem(R.id.give_admin_privileges); + MenuItem removeAdminPrivileges = menu.findItem(R.id.remove_admin_privileges); + MenuItem removeFromRoom = menu.findItem(R.id.remove_from_room); + MenuItem banFromConference = menu.findItem(R.id.ban_from_conference); + MenuItem invite = menu.findItem(R.id.invite); + startConversation.setVisible(true); + final Contact contact = user.getContact(); + final User self = conversation.getMucOptions().getSelf(); + if (contact != null && contact.showInRoster()) { + showContactDetails.setVisible(!contact.isSelf()); + } + if (activity instanceof ConferenceDetailsActivity && user.getRole() == MucOptions.Role.NONE) { + invite.setVisible(true); + } + if (self.getAffiliation().ranks(MucOptions.Affiliation.ADMIN) && + self.getAffiliation().outranks(user.getAffiliation())) { + if (advancedMode) { + if (user.getAffiliation() == MucOptions.Affiliation.NONE) { + giveMembership.setVisible(true); + } else { + removeMembership.setVisible(true); + } + if (!Config.DISABLE_BAN) { + banFromConference.setVisible(true); + } + } else { + if (!Config.DISABLE_BAN || conversation.getMucOptions().membersOnly()) { + removeFromRoom.setVisible(true); + } + } + if (user.getAffiliation() != MucOptions.Affiliation.ADMIN) { + giveAdminPrivileges.setVisible(true); + } else { + removeAdminPrivileges.setVisible(true); + } + } + } else { + final MucOptions mucOptions = conversation.getMucOptions(); + MenuItem sendPrivateMessage = menu.findItem(R.id.send_private_message); + sendPrivateMessage.setVisible(true); + sendPrivateMessage.setEnabled(user != null && mucOptions.allowPm() && user.getRole().ranks(MucOptions.Role.VISITOR)); + } + } + + public static boolean onContextItemSelected(MenuItem item, User user, Conversation conversation, XmppActivity activity) { + final XmppConnectionService.OnAffiliationChanged onAffiliationChanged = activity instanceof XmppConnectionService.OnAffiliationChanged ? (XmppConnectionService.OnAffiliationChanged) activity : null; + final XmppConnectionService.OnRoleChanged onRoleChanged = activity instanceof XmppConnectionService.OnRoleChanged ? (XmppConnectionService.OnRoleChanged) activity : null; + Jid jid = user.getRealJid(); + switch (item.getItemId()) { + case R.id.action_contact_details: + Contact contact = user.getContact(); + if (contact != null) { + activity.switchToContactDetails(contact); + } + return true; + case R.id.start_conversation: + startConversation(user, conversation, activity); + return true; + case R.id.give_admin_privileges: + activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.ADMIN, onAffiliationChanged); + return true; + case R.id.give_membership: + activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.MEMBER, onAffiliationChanged); + return true; + case R.id.remove_membership: + activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.NONE, onAffiliationChanged); + return true; + case R.id.remove_admin_privileges: + activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.MEMBER, onAffiliationChanged); + return true; + case R.id.remove_from_room: + removeFromRoom(user, conversation, activity, onAffiliationChanged, onRoleChanged); + return true; + case R.id.ban_from_conference: + activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.OUTCAST, onAffiliationChanged); + if (user.getRole() != MucOptions.Role.NONE) { + activity.xmppConnectionService.changeRoleInConference(conversation, user.getName(), MucOptions.Role.NONE, onRoleChanged); + } + return true; + case R.id.send_private_message: + if (activity instanceof ConversationsActivity) { + ConversationFragment conversationFragment = ConversationFragment.get(activity); + if (conversationFragment != null) { + conversationFragment.privateMessageWith(user.getFullJid()); + return true; + } + } + activity.privateMsgInMuc(conversation, user.getName()); + return true; + case R.id.invite: + activity.xmppConnectionService.directInvite(conversation, jid); + return true; + default: + return false; + } + } + + public static void removeFromRoom(final User user, Conversation conversation, XmppActivity activity, XmppConnectionService.OnAffiliationChanged onAffiliationChanged, XmppConnectionService.OnRoleChanged onRoleChanged) { + if (conversation.getMucOptions().membersOnly()) { + activity.xmppConnectionService.changeAffiliationInConference(conversation, user.getRealJid(), MucOptions.Affiliation.NONE, onAffiliationChanged); + if (user.getRole() != MucOptions.Role.NONE) { + activity.xmppConnectionService.changeRoleInConference(conversation, user.getName(), MucOptions.Role.NONE, onRoleChanged); + } + } else { + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(R.string.ban_from_conference); + String jid = user.getRealJid().asBareJid().toString(); + SpannableString message = new SpannableString(activity.getString(R.string.removing_from_public_conference, jid)); + int start = message.toString().indexOf(jid); + if (start >= 0) { + message.setSpan(new TypefaceSpan("monospace"), start, start + jid.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + builder.setMessage(message); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.ban_now, (dialog, which) -> { + activity.xmppConnectionService.changeAffiliationInConference(conversation, user.getRealJid(), MucOptions.Affiliation.OUTCAST, onAffiliationChanged); + if (user.getRole() != MucOptions.Role.NONE) { + activity.xmppConnectionService.changeRoleInConference(conversation, user.getName(), MucOptions.Role.NONE, onRoleChanged); + } + }); + builder.create().show(); + } + } + + public static void startConversation(User user, Conversation conversation, XmppActivity activity) { + if (user.getRealJid() != null) { + Conversation newConversation = activity.xmppConnectionService.findOrCreateConversation(conversation.getAccount(), user.getRealJid().asBareJid(), false, true); + activity.switchToConversation(newConversation); + } + } +}
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/ui/util/PendingItem.java b/src/main/java/de/pixart/messenger/ui/util/PendingItem.java index eaddf3a8c..d06f5bfc9 100644 --- a/src/main/java/de/pixart/messenger/ui/util/PendingItem.java +++ b/src/main/java/de/pixart/messenger/ui/util/PendingItem.java @@ -46,4 +46,10 @@ public class PendingItem<T> { public synchronized T peek() { return item; } + + public synchronized boolean clear() { + boolean notNull = this.item != null; + this.item = null; + return notNull; + } }
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/ui/util/SendButtonAction.java b/src/main/java/de/pixart/messenger/ui/util/SendButtonAction.java index 9f9fabc5a..c7feb2105 100644 --- a/src/main/java/de/pixart/messenger/ui/util/SendButtonAction.java +++ b/src/main/java/de/pixart/messenger/ui/util/SendButtonAction.java @@ -33,10 +33,11 @@ import static de.pixart.messenger.ui.ConversationFragment.ATTACHMENT_CHOICE; import static de.pixart.messenger.ui.ConversationFragment.ATTACHMENT_CHOICE_CHOOSE_IMAGE; import static de.pixart.messenger.ui.ConversationFragment.ATTACHMENT_CHOICE_LOCATION; import static de.pixart.messenger.ui.ConversationFragment.ATTACHMENT_CHOICE_RECORD_VOICE; -import static de.pixart.messenger.ui.ConversationFragment.ATTACHMENT_CHOICE_TAKE_FROM_CAMERA; +import static de.pixart.messenger.ui.ConversationFragment.ATTACHMENT_CHOICE_RECORD_VIDEO; +import static de.pixart.messenger.ui.ConversationFragment.ATTACHMENT_CHOICE_TAKE_PHOTO; public enum SendButtonAction { - TEXT, TAKE_FROM_CAMERA, SEND_LOCATION, RECORD_VOICE, CANCEL, CHOOSE_PICTURE, CHOOSE_ATTACHMENT; + TEXT, TAKE_PHOTO, SEND_LOCATION, RECORD_VOICE, CANCEL, CHOOSE_PICTURE, RECORD_VIDEO, CHOOSE_ATTACHMENT; public static SendButtonAction valueOfOrDefault(String setting, SendButtonAction text) { try { @@ -52,8 +53,10 @@ public enum SendButtonAction { return SEND_LOCATION; case ATTACHMENT_CHOICE_RECORD_VOICE: return RECORD_VOICE; - case ATTACHMENT_CHOICE_TAKE_FROM_CAMERA: - return TAKE_FROM_CAMERA; + case ATTACHMENT_CHOICE_RECORD_VIDEO: + return RECORD_VIDEO; + case ATTACHMENT_CHOICE_TAKE_PHOTO: + return TAKE_PHOTO; case ATTACHMENT_CHOICE_CHOOSE_IMAGE: return CHOOSE_PICTURE; case ATTACHMENT_CHOICE: @@ -65,8 +68,10 @@ public enum SendButtonAction { public int toChoice() { switch (this) { - case TAKE_FROM_CAMERA: - return ATTACHMENT_CHOICE_TAKE_FROM_CAMERA; + case TAKE_PHOTO: + return ATTACHMENT_CHOICE_TAKE_PHOTO; + case RECORD_VIDEO: + return ATTACHMENT_CHOICE_RECORD_VIDEO; case SEND_LOCATION: return ATTACHMENT_CHOICE_LOCATION; case RECORD_VOICE: diff --git a/src/main/java/de/pixart/messenger/ui/util/SendButtonTool.java b/src/main/java/de/pixart/messenger/ui/util/SendButtonTool.java index 291edb822..96bcad183 100644 --- a/src/main/java/de/pixart/messenger/ui/util/SendButtonTool.java +++ b/src/main/java/de/pixart/messenger/ui/util/SendButtonTool.java @@ -38,14 +38,12 @@ import de.pixart.messenger.R; import de.pixart.messenger.entities.Conversation; import de.pixart.messenger.entities.Presence; import de.pixart.messenger.ui.ConversationFragment; +import de.pixart.messenger.ui.SettingsActivity; import de.pixart.messenger.utils.UIHelper; -import static de.pixart.messenger.Config.QUICK_SHARE_ATTACHMENT_CHOICE; - public class SendButtonTool { public static SendButtonAction getAction(Activity activity, Conversation c, String text) { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); final boolean empty = text.length() == 0; final boolean conference = c.getMode() == Conversation.MODE_MULTI; if (c.getCorrectingMessage() != null && (empty || text.equals(c.getCorrectingMessage().getBody()))) { @@ -58,15 +56,16 @@ public class SendButtonTool { } } else { if (empty) { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); if (conference && c.getNextCounterpart() != null) { return SendButtonAction.CANCEL; } else { - if (QUICK_SHARE_ATTACHMENT_CHOICE && AttachmentsVisible(c)) { + String setting = preferences.getString("quick_action", activity.getResources().getString(R.string.quick_action)); + if (quickShareChoice(activity) && AttachmentsVisible(c)) { return SendButtonAction.CHOOSE_ATTACHMENT; - } else if (QUICK_SHARE_ATTACHMENT_CHOICE && !AttachmentsVisible(c)) { + } else if (quickShareChoice(activity) && !AttachmentsVisible(c)) { return SendButtonAction.TEXT; } else { - String setting = preferences.getString("quick_action", activity.getResources().getString(R.string.quick_action)); if (!setting.equals("none") && UIHelper.receivedLocationQuestion(c.getLatestMessage())) { return SendButtonAction.SEND_LOCATION; } else { @@ -106,7 +105,20 @@ public class SendButtonTool { default: return getThemeResource(activity, R.attr.ic_send_text_offline, R.drawable.ic_send_text_offline); } - case TAKE_FROM_CAMERA: + case RECORD_VIDEO: + switch (status) { + case CHAT: + case ONLINE: + return R.drawable.ic_send_videocam_online; + case AWAY: + return R.drawable.ic_send_videocam_away; + case XA: + case DND: + return R.drawable.ic_send_videocam_dnd; + default: + return getThemeResource(activity, R.attr.ic_send_videocam_offline, R.drawable.ic_send_videocam_offline); + } + case TAKE_PHOTO: switch (status) { case CHAT: case ONLINE: @@ -199,4 +211,8 @@ public class SendButtonTool { return res; } + public static boolean quickShareChoice(Activity activity) { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); + return preferences.getBoolean(SettingsActivity.QUICK_SHARE_ATTACHMENT_CHOICE, activity.getResources().getBoolean(R.bool.quick_share_attachment_choice)); + } }
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/ui/util/Color.java b/src/main/java/de/pixart/messenger/ui/util/StyledAttributes.java index 58ce6a465..cd4ad3e49 100644 --- a/src/main/java/de/pixart/messenger/ui/util/Color.java +++ b/src/main/java/de/pixart/messenger/ui/util/StyledAttributes.java @@ -35,14 +35,26 @@ import android.content.res.TypedArray; import android.support.annotation.AttrRes; import android.support.annotation.ColorInt; -public class Color { +public class StyledAttributes { + public static android.graphics.drawable.Drawable getDrawable(Context context, @AttrRes int id) { + TypedArray typedArray = context.obtainStyledAttributes(new int[]{id}); + android.graphics.drawable.Drawable drawable = typedArray.getDrawable(0); + typedArray.recycle(); + return drawable; + } + + public static float getFloat(Context context, @AttrRes int id) { + TypedArray typedArray = context.obtainStyledAttributes(new int[]{id}); + float value = typedArray.getFloat(0, 0f); + typedArray.recycle(); + return value; + } public static @ColorInt - int get(Context context, @AttrRes int attr) { + int getColor(Context context, @AttrRes int attr) { TypedArray typedArray = context.obtainStyledAttributes(new int[]{attr}); int color = typedArray.getColor(0, 0); typedArray.recycle(); return color; } - }
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/ui/util/ViewUtil.java b/src/main/java/de/pixart/messenger/ui/util/ViewUtil.java new file mode 100644 index 000000000..b1905479b --- /dev/null +++ b/src/main/java/de/pixart/messenger/ui/util/ViewUtil.java @@ -0,0 +1,72 @@ +package de.pixart.messenger.ui.util; + +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.util.Log; +import android.widget.Toast; + +import java.io.File; +import java.util.List; + +import de.pixart.messenger.Config; +import de.pixart.messenger.R; +import de.pixart.messenger.persistance.FileBackend; +import de.pixart.messenger.ui.ShowFullscreenMessageActivity; + +public class ViewUtil { + + public static void view(Context context, Attachment attachment) { + File file = new File(attachment.getUri().getPath()); + final String mime = attachment.getMime() == null ? "*/*" : attachment.getMime(); + view(context, file, mime); + } + + public static void view(Context context, File file, String mime) { + Uri uri; + try { + uri = FileBackend.getUriForFile(context, file); + } catch (SecurityException e) { + Log.d(Config.LOGTAG, "No permission to access " + file.getAbsolutePath(), e); + Toast.makeText(context, context.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show(); + return; + } + // use internal viewer for images and videos + if (mime.startsWith("image/")) { + Intent intent = new Intent(context, ShowFullscreenMessageActivity.class); + intent.putExtra("image", Uri.fromFile(file)); + try { + context.startActivity(intent); + return; + } catch (ActivityNotFoundException e) { + //ignored + } + } else if (mime.startsWith("video/")) { + Intent intent = new Intent(context, ShowFullscreenMessageActivity.class); + intent.putExtra("video", Uri.fromFile(file)); + try { + context.startActivity(intent); + return; + } catch (ActivityNotFoundException e) { + //ignored + } + } else { + Intent openIntent = new Intent(Intent.ACTION_VIEW); + openIntent.setDataAndType(uri, mime); + openIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + PackageManager manager = context.getPackageManager(); + List<ResolveInfo> info = manager.queryIntentActivities(openIntent, 0); + if (info.size() == 0) { + openIntent.setDataAndType(uri, "*/*"); + } + try { + context.startActivity(openIntent); + } catch (ActivityNotFoundException e) { + Toast.makeText(context, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show(); + } + } + } +}
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/ui/widget/SquareFrameLayout.java b/src/main/java/de/pixart/messenger/ui/widget/SquareFrameLayout.java new file mode 100644 index 000000000..4d2fcf92d --- /dev/null +++ b/src/main/java/de/pixart/messenger/ui/widget/SquareFrameLayout.java @@ -0,0 +1,25 @@ +package de.pixart.messenger.ui.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +public class SquareFrameLayout extends FrameLayout { + public SquareFrameLayout(Context context) { + super(context); + } + + public SquareFrameLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SquareFrameLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + //noinspection SuspiciousNameCombination + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + } +}
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/utils/Compatibility.java b/src/main/java/de/pixart/messenger/utils/Compatibility.java new file mode 100644 index 000000000..3ebc7c4f6 --- /dev/null +++ b/src/main/java/de/pixart/messenger/utils/Compatibility.java @@ -0,0 +1,76 @@ +package de.pixart.messenger.utils; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Build; +import android.preference.Preference; +import android.preference.PreferenceCategory; +import android.preference.PreferenceManager; +import android.preference.PreferenceScreen; +import android.support.annotation.BoolRes; +import android.support.v4.content.ContextCompat; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import de.pixart.messenger.R; +import de.pixart.messenger.ui.SettingsActivity; +import de.pixart.messenger.ui.SettingsFragment; + +public class Compatibility { + private static final List<String> UNUSED_SETTINGS_POST_TWENTYSIX = Arrays.asList( + SettingsActivity.SHOW_FOREGROUND_SERVICE, + "led", + "notification_ringtone", + "notification_headsup", + "vibrate_on_notification"); + private static final List<String> UNUESD_SETTINGS_PRE_TWENTYSIX = Collections.singletonList("more_notification_settings"); + + public static boolean hasStoragePermission(Context context) { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + } + + public static boolean twentySix() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; + } + + public static boolean twentyTwo() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1; + } + + private static boolean getBooleanPreference(Context context, String name, @BoolRes int res) { + return getPreferences(context).getBoolean(name, context.getResources().getBoolean(res)); + } + + private static SharedPreferences getPreferences(final Context context) { + return PreferenceManager.getDefaultSharedPreferences(context); + } + + public static boolean keepForegroundService(Context context) { + return twentySix() || getBooleanPreference(context, SettingsActivity.SHOW_FOREGROUND_SERVICE, R.bool.show_foreground_service); + } + + public static void removeUnusedPreferences(SettingsFragment settingsFragment) { + List<PreferenceScreen> screens = Arrays.asList( + (PreferenceScreen) settingsFragment.findPreference("notifications")); + List<PreferenceCategory> categories = Arrays.asList( + (PreferenceCategory) settingsFragment.findPreference("general")); + for (String key : (twentySix() ? UNUSED_SETTINGS_POST_TWENTYSIX : UNUESD_SETTINGS_PRE_TWENTYSIX)) { + Preference preference = settingsFragment.findPreference(key); + if (preference != null) { + for (PreferenceScreen screen : screens) { + if (screen != null) { + screen.removePreference(preference); + } + } + for (PreferenceCategory category : categories) { + if (category != null) { + category.removePreference(preference); + } + } + } + } + } +}
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/utils/ConversationsFileObserver.java b/src/main/java/de/pixart/messenger/utils/ConversationsFileObserver.java index 4e275df3f..73d5589e4 100644 --- a/src/main/java/de/pixart/messenger/utils/ConversationsFileObserver.java +++ b/src/main/java/de/pixart/messenger/utils/ConversationsFileObserver.java @@ -2,12 +2,15 @@ package de.pixart.messenger.utils; import android.os.FileObserver; +import android.util.Log; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Stack; +import de.pixart.messenger.Config; + /** * Copyright (C) 2012 Bartek Przybylski * Copyright (C) 2015 ownCloud Inc. @@ -19,7 +22,7 @@ public abstract class ConversationsFileObserver { private final String path; private final List<SingleFileObserver> mObservers = new ArrayList<>(); - public ConversationsFileObserver(String path) { + protected ConversationsFileObserver(String path) { this.path = path; } @@ -88,13 +91,17 @@ public abstract class ConversationsFileObserver { private class SingleFileObserver extends FileObserver { private final String path; - public SingleFileObserver(String path, int mask) { + SingleFileObserver(String path, int mask) { super(path, mask); this.path = path; } @Override public void onEvent(int event, String filename) { + if (filename == null) { + Log.d(Config.LOGTAG, "ignored file event with NULL filename (event=" + event + ")"); + return; + } ConversationsFileObserver.this.onEvent(event, path + '/' + filename); } diff --git a/src/main/java/de/pixart/messenger/utils/IrregularUnicodeDetector.java b/src/main/java/de/pixart/messenger/utils/IrregularUnicodeDetector.java index b54f8d074..3cd715a4c 100644 --- a/src/main/java/de/pixart/messenger/utils/IrregularUnicodeDetector.java +++ b/src/main/java/de/pixart/messenger/utils/IrregularUnicodeDetector.java @@ -50,7 +50,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import de.pixart.messenger.R; -import de.pixart.messenger.ui.util.Color; +import de.pixart.messenger.ui.util.StyledAttributes; import rocks.xmpp.addr.Jid; public class IrregularUnicodeDetector { @@ -73,7 +73,7 @@ public class IrregularUnicodeDetector { } public static Spannable style(Context context, Jid jid) { - return style(jid, Color.get(context, R.attr.color_warning)); + return style(jid, StyledAttributes.getColor(context, R.attr.color_warning)); } private static Spannable style(Jid jid, @ColorInt int color) { diff --git a/src/main/java/de/pixart/messenger/utils/Namespace.java b/src/main/java/de/pixart/messenger/utils/Namespace.java index d76e4fede..146a9784d 100644 --- a/src/main/java/de/pixart/messenger/utils/Namespace.java +++ b/src/main/java/de/pixart/messenger/utils/Namespace.java @@ -20,4 +20,6 @@ public final class Namespace { public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline"; public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind"; public static final String P1_S3_FILE_TRANSFER = "p1:s3filetransfer"; + public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0"; + public static final String BOOKMARKS = "storage:bookmarks"; } diff --git a/src/main/java/de/pixart/messenger/utils/Resolver.java b/src/main/java/de/pixart/messenger/utils/Resolver.java index 8f8138cb3..4bae6fae7 100644 --- a/src/main/java/de/pixart/messenger/utils/Resolver.java +++ b/src/main/java/de/pixart/messenger/utils/Resolver.java @@ -6,14 +6,15 @@ import android.support.annotation.NonNull; import android.util.Log; import java.io.IOException; +import java.lang.reflect.Field; import java.net.Inet4Address; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.List; +import de.measite.minidns.AbstractDNSClient; import de.measite.minidns.DNSClient; import de.measite.minidns.DNSName; import de.measite.minidns.Question; @@ -23,13 +24,13 @@ import de.measite.minidns.dnsserverlookup.AndroidUsingExec; import de.measite.minidns.hla.DnssecResolverApi; import de.measite.minidns.hla.ResolverApi; import de.measite.minidns.hla.ResolverResult; +import de.measite.minidns.iterative.ReliableDNSClient; import de.measite.minidns.record.A; import de.measite.minidns.record.AAAA; import de.measite.minidns.record.CNAME; import de.measite.minidns.record.Data; import de.measite.minidns.record.InternetAddressRR; import de.measite.minidns.record.SRV; -import de.measite.minidns.util.MultipleIoException; import de.pixart.messenger.Config; import de.pixart.messenger.R; import de.pixart.messenger.services.XmppConnectionService; @@ -39,8 +40,6 @@ public class Resolver { private static final String DIRECT_TLS_SERVICE = "_xmpps-client"; private static final String STARTTLS_SERICE = "_xmpp-client"; - private static final String NETWORK_IS_UNREACHABLE = "Network is unreachable"; - private static XmppConnectionService SERVICE = null; @@ -49,74 +48,122 @@ public class Resolver { DNSClient.removeDNSServerLookupMechanism(AndroidUsingExec.INSTANCE); DNSClient.addDnsServerLookupMechanism(AndroidUsingExecLowPriority.INSTANCE); DNSClient.addDnsServerLookupMechanism(new AndroidUsingLinkProperties(service)); + final AbstractDNSClient client = ResolverApi.INSTANCE.getClient(); + if (client instanceof ReliableDNSClient) { + disableHardcodedDnsServers((ReliableDNSClient) client); + } } - public static List<Result> resolve(String domain) throws NetworkIsUnreachableException { - List<Result> results = new ArrayList<>(); - HashSet<String> messages = new HashSet<>(); - try { - results.addAll(resolveSrv(domain, true)); - } catch (MultipleIoException e) { - messages.addAll(extractMessages(e)); - } catch (Throwable throwable) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (direct TLS)", throwable); - } + private static void disableHardcodedDnsServers(ReliableDNSClient reliableDNSClient) { try { - results.addAll(resolveSrv(domain, false)); - } catch (MultipleIoException e) { - messages.addAll(extractMessages(e)); - } catch (Throwable throwable) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (STARTTLS)", throwable); - } - if (results.size() == 0) { - if (messages.size() == 1 && messages.contains(NETWORK_IS_UNREACHABLE)) { - throw new NetworkIsUnreachableException(); - } - results.addAll(resolveNoSrvRecords(DNSName.from(domain), true)); + final Field dnsClientField = ReliableDNSClient.class.getDeclaredField("dnsClient"); + dnsClientField.setAccessible(true); + final DNSClient dnsClient = (DNSClient) dnsClientField.get(reliableDNSClient); + dnsClient.getDataSource().setTimeout(3000); + final Field useHardcodedDnsServers = DNSClient.class.getDeclaredField("useHardcodedDnsServers"); + useHardcodedDnsServers.setAccessible(true); + useHardcodedDnsServers.setBoolean(dnsClient, false); + } catch (NoSuchFieldException e) { + Log.e(Config.LOGTAG, "Unable to disable hardcoded DNS servers", e); + } catch (IllegalAccessException e) { + Log.e(Config.LOGTAG, "Unable to disable hardcoded DNS servers", e); } - Collections.sort(results); - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results.toString()); - return results; } - private static HashSet<String> extractMessages(MultipleIoException e) { - HashSet<String> messages = new HashSet<>(); - for (Exception inner : e.getExceptions()) { - if (inner instanceof MultipleIoException) { - messages.addAll(extractMessages((MultipleIoException) inner)); + public static List<Result> resolve(String domain) { + final List<Result> results = new ArrayList<>(); + final List<Result> fallbackResults = new ArrayList<>(); + Thread[] threads = new Thread[3]; + threads[0] = new Thread(() -> { + try { + final List<Result> list = resolveSrv(domain, true); + synchronized (results) { + results.addAll(list); + } + } catch (Throwable throwable) { + Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (direct TLS)", throwable); + } + }); + threads[1] = new Thread(() -> { + try { + final List<Result> list = resolveSrv(domain, false); + synchronized (results) { + results.addAll(list); + } + } catch (Throwable throwable) { + Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (STARTTLS)", throwable); + } + }); + threads[2] = new Thread(() -> { + List<Result> list = resolveNoSrvRecords(DNSName.from(domain), true); + synchronized (fallbackResults) { + fallbackResults.addAll(list); + } + }); + for (Thread thread : threads) { + thread.start(); + } + try { + threads[0].join(); + threads[1].join(); + if (results.size() > 0) { + threads[2].interrupt(); + Collections.sort(results); + Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results.toString()); + return results; } else { - messages.add(inner.getMessage()); + threads[2].join(); + Collections.sort(fallbackResults); + Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + fallbackResults.toString()); + return fallbackResults; } + } catch (InterruptedException e) { + return results; } - return messages; } private static List<Result> resolveSrv(String domain, final boolean directTls) throws IOException { - if (Thread.currentThread().isInterrupted()) { - return Collections.emptyList(); - } DNSName dnsName = DNSName.from((directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERICE) + "._tcp." + domain); ResolverResult<SRV> result = resolveWithFallback(dnsName, SRV.class); - List<Result> results = new ArrayList<>(); + final List<Result> results = new ArrayList<>(); + final List<Thread> threads = new ArrayList<>(); for (SRV record : result.getAnswersOrEmptySet()) { if (record.name.length() == 0 && record.priority == 0) { continue; } - final boolean addedIPv4 = results.addAll(resolveIp(record, A.class, result.isAuthenticData(), directTls)); - results.addAll(resolveIp(record, AAAA.class, result.isAuthenticData(), directTls)); - if (!addedIPv4 && !Thread.currentThread().isInterrupted()) { - Result resolverResult = Result.fromRecord(record, directTls); - resolverResult.authenticated = resolverResult.isAuthenticated(); - results.add(resolverResult); + threads.add(new Thread(() -> { + final List<Result> ipv4s = resolveIp(record, A.class, result.isAuthenticData(), directTls); + if (ipv4s.size() == 0) { + Result resolverResult = Result.fromRecord(record, directTls); + resolverResult.authenticated = resolverResult.isAuthenticated(); + ipv4s.add(resolverResult); + } + synchronized (results) { + results.addAll(ipv4s); + } + + })); + threads.add(new Thread(() -> { + final List<Result> ipv6s = resolveIp(record, AAAA.class, result.isAuthenticData(), directTls); + synchronized (results) { + results.addAll(ipv6s); + } + })); + } + for (Thread thread : threads) { + thread.start(); + } + for (Thread thread : threads) { + try { + thread.join(); + } catch (InterruptedException e) { + return Collections.emptyList(); } } return results; } private static <D extends InternetAddressRR> List<Result> resolveIp(SRV srv, Class<D> type, boolean authenticated, boolean directTls) { - if (Thread.currentThread().isInterrupted()) { - return Collections.emptyList(); - } List<Result> list = new ArrayList<>(); try { ResolverResult<D> results = resolveWithFallback(srv.name, type, authenticated); @@ -179,6 +226,56 @@ public class Resolver { } public static class Result implements Comparable<Result> { + public static final String DOMAIN = "domain"; + public static final String IP = "ip"; + public static final String HOSTNAME = "hostname"; + public static final String PORT = "port"; + public static final String PRIORITY = "priority"; + public static final String DIRECT_TLS = "directTls"; + public static final String AUTHENTICATED = "authenticated"; + private InetAddress ip; + private DNSName hostname; + private int port = 5222; + private boolean directTls = false; + private boolean authenticated = false; + private int priority; + + static Result fromRecord(SRV srv, boolean directTls) { + Result result = new Result(); + result.port = srv.port; + result.hostname = srv.name; + result.directTls = directTls; + result.priority = srv.priority; + return result; + } + + static Result createDefault(DNSName hostname, InetAddress ip) { + Result result = new Result(); + result.port = 5222; + result.hostname = hostname; + result.ip = ip; + return result; + } + + static Result createDefault(DNSName hostname) { + return createDefault(hostname, null); + } + + public static Result fromCursor(Cursor cursor) { + final Result result = new Result(); + try { + result.ip = InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndex(IP))); + } catch (UnknownHostException e) { + result.ip = null; + } + result.hostname = DNSName.from(cursor.getString(cursor.getColumnIndex(HOSTNAME))); + result.port = cursor.getInt(cursor.getColumnIndex(PORT)); + result.priority = cursor.getInt(cursor.getColumnIndex(PRIORITY)); + result.authenticated = cursor.getInt(cursor.getColumnIndex(AUTHENTICATED)) > 0; + result.directTls = cursor.getInt(cursor.getColumnIndex(DIRECT_TLS)) > 0; + return result; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -205,22 +302,6 @@ public class Resolver { return result; } - public static final String DOMAIN = "domain"; - - public static final String IP = "ip"; - public static final String HOSTNAME = "hostname"; - public static final String PORT = "port"; - public static final String PRIORITY = "priority"; - public static final String DIRECT_TLS = "directTls"; - public static final String AUTHENTICATED = "authenticated"; - - private InetAddress ip; - private DNSName hostname; - private int port = 5222; - private boolean directTls = false; - private boolean authenticated = false; - private int priority; - public InetAddress getIp() { return ip; } @@ -276,42 +357,6 @@ public class Resolver { } } - public static Result fromRecord(SRV srv, boolean directTls) { - Result result = new Result(); - result.port = srv.port; - result.hostname = srv.name; - result.directTls = directTls; - result.priority = srv.priority; - return result; - } - - public static Result createDefault(DNSName hostname, InetAddress ip) { - Result result = new Result(); - result.port = 5222; - result.hostname = hostname; - result.ip = ip; - return result; - } - - public static Result createDefault(DNSName hostname) { - return createDefault(hostname, null); - } - - public static Result fromCursor(Cursor cursor) { - final Result result = new Result(); - try { - result.ip = InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndex(IP))); - } catch (UnknownHostException e) { - result.ip = null; - } - result.hostname = DNSName.from(cursor.getString(cursor.getColumnIndex(HOSTNAME))); - result.port = cursor.getInt(cursor.getColumnIndex(PORT)); - result.priority = cursor.getInt(cursor.getColumnIndex(PRIORITY)); - result.authenticated = cursor.getInt(cursor.getColumnIndex(AUTHENTICATED)) > 0; - result.directTls = cursor.getInt(cursor.getColumnIndex(DIRECT_TLS)) > 0; - return result; - } - public ContentValues toContentValues() { final ContentValues contentValues = new ContentValues(); contentValues.put(IP, ip == null ? null : ip.getAddress()); @@ -324,7 +369,4 @@ public class Resolver { } } - public static class NetworkIsUnreachableException extends Exception { - - } }
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/utils/UIHelper.java b/src/main/java/de/pixart/messenger/utils/UIHelper.java index f5730e981..b603f68ee 100644 --- a/src/main/java/de/pixart/messenger/utils/UIHelper.java +++ b/src/main/java/de/pixart/messenger/utils/UIHelper.java @@ -222,6 +222,9 @@ public class UIHelper { } public static int getColorForName(String name, boolean safe) { + if (Config.XEP_0392) { + return XEP0392Helper.rgbFromNick(name); + } if (name == null || name.isEmpty()) { return 0xFF202020; } diff --git a/src/main/java/de/pixart/messenger/utils/XEP0392Helper.java b/src/main/java/de/pixart/messenger/utils/XEP0392Helper.java new file mode 100644 index 000000000..bbfe50898 --- /dev/null +++ b/src/main/java/de/pixart/messenger/utils/XEP0392Helper.java @@ -0,0 +1,30 @@ +package de.pixart.messenger.utils; + +import android.graphics.Color; + +import org.hsluv.HUSLColorConverter; + +import java.security.MessageDigest; + +public class XEP0392Helper { + + private static double angle(String nickname) { + try { + MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); + byte[] digest = sha1.digest(nickname.getBytes("UTF-8")); + int angle = ((int) (digest[0]) & 0xff) + ((int) (digest[1]) & 0xff) * 256; + return angle / 65536.; + } catch (Exception e) { + return 0.0; + } + } + + public static int rgbFromNick(String name) { + double[] hsluv = new double[3]; + hsluv[0] = angle(name) * 360; + hsluv[1] = 100; + hsluv[2] = 50; + double[] rgb = HUSLColorConverter.hsluvToRgb(hsluv); + return Color.rgb((int) Math.round(rgb[0] * 255), (int) Math.round(rgb[1] * 255), (int) Math.round(rgb[2] * 255)); + } +}
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/xmpp/InvalidJid.java b/src/main/java/de/pixart/messenger/xmpp/InvalidJid.java index ce381abdc..fdaadeab2 100644 --- a/src/main/java/de/pixart/messenger/xmpp/InvalidJid.java +++ b/src/main/java/de/pixart/messenger/xmpp/InvalidJid.java @@ -69,6 +69,11 @@ public class InvalidJid implements Jid { } @Override + public boolean isDomainJid() { + throw new AssertionError("Not implemented"); + } + + @Override public Jid asBareJid() { throw new AssertionError("Not implemented"); } diff --git a/src/main/java/de/pixart/messenger/xmpp/XmppConnection.java b/src/main/java/de/pixart/messenger/xmpp/XmppConnection.java index a6fe3756a..cbfdbc365 100644 --- a/src/main/java/de/pixart/messenger/xmpp/XmppConnection.java +++ b/src/main/java/de/pixart/messenger/xmpp/XmppConnection.java @@ -183,13 +183,12 @@ public class XmppConnection implements Runnable { private SaslMechanism saslMechanism; private URL redirectionUrl = null; private String verifiedHostname = null; - private Thread mThread; + private volatile Thread mThread; private CountDownLatch mStreamCountDownLatch; public XmppConnection(final Account account, final XmppConnectionService service) { this.account = account; - final String tag = account.getJid().asBareJid().toString(); - mXmppConnectionService = service; + this.mXmppConnectionService = service; } private static void fixResource(Context context, Account account) { @@ -443,8 +442,6 @@ public class XmppConnection implements Runnable { this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION); } catch (final StateChangingException e) { this.changeStatus(e.state); - } catch (final Resolver.NetworkIsUnreachableException e) { - this.changeStatus(Account.State.NETWORK_IS_UNREACHABLE); } catch (final UnknownHostException | ConnectException e) { this.changeStatus(Account.State.SERVER_NOT_FOUND); } catch (final SocksSocketFactory.SocksProxyNotFoundException e) { @@ -482,6 +479,9 @@ public class XmppConnection implements Runnable { tagWriter.beginDocument(); sendStartStream(); final Tag tag = tagReader.readTag(); + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); + } return tag != null && tag.isStart("stream"); } @@ -514,7 +514,7 @@ public class XmppConnection implements Runnable { connect(); } - private void processStream() throws XmlPullParserException, IOException, NoSuchAlgorithmException { + private void processStream() throws XmlPullParserException, IOException { final CountDownLatch streamCountDownLatch = new CountDownLatch(1); this.mStreamCountDownLatch = streamCountDownLatch; Tag nextTag = tagReader.readTag(); @@ -1789,6 +1789,10 @@ public class XmppConnection implements Runnable { return hasDiscoFeature(Jid.of(account.getServer()), "urn:xmpp:carbons:2"); } + public boolean bookmarksConversion() { + return hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS_CONVERSION) && pepPublishOptions(); + } + public boolean blocking() { return hasDiscoFeature(Jid.of(account.getServer()), Namespace.BLOCKING); } diff --git a/src/main/java/de/pixart/messenger/xmpp/pep/PublishOptions.java b/src/main/java/de/pixart/messenger/xmpp/pep/PublishOptions.java index 534f4005c..56b85169c 100644 --- a/src/main/java/de/pixart/messenger/xmpp/pep/PublishOptions.java +++ b/src/main/java/de/pixart/messenger/xmpp/pep/PublishOptions.java @@ -14,4 +14,11 @@ public class PublishOptions { return options; } + public static Bundle persistentWhitelistAccess() { + final Bundle options = new Bundle(); + options.putString("pubsub#persist_items", "true"); + options.putString("pubsub#access_model", "whitelist"); + return options; + } + }
\ No newline at end of file diff --git a/src/main/res/drawable-hdpi/ic_android_black_48dp.png b/src/main/res/drawable-hdpi/ic_android_black_48dp.png Binary files differnew file mode 100644 index 000000000..38f6c05e8 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_android_black_48dp.png diff --git a/src/main/res/drawable-hdpi/ic_android_white_48dp.png b/src/main/res/drawable-hdpi/ic_android_white_48dp.png Binary files differnew file mode 100644 index 000000000..13108b25b --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_android_white_48dp.png diff --git a/src/main/res/drawable-hdpi/ic_archive_black_48dp.png b/src/main/res/drawable-hdpi/ic_archive_black_48dp.png Binary files differnew file mode 100644 index 000000000..1faf474cd --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_archive_black_48dp.png diff --git a/src/main/res/drawable-hdpi/ic_archive_white_48dp.png b/src/main/res/drawable-hdpi/ic_archive_white_48dp.png Binary files differnew file mode 100644 index 000000000..462016ade --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_archive_white_48dp.png diff --git a/src/main/res/drawable-hdpi/ic_attach_videocam.png b/src/main/res/drawable-hdpi/ic_attach_videocam.png Binary files differnew file mode 100644 index 000000000..f95861633 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_attach_videocam.png diff --git a/src/main/res/drawable-hdpi/ic_contact_white_24dp.png b/src/main/res/drawable-hdpi/ic_contact_white_24dp.png Binary files differnew file mode 100644 index 000000000..7c046b21c --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_contact_white_24dp.png diff --git a/src/main/res/drawable-hdpi/ic_description_black_48dp.png b/src/main/res/drawable-hdpi/ic_description_black_48dp.png Binary files differnew file mode 100644 index 000000000..193ac1011 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_description_black_48dp.png diff --git a/src/main/res/drawable-hdpi/ic_description_white_48dp.png b/src/main/res/drawable-hdpi/ic_description_white_48dp.png Binary files differnew file mode 100644 index 000000000..bfb5baa97 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_description_white_48dp.png diff --git a/src/main/res/drawable-hdpi/ic_event_black_48dp.png b/src/main/res/drawable-hdpi/ic_event_black_48dp.png Binary files differnew file mode 100644 index 000000000..5cd32f4c2 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_event_black_48dp.png diff --git a/src/main/res/drawable-hdpi/ic_event_white_48dp.png b/src/main/res/drawable-hdpi/ic_event_white_48dp.png Binary files differnew file mode 100644 index 000000000..653d15b2b --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_event_white_48dp.png diff --git a/src/main/res/drawable-hdpi/ic_headset_black_48dp.png b/src/main/res/drawable-hdpi/ic_headset_black_48dp.png Binary files differnew file mode 100644 index 000000000..8a97fe7ec --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_headset_black_48dp.png diff --git a/src/main/res/drawable-hdpi/ic_headset_white_48dp.png b/src/main/res/drawable-hdpi/ic_headset_white_48dp.png Binary files differnew file mode 100644 index 000000000..a4810f277 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_headset_white_48dp.png diff --git a/src/main/res/drawable-hdpi/ic_help_black_48dp.png b/src/main/res/drawable-hdpi/ic_help_black_48dp.png Binary files differnew file mode 100644 index 000000000..b2ea7914a --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_help_black_48dp.png diff --git a/src/main/res/drawable-hdpi/ic_help_white_48dp.png b/src/main/res/drawable-hdpi/ic_help_white_48dp.png Binary files differnew file mode 100644 index 000000000..6827c3fac --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_help_white_48dp.png diff --git a/src/main/res/drawable-hdpi/ic_mic_black_48dp.png b/src/main/res/drawable-hdpi/ic_mic_black_48dp.png Binary files differnew file mode 100644 index 000000000..4ed4168cd --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_mic_black_48dp.png diff --git a/src/main/res/drawable-hdpi/ic_mic_white_48dp.png b/src/main/res/drawable-hdpi/ic_mic_white_48dp.png Binary files differnew file mode 100644 index 000000000..2d729e773 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_mic_white_48dp.png diff --git a/src/main/res/drawable-hdpi/ic_person_black_48dp.png b/src/main/res/drawable-hdpi/ic_person_black_48dp.png Binary files differnew file mode 100644 index 000000000..7401e00b5 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_person_black_48dp.png diff --git a/src/main/res/drawable-hdpi/ic_person_white_48dp.png b/src/main/res/drawable-hdpi/ic_person_white_48dp.png Binary files differnew file mode 100644 index 000000000..667c220dd --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_person_white_48dp.png diff --git a/src/main/res/drawable-hdpi/ic_room_black_48dp.png b/src/main/res/drawable-hdpi/ic_room_black_48dp.png Binary files differnew file mode 100644 index 000000000..2ebc54430 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_room_black_48dp.png diff --git a/src/main/res/drawable-hdpi/ic_room_white_48dp.png b/src/main/res/drawable-hdpi/ic_room_white_48dp.png Binary files differnew file mode 100644 index 000000000..29d2981b2 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_room_white_48dp.png diff --git a/src/main/res/drawable-hdpi/ic_send_videocam_away.png b/src/main/res/drawable-hdpi/ic_send_videocam_away.png Binary files differnew file mode 100644 index 000000000..950db9d9e --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_send_videocam_away.png diff --git a/src/main/res/drawable-hdpi/ic_send_videocam_dnd.png b/src/main/res/drawable-hdpi/ic_send_videocam_dnd.png Binary files differnew file mode 100644 index 000000000..8a3588678 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_send_videocam_dnd.png diff --git a/src/main/res/drawable-hdpi/ic_send_videocam_online.png b/src/main/res/drawable-hdpi/ic_send_videocam_online.png Binary files differnew file mode 100644 index 000000000..ac5af607f --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_send_videocam_online.png diff --git a/src/main/res/drawable-mdpi/ic_android_black_48dp.png b/src/main/res/drawable-mdpi/ic_android_black_48dp.png Binary files differnew file mode 100644 index 000000000..4f25964be --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_android_black_48dp.png diff --git a/src/main/res/drawable-mdpi/ic_android_white_48dp.png b/src/main/res/drawable-mdpi/ic_android_white_48dp.png Binary files differnew file mode 100644 index 000000000..032fe2781 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_android_white_48dp.png diff --git a/src/main/res/drawable-mdpi/ic_archive_black_48dp.png b/src/main/res/drawable-mdpi/ic_archive_black_48dp.png Binary files differnew file mode 100644 index 000000000..e9d5ed1b4 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_archive_black_48dp.png diff --git a/src/main/res/drawable-mdpi/ic_archive_white_48dp.png b/src/main/res/drawable-mdpi/ic_archive_white_48dp.png Binary files differnew file mode 100644 index 000000000..82be16407 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_archive_white_48dp.png diff --git a/src/main/res/drawable-mdpi/ic_attach_videocam.png b/src/main/res/drawable-mdpi/ic_attach_videocam.png Binary files differnew file mode 100644 index 000000000..66aa2fe49 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_attach_videocam.png diff --git a/src/main/res/drawable-mdpi/ic_contact_white_24dp.png b/src/main/res/drawable-mdpi/ic_contact_white_24dp.png Binary files differnew file mode 100644 index 000000000..c61747dc2 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_contact_white_24dp.png diff --git a/src/main/res/drawable-mdpi/ic_description_black_48dp.png b/src/main/res/drawable-mdpi/ic_description_black_48dp.png Binary files differnew file mode 100644 index 000000000..7bc6a6846 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_description_black_48dp.png diff --git a/src/main/res/drawable-mdpi/ic_description_white_48dp.png b/src/main/res/drawable-mdpi/ic_description_white_48dp.png Binary files differnew file mode 100644 index 000000000..9759bde29 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_description_white_48dp.png diff --git a/src/main/res/drawable-mdpi/ic_event_black_48dp.png b/src/main/res/drawable-mdpi/ic_event_black_48dp.png Binary files differnew file mode 100644 index 000000000..8266743b4 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_event_black_48dp.png diff --git a/src/main/res/drawable-mdpi/ic_event_white_48dp.png b/src/main/res/drawable-mdpi/ic_event_white_48dp.png Binary files differnew file mode 100644 index 000000000..2fb9a7a2c --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_event_white_48dp.png diff --git a/src/main/res/drawable-mdpi/ic_headset_black_48dp.png b/src/main/res/drawable-mdpi/ic_headset_black_48dp.png Binary files differnew file mode 100644 index 000000000..36b513948 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_headset_black_48dp.png diff --git a/src/main/res/drawable-mdpi/ic_headset_white_48dp.png b/src/main/res/drawable-mdpi/ic_headset_white_48dp.png Binary files differnew file mode 100644 index 000000000..798e229c7 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_headset_white_48dp.png diff --git a/src/main/res/drawable-mdpi/ic_help_black_48dp.png b/src/main/res/drawable-mdpi/ic_help_black_48dp.png Binary files differnew file mode 100644 index 000000000..3678f6f23 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_help_black_48dp.png diff --git a/src/main/res/drawable-mdpi/ic_help_white_48dp.png b/src/main/res/drawable-mdpi/ic_help_white_48dp.png Binary files differnew file mode 100644 index 000000000..68e4b1563 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_help_white_48dp.png diff --git a/src/main/res/drawable-mdpi/ic_mic_black_48dp.png b/src/main/res/drawable-mdpi/ic_mic_black_48dp.png Binary files differnew file mode 100644 index 000000000..9c7caa295 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_mic_black_48dp.png diff --git a/src/main/res/drawable-mdpi/ic_mic_white_48dp.png b/src/main/res/drawable-mdpi/ic_mic_white_48dp.png Binary files differnew file mode 100644 index 000000000..80d851e95 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_mic_white_48dp.png diff --git a/src/main/res/drawable-mdpi/ic_person_black_48dp.png b/src/main/res/drawable-mdpi/ic_person_black_48dp.png Binary files differnew file mode 100644 index 000000000..76858f701 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_person_black_48dp.png diff --git a/src/main/res/drawable-mdpi/ic_person_white_48dp.png b/src/main/res/drawable-mdpi/ic_person_white_48dp.png Binary files differnew file mode 100644 index 000000000..496699985 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_person_white_48dp.png diff --git a/src/main/res/drawable-mdpi/ic_room_black_48dp.png b/src/main/res/drawable-mdpi/ic_room_black_48dp.png Binary files differnew file mode 100644 index 000000000..e60454537 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_room_black_48dp.png diff --git a/src/main/res/drawable-mdpi/ic_room_white_48dp.png b/src/main/res/drawable-mdpi/ic_room_white_48dp.png Binary files differnew file mode 100644 index 000000000..8c51d01ad --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_room_white_48dp.png diff --git a/src/main/res/drawable-mdpi/ic_send_videocam_away.png b/src/main/res/drawable-mdpi/ic_send_videocam_away.png Binary files differnew file mode 100644 index 000000000..55399ac8d --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_send_videocam_away.png diff --git a/src/main/res/drawable-mdpi/ic_send_videocam_dnd.png b/src/main/res/drawable-mdpi/ic_send_videocam_dnd.png Binary files differnew file mode 100644 index 000000000..608947d59 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_send_videocam_dnd.png diff --git a/src/main/res/drawable-mdpi/ic_send_videocam_online.png b/src/main/res/drawable-mdpi/ic_send_videocam_online.png Binary files differnew file mode 100644 index 000000000..90a152504 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_send_videocam_online.png diff --git a/src/main/res/drawable-xhdpi/ic_android_black_48dp.png b/src/main/res/drawable-xhdpi/ic_android_black_48dp.png Binary files differnew file mode 100644 index 000000000..9f85a42bf --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_android_black_48dp.png diff --git a/src/main/res/drawable-xhdpi/ic_android_white_48dp.png b/src/main/res/drawable-xhdpi/ic_android_white_48dp.png Binary files differnew file mode 100644 index 000000000..913ebd69f --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_android_white_48dp.png diff --git a/src/main/res/drawable-xhdpi/ic_archive_black_48dp.png b/src/main/res/drawable-xhdpi/ic_archive_black_48dp.png Binary files differnew file mode 100644 index 000000000..6c383ee81 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_archive_black_48dp.png diff --git a/src/main/res/drawable-xhdpi/ic_archive_white_48dp.png b/src/main/res/drawable-xhdpi/ic_archive_white_48dp.png Binary files differnew file mode 100644 index 000000000..dbbf9f870 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_archive_white_48dp.png diff --git a/src/main/res/drawable-xhdpi/ic_attach_videocam.png b/src/main/res/drawable-xhdpi/ic_attach_videocam.png Binary files differnew file mode 100644 index 000000000..5f6054b81 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_attach_videocam.png diff --git a/src/main/res/drawable-xhdpi/ic_contact_white_24dp.png b/src/main/res/drawable-xhdpi/ic_contact_white_24dp.png Binary files differnew file mode 100644 index 000000000..eedda1b40 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_contact_white_24dp.png diff --git a/src/main/res/drawable-xhdpi/ic_description_black_48dp.png b/src/main/res/drawable-xhdpi/ic_description_black_48dp.png Binary files differnew file mode 100644 index 000000000..0da5f7798 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_description_black_48dp.png diff --git a/src/main/res/drawable-xhdpi/ic_description_white_48dp.png b/src/main/res/drawable-xhdpi/ic_description_white_48dp.png Binary files differnew file mode 100644 index 000000000..e1b8e9aec --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_description_white_48dp.png diff --git a/src/main/res/drawable-xhdpi/ic_event_black_48dp.png b/src/main/res/drawable-xhdpi/ic_event_black_48dp.png Binary files differnew file mode 100644 index 000000000..8e43444b7 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_event_black_48dp.png diff --git a/src/main/res/drawable-xhdpi/ic_event_white_48dp.png b/src/main/res/drawable-xhdpi/ic_event_white_48dp.png Binary files differnew file mode 100644 index 000000000..be533de42 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_event_white_48dp.png diff --git a/src/main/res/drawable-xhdpi/ic_headset_black_48dp.png b/src/main/res/drawable-xhdpi/ic_headset_black_48dp.png Binary files differnew file mode 100644 index 000000000..7f85eb8bb --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_headset_black_48dp.png diff --git a/src/main/res/drawable-xhdpi/ic_headset_white_48dp.png b/src/main/res/drawable-xhdpi/ic_headset_white_48dp.png Binary files differnew file mode 100644 index 000000000..b815a1944 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_headset_white_48dp.png diff --git a/src/main/res/drawable-xhdpi/ic_help_black_48dp.png b/src/main/res/drawable-xhdpi/ic_help_black_48dp.png Binary files differnew file mode 100644 index 000000000..066c88bbe --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_help_black_48dp.png diff --git a/src/main/res/drawable-xhdpi/ic_help_white_48dp.png b/src/main/res/drawable-xhdpi/ic_help_white_48dp.png Binary files differnew file mode 100644 index 000000000..805ea1efb --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_help_white_48dp.png diff --git a/src/main/res/drawable-xhdpi/ic_mic_black_48dp.png b/src/main/res/drawable-xhdpi/ic_mic_black_48dp.png Binary files differnew file mode 100644 index 000000000..e39b8170c --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_mic_black_48dp.png diff --git a/src/main/res/drawable-xhdpi/ic_mic_white_48dp.png b/src/main/res/drawable-xhdpi/ic_mic_white_48dp.png Binary files differnew file mode 100644 index 000000000..ec4405d44 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_mic_white_48dp.png diff --git a/src/main/res/drawable-xhdpi/ic_person_black_48dp.png b/src/main/res/drawable-xhdpi/ic_person_black_48dp.png Binary files differnew file mode 100644 index 000000000..2751da525 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_person_black_48dp.png diff --git a/src/main/res/drawable-xhdpi/ic_person_white_48dp.png b/src/main/res/drawable-xhdpi/ic_person_white_48dp.png Binary files differnew file mode 100644 index 000000000..6a77a6a38 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_person_white_48dp.png diff --git a/src/main/res/drawable-xhdpi/ic_room_black_48dp.png b/src/main/res/drawable-xhdpi/ic_room_black_48dp.png Binary files differnew file mode 100644 index 000000000..9d46eb841 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_room_black_48dp.png diff --git a/src/main/res/drawable-xhdpi/ic_room_white_48dp.png b/src/main/res/drawable-xhdpi/ic_room_white_48dp.png Binary files differnew file mode 100644 index 000000000..b7a61a2f5 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_room_white_48dp.png diff --git a/src/main/res/drawable-xhdpi/ic_send_videocam_away.png b/src/main/res/drawable-xhdpi/ic_send_videocam_away.png Binary files differnew file mode 100644 index 000000000..69e340800 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_send_videocam_away.png diff --git a/src/main/res/drawable-xhdpi/ic_send_videocam_dnd.png b/src/main/res/drawable-xhdpi/ic_send_videocam_dnd.png Binary files differnew file mode 100644 index 000000000..e7f784d8b --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_send_videocam_dnd.png diff --git a/src/main/res/drawable-xhdpi/ic_send_videocam_online.png b/src/main/res/drawable-xhdpi/ic_send_videocam_online.png Binary files differnew file mode 100644 index 000000000..7cda9e6f0 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_send_videocam_online.png diff --git a/src/main/res/drawable-xxhdpi/ic_android_black_48dp.png b/src/main/res/drawable-xxhdpi/ic_android_black_48dp.png Binary files differnew file mode 100644 index 000000000..61b806d38 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_android_black_48dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_android_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_android_white_48dp.png Binary files differnew file mode 100644 index 000000000..c3bf2bdc3 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_android_white_48dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_archive_black_48dp.png b/src/main/res/drawable-xxhdpi/ic_archive_black_48dp.png Binary files differnew file mode 100644 index 000000000..0155c8104 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_archive_black_48dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_archive_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_archive_white_48dp.png Binary files differnew file mode 100644 index 000000000..5a18abe12 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_archive_white_48dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_attach_videocam.png b/src/main/res/drawable-xxhdpi/ic_attach_videocam.png Binary files differnew file mode 100644 index 000000000..e74d2a776 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_attach_videocam.png diff --git a/src/main/res/drawable-xxhdpi/ic_contact_white_24dp.png b/src/main/res/drawable-xxhdpi/ic_contact_white_24dp.png Binary files differnew file mode 100644 index 000000000..91e0d9a6b --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_contact_white_24dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_description_black_48dp.png b/src/main/res/drawable-xxhdpi/ic_description_black_48dp.png Binary files differnew file mode 100644 index 000000000..9ddffa1f3 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_description_black_48dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_description_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_description_white_48dp.png Binary files differnew file mode 100644 index 000000000..b42cec8eb --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_description_white_48dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_event_black_48dp.png b/src/main/res/drawable-xxhdpi/ic_event_black_48dp.png Binary files differnew file mode 100644 index 000000000..3eb5d3073 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_event_black_48dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_event_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_event_white_48dp.png Binary files differnew file mode 100644 index 000000000..44ead726f --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_event_white_48dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_headset_black_48dp.png b/src/main/res/drawable-xxhdpi/ic_headset_black_48dp.png Binary files differnew file mode 100644 index 000000000..2a8236f35 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_headset_black_48dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_headset_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_headset_white_48dp.png Binary files differnew file mode 100644 index 000000000..2b6b2644f --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_headset_white_48dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_help_black_48dp.png b/src/main/res/drawable-xxhdpi/ic_help_black_48dp.png Binary files differnew file mode 100644 index 000000000..2f8af6e41 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_help_black_48dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_help_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_help_white_48dp.png Binary files differnew file mode 100644 index 000000000..1a3e87205 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_help_white_48dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_mic_black_48dp.png b/src/main/res/drawable-xxhdpi/ic_mic_black_48dp.png Binary files differnew file mode 100644 index 000000000..44ef2fb4a --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_mic_black_48dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_mic_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_mic_white_48dp.png Binary files differnew file mode 100644 index 000000000..0fc42bfad --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_mic_white_48dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_person_black_48dp.png b/src/main/res/drawable-xxhdpi/ic_person_black_48dp.png Binary files differnew file mode 100644 index 000000000..0001aee6b --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_person_black_48dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_person_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_person_white_48dp.png Binary files differnew file mode 100644 index 000000000..32fe521d0 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_person_white_48dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_room_black_48dp.png b/src/main/res/drawable-xxhdpi/ic_room_black_48dp.png Binary files differnew file mode 100644 index 000000000..e83824a17 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_room_black_48dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_room_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_room_white_48dp.png Binary files differnew file mode 100644 index 000000000..dd8eb11d3 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_room_white_48dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_send_videocam_away.png b/src/main/res/drawable-xxhdpi/ic_send_videocam_away.png Binary files differnew file mode 100644 index 000000000..ce04c7790 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_send_videocam_away.png diff --git a/src/main/res/drawable-xxhdpi/ic_send_videocam_dnd.png b/src/main/res/drawable-xxhdpi/ic_send_videocam_dnd.png Binary files differnew file mode 100644 index 000000000..24890da09 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_send_videocam_dnd.png diff --git a/src/main/res/drawable-xxhdpi/ic_send_videocam_online.png b/src/main/res/drawable-xxhdpi/ic_send_videocam_online.png Binary files differnew file mode 100644 index 000000000..079cdef19 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_send_videocam_online.png diff --git a/src/main/res/drawable-xxxhdpi/ic_android_black_48dp.png b/src/main/res/drawable-xxxhdpi/ic_android_black_48dp.png Binary files differnew file mode 100644 index 000000000..1d1492b08 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_android_black_48dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_android_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_android_white_48dp.png Binary files differnew file mode 100644 index 000000000..d040e8639 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_android_white_48dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_archive_black_48dp.png b/src/main/res/drawable-xxxhdpi/ic_archive_black_48dp.png Binary files differnew file mode 100644 index 000000000..4dd5e44ab --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_archive_black_48dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_archive_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_archive_white_48dp.png Binary files differnew file mode 100644 index 000000000..5ac82a8df --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_archive_white_48dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_attach_videocam.png b/src/main/res/drawable-xxxhdpi/ic_attach_videocam.png Binary files differnew file mode 100644 index 000000000..02fddb640 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_attach_videocam.png diff --git a/src/main/res/drawable-xxxhdpi/ic_contact_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_contact_white_24dp.png Binary files differnew file mode 100644 index 000000000..f36920906 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_contact_white_24dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_description_black_48dp.png b/src/main/res/drawable-xxxhdpi/ic_description_black_48dp.png Binary files differnew file mode 100644 index 000000000..f405b2931 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_description_black_48dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_description_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_description_white_48dp.png Binary files differnew file mode 100644 index 000000000..1d32d8841 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_description_white_48dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_event_black_48dp.png b/src/main/res/drawable-xxxhdpi/ic_event_black_48dp.png Binary files differnew file mode 100644 index 000000000..038df57d5 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_event_black_48dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_event_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_event_white_48dp.png Binary files differnew file mode 100644 index 000000000..1937048aa --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_event_white_48dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_headset_black_48dp.png b/src/main/res/drawable-xxxhdpi/ic_headset_black_48dp.png Binary files differnew file mode 100644 index 000000000..9a9a1b7f6 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_headset_black_48dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_headset_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_headset_white_48dp.png Binary files differnew file mode 100644 index 000000000..ba48f28a1 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_headset_white_48dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_help_black_48dp.png b/src/main/res/drawable-xxxhdpi/ic_help_black_48dp.png Binary files differnew file mode 100644 index 000000000..45e2e7b21 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_help_black_48dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_help_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_help_white_48dp.png Binary files differnew file mode 100644 index 000000000..dc996ae81 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_help_white_48dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_mic_black_48dp.png b/src/main/res/drawable-xxxhdpi/ic_mic_black_48dp.png Binary files differnew file mode 100644 index 000000000..f2247b7ff --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_mic_black_48dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_mic_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_mic_white_48dp.png Binary files differnew file mode 100644 index 000000000..e9079bb3b --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_mic_white_48dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_person_black_48dp.png b/src/main/res/drawable-xxxhdpi/ic_person_black_48dp.png Binary files differnew file mode 100644 index 000000000..6d377123a --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_person_black_48dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_person_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_person_white_48dp.png Binary files differnew file mode 100644 index 000000000..5bb65aed1 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_person_white_48dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_room_black_48dp.png b/src/main/res/drawable-xxxhdpi/ic_room_black_48dp.png Binary files differnew file mode 100644 index 000000000..51d418cf7 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_room_black_48dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_room_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_room_white_48dp.png Binary files differnew file mode 100644 index 000000000..0f7de3f5d --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_room_white_48dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_send_videocam_away.png b/src/main/res/drawable-xxxhdpi/ic_send_videocam_away.png Binary files differnew file mode 100644 index 000000000..7e517e6be --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_send_videocam_away.png diff --git a/src/main/res/drawable-xxxhdpi/ic_send_videocam_dnd.png b/src/main/res/drawable-xxxhdpi/ic_send_videocam_dnd.png Binary files differnew file mode 100644 index 000000000..3077ee233 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_send_videocam_dnd.png diff --git a/src/main/res/drawable-xxxhdpi/ic_send_videocam_online.png b/src/main/res/drawable-xxxhdpi/ic_send_videocam_online.png Binary files differnew file mode 100644 index 000000000..0baef17f4 --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_send_videocam_online.png diff --git a/src/main/res/drawable/date_bubble.xml b/src/main/res/drawable/date_bubble.xml index 3edf349e9..0ea7b0761 100644 --- a/src/main/res/drawable/date_bubble.xml +++ b/src/main/res/drawable/date_bubble.xml @@ -11,11 +11,7 @@ android:left="6dp" android:right="6dp" android:top="6dp" /> - <stroke - android:width="1dp" - android:color="@color/grey500"> - </stroke> <solid android:color="@color/lightgreen"> </solid> -</shape>
\ No newline at end of file +</shape> diff --git a/src/main/res/drawable/date_bubble_dark.xml b/src/main/res/drawable/date_bubble_dark.xml index 345c315c1..64f5c1515 100644 --- a/src/main/res/drawable/date_bubble_dark.xml +++ b/src/main/res/drawable/date_bubble_dark.xml @@ -11,11 +11,7 @@ android:left="6dp" android:right="6dp" android:top="6dp" /> - <stroke - android:width="1dp" - android:color="@color/grey800"> - </stroke> <solid android:color="@color/darkgreen"> </solid> -</shape>
\ No newline at end of file +</shape> diff --git a/src/main/res/drawable/message_bubble_received_light.xml b/src/main/res/drawable/message_bubble_received_light.xml index 32e146460..6619984b3 100644 --- a/src/main/res/drawable/message_bubble_received_light.xml +++ b/src/main/res/drawable/message_bubble_received_light.xml @@ -1,21 +1,29 @@ <?xml version="1.0" encoding="utf-8"?> -<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> - <corners - android:topLeftRadius="0dp" - android:topRightRadius="5dp" - android:bottomRightRadius="5dp" - android:bottomLeftRadius="5dp"> - </corners> - <padding - android:bottom="2dp" - android:left="6dp" - android:right="6dp" - android:top="2dp" /> - <stroke - android:width="1dp" - android:color="@color/grey500"> - </stroke> - <solid - android:color="@color/lightwhite"> - </solid> -</shape>
\ No newline at end of file +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:gravity="top|left"> + <vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="24dp" + android:viewportWidth="36" + android:viewportHeight="24" > + <path + android:fillColor="@color/lightwhite" + android:pathData="m2,0 c6,0 10,6 10,10L18,0z" /> + </vector> + </item> + <item android:left="12dp"> + <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> + <corners + android:topLeftRadius="0dp" + android:topRightRadius="5dp" + android:bottomRightRadius="5dp" + android:bottomLeftRadius="5dp"/> + <padding + android:bottom="2dp" + android:left="18dp" + android:right="6dp" + android:top="2dp" /> + <solid android:color="@color/lightwhite" /> + </shape> + </item> +</layer-list> diff --git a/src/main/res/drawable/message_bubble_received_light_dark.xml b/src/main/res/drawable/message_bubble_received_light_dark.xml index 8f9132566..9514bc757 100644 --- a/src/main/res/drawable/message_bubble_received_light_dark.xml +++ b/src/main/res/drawable/message_bubble_received_light_dark.xml @@ -1,21 +1,29 @@ <?xml version="1.0" encoding="utf-8"?> -<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> - <corners - android:topLeftRadius="0dp" - android:topRightRadius="5dp" - android:bottomRightRadius="5dp" - android:bottomLeftRadius="5dp"> - </corners> - <padding - android:bottom="2dp" - android:left="6dp" - android:right="6dp" - android:top="2dp" /> - <stroke - android:width="1dp" - android:color="@color/grey700"> - </stroke> - <solid - android:color="@color/darkwhite"> - </solid> -</shape>
\ No newline at end of file +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:gravity="top|left"> + <vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="24dp" + android:viewportWidth="36" + android:viewportHeight="24" > + <path + android:fillColor="@color/darkwhite" + android:pathData="m2,0 c6,0 10,6 10,10L18,0z" /> + </vector> + </item> + <item android:left="12dp"> + <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> + <corners + android:topLeftRadius="0dp" + android:topRightRadius="5dp" + android:bottomRightRadius="5dp" + android:bottomLeftRadius="5dp" /> + <padding + android:bottom="2dp" + android:left="18dp" + android:right="6dp" + android:top="2dp" /> + <solid android:color="@color/darkwhite" /> + </shape> + </item> +</layer-list> diff --git a/src/main/res/drawable/message_bubble_received_warning.xml b/src/main/res/drawable/message_bubble_received_warning.xml index b3bf6d451..ae3222a9e 100644 --- a/src/main/res/drawable/message_bubble_received_warning.xml +++ b/src/main/res/drawable/message_bubble_received_warning.xml @@ -1,21 +1,29 @@ <?xml version="1.0" encoding="utf-8"?> -<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> - <corners - android:topLeftRadius="0dp" - android:topRightRadius="5dp" - android:bottomRightRadius="5dp" - android:bottomLeftRadius="5dp"> - </corners> - <padding - android:bottom="4dp" - android:left="6dp" - android:right="6dp" - android:top="4dp" /> - <stroke - android:width="1dp" - android:color="@color/grey500"> - </stroke> - <solid - android:color="@color/lightred"> - </solid> -</shape>
\ No newline at end of file +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:gravity="top|left"> + <vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="24dp" + android:viewportWidth="36" + android:viewportHeight="24" > + <path + android:fillColor="@color/lightred" + android:pathData="m2,0 c6,0 10,6 10,10L18,0z" /> + </vector> + </item> + <item android:left="12dp"> + <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> + <corners + android:topLeftRadius="0dp" + android:topRightRadius="5dp" + android:bottomRightRadius="5dp" + android:bottomLeftRadius="5dp" /> + <padding + android:bottom="2dp" + android:left="18dp" + android:right="6dp" + android:top="2dp" /> + <solid android:color="@color/lightred" /> + </shape> + </item> +</layer-list> diff --git a/src/main/res/drawable/message_bubble_received_warning_dark.xml b/src/main/res/drawable/message_bubble_received_warning_dark.xml index 5867f67ce..9f8aa779e 100644 --- a/src/main/res/drawable/message_bubble_received_warning_dark.xml +++ b/src/main/res/drawable/message_bubble_received_warning_dark.xml @@ -1,21 +1,29 @@ <?xml version="1.0" encoding="utf-8"?> -<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> - <corners - android:topLeftRadius="0dp" - android:topRightRadius="5dp" - android:bottomRightRadius="5dp" - android:bottomLeftRadius="5dp"> - </corners> - <padding - android:bottom="4dp" - android:left="6dp" - android:right="6dp" - android:top="4dp" /> - <stroke - android:width="1dp" - android:color="@color/grey700"> - </stroke> - <solid - android:color="@color/darkred"> - </solid> -</shape>
\ No newline at end of file +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:gravity="top|left"> + <vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="24dp" + android:viewportWidth="36" + android:viewportHeight="24" > + <path + android:fillColor="@color/darkred" + android:pathData="m2,0 c6,0 10,6 10,10L18,0z" /> + </vector> + </item> + <item android:left="12dp"> + <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> + <corners + android:topLeftRadius="0dp" + android:topRightRadius="5dp" + android:bottomRightRadius="5dp" + android:bottomLeftRadius="5dp" /> + <padding + android:bottom="2dp" + android:left="18dp" + android:right="6dp" + android:top="2dp" /> + <solid android:color="@color/darkred" /> + </shape> + </item> +</layer-list> diff --git a/src/main/res/drawable/message_bubble_sent_blue.xml b/src/main/res/drawable/message_bubble_sent_blue.xml index 1ee3ad0c8..8e519ff6b 100644 --- a/src/main/res/drawable/message_bubble_sent_blue.xml +++ b/src/main/res/drawable/message_bubble_sent_blue.xml @@ -1,21 +1,30 @@ <?xml version="1.0" encoding="utf-8"?> -<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> - <corners - android:topLeftRadius="5dp" - android:topRightRadius="5dp" - android:bottomRightRadius="0dp" - android:bottomLeftRadius="5dp"> - </corners> - <padding - android:bottom="4dp" - android:left="6dp" - android:right="6dp" - android:top="4dp" /> - <stroke - android:width="1dp" - android:color="@color/grey500"> - </stroke> - <solid - android:color="@color/lightblue"> - </solid> -</shape>
\ No newline at end of file +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:gravity="bottom|right"> + <vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="24dp" + android:viewportWidth="36" + android:viewportHeight="24" > + <path + android:fillColor="@color/lightblue" + android:pathData="m 34,24 c-6,0 -10,-6 -10,-10 l -6,12 z" /> + + </vector> + </item> + <item android:right="12dp"> + <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> + <corners + android:topLeftRadius="5dp" + android:topRightRadius="5dp" + android:bottomRightRadius="0dp" + android:bottomLeftRadius="5dp" /> + <padding + android:bottom="2dp" + android:left="6dp" + android:right="18dp" + android:top="2dp" /> + <solid android:color="@color/lightblue" /> + </shape> + </item> +</layer-list> diff --git a/src/main/res/drawable/message_bubble_sent_blue_dark.xml b/src/main/res/drawable/message_bubble_sent_blue_dark.xml index d9853bf74..e6f3c88c1 100644 --- a/src/main/res/drawable/message_bubble_sent_blue_dark.xml +++ b/src/main/res/drawable/message_bubble_sent_blue_dark.xml @@ -1,21 +1,29 @@ <?xml version="1.0" encoding="utf-8"?> -<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> - <corners - android:topLeftRadius="5dp" - android:topRightRadius="5dp" - android:bottomRightRadius="0dp" - android:bottomLeftRadius="5dp"> - </corners> - <padding - android:bottom="4dp" - android:left="6dp" - android:right="6dp" - android:top="4dp" /> - <stroke - android:width="1dp" - android:color="@color/grey700"> - </stroke> - <solid - android:color="@color/darkblue"> - </solid> -</shape>
\ No newline at end of file +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:gravity="bottom|right"> + <vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="24dp" + android:viewportWidth="36" + android:viewportHeight="24" > + <path + android:fillColor="@color/darkblue" + android:pathData="m 34,24 c-6,0 -10,-6 -10,-10 l -6,12 z" /> + </vector> + </item> + <item android:right="12dp"> + <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> + <corners + android:topLeftRadius="5dp" + android:topRightRadius="5dp" + android:bottomRightRadius="0dp" + android:bottomLeftRadius="5dp" /> + <padding + android:bottom="2dp" + android:left="6dp" + android:right="18dp" + android:top="2dp" /> + <solid android:color="@color/darkblue" /> + </shape> + </item> +</layer-list> diff --git a/src/main/res/layout/account_row.xml b/src/main/res/layout/account_row.xml index e56ca6a95..9b638172a 100644 --- a/src/main/res/layout/account_row.xml +++ b/src/main/res/layout/account_row.xml @@ -15,8 +15,6 @@ android:layout_alignParentLeft="true" android:contentDescription="@string/account_image_description" android:padding="1dp" - app:riv_border_color="?attr/color_border" - app:riv_border_width="1dp" app:riv_corner_radius="@dimen/rounded_image_border" /> <LinearLayout diff --git a/src/main/res/layout/activity_choose_contact.xml b/src/main/res/layout/activity_choose_contact.xml index f720864a3..26f1c7494 100644 --- a/src/main/res/layout/activity_choose_contact.xml +++ b/src/main/res/layout/activity_choose_contact.xml @@ -13,7 +13,7 @@ <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?attr/color_background_primary"> + android:background="?attr/color_background_secondary"> <ListView android:id="@+id/choose_contact_list" diff --git a/src/main/res/layout/activity_contact_details.xml b/src/main/res/layout/activity_contact_details.xml index dd3ff2c31..a6a720995 100644 --- a/src/main/res/layout/activity_contact_details.xml +++ b/src/main/res/layout/activity_contact_details.xml @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<layout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto"> +<layout xmlns:android="http://schemas.android.com/apk/res/android"> <LinearLayout android:layout_width="match_parent" @@ -102,8 +101,7 @@ android:layout_marginBottom="4dp" android:layout_marginLeft="-2dp" android:layout_marginTop="4dp" - android:orientation="horizontal"> - </com.wefika.flowlayout.FlowLayout> + android:orientation="horizontal"></com.wefika.flowlayout.FlowLayout> <TextView android:id="@+id/details_lastseen" @@ -235,6 +233,52 @@ </android.support.v7.widget.CardView> <android.support.v7.widget.CardView + android:id="@+id/media_wrapper" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/activity_vertical_margin" + android:layout_marginLeft="@dimen/activity_horizontal_margin" + android:layout_marginRight="@dimen/activity_horizontal_margin" + android:layout_marginTop="@dimen/activity_vertical_margin"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <android.support.v7.widget.RecyclerView + android:id="@+id/media" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="-2dp" + android:layout_marginStart="-2dp" + android:orientation="horizontal" + android:paddingBottom="@dimen/card_padding_list" + android:paddingEnd="@dimen/card_padding_regular" + android:paddingStart="@dimen/card_padding_regular" + android:paddingTop="@dimen/card_padding_regular" /> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_gravity="end" + android:orientation="horizontal"> + + <Button + android:id="@+id/show_media" + style="@style/Widget.Conversations.Button.Borderless" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="0dp" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:text="@string/view_media" + android:textColor="?attr/colorAccent" /> + </LinearLayout> + </LinearLayout> + </android.support.v7.widget.CardView> + + <android.support.v7.widget.CardView android:id="@+id/keys_wrapper" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -254,7 +298,7 @@ android:layout_height="wrap_content" android:divider="?android:dividerHorizontal" android:orientation="vertical" - android:padding="@dimen/card_padding_list"></LinearLayout> + android:padding="@dimen/card_padding_list" /> <LinearLayout android:layout_width="wrap_content" diff --git a/src/main/res/layout/activity_edit_account.xml b/src/main/res/layout/activity_edit_account.xml index 41e00b4ba..02d03b941 100644 --- a/src/main/res/layout/activity_edit_account.xml +++ b/src/main/res/layout/activity_edit_account.xml @@ -53,8 +53,6 @@ android:maxHeight="384dp" android:maxWidth="384dp" android:padding="1dp" - app:riv_border_color="?attr/color_border" - app:riv_border_width="1dp" app:riv_corner_radius="@dimen/rounded_image_border" /> <LinearLayout @@ -128,7 +126,7 @@ style="@style/Widget.Conversations.EditText" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:inputType="textNoSuggestions" /> + android:inputType="textWebEmailAddress" /> </android.support.design.widget.TextInputLayout> </LinearLayout> diff --git a/src/main/res/layout/activity_manage_accounts.xml b/src/main/res/layout/activity_manage_accounts.xml index e80aa01e0..2dc266bf3 100644 --- a/src/main/res/layout/activity_manage_accounts.xml +++ b/src/main/res/layout/activity_manage_accounts.xml @@ -2,7 +2,7 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" - android:background="?attr/color_background_primary" + android:background="?attr/color_background_secondary" android:orientation="vertical"> <include layout="@layout/toolbar" /> diff --git a/src/main/res/layout/activity_media_browser.xml b/src/main/res/layout/activity_media_browser.xml new file mode 100644 index 000000000..149f2c164 --- /dev/null +++ b/src/main/res/layout/activity_media_browser.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout xmlns:android="http://schemas.android.com/apk/res/android"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?attr/color_background_primary" + android:orientation="vertical"> + + <include + android:id="@+id/toolbar" + layout="@layout/toolbar" /> + + <android.support.v7.widget.RecyclerView + android:id="@+id/media" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipToPadding="false" + android:orientation="horizontal" + android:padding="2dp" + android:scrollbars="vertical" /> + + </LinearLayout> +</layout>
\ No newline at end of file diff --git a/src/main/res/layout/activity_muc_details.xml b/src/main/res/layout/activity_muc_details.xml index 2a450f964..b46ad8032 100644 --- a/src/main/res/layout/activity_muc_details.xml +++ b/src/main/res/layout/activity_muc_details.xml @@ -155,8 +155,6 @@ android:maxWidth="384dp" android:padding="1dp" android:scaleType="centerCrop" - app:riv_border_color="?attr/color_border" - app:riv_border_width="1dp" app:riv_corner_radius="@dimen/rounded_image_border" /> </RelativeLayout> @@ -278,6 +276,52 @@ </android.support.v7.widget.CardView> <android.support.v7.widget.CardView + android:id="@+id/media_wrapper" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/activity_vertical_margin" + android:layout_marginLeft="@dimen/activity_horizontal_margin" + android:layout_marginRight="@dimen/activity_horizontal_margin" + android:layout_marginTop="@dimen/activity_vertical_margin"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <android.support.v7.widget.RecyclerView + android:id="@+id/media" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="-2dp" + android:layout_marginStart="-2dp" + android:orientation="horizontal" + android:paddingBottom="@dimen/card_padding_list" + android:paddingEnd="@dimen/card_padding_regular" + android:paddingStart="@dimen/card_padding_regular" + android:paddingTop="@dimen/card_padding_regular" /> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_gravity="end" + android:orientation="horizontal"> + + <Button + android:id="@+id/show_media" + style="@style/Widget.Conversations.Button.Borderless" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="0dp" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:text="@string/view_media" + android:textColor="?attr/colorAccent" /> + </LinearLayout> + </LinearLayout> + </android.support.v7.widget.CardView> + + <android.support.v7.widget.CardView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/activity_vertical_margin" @@ -311,8 +355,6 @@ android:layout_alignParentLeft="true" android:layout_alignParentRight="false" android:padding="1dp" - app:riv_border_color="?attr/color_border" - app:riv_border_width="1dp" app:riv_corner_radius="@dimen/rounded_image_border" /> <LinearLayout diff --git a/src/main/res/layout/activity_publish_profile_picture.xml b/src/main/res/layout/activity_publish_profile_picture.xml index eebfa76a6..90dde5b01 100644 --- a/src/main/res/layout/activity_publish_profile_picture.xml +++ b/src/main/res/layout/activity_publish_profile_picture.xml @@ -43,8 +43,6 @@ android:maxWidth="384dp" android:padding="1dp" android:scaleType="centerCrop" - app:riv_border_color="?attr/color_border" - app:riv_border_width="1dp" app:riv_corner_radius="@dimen/rounded_image_border" /> </FrameLayout> diff --git a/src/main/res/layout/activity_recording.xml b/src/main/res/layout/activity_recording.xml index 35097fef4..e9dd345c6 100644 --- a/src/main/res/layout/activity_recording.xml +++ b/src/main/res/layout/activity_recording.xml @@ -46,6 +46,7 @@ android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" + android:layout_margin="8dp" android:text="0:00.0" android:textColor="?attr/colorAccent" android:textSize="50sp" diff --git a/src/main/res/layout/activity_start_conversation.xml b/src/main/res/layout/activity_start_conversation.xml index 1cb95b456..7adb8714a 100644 --- a/src/main/res/layout/activity_start_conversation.xml +++ b/src/main/res/layout/activity_start_conversation.xml @@ -29,7 +29,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@id/tab_layout" - android:background="?attr/color_background_primary" /> + android:background="?attr/color_background_secondary" /> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" diff --git a/src/main/res/layout/contact.xml b/src/main/res/layout/contact.xml index 0218e299b..f5bd1506b 100644 --- a/src/main/res/layout/contact.xml +++ b/src/main/res/layout/contact.xml @@ -13,8 +13,6 @@ android:layout_width="56dp" android:layout_height="56dp" android:layout_alignParentLeft="true" - app:riv_border_color="?attr/color_border" - app:riv_border_width="1dp" app:riv_corner_radius="@dimen/rounded_image_border" /> <LinearLayout diff --git a/src/main/res/layout/conversation_list_row.xml b/src/main/res/layout/conversation_list_row.xml index b00be5a4b..cbd884ae2 100644 --- a/src/main/res/layout/conversation_list_row.xml +++ b/src/main/res/layout/conversation_list_row.xml @@ -8,12 +8,12 @@ android:id="@+id/frame" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:background="?attr/color_background_primary"> + android:background="?attr/color_background_secondary"> <RelativeLayout android:layout_width="fill_parent" android:layout_height="wrap_content" - android:background="?attr/color_background_secondary" + android:background="?android:selectableItemBackground" android:orientation="horizontal" android:padding="8dp"> @@ -24,8 +24,6 @@ android:layout_alignParentLeft="true" android:padding="1dp" android:scaleType="centerCrop" - app:riv_border_color="?attr/color_border" - app:riv_border_width="1dp" app:riv_corner_radius="@dimen/rounded_image_border" /> <RelativeLayout diff --git a/src/main/res/layout/fragment_conversation.xml b/src/main/res/layout/fragment_conversation.xml index d081b6543..16f48bde2 100644 --- a/src/main/res/layout/fragment_conversation.xml +++ b/src/main/res/layout/fragment_conversation.xml @@ -5,7 +5,7 @@ <RelativeLayout xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?attr/color_background_secondary" + android:background="?attr/color_background_tertiary" android:clickable="false"> <ListView @@ -14,8 +14,9 @@ android:layout_height="match_parent" android:layout_above="@+id/snackbar" android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" android:layout_alignParentTop="true" - android:background="?attr/color_background_secondary" + android:background="?attr/color_background_tertiary" android:divider="@null" android:dividerHeight="0dp" android:listSelector="@android:color/transparent" @@ -53,7 +54,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" - android:layout_alignParentLeft="true"> + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true"> <RelativeLayout android:id="@+id/textsend" @@ -61,7 +63,7 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_alignParentLeft="true" - android:background="?attr/color_background_secondary" + android:background="?attr/color_background_tertiary" android:clickable="true" android:paddingBottom="2dp" android:paddingLeft="2dp" @@ -73,18 +75,34 @@ android:layout_height="48dp" android:layout_alignParentLeft="true" android:layout_centerVertical="true" - android:background="?attr/color_background_secondary" + android:background="?attr/color_background_tertiary" android:contentDescription="@string/attach_record_voice" android:src="@drawable/ic_send_voice_offline" android:visibility="gone" /> + <android.support.v7.widget.RecyclerView + android:id="@+id/media_preview" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_toStartOf="@+id/textSendButton" + android:background="@drawable/message_bubble_sent_blue" + android:orientation="horizontal" + android:paddingTop="8dp" + android:requiresFadingEdge="horizontal" + android:visibility="gone" + app:layoutManager="android.support.v7.widget.LinearLayoutManager" + tools:listitem="@layout/media_preview"></android.support.v7.widget.RecyclerView> + <de.pixart.messenger.ui.widget.EditMessage android:id="@+id/textinput" style="@style/Widget.Conversations.EditText" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_toEndOf="@+id/recordVoiceButton" android:layout_toLeftOf="@+id/textSendButton" android:layout_toRightOf="@+id/recordVoiceButton" + android:layout_toStartOf="@+id/textSendButton" android:background="@drawable/message_bubble_sent_blue" android:ems="10" android:imeOptions="flagNoExtractUi|actionSend" @@ -105,9 +123,10 @@ android:id="@+id/textSendButton" android:layout_width="48dp" android:layout_height="48dp" + android:layout_alignParentEnd="true" android:layout_alignParentRight="true" android:layout_centerVertical="true" - android:background="?attr/color_background_secondary" + android:background="?attr/color_background_tertiary" android:contentDescription="@string/send_message" android:src="@drawable/ic_send_text_offline" /> </RelativeLayout> @@ -130,15 +149,19 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" android:layout_centerVertical="true" android:layout_toLeftOf="@+id/snackbar_action" + android:layout_toStartOf="@+id/snackbar_action" android:paddingLeft="24dp" + android:paddingStart="24dp" android:textAppearance="@style/TextAppearance.Conversations.Body1.OnDark" /> <TextView android:id="@+id/snackbar_action" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_alignParentEnd="true" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:paddingBottom="16dp" @@ -166,8 +189,10 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" android:layout_centerVertical="true" android:paddingLeft="24dp" + android:paddingStart="24dp" android:textColor="@color/realblack" android:textSize="?attr/TextSizeBody" /> </RelativeLayout> diff --git a/src/main/res/layout/media.xml b/src/main/res/layout/media.xml new file mode 100644 index 000000000..f95ddeda3 --- /dev/null +++ b/src/main/res/layout/media.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout xmlns:android="http://schemas.android.com/apk/res/android"> + + <de.pixart.messenger.ui.widget.SquareFrameLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="2dp"> + + <ImageView + android:id="@+id/media" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/black54" + android:scaleType="centerInside" /> + </de.pixart.messenger.ui.widget.SquareFrameLayout> +</layout>
\ No newline at end of file diff --git a/src/main/res/layout/media_preview.xml b/src/main/res/layout/media_preview.xml new file mode 100644 index 000000000..8849e7b4a --- /dev/null +++ b/src/main/res/layout/media_preview.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout xmlns:android="http://schemas.android.com/apk/res/android"> + + <RelativeLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <ImageView + android:id="@+id/media_preview" + android:layout_width="@dimen/media_preview_size" + android:layout_height="@dimen/media_preview_size" + android:layout_centerInParent="true" + android:layout_margin="12dp" + android:background="@color/black54" + android:scaleType="center" /> + + <ImageButton + android:id="@+id/delete_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_alignParentRight="true" + android:layout_alignParentTop="true" + android:alpha="?attr/delete_icon_alpha" + android:background="?attr/selectableItemBackgroundBorderless" + android:src="?attr/icon_cancel" /> + </RelativeLayout> +</layout>
\ No newline at end of file diff --git a/src/main/res/layout/message_content.xml b/src/main/res/layout/message_content.xml index 044f8c64d..296944565 100644 --- a/src/main/res/layout/message_content.xml +++ b/src/main/res/layout/message_content.xml @@ -13,8 +13,6 @@ android:maxHeight="500dp" android:maxWidth="500dp" android:scaleType="centerCrop" - app:riv_border_color="?attr/color_border" - app:riv_border_width="1dp" app:riv_corner_radius="@dimen/rounded_image_border" /> <de.pixart.messenger.ui.widget.CopyTextView diff --git a/src/main/res/layout/message_received.xml b/src/main/res/layout/message_received.xml index 2c6fd044a..f8b5fce95 100644 --- a/src/main/res/layout/message_received.xml +++ b/src/main/res/layout/message_received.xml @@ -16,8 +16,6 @@ android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:scaleType="fitXY" - app:riv_border_color="?attr/color_border" - app:riv_border_width="1dip" app:riv_corner_radius="@dimen/rounded_image_border" /> <LinearLayout @@ -25,7 +23,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" - android:layout_marginLeft="4dp" + android:layout_marginLeft="-7dp" android:layout_marginRight="4dp" android:layout_toRightOf="@+id/message_photo" android:background="@drawable/message_bubble_received_light" diff --git a/src/main/res/layout/message_sent.xml b/src/main/res/layout/message_sent.xml index 63ce65b76..188e466d5 100644 --- a/src/main/res/layout/message_sent.xml +++ b/src/main/res/layout/message_sent.xml @@ -9,17 +9,27 @@ android:paddingRight="8dp" android:paddingTop="3dp"> - <com.makeramen.roundedimageview.RoundedImageView - android:id="@+id/message_photo" - android:layout_width="48dp" - android:layout_height="48dp" + <LinearLayout + android:id="@+id/message_photo_box" + android:layout_width="wrap_content" + android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentEnd="true" android:layout_alignParentRight="true" - android:scaleType="fitXY" - app:riv_border_color="?attr/color_border" - app:riv_border_width="1dp" - app:riv_corner_radius="@dimen/rounded_image_border" /> + android:orientation="vertical"> + + <com.makeramen.roundedimageview.RoundedImageView + android:id="@+id/message_photo" + android:layout_width="48dp" + android:layout_height="48dp" + android:scaleType="fitXY" + app:riv_corner_radius="@dimen/rounded_image_border" /> + + <View + android:id="@+id/placeholder" + android:layout_width="48dp" + android:layout_height="3dp" /> + </LinearLayout> <LinearLayout android:id="@+id/message_box" @@ -27,9 +37,9 @@ android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_marginLeft="4dp" - android:layout_marginRight="4dp" - android:layout_toLeftOf="@+id/message_photo" - android:layout_toStartOf="@+id/message_photo" + android:layout_marginRight="-7dp" + android:layout_toLeftOf="@+id/message_photo_box" + android:layout_toStartOf="@+id/message_photo_box" android:background="@drawable/message_bubble_sent_blue" android:longClickable="true" android:minHeight="48dp"> diff --git a/src/main/res/layout/message_status.xml b/src/main/res/layout/message_status.xml index d42637175..bd731cbe4 100644 --- a/src/main/res/layout/message_status.xml +++ b/src/main/res/layout/message_status.xml @@ -29,8 +29,6 @@ android:padding="0dp" android:scaleType="fitXY" android:visibility="gone" - app:riv_border_color="?attr/color_border" - app:riv_border_width="1dp" app:riv_corner_radius="@dimen/rounded_image_border" /> <TextView diff --git a/src/main/res/menu/choose_attachment.xml b/src/main/res/menu/choose_attachment.xml index ae6f47a95..a9a57404d 100644 --- a/src/main/res/menu/choose_attachment.xml +++ b/src/main/res/menu/choose_attachment.xml @@ -14,7 +14,12 @@ <item android:id="@+id/attach_take_picture" android:icon="?attr/ic_attach_camera" - android:title="@string/attach_take_from_camera" /> + android:title="@string/action_take_photo" /> + + <item + android:id="@+id/attach_record_video" + android:icon="?attr/ic_attach_videocam" + android:title="@string/action_take_video" /> <item android:id="@+id/attach_record_voice" android:icon="?attr/ic_attach_record" diff --git a/src/main/res/menu/fragment_conversation.xml b/src/main/res/menu/fragment_conversation.xml index d67bf5253..088501bcb 100644 --- a/src/main/res/menu/fragment_conversation.xml +++ b/src/main/res/menu/fragment_conversation.xml @@ -5,8 +5,8 @@ android:id="@+id/action_security" android:icon="?attr/icon_not_secure" android:orderInCategory="20" - app:showAsAction="always" - android:title="@string/action_secure" > + android:title="@string/action_secure" + app:showAsAction="always"> <menu> <group android:checkableBehavior="single"> <item @@ -28,8 +28,8 @@ android:id="@+id/action_attach_file" android:icon="?attr/icon_new_attachment" android:orderInCategory="30" - app:showAsAction="always" - android:title="@string/attach_file" > + android:title="@string/attach_file" + app:showAsAction="always"> <menu> <item @@ -45,7 +45,13 @@ <item android:id="@+id/attach_take_picture" android:icon="?attr/ic_attach_camera" - android:title="@string/attach_take_from_camera" /> + android:title="@string/action_take_photo" /> + + <item + android:id="@+id/attach_record_video" + android:icon="?attr/ic_attach_videocam" + android:title="@string/action_take_video" /> + <item android:id="@+id/attach_record_voice" android:icon="?attr/ic_attach_record" @@ -60,21 +66,33 @@ <item android:id="@+id/action_invite" android:orderInCategory="45" - app:showAsAction="never" - android:title="@string/invite_contact" /> + android:title="@string/invite_contact" + app:showAsAction="never" /> <item android:id="@+id/action_clear_history" android:orderInCategory="50" - app:showAsAction="never" - android:title="@string/action_clear_history" /> + android:title="@string/action_clear_history" + app:showAsAction="never" /> <item android:id="@+id/action_archive_chat" android:orderInCategory="60" - app:showAsAction="never" - android:title="@string/action_end_conversation" /> + android:title="@string/action_end_conversation" + app:showAsAction="never" /> <item android:id="@+id/action_create_issue" android:orderInCategory="100" - app:showAsAction="never" - android:title="@string/create_issue" /> + android:title="@string/create_issue" + app:showAsAction="never" /> + <item + android:id="@+id/action_group_details" + android:icon="@drawable/ic_group_white_24dp" + android:orderInCategory="100" + android:title="@string/action_group_details" + app:showAsAction="always" /> + <item + android:id="@+id/action_contact_details" + android:icon="@drawable/ic_contact_white_24dp" + android:orderInCategory="100" + android:title="@string/action_contact_details" + app:showAsAction="always" /> </menu>
\ No newline at end of file diff --git a/src/main/res/values-h360dp/dimens.xml b/src/main/res/values-h360dp/dimens.xml new file mode 100644 index 000000000..5e432c034 --- /dev/null +++ b/src/main/res/values-h360dp/dimens.xml @@ -0,0 +1,4 @@ +<resources> + <dimen name="publish_avatar_top_margin">16dp</dimen> + <dimen name="publish_avatar_size">128dp</dimen> +</resources>
\ No newline at end of file diff --git a/src/main/res/values-h500dp/dimens.xml b/src/main/res/values-h500dp/dimens.xml new file mode 100644 index 000000000..deea1124f --- /dev/null +++ b/src/main/res/values-h500dp/dimens.xml @@ -0,0 +1,4 @@ +<resources> + <dimen name="publish_avatar_top_margin">24dp</dimen> + <dimen name="publish_avatar_size">192dp</dimen> +</resources>
\ No newline at end of file diff --git a/src/main/res/values-w384dp/dimens.xml b/src/main/res/values-w384dp/dimens.xml index 430845b1c..e9350addd 100644 --- a/src/main/res/values-w384dp/dimens.xml +++ b/src/main/res/values-w384dp/dimens.xml @@ -1,5 +1,9 @@ <resources> <!-- 384dp is the screen width of the Nexus 4. Something like a Moto G is smaller but a Nexus 5X is larger --> <!-- https://material.io/devices/ --> + <dimen name="fineprint_size">12sp</dimen> + <dimen name="swipe_handle_size">48dp</dimen> <dimen name="audio_player_width">288dp</dimen> + <dimen name="avatar_on_details_screen_size">72dp</dimen> + <dimen name="media_size">64dp</dimen> <!-- ideally not larger than avatar_on_details_screen --> </resources>
\ No newline at end of file diff --git a/src/main/res/values-w585dp/dimens.xml b/src/main/res/values-w585dp/dimens.xml index 38ca33a11..067401781 100644 --- a/src/main/res/values-w585dp/dimens.xml +++ b/src/main/res/values-w585dp/dimens.xml @@ -1,3 +1,6 @@ <resources> <dimen name="activity_horizontal_margin">32dp</dimen> + <dimen name="avatar_on_details_screen_size">96dp</dimen> + <dimen name="media_size">80dp</dimen> <!-- ideally not larger than avatar_on_details_screen --> + <dimen name="browser_media_size">128dp</dimen> </resources> diff --git a/src/main/res/values/arrays.xml b/src/main/res/values/arrays.xml index ff36fe0ae..93da9bac4 100644 --- a/src/main/res/values/arrays.xml +++ b/src/main/res/values/arrays.xml @@ -37,7 +37,8 @@ <string-array name="quick_actions"> <item>@string/none</item> <item>@string/recently_used</item> - <item>@string/attach_take_from_camera</item> + <item>@string/action_take_photo</item> + <item>@string/action_take_video</item> <item>@string/attach_choose_picture</item> <item>@string/attach_record_voice</item> <item>@string/send_location</item> @@ -46,7 +47,8 @@ <string-array name="quick_action_values"> <item>none</item> <item>recent</item> - <item>TAKE_FROM_CAMERA</item> + <item>TAKE_PHOTO</item> + <item>RECORD_VIDEO</item> <item>CHOOSE_PICTURE</item> <item>RECORD_VOICE</item> <item>SEND_LOCATION</item> diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml index e89ed2944..672cb00fb 100644 --- a/src/main/res/values/attrs.xml +++ b/src/main/res/values/attrs.xml @@ -42,6 +42,16 @@ <attr name="conversations_overview_background" format="reference|color" /> + <attr name="media_preview_document" format="reference" /> + <attr name="media_preview_recording" format="reference" /> + <attr name="media_preview_audio" format="reference" /> + <attr name="media_preview_location" format="reference" /> + <attr name="media_preview_contact" format="reference" /> + <attr name="media_preview_app" format="reference" /> + <attr name="media_preview_calendar" format="reference" /> + <attr name="media_preview_archive" format="reference" /> + <attr name="media_preview_unknown" format="reference" /> + <attr name="icon_add_group" format="reference" /> <attr name="icon_add_person" format="reference" /> <attr name="icon_cancel" format="reference" /> @@ -83,6 +93,7 @@ <attr name="ic_attach_photo" format="reference" /> <attr name="ic_attach_record" format="reference" /> <attr name="ic_attach_video" format="reference" /> + <attr name="ic_attach_videocam" format="reference" /> <attr name="ic_file_apk" format="reference" /> <attr name="ic_file_pdf" format="reference" /> @@ -90,6 +101,7 @@ <attr name="ic_file_calendar" format="reference" /> <attr name="icon_alpha" format="float" /> + <attr name="delete_icon_alpha" format="float" /> <attr name="popupOverlayStyle" format="reference" /> diff --git a/src/main/res/values/defaults.xml b/src/main/res/values/defaults.xml index 29a256afb..e213ea773 100644 --- a/src/main/res/values/defaults.xml +++ b/src/main/res/values/defaults.xml @@ -62,7 +62,6 @@ <integer name="auto_accept_filesize_wifi">10485760</integer> <integer name="auto_accept_filesize_mobile">524288</integer> <integer name="auto_accept_filesize_roaming">0</integer> - <bool name="show_notification">true</bool> <bool name="vibrate_on_notification">true</bool> <bool name="led">true</bool> <string name="notification_ringtone">content://settings/system/notification_sound</string> @@ -87,7 +86,6 @@ <bool name="btbv">true</bool> <bool name="send_button_status">true</bool> <string name="quick_action">recent</string> - <bool name="return_to_previous">false</bool> <bool name="display_enter_key">false</bool> <bool name="show_dynamic_tags">false</bool> <integer name="grace_period">144</integer> @@ -108,11 +106,13 @@ <string name="default_font_size">small</string> <bool name="start_searching">false</bool> <bool name="show_record_voice_btn">true</bool> + <bool name="quick_share_attachment_choice">true</bool> <string-array name="domains"> <item>pix-art.de</item> <item>conversations.im</item> <item>jabber.cat</item> + <item>jabbers.one</item> <item>jabjab.de</item> <item>im.koderoot.net</item> <item>riotcat.org</item> diff --git a/src/main/res/values/dimens.xml b/src/main/res/values/dimens.xml index 50f955c89..526eb7d40 100644 --- a/src/main/res/values/dimens.xml +++ b/src/main/res/values/dimens.xml @@ -10,6 +10,9 @@ <dimen name="audio_player_width">224dp</dimen> <dimen name="avatar_item_distance">16dp</dimen> + <dimen name="media_preview_size">80dp</dimen> + <dimen name="media_size">56dp</dimen> <!-- ideally not larger than avatar_on_details_screen --> + <dimen name="browser_media_size">96dp</dimen> <dimen name="toolbar_elevation">4dp</dimen> <dimen name="publish_avatar_top_margin">8dp</dimen> @@ -24,6 +27,5 @@ <dimen name="scan_dot_size">8dp</dimen> <dimen name="background_image_opacity">0.12</dimen> - - <dimen name="rounded_image_border">5dp</dimen> + <dimen name="rounded_image_border">8dp</dimen> </resources> diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 321a5f97a..62b415cf3 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -107,8 +107,6 @@ <string name="pref_accept_files_summary_mobileroaming">When connected with mobile data in roaming automatically accept files smaller than…</string> <string name="pref_attachments">Attachments</string> <string name="pref_notification_settings">Notification</string> - <string name="pref_notifications">Notifications</string> - <string name="pref_notifications_summary">Notify when a new message arrives</string> <string name="pref_vibrate">Vibrate</string> <string name="pref_vibrate_summary">Vibrate when a new message arrives</string> <string name="pref_led">LED Notification</string> @@ -701,7 +699,6 @@ <string name="yesterday">Yesterday</string> <string name="pref_validate_hostname">Validate hostname with DNSSEC</string> <string name="pref_validate_hostname_summary">Server certificates that contain the validated hostname are considered verified</string> - <string name="network_is_unreachable">Network is unreachable</string> <string name="certificate_does_not_contain_jid">Certificate does not contain a Jabber ID</string> <string name="error_no_keys_to_trust_presence">There are no usable keys available for this contact.\nMake sure you have mutual presence subscription.</string> <string name="mark_as_read">Mark as read</string> @@ -715,8 +712,6 @@ <string name="contacts_have_read_up_to_this_point">%s have read up to this point</string> <string name="contact_has_read_up_to_this_point">%s has read up to this point</string> <string name="contacts_and_n_more_have_read_up_to_this_point">%1$s and %2$d more have read up to this point</string> - <string name="pref_return_to_previous">Quick Sharing</string> - <string name="pref_return_to_previous_summary">Immediately return to previous activity instead of opening the conversation after sharing something</string> <string name="conversation_unencrypted_hint">This chat is unencrypted, for security reasons you should activate message encryption by using the lock icon. The preferable encryption is OMEMO.</string> <string name="pref_warn_unencrypted_chat">Warn if chat is unencrypted</string> <string name="pref_warn_unencrypted_chat_summary">If message encryption is available, you should use it. If you don\'t use message encryption, show a warning message inside the chat.</string> @@ -791,7 +786,7 @@ <string name="show_termsofuse">Terms of Use</string> <string name="p1_s3_filetransfer">HTTP File Sharing for S3</string> <string name="pref_start_search">Direct Search</string> - <string name="pref_start_search_summary">At 'Start Conversation' screen open keyboard and place cursor in search field</string> + <string name="pref_start_search_summary">At ‘Start Conversation’ screen open keyboard and place cursor in search field</string> <string name="paste_as_quote">Paste as quote</string> <string name="group_chat_avatar">Group chat avatar</string> <string name="host_does_not_support_group_chat_avatars">Host does not support group chat avatars</string> @@ -810,4 +805,25 @@ <string name="no_location_permission">Pix-Art Messenger needs access to location services</string> <string name="pref_show_record_voice_btn_summary">Show record voice button in chats as quick action</string> <string name="pref_show_record_voice_btn">Show record voice button</string> + <string name="pref_use_quick_share_attachment_choice_summary">Replace send button with attachment choice if no message is typed. Otherwise show quick actions, which can be cofigured in the following setting.</string> + <string name="pref_quick_share_attachment_choice">Attachment choice</string> + <string name="phone_book">Address book</string> + <string name="unable_to_save_recording">Unable to save recording</string> + <string name="foreground_service_channel_name">Foreground service</string> + <string name="foreground_service_channel_description">This notification category is used to display a permanent notification indicating that Pix-Art Messenger is running.</string> + <string name="notification_group_status_information">Status Information</string> + <string name="error_channel_name">Connectivity Problems</string> + <string name="error_channel_description">This notification category is used to display a notification in case there is a problem connecting to an account.</string> + <string name="notification_group_messages">Messages</string> + <string name="messages_channel_name">Messages</string> + <string name="silent_messages_channel_name">Silent messages</string> + <string name="silent_messages_channel_description">This notification category is used to display notifications that should not trigger any sound. For example when being active on another device (Grace Period).</string> + <string name="pref_more_notification_settings">Notification Settings</string> + <string name="pref_more_notification_settings_summary">Importance, Sound, Vibrate</string> + <string name="video_compression_channel_name">Video compression</string> + <string name="backup_channel_name">Database backup</string> + <string name="app_update_channel_name">App update</string> + <string name="action_group_details">Group details</string> + <string name="view_media">View media</string> + <string name="media_browser">Media browser</string> </resources> diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index c8a0a97a1..50cf8e93a 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -65,6 +65,16 @@ </item> <item name="ic_send_voice_offline" type="reference">@drawable/ic_send_voice_offline</item> + <item name="media_preview_document" type="reference">@drawable/ic_description_black_48dp</item> + <item name="media_preview_recording" type="reference">@drawable/ic_mic_black_48dp</item> + <item name="media_preview_audio" type="reference">@drawable/ic_headset_black_48dp</item> + <item name="media_preview_location" type="reference">@drawable/ic_room_black_48dp</item> + <item name="media_preview_contact" type="reference">@drawable/ic_person_black_48dp</item> + <item name="media_preview_app" type="reference">@drawable/ic_android_black_48dp</item> + <item name="media_preview_calendar" type="reference">@drawable/ic_event_black_48dp</item> + <item name="media_preview_archive" type="reference">@drawable/ic_archive_black_48dp</item> + <item name="media_preview_unknown" type="reference">@drawable/ic_help_black_48dp</item> + <item name="icon_add_group" type="reference">@drawable/ic_group_add_white_24dp</item> <item name="icon_add_person" type="reference">@drawable/ic_person_add_white_24dp</item> <item name="icon_cancel" type="reference">@drawable/ic_cancel_black_24dp</item> @@ -106,8 +116,10 @@ <item name="dialog_horizontal_padding">16dp</item> <item name="dialog_vertical_padding">16dp</item> <item name="icon_alpha" type="float">0.54</item> + <item name="delete_icon_alpha" type="float">0.70</item> <item name="ic_attach_camera" type="reference">@drawable/ic_attach_camera</item> + <item type="reference" name="ic_attach_videocam">@drawable/ic_attach_videocam</item> <item name="ic_attach_document" type="reference">@drawable/ic_attach_document</item> <item name="ic_attach_location" type="reference">@drawable/ic_attach_location</item> <item name="ic_attach_photo" type="reference">@drawable/ic_attach_photo</item> @@ -150,9 +162,9 @@ <item name="colorControlActivated">@color/accent</item> <item name="text_Color_Main">@color/realwhite</item> - <item name="color_background_primary">@color/grey800</item> - <item name="color_background_secondary">@color/grey900</item> - <item name="color_background_tertiary">@color/grey700</item> + <item name="color_background_primary">@color/grey700</item> + <item name="color_background_secondary">@color/grey800</item> + <item name="color_background_tertiary">@color/grey900</item> <item name="color_warning">@color/red_a700</item> <item name="TextColorOnline">@color/green500</item> <item name="TextColorError">@color/red500</item> @@ -210,6 +222,7 @@ </item> <item name="ic_attach_camera" type="reference">@drawable/ic_attach_camera_white</item> + <item name="ic_attach_videocam" type="reference">@drawable/ic_attach_videocam_white</item> <item name="ic_attach_video" type="reference">@drawable/ic_attach_videocam_white</item> <item name="ic_attach_document" type="reference">@drawable/ic_attach_document_white</item> <item name="ic_attach_location" type="reference">@drawable/ic_attach_location_white</item> @@ -219,10 +232,21 @@ <item name="conversations_overview_background">@color/primary_dark</item> <item name="icon_alpha" type="float">0.7</item> + <item name="delete_icon_alpha" type="float">0.7</item> <item name="dialog_horizontal_padding">24dp</item> <item name="dialog_vertical_padding">16dp</item> + <item name="media_preview_document" type="reference">@drawable/ic_description_white_48dp</item> + <item name="media_preview_recording" type="reference">@drawable/ic_mic_white_48dp</item> + <item name="media_preview_audio" type="reference">@drawable/ic_headset_white_48dp</item> + <item name="media_preview_location" type="reference">@drawable/ic_room_white_48dp</item> + <item name="media_preview_contact" type="reference">@drawable/ic_person_white_48dp</item> + <item name="media_preview_app" type="reference">@drawable/ic_android_white_48dp</item> + <item name="media_preview_calendar" type="reference">@drawable/ic_event_white_48dp</item> + <item name="media_preview_archive" type="reference">@drawable/ic_archive_white_48dp</item> + <item name="media_preview_unknown" type="reference">@drawable/ic_help_white_48dp</item> + <item name="icon_add_group" type="reference">@drawable/ic_group_add_white_24dp</item> <item name="icon_add_person" type="reference">@drawable/ic_person_add_white_24dp</item> <item name="icon_cancel" type="reference">@drawable/ic_cancel_white_24dp</item> @@ -284,6 +308,7 @@ <item name="colorAccent">@color/accent</item> <item name="color_background_primary">@color/grey50</item> <item name="divider">@color/black12</item> + <item name="TextSizeTitle">18sp</item> <item name="TextSizeBody2">14sp</item> <item name="TextSizeDisplay2">45sp</item> <item name="android:windowNoTitle">true</item> diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index 31ca6158b..819fcded9 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -38,6 +38,11 @@ android:key="presence_colored_names" android:summary="@string/pref_use_colored_names_to_indicate_status_summary" android:title="@string/pref_use_colored_names_to_indicate_status" /> + <CheckBoxPreference + android:defaultValue="@bool/quick_share_attachment_choice" + android:key="quick_share_attachment_choice" + android:summary="@string/pref_use_quick_share_attachment_choice_summary" + android:title="@string/pref_quick_share_attachment_choice" /> <ListPreference android:defaultValue="@string/quick_action" android:dialogTitle="@string/choose_quick_action" @@ -81,18 +86,11 @@ android:value="notifications" /> </intent> <CheckBoxPreference - android:defaultValue="@bool/show_notification" - android:key="show_notification" - android:summary="@string/pref_notifications_summary" - android:title="@string/pref_notifications" /> - <CheckBoxPreference android:defaultValue="@bool/headsup_notifications" - android:dependency="show_notification" android:key="notification_headsup" android:summary="@string/pref_headsup_notifications_summary" android:title="@string/pref_headsup_notifications" /> <PreferenceScreen - android:dependency="show_notification" android:key="quiet_hours" android:summary="@string/pref_quiet_hours_summary" android:title="@string/title_pref_quiet_hours"> @@ -124,29 +122,38 @@ </PreferenceScreen> <CheckBoxPreference android:defaultValue="@bool/vibrate_on_notification" - android:dependency="show_notification" android:key="vibrate_on_notification" android:summary="@string/pref_vibrate_summary" android:title="@string/pref_vibrate" /> <CheckBoxPreference android:defaultValue="@bool/led" - android:dependency="show_notification" android:key="led" android:summary="@string/pref_led_summary" android:title="@string/pref_led" /> <RingtonePreference android:defaultValue="@string/notification_ringtone" - android:dependency="show_notification" android:key="notification_ringtone" android:ringtoneType="notification" android:summary="@string/pref_sound_summary" android:title="@string/pref_sound" /> <CheckBoxPreference android:defaultValue="@bool/notifications_from_strangers" - android:dependency="show_notification" android:key="notifications_from_strangers" android:summary="@string/pref_notifications_from_strangers_summary" android:title="@string/pref_notifications_from_strangers" /> + <PreferenceScreen + android:key="more_notification_settings" + android:summary="@string/pref_more_notification_settings_summary" + android:title="@string/pref_more_notification_settings"> + <intent android:action="android.settings.CHANNEL_NOTIFICATION_SETTINGS"> + <extra + android:name="android.provider.extra.APP_PACKAGE" + android:value="@string/applicationId" /> + <extra + android:name="android.provider.extra.CHANNEL_ID" + android:value="messages" /> + </intent> + </PreferenceScreen> </PreferenceScreen> <!--Attachments--> <PreferenceScreen @@ -196,11 +203,6 @@ android:key="video_compression" android:summary="@string/pref_video_compression_summary" android:title="@string/pref_video_compression" /> - <CheckBoxPreference - android:defaultValue="@bool/return_to_previous" - android:key="return_to_previous" - android:summary="@string/pref_return_to_previous_summary" - android:title="@string/pref_return_to_previous" /> </PreferenceScreen> <!--status--> <PreferenceScreen |