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:appcompat-v7:25.1.0'
compile 'com.android.support:recyclerview-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.volley:volley:1.0.0'
compile 'com.android.support:design:25.1.0' compile 'com.android.support:design:25.1.0'
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'

View File

@ -15,7 +15,7 @@
package com.keylesspalace.tusky; 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 { class Assert {
private static boolean ENABLED = BuildConfig.DEBUG; private static boolean ENABLED = BuildConfig.DEBUG;

View File

@ -23,6 +23,7 @@ import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources; import android.content.res.Resources;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
@ -42,20 +43,29 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar; 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.app.ActivityCompat;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.text.Editable; import android.text.Editable;
import android.text.InputType;
import android.text.Spannable; import android.text.Spannable;
import android.text.Spanned; import android.text.Spanned;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.view.Gravity;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
@ -74,6 +84,7 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; 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 STATUS_MEDIA_SIZE_LIMIT = 4000000; // 4MB
private static final int MEDIA_PICK_RESULT = 1; private static final int MEDIA_PICK_RESULT = 1;
private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1; private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1;
private static final int MEDIA_SIZE_UNKNOWN = -1;
private String inReplyToId; private String inReplyToId;
private String domain; private String domain;
@ -102,6 +114,9 @@ public class ComposeActivity extends BaseActivity {
private boolean statusHideText; // private boolean statusHideText; //
private View contentWarningBar; private View contentWarningBar;
private boolean statusAlreadyInFlight; // to prevent duplicate sends by mashing the send button 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 { private static class QueuedMedia {
enum Type { enum Type {
@ -312,6 +327,13 @@ public class ComposeActivity extends BaseActivity {
statusHideText = savedInstanceState.getBoolean("statusHideText"); statusHideText = savedInstanceState.getBoolean("statusHideText");
// Keep these until everything needed to put them in the queue is finished initializing. // Keep these until everything needed to put them in the queue is finished initializing.
savedMediaQueued = savedInstanceState.getParcelableArrayList("savedMediaQueued"); 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 { } else {
showMarkSensitive = false; showMarkSensitive = false;
statusVisibility = preferences.getString("rememberedVisibility", "public"); statusVisibility = preferences.getString("rememberedVisibility", "public");
@ -329,7 +351,12 @@ public class ComposeActivity extends BaseActivity {
domain = preferences.getString("domain", null); domain = preferences.getString("domain", null);
accessToken = preferences.getString("accessToken", 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 TextView charactersLeft = (TextView) findViewById(R.id.characters_left);
final int mentionColour = ThemeUtils.getColor(this, R.attr.compose_mention_color); final int mentionColour = ThemeUtils.getColor(this, R.attr.compose_mention_color);
TextWatcher textEditorWatcher = new TextWatcher() { TextWatcher textEditorWatcher = new TextWatcher() {
@ -457,6 +484,14 @@ public class ComposeActivity extends BaseActivity {
outState.putString("statusVisibility", statusVisibility); outState.putString("statusVisibility", statusVisibility);
outState.putBoolean("statusMarkSensitive", statusMarkSensitive); outState.putBoolean("statusMarkSensitive", statusMarkSensitive);
outState.putBoolean("statusHideText", statusHideText); 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); super.onSaveInstanceState(outState);
} }
@ -476,6 +511,101 @@ public class ComposeActivity extends BaseActivity {
VolleySingleton.getInstance(this).cancelAll(TAG); 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, private void sendStatus(String content, String visibility, boolean sensitive,
String spoilerText) { String spoilerText) {
String endpoint = getString(R.string.endpoint_status); 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, private void readyStatus(final String content, final String visibility, final boolean sensitive,
final String spoilerText) { final String spoilerText) {
final ProgressDialog dialog = ProgressDialog.show( finishingUploadDialog = ProgressDialog.show(
this, getString(R.string.dialog_title_finishing_media_upload), this, getString(R.string.dialog_title_finishing_media_upload),
getString(R.string.dialog_message_uploading_media), true, true); getString(R.string.dialog_message_uploading_media), true, true);
final AsyncTask<Void, Void, Boolean> waitForMediaTask = final AsyncTask<Void, Void, Boolean> waitForMediaTask =
@ -553,7 +683,8 @@ public class ComposeActivity extends BaseActivity {
@Override @Override
protected void onPostExecute(Boolean successful) { protected void onPostExecute(Boolean successful) {
super.onPostExecute(successful); super.onPostExecute(successful);
dialog.dismiss(); finishingUploadDialog.dismiss();
finishingUploadDialog = null;
if (successful) { if (successful) {
sendStatus(content, visibility, sensitive, spoilerText); sendStatus(content, visibility, sensitive, spoilerText);
} else { } else {
@ -568,7 +699,7 @@ public class ComposeActivity extends BaseActivity {
super.onCancelled(); super.onCancelled();
} }
}; };
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { finishingUploadDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override @Override
public void onCancel(DialogInterface dialog) { public void onCancel(DialogInterface dialog) {
/* Generating an interrupt by passing true here is important because an interrupt /* 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) { private void onUploadFailure(QueuedMedia item) {
displayTransientError(R.string.error_media_upload_sending); displayTransientError(R.string.error_media_upload_sending);
if (finishingUploadDialog != null) {
finishingUploadDialog.cancel();
}
removeMediaFromQueue(item); removeMediaFromQueue(item);
} }
@ -867,69 +1001,78 @@ public class ComposeActivity extends BaseActivity {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
if (requestCode == MEDIA_PICK_RESULT && resultCode == RESULT_OK && data != null) { if (requestCode == MEDIA_PICK_RESULT && resultCode == RESULT_OK && data != null) {
Uri uri = data.getData(); Uri uri = data.getData();
ContentResolver contentResolver = getContentResolver(); long mediaSize;
Cursor cursor = getContentResolver().query(uri, null, null, null, null); Cursor cursor = getContentResolver().query(uri, null, null, null, null);
if (cursor == null) { if (cursor != null) {
displayTransientError(R.string.error_media_upload_opening); int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
return; cursor.moveToFirst();
} mediaSize = cursor.getLong(sizeIndex);
int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE); cursor.close();
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;
}
}
} else { } 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); return s.subSequence(0, i + 1);
} }
@SuppressWarnings("deprecation")
static Spanned fromHtml(String html) { static Spanned fromHtml(String html) {
Spanned result; Spanned result;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@ -40,6 +41,7 @@ class HtmlUtils {
return (Spanned) trimTrailingWhitespace(result); return (Spanned) trimTrailingWhitespace(result);
} }
@SuppressWarnings("deprecation")
static String toHtml(Spanned text) { static String toHtml(Spanned text) {
String result; String result;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 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) { 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() { avatar.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {

View File

@ -50,23 +50,18 @@
<RelativeLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1"> android:layout_weight="1"
android:id="@+id/compose_edit_area">
<EditText <!--An special EditText is created at runtime here, because it has to be a modified
android:layout_width="match_parent" * anonymous class to support image/GIF picking from the soft keyboard.-->
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" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:id="@+id/compose_media_preview_bar" 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.--> <!--This is filled at runtime with ImageView's for each preview in the upload queue.-->