From 91ad3acc79048de3bec8eaa18f589eeafa3f91c0 Mon Sep 17 00:00:00 2001 From: Vavassor Date: Fri, 3 Mar 2017 20:44:44 -0500 Subject: [PATCH] Unfinished keyboard GIF picking stuff? Not accessible by the user, yet. --- app/build.gradle | 1 + .../java/com/keylesspalace/tusky/Assert.java | 2 +- .../keylesspalace/tusky/ComposeActivity.java | 265 ++++++++++++++---- .../com/keylesspalace/tusky/HtmlUtils.java | 2 + .../keylesspalace/tusky/StatusViewHolder.java | 4 + app/src/main/res/layout/activity_compose.xml | 15 +- 6 files changed, 217 insertions(+), 72 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ea1a33d4..56949fb9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,6 +27,7 @@ dependencies { }) compile 'com.android.support:appcompat-v7:25.1.0' compile 'com.android.support:recyclerview-v7:25.1.0' + compile 'com.android.support:support-v13:25.1.0' compile 'com.android.volley:volley:1.0.0' compile 'com.android.support:design:25.1.0' testCompile 'junit:junit:4.12' diff --git a/app/src/main/java/com/keylesspalace/tusky/Assert.java b/app/src/main/java/com/keylesspalace/tusky/Assert.java index 11b6af3e..46c27c43 100644 --- a/app/src/main/java/com/keylesspalace/tusky/Assert.java +++ b/app/src/main/java/com/keylesspalace/tusky/Assert.java @@ -15,7 +15,7 @@ package com.keylesspalace.tusky; -/** Android Studio complains about built-in assertions so here's this is an alternative. */ +/** Android Studio complains about built-in assertions so this is an alternative. */ class Assert { private static boolean ENABLED = BuildConfig.DEBUG; diff --git a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java index c6b76523..5f45a59e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java @@ -23,6 +23,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; @@ -42,20 +43,29 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.design.widget.Snackbar; +import android.support.v13.view.inputmethod.EditorInfoCompat; +import android.support.v13.view.inputmethod.InputConnectionCompat; +import android.support.v13.view.inputmethod.InputContentInfoCompat; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.text.Editable; +import android.text.InputType; import android.text.Spannable; import android.text.Spanned; import android.text.TextWatcher; import android.text.style.ForegroundColorSpan; +import android.view.Gravity; import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; import android.webkit.MimeTypeMap; import android.widget.Button; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; @@ -74,6 +84,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Iterator; @@ -87,6 +98,7 @@ public class ComposeActivity extends BaseActivity { private static final int STATUS_MEDIA_SIZE_LIMIT = 4000000; // 4MB private static final int MEDIA_PICK_RESULT = 1; private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1; + private static final int MEDIA_SIZE_UNKNOWN = -1; private String inReplyToId; private String domain; @@ -102,6 +114,9 @@ public class ComposeActivity extends BaseActivity { private boolean statusHideText; // private View contentWarningBar; private boolean statusAlreadyInFlight; // to prevent duplicate sends by mashing the send button + private InputContentInfoCompat currentInputContentInfo; + private int currentFlags; + private ProgressDialog finishingUploadDialog; private static class QueuedMedia { enum Type { @@ -312,6 +327,13 @@ public class ComposeActivity extends BaseActivity { statusHideText = savedInstanceState.getBoolean("statusHideText"); // Keep these until everything needed to put them in the queue is finished initializing. savedMediaQueued = savedInstanceState.getParcelableArrayList("savedMediaQueued"); + // These are for restoring an in-progress commit content operation. + InputContentInfoCompat previousInputContentInfo = InputContentInfoCompat.wrap( + savedInstanceState.getParcelable("commitContentInputContentInfo")); + int previousFlags = savedInstanceState.getInt("commitContentFlags"); + if (previousInputContentInfo != null) { + onCommitContentInternal(previousInputContentInfo, previousFlags); + } } else { showMarkSensitive = false; statusVisibility = preferences.getString("rememberedVisibility", "public"); @@ -329,7 +351,12 @@ public class ComposeActivity extends BaseActivity { domain = preferences.getString("domain", null); accessToken = preferences.getString("accessToken", null); - textEditor = (EditText) findViewById(R.id.field_status); + textEditor = createEditText(null); // new String[] { "image/gif", "image/webp" } + if (savedInstanceState != null) { + textEditor.onRestoreInstanceState(savedInstanceState.getParcelable("textEditorState")); + } + RelativeLayout editArea = (RelativeLayout) findViewById(R.id.compose_edit_area); + editArea.addView(textEditor); final TextView charactersLeft = (TextView) findViewById(R.id.characters_left); final int mentionColour = ThemeUtils.getColor(this, R.attr.compose_mention_color); TextWatcher textEditorWatcher = new TextWatcher() { @@ -457,6 +484,14 @@ public class ComposeActivity extends BaseActivity { outState.putString("statusVisibility", statusVisibility); outState.putBoolean("statusMarkSensitive", statusMarkSensitive); outState.putBoolean("statusHideText", statusHideText); + outState.putParcelable("textEditorState", textEditor.onSaveInstanceState()); + if (currentInputContentInfo != null) { + outState.putParcelable("commitContentInputContentInfo", + (Parcelable) currentInputContentInfo.unwrap()); + outState.putInt("commitContentFlags", currentFlags); + } + currentInputContentInfo = null; + currentFlags = 0; super.onSaveInstanceState(outState); } @@ -476,6 +511,101 @@ public class ComposeActivity extends BaseActivity { VolleySingleton.getInstance(this).cancelAll(TAG); } + private EditText createEditText(String[] contentMimeTypes) { + final String[] mimeTypes; + if (contentMimeTypes == null || contentMimeTypes.length == 0) { + mimeTypes = new String[0]; + } else { + mimeTypes = Arrays.copyOf(contentMimeTypes, contentMimeTypes.length); + } + EditText editText = new EditText(this) { + @Override + public InputConnection onCreateInputConnection(EditorInfo editorInfo) { + final InputConnection ic = super.onCreateInputConnection(editorInfo); + EditorInfoCompat.setContentMimeTypes(editorInfo, mimeTypes); + final InputConnectionCompat.OnCommitContentListener callback = + new InputConnectionCompat.OnCommitContentListener() { + @Override + public boolean onCommitContent(InputContentInfoCompat inputContentInfo, + int flags, Bundle opts) { + return ComposeActivity.this.onCommitContent(inputContentInfo, flags, + mimeTypes); + } + }; + return InputConnectionCompat.createWrapper(ic, editorInfo, callback); + } + }; + ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + editText.setLayoutParams(layoutParams); + editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE); + editText.setEms(10); + editText.setGravity(Gravity.START | Gravity.TOP); + editText.setHint(R.string.hint_compose); + return editText; + } + + private boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, + String[] mimeTypes) { + try { + if (currentInputContentInfo != null) { + currentInputContentInfo.releasePermission(); + } + } catch (Exception e) { + Log.e(TAG, "InputContentInfoCompat#releasePermission() failed." + e.getMessage()); + } finally { + currentInputContentInfo = null; + } + + // Verify the returned content's type is actually in the list of MIME types requested. + boolean supported = false; + for (final String mimeType : mimeTypes) { + if (inputContentInfo.getDescription().hasMimeType(mimeType)) { + supported = true; + break; + } + } + + return supported && onCommitContentInternal(inputContentInfo, flags); + } + + private boolean onCommitContentInternal(InputContentInfoCompat inputContentInfo, int flags) { + if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) { + try { + inputContentInfo.requestPermission(); + } catch (Exception e) { + Log.e(TAG, "InputContentInfoCompat#requestPermission() failed." + e.getMessage()); + return false; + } + } + + // Determine the file size before putting handing it off to be put in the queue. + Uri uri = inputContentInfo.getContentUri(); + long mediaSize; + AssetFileDescriptor descriptor = null; + try { + descriptor = getContentResolver().openAssetFileDescriptor(uri, "r"); + } catch (FileNotFoundException e) { + // Eat this exception, having the descriptor be null is sufficient. + } + if (descriptor != null) { + mediaSize = descriptor.getLength(); + try { + descriptor.close(); + } catch (IOException e) { + // Just eat this exception. + } + } else { + mediaSize = MEDIA_SIZE_UNKNOWN; + } + pickMedia(uri, mediaSize); + + currentInputContentInfo = inputContentInfo; + currentFlags = flags; + + return true; + } + private void sendStatus(String content, String visibility, boolean sensitive, String spoilerText) { String endpoint = getString(R.string.endpoint_status); @@ -535,7 +665,7 @@ public class ComposeActivity extends BaseActivity { private void readyStatus(final String content, final String visibility, final boolean sensitive, final String spoilerText) { - final ProgressDialog dialog = ProgressDialog.show( + finishingUploadDialog = ProgressDialog.show( this, getString(R.string.dialog_title_finishing_media_upload), getString(R.string.dialog_message_uploading_media), true, true); final AsyncTask waitForMediaTask = @@ -553,7 +683,8 @@ public class ComposeActivity extends BaseActivity { @Override protected void onPostExecute(Boolean successful) { super.onPostExecute(successful); - dialog.dismiss(); + finishingUploadDialog.dismiss(); + finishingUploadDialog = null; if (successful) { sendStatus(content, visibility, sensitive, spoilerText); } else { @@ -568,7 +699,7 @@ public class ComposeActivity extends BaseActivity { super.onCancelled(); } }; - dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + finishingUploadDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { /* Generating an interrupt by passing true here is important because an interrupt @@ -848,6 +979,9 @@ public class ComposeActivity extends BaseActivity { private void onUploadFailure(QueuedMedia item) { displayTransientError(R.string.error_media_upload_sending); + if (finishingUploadDialog != null) { + finishingUploadDialog.cancel(); + } removeMediaFromQueue(item); } @@ -867,69 +1001,78 @@ public class ComposeActivity extends BaseActivity { super.onActivityResult(requestCode, resultCode, data); if (requestCode == MEDIA_PICK_RESULT && resultCode == RESULT_OK && data != null) { Uri uri = data.getData(); - ContentResolver contentResolver = getContentResolver(); + long mediaSize; Cursor cursor = getContentResolver().query(uri, null, null, null, null); - if (cursor == null) { - displayTransientError(R.string.error_media_upload_opening); - return; - } - int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE); - cursor.moveToFirst(); - long mediaSize = cursor.getLong(sizeIndex); - cursor.close(); - String mimeType = contentResolver.getType(uri); - if (mimeType != null) { - String topLevelType = mimeType.substring(0, mimeType.indexOf('/')); - switch (topLevelType) { - case "video": { - if (mediaSize > STATUS_MEDIA_SIZE_LIMIT) { - displayTransientError(R.string.error_media_upload_size); - return; - } - if (mediaQueued.size() > 0 - && mediaQueued.get(0).type == QueuedMedia.Type.IMAGE) { - displayTransientError(R.string.error_media_upload_image_or_video); - return; - } - MediaMetadataRetriever retriever = new MediaMetadataRetriever(); - retriever.setDataSource(this, uri); - Bitmap source = retriever.getFrameAtTime(); - Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, 96, 96); - source.recycle(); - addMediaToQueue(QueuedMedia.Type.VIDEO, bitmap, uri, mediaSize); - break; + if (cursor != null) { + int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE); + cursor.moveToFirst(); + mediaSize = cursor.getLong(sizeIndex); + cursor.close(); + } else { + mediaSize = MEDIA_SIZE_UNKNOWN; + } + pickMedia(uri, mediaSize); + } + } + + private void pickMedia(Uri uri, long mediaSize) { + ContentResolver contentResolver = getContentResolver(); + if (mediaSize == MEDIA_SIZE_UNKNOWN) { + displayTransientError(R.string.error_media_upload_opening); + return; + } + String mimeType = contentResolver.getType(uri); + if (mimeType != null) { + String topLevelType = mimeType.substring(0, mimeType.indexOf('/')); + switch (topLevelType) { + case "video": { + if (mediaSize > STATUS_MEDIA_SIZE_LIMIT) { + displayTransientError(R.string.error_media_upload_size); + return; } - case "image": { - InputStream stream; - try { - stream = contentResolver.openInputStream(uri); - } catch (FileNotFoundException e) { - displayTransientError(R.string.error_media_upload_opening); - return; - } - Bitmap source = BitmapFactory.decodeStream(stream); - Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, 96, 96); - source.recycle(); - try { - if (stream != null) { - stream.close(); - } - } catch (IOException e) { - bitmap.recycle(); - displayTransientError(R.string.error_media_upload_opening); - return; - } - addMediaToQueue(QueuedMedia.Type.IMAGE, bitmap, uri, mediaSize); - break; + if (mediaQueued.size() > 0 + && mediaQueued.get(0).type == QueuedMedia.Type.IMAGE) { + displayTransientError(R.string.error_media_upload_image_or_video); + return; + } + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(this, uri); + Bitmap source = retriever.getFrameAtTime(); + Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, 96, 96); + source.recycle(); + addMediaToQueue(QueuedMedia.Type.VIDEO, bitmap, uri, mediaSize); + break; + } + case "image": { + InputStream stream; + try { + stream = contentResolver.openInputStream(uri); + } catch (FileNotFoundException e) { + displayTransientError(R.string.error_media_upload_opening); + return; } - default: { - displayTransientError(R.string.error_media_upload_type); - break; + Bitmap source = BitmapFactory.decodeStream(stream); + Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, 96, 96); + source.recycle(); + try { + if (stream != null) { + stream.close(); + } + } catch (IOException e) { + bitmap.recycle(); + displayTransientError(R.string.error_media_upload_opening); + return; } + addMediaToQueue(QueuedMedia.Type.IMAGE, bitmap, uri, mediaSize); + break; + } + default: { + displayTransientError(R.string.error_media_upload_type); + break; } - } else { - displayTransientError(R.string.error_media_upload_type); } + } else { + displayTransientError(R.string.error_media_upload_type); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/HtmlUtils.java b/app/src/main/java/com/keylesspalace/tusky/HtmlUtils.java index 6527489a..81b72835 100644 --- a/app/src/main/java/com/keylesspalace/tusky/HtmlUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/HtmlUtils.java @@ -28,6 +28,7 @@ class HtmlUtils { return s.subSequence(0, i + 1); } + @SuppressWarnings("deprecation") static Spanned fromHtml(String html) { Spanned result; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { @@ -40,6 +41,7 @@ class HtmlUtils { return (Spanned) trimTrailingWhitespace(result); } + @SuppressWarnings("deprecation") static String toHtml(Spanned text) { String result; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java index dd72a599..bc5d91f7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java @@ -295,6 +295,10 @@ class StatusViewHolder extends RecyclerView.ViewHolder { } private void setupButtons(final StatusActionListener listener, final String accountId) { + /* Originally position was passed through to all these listeners, but it caused several + * bugs where other statuses in the list would be removed or added and cause the position + * here to become outdated. So, getting the adapter position at the time the listener is + * actually called is the appropriate solution. */ avatar.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { diff --git a/app/src/main/res/layout/activity_compose.xml b/app/src/main/res/layout/activity_compose.xml index b063c158..18c1b719 100644 --- a/app/src/main/res/layout/activity_compose.xml +++ b/app/src/main/res/layout/activity_compose.xml @@ -50,23 +50,18 @@ + android:layout_weight="1" + android:id="@+id/compose_edit_area"> - + + android:layout_alignParentBottom="true">