Do not compress GIFs, allow GBoard to send GIFs

Michael Eden created

Change summary

build.gradle                                                             |  2 
src/main/java/eu/siacs/conversations/persistance/FileBackend.java        |  3 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java |  7 
src/main/java/eu/siacs/conversations/ui/ConversationActivity.java        |  4 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java        | 36 
src/main/java/eu/siacs/conversations/ui/EditMessage.java                 | 37 
src/main/java/eu/siacs/conversations/utils/MimeUtils.java                | 21 
7 files changed, 107 insertions(+), 3 deletions(-)

Detailed changes

build.gradle 🔗

@@ -26,7 +26,7 @@ dependencies {
     playstoreCompile 'com.google.android.gms:play-services-gcm:9.4.0'
     compile 'org.sufficientlysecure:openpgp-api:10.0'
     compile 'com.soundcloud.android:android-crop:1.0.1@aar'
-    compile 'com.android.support:support-v13:24.2.0'
+    compile 'com.android.support:support-v13:25.1.0'
     compile 'org.bouncycastle:bcprov-jdk15on:1.52'
     compile 'org.bouncycastle:bcmail-jdk15on:1.52'
     compile 'org.jitsi:org.otr4j:0.22'

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

@@ -55,6 +55,7 @@ import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.ExifHelper;
 import eu.siacs.conversations.utils.FileUtils;
 import eu.siacs.conversations.utils.FileWriterException;
+import eu.siacs.conversations.utils.MimeUtils;
 import eu.siacs.conversations.xmpp.pep.Avatar;
 
 public class FileBackend {
@@ -276,7 +277,7 @@ public class FileBackend {
 	}
 
 	public void copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException {
-		String mime = mXmppConnectionService.getContentResolver().getType(uri);
+		String mime = MimeUtils.guessMimeTypeFromUri(mXmppConnectionService, uri);
 		Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage (mime="+mime+")");
 		String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime);
 		if (extension == null) {

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

@@ -98,6 +98,7 @@ import eu.siacs.conversations.ui.UiCallback;
 import eu.siacs.conversations.utils.ConversationsFileObserver;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.ExceptionHelper;
+import eu.siacs.conversations.utils.MimeUtils;
 import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
 import eu.siacs.conversations.utils.PRNGFixes;
 import eu.siacs.conversations.utils.PhoneHelper;
@@ -493,9 +494,13 @@ public class XmppConnectionService extends Service {
 			callback.error(R.string.security_error_invalid_file_access, null);
 			return;
 		}
+
+		final String mimeType = MimeUtils.guessMimeTypeFromUri(this, uri);
 		final String compressPictures = getCompressPicturesPreference();
+
 		if ("never".equals(compressPictures)
-				|| ("auto".equals(compressPictures) && getFileBackend().useImageAsIs(uri))) {
+				|| ("auto".equals(compressPictures) && getFileBackend().useImageAsIs(uri))
+				|| (mimeType != null && mimeType.endsWith("/gif"))) {
 			Log.d(Config.LOGTAG,conversation.getAccount().getJid().toBareJid()+ ": not compressing picture. sending as file");
 			attachFileToConversation(conversation, uri, callback);
 			return;

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

@@ -1560,6 +1560,10 @@ public class ConversationActivity extends XmppActivity
 		});
 	}
 
+	public void attachImageToConversation(Uri uri) {
+		this.attachImageToConversation(getSelectedConversation(), uri);
+	}
+
 	private void attachImageToConversation(Conversation conversation, Uri uri) {
 		if (conversation == null) {
 			return;

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

@@ -11,6 +11,8 @@ import android.content.Intent;
 import android.content.IntentSender.SendIntentException;
 import android.os.Bundle;
 import android.os.Handler;
+import android.support.v13.view.inputmethod.InputConnectionCompat;
+import android.support.v13.view.inputmethod.InputContentInfoCompat;
 import android.text.Editable;
 import android.text.InputType;
 import android.util.Log;
@@ -285,6 +287,37 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 			}
 		}
 	};
