2017-01-20 19:09:10 +11:00
|
|
|
/* Copyright 2017 Andrew Dawson
|
|
|
|
*
|
|
|
|
* This file is part of Tusky.
|
|
|
|
*
|
|
|
|
* Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU
|
|
|
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
|
|
|
* License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
|
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
|
|
* Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License along with Tusky. If not, see
|
|
|
|
* <http://www.gnu.org/licenses/>. */
|
|
|
|
|
2017-01-08 09:24:02 +11:00
|
|
|
package com.keylesspalace.tusky;
|
|
|
|
|
2017-01-17 05:15:42 +11:00
|
|
|
import android.Manifest;
|
|
|
|
import android.app.ProgressDialog;
|
|
|
|
import android.content.ContentResolver;
|
2017-01-08 09:24:02 +11:00
|
|
|
import android.content.Context;
|
2017-01-19 05:35:07 +11:00
|
|
|
import android.content.DialogInterface;
|
2017-01-17 05:15:42 +11:00
|
|
|
import android.content.Intent;
|
2017-01-08 09:24:02 +11:00
|
|
|
import android.content.SharedPreferences;
|
2017-01-17 05:15:42 +11:00
|
|
|
import android.content.pm.PackageManager;
|
|
|
|
import android.content.res.Resources;
|
|
|
|
import android.database.Cursor;
|
|
|
|
import android.graphics.Bitmap;
|
|
|
|
import android.graphics.BitmapFactory;
|
|
|
|
import android.media.MediaMetadataRetriever;
|
|
|
|
import android.media.ThumbnailUtils;
|
|
|
|
import android.net.Uri;
|
|
|
|
import android.os.AsyncTask;
|
|
|
|
import android.os.Build;
|
2017-01-08 09:24:02 +11:00
|
|
|
import android.os.Bundle;
|
2017-02-04 11:53:33 +11:00
|
|
|
import android.os.Parcel;
|
2017-01-17 05:15:42 +11:00
|
|
|
import android.provider.OpenableColumns;
|
|
|
|
import android.support.annotation.NonNull;
|
|
|
|
import android.support.annotation.Nullable;
|
|
|
|
import android.support.design.widget.Snackbar;
|
|
|
|
import android.support.v4.app.ActivityCompat;
|
|
|
|
import android.support.v4.content.ContextCompat;
|
2017-01-08 09:24:02 +11:00
|
|
|
import android.support.v7.app.AppCompatActivity;
|
|
|
|
import android.text.Editable;
|
2017-01-20 15:59:21 +11:00
|
|
|
import android.text.Spannable;
|
|
|
|
import android.text.Spanned;
|
2017-01-08 09:24:02 +11:00
|
|
|
import android.text.TextWatcher;
|
2017-01-20 15:59:21 +11:00
|
|
|
import android.text.style.ForegroundColorSpan;
|
2017-01-08 09:24:02 +11:00
|
|
|
import android.view.View;
|
2017-01-17 05:15:42 +11:00
|
|
|
import android.webkit.MimeTypeMap;
|
2017-01-08 09:24:02 +11:00
|
|
|
import android.widget.Button;
|
|
|
|
import android.widget.EditText;
|
2017-01-17 05:15:42 +11:00
|
|
|
import android.widget.ImageButton;
|
|
|
|
import android.widget.ImageView;
|
|
|
|
import android.widget.LinearLayout;
|
2017-01-08 09:24:02 +11:00
|
|
|
import android.widget.TextView;
|
|
|
|
import android.widget.Toast;
|
|
|
|
|
|
|
|
import com.android.volley.AuthFailureError;
|
|
|
|
import com.android.volley.Request;
|
|
|
|
import com.android.volley.Response;
|
|
|
|
import com.android.volley.VolleyError;
|
|
|
|
import com.android.volley.toolbox.JsonObjectRequest;
|
|
|
|
|
2017-01-17 05:15:42 +11:00
|
|
|
import org.json.JSONArray;
|
2017-01-08 09:24:02 +11:00
|
|
|
import org.json.JSONException;
|
|
|
|
import org.json.JSONObject;
|
|
|
|
|
2017-01-17 05:15:42 +11:00
|
|
|
import java.io.ByteArrayOutputStream;
|
|
|
|
import java.io.FileNotFoundException;
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.io.InputStream;
|
|
|
|
import java.util.ArrayList;
|
2017-01-20 15:59:21 +11:00
|
|
|
import java.util.Collections;
|
|
|
|
import java.util.Comparator;
|
2017-01-17 05:15:42 +11:00
|
|
|
import java.util.Date;
|
2017-01-08 09:24:02 +11:00
|
|
|
import java.util.HashMap;
|
2017-01-19 05:35:07 +11:00
|
|
|
import java.util.Iterator;
|
2017-01-17 05:15:42 +11:00
|
|
|
import java.util.List;
|
2017-01-08 09:24:02 +11:00
|
|
|
import java.util.Map;
|
2017-01-17 05:15:42 +11:00
|
|
|
import java.util.Random;
|
2017-01-20 15:59:21 +11:00
|
|
|
import java.util.regex.Matcher;
|
|
|
|
import java.util.regex.Pattern;
|
2017-01-08 09:24:02 +11:00
|
|
|
|
|
|
|
public class ComposeActivity extends AppCompatActivity {
|
2017-01-17 05:15:42 +11:00
|
|
|
private static final int STATUS_CHARACTER_LIMIT = 500;
|
|
|
|
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;
|
2017-01-08 09:24:02 +11:00
|
|
|
|
2017-01-20 15:59:21 +11:00
|
|
|
private String inReplyToId;
|
2017-01-08 09:24:02 +11:00
|
|
|
private String domain;
|
|
|
|
private String accessToken;
|
|
|
|
private EditText textEditor;
|
2017-01-17 05:15:42 +11:00
|
|
|
private ImageButton mediaPick;
|
|
|
|
private LinearLayout mediaPreviewBar;
|
|
|
|
private List<QueuedMedia> mediaQueued;
|
|
|
|
private CountUpDownLatch waitForMediaLatch;
|
2017-02-04 11:53:33 +11:00
|
|
|
private boolean showMarkSensitive;
|
|
|
|
private String statusVisibility; // The current values of the options that will be applied
|
|
|
|
private boolean statusMarkSensitive; // to the status being composed.
|
|
|
|
private boolean statusHideText; //
|
|
|
|
private View contentWarningBar;
|
2017-01-17 05:15:42 +11:00
|
|
|
|
|
|
|
private static class QueuedMedia {
|
|
|
|
public enum Type {
|
|
|
|
IMAGE,
|
|
|
|
VIDEO
|
|
|
|
}
|
|
|
|
|
|
|
|
public enum ReadyStage {
|
|
|
|
DOWNSIZING,
|
|
|
|
UPLOADING,
|
|
|
|
}
|
|
|
|
|
|
|
|
private Type type;
|
|
|
|
private ImageView preview;
|
|
|
|
private Uri uri;
|
|
|
|
private String id;
|
|
|
|
private ReadyStage readyStage;
|
|
|
|
private byte[] content;
|
|
|
|
|
|
|
|
public QueuedMedia(Type type, Uri uri, ImageView preview) {
|
|
|
|
this.type = type;
|
|
|
|
this.uri = uri;
|
|
|
|
this.preview = preview;
|
|
|
|
}
|
|
|
|
|
|
|
|
public Type getType() {
|
|
|
|
return type;
|
|
|
|
}
|
|
|
|
|
|
|
|
public ImageView getPreview() {
|
|
|
|
return preview;
|
|
|
|
}
|
|
|
|
|
|
|
|
public Uri getUri() {
|
|
|
|
return uri;
|
|
|
|
}
|
|
|
|
|
|
|
|
public String getId() {
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
|
|
|
public byte[] getContent() {
|
|
|
|
return content;
|
|
|
|
}
|
|
|
|
|
|
|
|
public ReadyStage getReadyStage() {
|
|
|
|
return readyStage;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setId(String id) {
|
|
|
|
this.id = id;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setReadyStage(ReadyStage readyStage) {
|
|
|
|
this.readyStage = readyStage;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setContent(byte[] content) {
|
|
|
|
this.content = content;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void doErrorDialog(int descriptionId, int actionId, View.OnClickListener listener) {
|
|
|
|
Snackbar bar = Snackbar.make(findViewById(R.id.activity_compose), getString(descriptionId),
|
|
|
|
Snackbar.LENGTH_SHORT);
|
|
|
|
bar.setAction(actionId, listener);
|
|
|
|
bar.show();
|
|
|
|
}
|
|
|
|
|
|
|
|
private void displayTransientError(int stringId) {
|
|
|
|
Snackbar.make(findViewById(R.id.activity_compose), stringId, Snackbar.LENGTH_LONG).show();
|
|
|
|
}
|
2017-01-08 09:24:02 +11:00
|
|
|
|
2017-02-14 13:46:25 +11:00
|
|
|
private static int findStartOfMention(String string, int fromIndex) {
|
|
|
|
final int length = string.length();
|
|
|
|
while (fromIndex < length) {
|
|
|
|
int at = string.indexOf('@', fromIndex);
|
|
|
|
if (at < 0) {
|
|
|
|
break;
|
|
|
|
} else if (at == 0 || at >= 1 && Character.isWhitespace(string.codePointBefore(at))) {
|
|
|
|
return at;
|
|
|
|
} else {
|
|
|
|
fromIndex = at + 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static int findEndOfMention(String string, int fromIndex) {
|
|
|
|
int atCount = 0;
|
|
|
|
final int length = string.length();
|
|
|
|
for (int i = fromIndex; i < length; ) {
|
|
|
|
int codepoint = string.codePointAt(i);
|
|
|
|
if (Character.isWhitespace(codepoint)) {
|
|
|
|
return i;
|
|
|
|
} else if (codepoint == '@') {
|
|
|
|
atCount += 1;
|
|
|
|
if (atCount > 2) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
i += Character.charCount(codepoint);
|
|
|
|
}
|
|
|
|
return length;
|
2017-01-08 09:24:02 +11:00
|
|
|
}
|
|
|
|
|
2017-01-20 15:59:21 +11:00
|
|
|
private static void colourMentions(Spannable text, int colour) {
|
|
|
|
// Strip all existing colour spans.
|
|
|
|
int n = text.length();
|
|
|
|
ForegroundColorSpan[] oldSpans = text.getSpans(0, n, ForegroundColorSpan.class);
|
|
|
|
for (int i = oldSpans.length - 1; i >= 0; i--) {
|
|
|
|
text.removeSpan(oldSpans[i]);
|
2017-01-08 09:24:02 +11:00
|
|
|
}
|
2017-02-14 13:46:25 +11:00
|
|
|
// Colour the mentions.
|
|
|
|
String string = text.toString();
|
|
|
|
int start;
|
|
|
|
int end = 0;
|
|
|
|
while (end < n) {
|
|
|
|
start = findStartOfMention(string, end);
|
|
|
|
if (start < 0 || start >= n) {
|
|
|
|
break;
|
2017-01-17 05:15:42 +11:00
|
|
|
}
|
2017-02-14 13:46:25 +11:00
|
|
|
end = findEndOfMention(string, start);
|
|
|
|
if (end < 0) {
|
|
|
|
break;
|
2017-01-17 05:15:42 +11:00
|
|
|
}
|
2017-02-14 13:46:25 +11:00
|
|
|
text.setSpan(new ForegroundColorSpan(colour), start, end,
|
2017-01-20 15:59:21 +11:00
|
|
|
Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
|
|
|
}
|
2017-01-17 05:15:42 +11:00
|
|
|
}
|
|
|
|
|
2017-01-08 09:24:02 +11:00
|
|
|
@Override
|
|
|
|
public void onCreate(Bundle savedInstanceState) {
|
|
|
|
super.onCreate(savedInstanceState);
|
|
|
|
setContentView(R.layout.activity_compose);
|
|
|
|
|
2017-01-20 15:59:21 +11:00
|
|
|
Intent intent = getIntent();
|
|
|
|
String[] mentionedUsernames = null;
|
|
|
|
if (intent != null) {
|
|
|
|
inReplyToId = intent.getStringExtra("in_reply_to_id");
|
|
|
|
mentionedUsernames = intent.getStringArrayExtra("mentioned_usernames");
|
|
|
|
}
|
|
|
|
|
2017-01-08 09:24:02 +11:00
|
|
|
SharedPreferences preferences = getSharedPreferences(
|
|
|
|
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
|
|
|
domain = preferences.getString("domain", null);
|
|
|
|
accessToken = preferences.getString("accessToken", null);
|
|
|
|
|
|
|
|
textEditor = (EditText) findViewById(R.id.field_status);
|
|
|
|
final TextView charactersLeft = (TextView) findViewById(R.id.characters_left);
|
2017-01-20 15:59:21 +11:00
|
|
|
final int mentionColour = ContextCompat.getColor(this, R.color.compose_mention);
|
2017-01-08 09:24:02 +11:00
|
|
|
TextWatcher textEditorWatcher = new TextWatcher() {
|
|
|
|
@Override
|
|
|
|
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
|
|
|
int left = STATUS_CHARACTER_LIMIT - s.length();
|
|
|
|
charactersLeft.setText(Integer.toString(left));
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
|
|
|
|
|
|
|
@Override
|
2017-01-20 15:59:21 +11:00
|
|
|
public void afterTextChanged(Editable editable) {
|
|
|
|
colourMentions(editable, mentionColour);
|
|
|
|
}
|
2017-01-08 09:24:02 +11:00
|
|
|
};
|
|
|
|
textEditor.addTextChangedListener(textEditorWatcher);
|
|
|
|
|
2017-01-20 15:59:21 +11:00
|
|
|
if (mentionedUsernames != null) {
|
|
|
|
StringBuilder builder = new StringBuilder();
|
|
|
|
for (String name : mentionedUsernames) {
|
|
|
|
builder.append('@');
|
|
|
|
builder.append(name);
|
|
|
|
builder.append(' ');
|
|
|
|
}
|
|
|
|
textEditor.setText(builder);
|
|
|
|
textEditor.setSelection(textEditor.length());
|
|
|
|
}
|
|
|
|
|
2017-01-17 05:15:42 +11:00
|
|
|
mediaPreviewBar = (LinearLayout) findViewById(R.id.compose_media_preview_bar);
|
|
|
|
mediaQueued = new ArrayList<>();
|
|
|
|
waitForMediaLatch = new CountUpDownLatch();
|
|
|
|
|
2017-02-04 11:53:33 +11:00
|
|
|
contentWarningBar = findViewById(R.id.compose_content_warning_bar);
|
|
|
|
final EditText contentWarningEditor = (EditText) findViewById(R.id.field_content_warning);
|
|
|
|
showContentWarning(false);
|
|
|
|
|
2017-01-08 09:24:02 +11:00
|
|
|
final Button sendButton = (Button) findViewById(R.id.button_send);
|
|
|
|
sendButton.setOnClickListener(new View.OnClickListener() {
|
|
|
|
@Override
|
|
|
|
public void onClick(View v) {
|
|
|
|
Editable editable = textEditor.getText();
|
|
|
|
if (editable.length() <= STATUS_CHARACTER_LIMIT) {
|
2017-02-04 11:53:33 +11:00
|
|
|
String spoilerText = "";
|
|
|
|
if (statusHideText) {
|
|
|
|
spoilerText = contentWarningEditor.getText().toString();
|
2017-01-08 09:24:02 +11:00
|
|
|
}
|
2017-02-04 11:53:33 +11:00
|
|
|
readyStatus(editable.toString(), statusVisibility, statusMarkSensitive,
|
|
|
|
spoilerText);
|
2017-01-08 09:24:02 +11:00
|
|
|
} else {
|
|
|
|
textEditor.setError(getString(R.string.error_compose_character_limit));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2017-01-17 05:15:42 +11:00
|
|
|
|
|
|
|
mediaPick = (ImageButton) findViewById(R.id.compose_photo_pick);
|
|
|
|
mediaPick.setOnClickListener(new View.OnClickListener() {
|
|
|
|
@Override
|
|
|
|
public void onClick(View v) {
|
|
|
|
onMediaPick();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-02-04 11:53:33 +11:00
|
|
|
ImageButton options = (ImageButton) findViewById(R.id.compose_options);
|
|
|
|
options.setOnClickListener(new View.OnClickListener() {
|
|
|
|
@Override
|
|
|
|
public void onClick(View v) {
|
|
|
|
ComposeOptionsFragment fragment = ComposeOptionsFragment.newInstance(
|
|
|
|
statusVisibility, statusMarkSensitive, statusHideText,
|
|
|
|
showMarkSensitive,
|
|
|
|
new ComposeOptionsFragment.Listener() {
|
|
|
|
@Override
|
|
|
|
public int describeContents() {
|
|
|
|
return 0;
|
|
|
|
}
|
2017-01-20 15:59:21 +11:00
|
|
|
|
2017-02-04 11:53:33 +11:00
|
|
|
@Override
|
|
|
|
public void writeToParcel(Parcel dest, int flags) {}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onVisibilityChanged(String visibility) {
|
|
|
|
statusVisibility = visibility;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onMarkSensitiveChanged(boolean markSensitive) {
|
|
|
|
statusMarkSensitive = markSensitive;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onContentWarningChanged(boolean hideText) {
|
|
|
|
showContentWarning(hideText);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
fragment.show(getSupportFragmentManager(), null);
|
|
|
|
}
|
|
|
|
});
|
2017-01-20 15:59:21 +11:00
|
|
|
}
|
|
|
|
|
2017-02-04 11:53:33 +11:00
|
|
|
private void sendStatus(String content, String visibility, boolean sensitive,
|
|
|
|
String spoilerText) {
|
2017-01-20 15:59:21 +11:00
|
|
|
String endpoint = getString(R.string.endpoint_status);
|
|
|
|
String url = "https://" + domain + endpoint;
|
|
|
|
JSONObject parameters = new JSONObject();
|
|
|
|
try {
|
|
|
|
parameters.put("status", content);
|
|
|
|
parameters.put("visibility", visibility);
|
|
|
|
parameters.put("sensitive", sensitive);
|
2017-02-04 11:53:33 +11:00
|
|
|
parameters.put("spoiler_text", spoilerText);
|
2017-01-20 15:59:21 +11:00
|
|
|
if (inReplyToId != null) {
|
|
|
|
parameters.put("in_reply_to_id", inReplyToId);
|
|
|
|
}
|
|
|
|
JSONArray media_ids = new JSONArray();
|
|
|
|
for (QueuedMedia item : mediaQueued) {
|
|
|
|
media_ids.put(item.getId());
|
|
|
|
}
|
|
|
|
if (media_ids.length() > 0) {
|
|
|
|
parameters.put("media_ids", media_ids);
|
|
|
|
}
|
|
|
|
} catch (JSONException e) {
|
2017-02-04 11:53:33 +11:00
|
|
|
onSendFailure();
|
2017-01-20 15:59:21 +11:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
JsonObjectRequest request = new JsonObjectRequest(Request.Method.POST, url, parameters,
|
|
|
|
new Response.Listener<JSONObject>() {
|
|
|
|
@Override
|
|
|
|
public void onResponse(JSONObject response) {
|
|
|
|
onSendSuccess();
|
|
|
|
}
|
|
|
|
}, new Response.ErrorListener() {
|
|
|
|
@Override
|
|
|
|
public void onErrorResponse(VolleyError error) {
|
2017-02-04 11:53:33 +11:00
|
|
|
onSendFailure();
|
2017-01-20 15:59:21 +11:00
|
|
|
}
|
|
|
|
}) {
|
|
|
|
@Override
|
|
|
|
public Map<String, String> getHeaders() throws AuthFailureError {
|
|
|
|
Map<String, String> headers = new HashMap<>();
|
|
|
|
headers.put("Authorization", "Bearer " + accessToken);
|
|
|
|
return headers;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
VolleySingleton.getInstance(this).addToRequestQueue(request);
|
|
|
|
}
|
|
|
|
|
2017-02-04 11:53:33 +11:00
|
|
|
private void onSendSuccess() {
|
|
|
|
Toast.makeText(this, getString(R.string.confirmation_send), Toast.LENGTH_SHORT).show();
|
|
|
|
finish();
|
|
|
|
}
|
|
|
|
|
|
|
|
private void onSendFailure() {
|
|
|
|
textEditor.setError(getString(R.string.error_sending_status));
|
|
|
|
}
|
|
|
|
|
2017-01-20 15:59:21 +11:00
|
|
|
private void readyStatus(final String content, final String visibility,
|
2017-02-04 11:53:33 +11:00
|
|
|
final boolean sensitive, final String spoilerText) {
|
2017-02-07 18:05:50 +11:00
|
|
|
final ProgressDialog dialog = ProgressDialog.show(
|
|
|
|
this, getString(R.string.dialog_title_finishing_media_upload),
|
|
|
|
getString(R.string.dialog_message_uploading_media), true, true);
|
2017-01-20 15:59:21 +11:00
|
|
|
final AsyncTask<Void, Void, Boolean> waitForMediaTask =
|
|
|
|
new AsyncTask<Void, Void, Boolean>() {
|
|
|
|
@Override
|
|
|
|
protected Boolean doInBackground(Void... params) {
|
|
|
|
try {
|
|
|
|
waitForMediaLatch.await();
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void onPostExecute(Boolean successful) {
|
|
|
|
super.onPostExecute(successful);
|
|
|
|
dialog.dismiss();
|
|
|
|
if (successful) {
|
2017-02-04 11:53:33 +11:00
|
|
|
sendStatus(content, visibility, sensitive, spoilerText);
|
2017-01-20 15:59:21 +11:00
|
|
|
} else {
|
2017-02-04 11:53:33 +11:00
|
|
|
onReadyFailure(content, visibility, sensitive, spoilerText);
|
2017-01-20 15:59:21 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void onCancelled() {
|
|
|
|
removeAllMediaFromQueue();
|
|
|
|
super.onCancelled();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
|
|
|
@Override
|
|
|
|
public void onCancel(DialogInterface dialog) {
|
|
|
|
/* Generating an interrupt by passing true here is important because an interrupt
|
|
|
|
* exception is the only thing that will kick the latch out of its waiting loop
|
|
|
|
* early. */
|
|
|
|
waitForMediaTask.cancel(true);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
waitForMediaTask.execute();
|
|
|
|
}
|
|
|
|
|
2017-02-04 11:53:33 +11:00
|
|
|
private void onReadyFailure(final String content, final String visibility,
|
|
|
|
final boolean sensitive, final String spoilerText) {
|
2017-01-20 15:59:21 +11:00
|
|
|
doErrorDialog(R.string.error_media_upload_sending, R.string.action_retry,
|
|
|
|
new View.OnClickListener() {
|
|
|
|
@Override
|
|
|
|
public void onClick(View v) {
|
2017-02-04 11:53:33 +11:00
|
|
|
readyStatus(content, visibility, sensitive, spoilerText);
|
2017-01-20 15:59:21 +11:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-01-17 05:15:42 +11:00
|
|
|
private void onMediaPick() {
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
|
|
|
|
ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
|
|
|
!= PackageManager.PERMISSION_GRANTED) {
|
|
|
|
ActivityCompat.requestPermissions(this,
|
|
|
|
new String[] { Manifest.permission.READ_EXTERNAL_STORAGE },
|
|
|
|
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE);
|
|
|
|
} else {
|
|
|
|
initiateMediaPicking();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
|
|
|
|
@NonNull int[] grantResults) {
|
|
|
|
switch (requestCode) {
|
|
|
|
case PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE: {
|
|
|
|
if (grantResults.length > 0
|
|
|
|
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
|
|
initiateMediaPicking();
|
|
|
|
} else {
|
|
|
|
doErrorDialog(R.string.error_media_upload_permission, R.string.action_retry,
|
|
|
|
new View.OnClickListener() {
|
|
|
|
@Override
|
|
|
|
public void onClick(View v) {
|
|
|
|
onMediaPick();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void initiateMediaPicking() {
|
|
|
|
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
|
|
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
|
|
|
intent.setType("image/* video/*");
|
|
|
|
} else {
|
|
|
|
String[] mimeTypes = new String[] { "image/*", "video/*" };
|
|
|
|
intent.setType("*/*");
|
|
|
|
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
|
|
|
|
}
|
|
|
|
startActivityForResult(intent, MEDIA_PICK_RESULT);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void enableMediaPicking() {
|
|
|
|
mediaPick.setEnabled(true);
|
2017-02-14 13:46:25 +11:00
|
|
|
mediaPick.setImageResource(R.drawable.ic_media);
|
2017-01-17 05:15:42 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
private void disableMediaPicking() {
|
|
|
|
mediaPick.setEnabled(false);
|
2017-02-14 13:46:25 +11:00
|
|
|
mediaPick.setImageResource(R.drawable.ic_media_disabled);
|
2017-01-17 05:15:42 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
private void addMediaToQueue(QueuedMedia.Type type, Bitmap preview, Uri uri, long mediaSize) {
|
|
|
|
final QueuedMedia item = new QueuedMedia(type, uri, new ImageView(this));
|
|
|
|
ImageView view = item.getPreview();
|
|
|
|
Resources resources = getResources();
|
|
|
|
int side = resources.getDimensionPixelSize(R.dimen.compose_media_preview_side);
|
|
|
|
int margin = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin);
|
|
|
|
int marginBottom = resources.getDimensionPixelSize(
|
|
|
|
R.dimen.compose_media_preview_margin_bottom);
|
|
|
|
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(side, side);
|
|
|
|
layoutParams.setMargins(margin, margin, margin, marginBottom);
|
|
|
|
view.setLayoutParams(layoutParams);
|
|
|
|
view.setImageBitmap(preview);
|
|
|
|
view.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
|
|
|
view.setOnClickListener(new View.OnClickListener() {
|
|
|
|
@Override
|
|
|
|
public void onClick(View v) {
|
|
|
|
removeMediaFromQueue(item);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
mediaPreviewBar.addView(view);
|
|
|
|
mediaQueued.add(item);
|
|
|
|
int queuedCount = mediaQueued.size();
|
|
|
|
if (queuedCount == 1) {
|
|
|
|
/* The media preview bar is actually not inset in the EditText, it just overlays it and
|
|
|
|
* is aligned to the bottom. But, so that text doesn't get hidden under it, extra
|
|
|
|
* padding is added at the bottom of the EditText. */
|
|
|
|
int totalHeight = side + margin + marginBottom;
|
2017-02-08 08:05:55 +11:00
|
|
|
textEditor.setPadding(textEditor.getPaddingLeft(), textEditor.getPaddingTop(),
|
|
|
|
textEditor.getPaddingRight(), totalHeight);
|
2017-01-17 05:15:42 +11:00
|
|
|
// If there's one video in the queue it is full, so disable the button to queue more.
|
|
|
|
if (item.getType() == QueuedMedia.Type.VIDEO) {
|
|
|
|
disableMediaPicking();
|
|
|
|
}
|
|
|
|
} else if (queuedCount >= Status.MAX_MEDIA_ATTACHMENTS) {
|
|
|
|
// Limit the total media attachments, also.
|
|
|
|
disableMediaPicking();
|
|
|
|
}
|
|
|
|
if (queuedCount >= 1) {
|
2017-02-04 11:53:33 +11:00
|
|
|
showMarkSensitive(true);
|
2017-01-17 05:15:42 +11:00
|
|
|
}
|
|
|
|
waitForMediaLatch.countUp();
|
|
|
|
if (mediaSize > STATUS_MEDIA_SIZE_LIMIT && type == QueuedMedia.Type.IMAGE) {
|
|
|
|
downsizeMedia(item);
|
|
|
|
} else {
|
|
|
|
uploadMedia(item);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void removeMediaFromQueue(QueuedMedia item) {
|
|
|
|
mediaPreviewBar.removeView(item.getPreview());
|
|
|
|
mediaQueued.remove(item);
|
|
|
|
if (mediaQueued.size() == 0) {
|
2017-02-04 11:53:33 +11:00
|
|
|
showMarkSensitive(false);
|
2017-01-17 05:15:42 +11:00
|
|
|
/* If there are no image previews to show, the extra padding that was added to the
|
|
|
|
* EditText can be removed so there isn't unnecessary empty space. */
|
2017-02-08 08:05:55 +11:00
|
|
|
textEditor.setPadding(textEditor.getPaddingLeft(), textEditor.getPaddingTop(),
|
|
|
|
textEditor.getPaddingRight(), 0);
|
2017-01-17 05:15:42 +11:00
|
|
|
}
|
|
|
|
enableMediaPicking();
|
|
|
|
cancelReadyingMedia(item);
|
|
|
|
}
|
|
|
|
|
2017-01-19 05:35:07 +11:00
|
|
|
private void removeAllMediaFromQueue() {
|
|
|
|
for (Iterator<QueuedMedia> it = mediaQueued.iterator(); it.hasNext();) {
|
|
|
|
QueuedMedia item = it.next();
|
|
|
|
it.remove();
|
|
|
|
removeMediaFromQueue(item);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-17 05:15:42 +11:00
|
|
|
private void downsizeMedia(final QueuedMedia item) {
|
|
|
|
item.setReadyStage(QueuedMedia.ReadyStage.DOWNSIZING);
|
2017-01-20 15:59:21 +11:00
|
|
|
InputStream stream;
|
2017-01-17 05:15:42 +11:00
|
|
|
try {
|
|
|
|
stream = getContentResolver().openInputStream(item.getUri());
|
|
|
|
} catch (FileNotFoundException e) {
|
|
|
|
onMediaDownsizeFailure(item);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
Bitmap bitmap = BitmapFactory.decodeStream(stream);
|
2017-01-19 05:35:07 +11:00
|
|
|
IOUtils.closeQuietly(stream);
|
2017-01-17 05:15:42 +11:00
|
|
|
new DownsizeImageTask(STATUS_MEDIA_SIZE_LIMIT, new DownsizeImageTask.Listener() {
|
|
|
|
@Override
|
|
|
|
public void onSuccess(List<byte[]> contentList) {
|
|
|
|
item.setContent(contentList.get(0));
|
|
|
|
uploadMedia(item);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onFailure() {
|
|
|
|
onMediaDownsizeFailure(item);
|
|
|
|
}
|
|
|
|
}).execute(bitmap);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void onMediaDownsizeFailure(QueuedMedia item) {
|
|
|
|
displayTransientError(R.string.error_media_upload_size);
|
|
|
|
removeMediaFromQueue(item);
|
|
|
|
}
|
|
|
|
|
|
|
|
private static String randomAlphanumericString(int count) {
|
|
|
|
char[] chars = new char[count];
|
|
|
|
Random random = new Random();
|
|
|
|
final String POSSIBLE_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
|
|
chars[i] = POSSIBLE_CHARS.charAt(random.nextInt(POSSIBLE_CHARS.length()));
|
|
|
|
}
|
|
|
|
return new String(chars);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Nullable
|
|
|
|
private static byte[] inputStreamGetBytes(InputStream stream) {
|
|
|
|
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
|
|
|
int read;
|
|
|
|
byte[] data = new byte[16384];
|
|
|
|
try {
|
|
|
|
while ((read = stream.read(data, 0, data.length)) != -1) {
|
|
|
|
buffer.write(data, 0, read);
|
|
|
|
}
|
|
|
|
buffer.flush();
|
|
|
|
} catch (IOException e) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return buffer.toByteArray();
|
|
|
|
}
|
|
|
|
|
|
|
|
private void uploadMedia(final QueuedMedia item) {
|
|
|
|
item.setReadyStage(QueuedMedia.ReadyStage.UPLOADING);
|
|
|
|
|
|
|
|
String endpoint = getString(R.string.endpoint_media);
|
|
|
|
String url = "https://" + domain + endpoint;
|
|
|
|
|
|
|
|
final String mimeType = getContentResolver().getType(item.uri);
|
|
|
|
MimeTypeMap map = MimeTypeMap.getSingleton();
|
|
|
|
String fileExtension = map.getExtensionFromMimeType(mimeType);
|
|
|
|
final String filename = String.format("%s_%s_%s.%s",
|
|
|
|
getString(R.string.app_name),
|
|
|
|
String.valueOf(new Date().getTime()),
|
|
|
|
randomAlphanumericString(10),
|
|
|
|
fileExtension);
|
|
|
|
|
|
|
|
MultipartRequest request = new MultipartRequest(Request.Method.POST, url, null,
|
|
|
|
new Response.Listener<JSONObject>() {
|
|
|
|
@Override
|
|
|
|
public void onResponse(JSONObject response) {
|
|
|
|
try {
|
|
|
|
item.setId(response.getString("id"));
|
|
|
|
} catch (JSONException e) {
|
2017-02-04 11:53:33 +11:00
|
|
|
onUploadFailure(item);
|
2017-01-17 05:15:42 +11:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
waitForMediaLatch.countDown();
|
|
|
|
}
|
|
|
|
}, new Response.ErrorListener() {
|
|
|
|
@Override
|
|
|
|
public void onErrorResponse(VolleyError error) {
|
2017-02-04 11:53:33 +11:00
|
|
|
onUploadFailure(item);
|
2017-01-17 05:15:42 +11:00
|
|
|
}
|
|
|
|
}) {
|
|
|
|
@Override
|
|
|
|
public Map<String, String> getHeaders() throws AuthFailureError {
|
|
|
|
Map<String, String> headers = new HashMap<>();
|
|
|
|
headers.put("Authorization", "Bearer " + accessToken);
|
|
|
|
return headers;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public DataItem getData() {
|
|
|
|
byte[] content = item.getContent();
|
|
|
|
if (content == null) {
|
2017-01-20 15:59:21 +11:00
|
|
|
InputStream stream;
|
2017-01-17 05:15:42 +11:00
|
|
|
try {
|
|
|
|
stream = getContentResolver().openInputStream(item.getUri());
|
|
|
|
} catch (FileNotFoundException e) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
content = inputStreamGetBytes(stream);
|
2017-01-19 05:35:07 +11:00
|
|
|
IOUtils.closeQuietly(stream);
|
2017-01-17 05:15:42 +11:00
|
|
|
if (content == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
DataItem data = new DataItem();
|
|
|
|
data.name = "file";
|
|
|
|
data.filename = filename;
|
|
|
|
data.mimeType = mimeType;
|
|
|
|
data.content = content;
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
request.addMarker("media_" + item.getUri().toString());
|
|
|
|
VolleySingleton.getInstance(this).addToRequestQueue(request);
|
|
|
|
}
|
|
|
|
|
2017-02-04 11:53:33 +11:00
|
|
|
private void onUploadFailure(QueuedMedia item) {
|
2017-01-17 05:15:42 +11:00
|
|
|
displayTransientError(R.string.error_media_upload_sending);
|
|
|
|
removeMediaFromQueue(item);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void cancelReadyingMedia(QueuedMedia item) {
|
|
|
|
if (item.getReadyStage() == QueuedMedia.ReadyStage.UPLOADING) {
|
|
|
|
VolleySingleton.getInstance(this).cancelRequest("media_" + item.getUri().toString());
|
|
|
|
}
|
|
|
|
waitForMediaLatch.countDown();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
|
|
super.onActivityResult(requestCode, resultCode, data);
|
|
|
|
if (requestCode == MEDIA_PICK_RESULT && resultCode == RESULT_OK && data != null) {
|
|
|
|
Uri uri = data.getData();
|
|
|
|
ContentResolver contentResolver = getContentResolver();
|
|
|
|
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
|
2017-02-07 18:05:50 +11:00
|
|
|
if (cursor == null) {
|
|
|
|
displayTransientError(R.string.error_media_upload_opening);
|
|
|
|
return;
|
|
|
|
}
|
2017-01-17 05:15:42 +11:00
|
|
|
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).getType() == 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": {
|
2017-01-20 15:59:21 +11:00
|
|
|
InputStream stream;
|
2017-01-17 05:15:42 +11:00
|
|
|
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 {
|
2017-01-19 05:35:07 +11:00
|
|
|
if (stream != null) {
|
|
|
|
stream.close();
|
|
|
|
}
|
2017-01-17 05:15:42 +11:00
|
|
|
} 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);
|
|
|
|
}
|
|
|
|
}
|
2017-01-08 09:24:02 +11:00
|
|
|
}
|
2017-02-04 11:53:33 +11:00
|
|
|
|
|
|
|
void showMarkSensitive(boolean show) {
|
|
|
|
showMarkSensitive = show;
|
|
|
|
if(!showMarkSensitive) {
|
|
|
|
statusMarkSensitive = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void showContentWarning(boolean show) {
|
|
|
|
statusHideText = show;
|
|
|
|
if (show) {
|
|
|
|
contentWarningBar.setVisibility(View.VISIBLE);
|
|
|
|
} else {
|
|
|
|
contentWarningBar.setVisibility(View.GONE);
|
|
|
|
}
|
|
|
|
}
|
2017-01-08 09:24:02 +11:00
|
|
|
}
|