Unfinished keyboard GIF picking stuff? Not accessible by the user, yet.

This commit is contained in:
Vavassor 2017-03-03 20:44:44 -05:00
parent 9e49da64bf
commit 91ad3acc79
6 changed files with 220 additions and 75 deletions

View File

@ -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'

View File

@ -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;

View File

@ -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<Void, Void, Boolean> 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;
}
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;
}
default: {
displayTransientError(R.string.error_media_upload_type);
break;
}
}
if (cursor != null) {
int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
cursor.moveToFirst();
mediaSize = cursor.getLong(sizeIndex);
cursor.close();
} else {
displayTransientError(R.string.error_media_upload_type);
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;
}
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;
}
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);
}
}

View File

@ -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) {

View File

@ -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) {

View File

@ -50,23 +50,18 @@
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
android:layout_weight="1"
android:id="@+id/compose_edit_area">
<EditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textMultiLine"
android:ems="10"
android:gravity="top|start"
android:id="@+id/field_status"
android:hint="@string/hint_compose" />
<!--An special EditText is created at runtime here, because it has to be a modified
* anonymous class to support image/GIF picking from the soft keyboard.-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:id="@+id/compose_media_preview_bar"
android:layout_alignBottom="@id/field_status">
android:layout_alignParentBottom="true">
<!--This is filled at runtime with ImageView's for each preview in the upload queue.-->