+	private EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() {
+		@Override
+		public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] contentMimeTypes) {
+			// try to get permission to read the image, if applicable
+			if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
+				try {
+					inputContentInfo.requestPermission();
+				} catch (Exception e) {
+					Log.e(Config.LOGTAG, "InputContentInfoCompat#requestPermission() failed.", e);
+					Toast.makeText(
+							activity,
+							activity.getString(R.string.no_permission_to_access_x, inputContentInfo.getDescription()),
+							Toast.LENGTH_LONG
+					).show();
+					return false;
+				}
+			}
+
+			// send the image
+			activity.attachImageToConversation(inputContentInfo.getContentUri());
+
+			// TODO: revoke permissions?
+			// since uploading an image is async its tough to wire a callback to when
+			// the image has finished uploading.
+			// According to the docs: "calling IC#releasePermission() is just to be a
+			// good citizen. Even if we failed to call that method, the system would eventually revoke
+			// the permission sometime after inputContentInfo object gets garbage-collected."
+			// See: https://developer.android.com/samples/CommitContentSampleApp/src/com.example.android.commitcontent.app/MainActivity.html#l164
+			return true;
+		}
+	};
 	private OnClickListener mSendButtonListener = new OnClickListener() {
 
 		@Override
@@ -416,6 +449,8 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 	public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
 		final View view = inflater.inflate(R.layout.fragment_conversation, container, false);
 		view.setOnClickListener(null);
+
+		String[] allImagesMimeType = {"image/*"};
 		mEditMessage = (EditMessage) view.findViewById(R.id.textinput);
 		mEditMessage.setOnClickListener(new OnClickListener() {
 
@@ -427,6 +462,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 			}
 		});
 		mEditMessage.setOnEditorActionListener(mEditorActionListener);
+		mEditMessage.setRichContentListener(allImagesMimeType, mEditorContentListener);
 
 		mSendButton = (ImageButton) view.findViewById(R.id.textSendButton);
 		mSendButton.setOnClickListener(this.mSendButtonListener);

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

@@ -1,19 +1,33 @@
 package eu.siacs.conversations.ui;
 
+import android.support.v13.view.inputmethod.EditorInfoCompat;
+import android.support.v13.view.inputmethod.InputConnectionCompat;
+import android.support.v13.view.inputmethod.InputContentInfoCompat;
+
 import android.content.Context;
 import android.os.Build;
+import android.os.Bundle;
 import android.os.Handler;
 import android.text.Editable;
 import android.text.InputFilter;
 import android.text.Spanned;
 import android.util.AttributeSet;
 import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
 import android.widget.EditText;
 
 import eu.siacs.conversations.Config;
 
 public class EditMessage extends EditText {
 
+	public interface OnCommitContentListener {
+		boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] mimeTypes);
+	}
+
+	private OnCommitContentListener mCommitContentListener = null;
+	private String[] mimeTypes = null;
+
 	public EditMessage(Context context, AttributeSet attrs) {
 		super(context, attrs);
 	}
@@ -125,4 +139,27 @@ public class EditMessage extends EditText {
 			return super.onTextContextMenuItem(id);
 		}
 	}
+
+	public void setRichContentListener(String[] mimeTypes, OnCommitContentListener listener) {
+		this.mimeTypes = mimeTypes;
+		this.mCommitContentListener = listener;
+	}
+
+	@Override
+	public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
+		final InputConnection ic = super.onCreateInputConnection(editorInfo);
+
+		if (mimeTypes != null && mCommitContentListener != null) {
+			EditorInfoCompat.setContentMimeTypes(editorInfo, mimeTypes);
+			return InputConnectionCompat.createWrapper(ic, editorInfo, new InputConnectionCompat.OnCommitContentListener() {
+				@Override
+				public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts) {
+					return EditMessage.this.mCommitContentListener.onCommitContent(inputContentInfo, flags, opts, mimeTypes);
+				}
+			});
+		}
+		else {
+			return ic;
+		}
+	}
 }

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

@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 package eu.siacs.conversations.utils;
+import android.content.Context;
+import android.net.Uri;
+
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
@@ -484,4 +487,22 @@ public final class MimeUtils {
         }
         return mimeTypeToExtensionMap.get(mimeType);
     }
+
+    public static String guessMimeTypeFromUri(Context context, Uri uri) {
+        // try the content resolver
+        String mimeType = context.getContentResolver().getType(uri);
+        // try the extension
+        if (mimeType == null && uri.getPath() != null) {
+            String path = uri.getPath();
+            int start = path.lastIndexOf('.') + 1;
+            if (start < path.length()) {
+                mimeType = MimeUtils.guessMimeTypeFromExtension(path.substring(start));
+            }
+        }
+        // sometimes this works (as with the commit content api)
+        if (mimeType == null) {
+            mimeType = uri.getQueryParameter("mimeType");
+        }
+        return mimeType;
+    }
 }