Unfinished keyboard GIF picking stuff? Not accessible by the user, yet.
This commit is contained in:
parent
9e49da64bf
commit
91ad3acc79
6 changed files with 220 additions and 75 deletions
|
@ -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'
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.-->
|
||||
|
||||
|
|
Loading…
Reference in a new issue