|
|
@ -26,7 +26,10 @@ import android.support.v4.app.ActivityCompat; |
|
|
|
import android.support.v4.content.ContextCompat; |
|
|
|
import android.support.v4.content.ContextCompat; |
|
|
|
import android.support.v7.app.AppCompatActivity; |
|
|
|
import android.support.v7.app.AppCompatActivity; |
|
|
|
import android.text.Editable; |
|
|
|
import android.text.Editable; |
|
|
|
|
|
|
|
import android.text.Spannable; |
|
|
|
|
|
|
|
import android.text.Spanned; |
|
|
|
import android.text.TextWatcher; |
|
|
|
import android.text.TextWatcher; |
|
|
|
|
|
|
|
import android.text.style.ForegroundColorSpan; |
|
|
|
import android.view.View; |
|
|
|
import android.view.View; |
|
|
|
import android.webkit.MimeTypeMap; |
|
|
|
import android.webkit.MimeTypeMap; |
|
|
|
import android.widget.Button; |
|
|
|
import android.widget.Button; |
|
|
@ -54,19 +57,25 @@ 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.Collections; |
|
|
|
|
|
|
|
import java.util.Comparator; |
|
|
|
import java.util.Date; |
|
|
|
import java.util.Date; |
|
|
|
import java.util.HashMap; |
|
|
|
import java.util.HashMap; |
|
|
|
import java.util.Iterator; |
|
|
|
import java.util.Iterator; |
|
|
|
import java.util.List; |
|
|
|
import java.util.List; |
|
|
|
import java.util.Map; |
|
|
|
import java.util.Map; |
|
|
|
import java.util.Random; |
|
|
|
import java.util.Random; |
|
|
|
|
|
|
|
import java.util.regex.Matcher; |
|
|
|
|
|
|
|
import java.util.regex.Pattern; |
|
|
|
|
|
|
|
|
|
|
|
public class ComposeActivity extends AppCompatActivity { |
|
|
|
public class ComposeActivity extends AppCompatActivity { |
|
|
|
private static final int STATUS_CHARACTER_LIMIT = 500; |
|
|
|
private static final int STATUS_CHARACTER_LIMIT = 500; |
|
|
|
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 Pattern mentionPattern = Pattern.compile("\\B@[^\\s@]+@?[^\\s@]+"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private String inReplyToId; |
|
|
|
private String domain; |
|
|
|
private String domain; |
|
|
|
private String accessToken; |
|
|
|
private String accessToken; |
|
|
|
private EditText textEditor; |
|
|
|
private EditText textEditor; |
|
|
@ -148,6 +157,156 @@ public class ComposeActivity extends AppCompatActivity { |
|
|
|
Snackbar.make(findViewById(R.id.activity_compose), stringId, Snackbar.LENGTH_LONG).show(); |
|
|
|
Snackbar.make(findViewById(R.id.activity_compose), stringId, Snackbar.LENGTH_LONG).show(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static class Interval { |
|
|
|
|
|
|
|
public int start; |
|
|
|
|
|
|
|
public int end; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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]); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// Match a list of new colour spans.
|
|
|
|
|
|
|
|
List<Interval> intervals = new ArrayList<>(); |
|
|
|
|
|
|
|
Matcher matcher = mentionPattern.matcher(text); |
|
|
|
|
|
|
|
while (matcher.find()) { |
|
|
|
|
|
|
|
Interval interval = new Interval(); |
|
|
|
|
|
|
|
interval.start = matcher.start(); |
|
|
|
|
|
|
|
interval.end = matcher.end(); |
|
|
|
|
|
|
|
intervals.add(interval); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// Make sure intervals don't overlap.
|
|
|
|
|
|
|
|
Collections.sort(intervals, new Comparator<Interval>() { |
|
|
|
|
|
|
|
@Override |
|
|
|
|
|
|
|
public int compare(Interval a, Interval b) { |
|
|
|
|
|
|
|
return a.start - b.start; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
for (int i = 0, j = 0; i < intervals.size() - 1; i++, j++) { |
|
|
|
|
|
|
|
if (j != 0) { |
|
|
|
|
|
|
|
Interval a = intervals.get(j - 1); |
|
|
|
|
|
|
|
Interval b = intervals.get(i); |
|
|
|
|
|
|
|
if (a.start <= b.end) { |
|
|
|
|
|
|
|
while (j != 0 && a.start <= b.end) { |
|
|
|
|
|
|
|
a = intervals.get(j - 1); |
|
|
|
|
|
|
|
b = intervals.get(i); |
|
|
|
|
|
|
|
a.end = Math.max(a.end, b.end); |
|
|
|
|
|
|
|
a.start = Math.min(a.start, b.start); |
|
|
|
|
|
|
|
j--; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
intervals.set(j, b); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
intervals.set(j, intervals.get(i)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// Finally, set the spans.
|
|
|
|
|
|
|
|
for (Interval interval : intervals) { |
|
|
|
|
|
|
|
text.setSpan(new ForegroundColorSpan(colour), interval.start, interval.end, |
|
|
|
|
|
|
|
Spanned.SPAN_INCLUSIVE_EXCLUSIVE); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
|
|
|
public void onCreate(Bundle savedInstanceState) { |
|
|
|
|
|
|
|
super.onCreate(savedInstanceState); |
|
|
|
|
|
|
|
setContentView(R.layout.activity_compose); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Intent intent = getIntent(); |
|
|
|
|
|
|
|
String[] mentionedUsernames = null; |
|
|
|
|
|
|
|
if (intent != null) { |
|
|
|
|
|
|
|
inReplyToId = intent.getStringExtra("in_reply_to_id"); |
|
|
|
|
|
|
|
mentionedUsernames = intent.getStringArrayExtra("mentioned_usernames"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SharedPreferences preferences = getSharedPreferences( |
|
|
|
|
|
|
|
getString(R.string.preferences_file_key), Context.MODE_PRIVATE); |
|
|
|
|
|
|
|
domain = preferences.getString("domain", null); |
|
|
|
|
|
|
|
accessToken = preferences.getString("accessToken", null); |
|
|
|
|
|
|
|
assert(domain != null); |
|
|
|
|
|
|
|
assert(accessToken != null); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
textEditor = (EditText) findViewById(R.id.field_status); |
|
|
|
|
|
|
|
final TextView charactersLeft = (TextView) findViewById(R.id.characters_left); |
|
|
|
|
|
|
|
final int mentionColour = ContextCompat.getColor(this, R.color.compose_mention); |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
public void afterTextChanged(Editable editable) { |
|
|
|
|
|
|
|
colourMentions(editable, mentionColour); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
textEditor.addTextChangedListener(textEditorWatcher); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mediaPreviewBar = (LinearLayout) findViewById(R.id.compose_media_preview_bar); |
|
|
|
|
|
|
|
mediaQueued = new ArrayList<>(); |
|
|
|
|
|
|
|
waitForMediaLatch = new CountUpDownLatch(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
final RadioGroup radio = (RadioGroup) findViewById(R.id.radio_visibility); |
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
|
|
int id = radio.getCheckedRadioButtonId(); |
|
|
|
|
|
|
|
String visibility; |
|
|
|
|
|
|
|
switch (id) { |
|
|
|
|
|
|
|
default: |
|
|
|
|
|
|
|
case R.id.radio_public: { |
|
|
|
|
|
|
|
visibility = "public"; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
case R.id.radio_unlisted: { |
|
|
|
|
|
|
|
visibility = "unlisted"; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
case R.id.radio_private: { |
|
|
|
|
|
|
|
visibility = "private"; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
readyStatus(editable.toString(), visibility, markSensitive.isChecked()); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
textEditor.setError(getString(R.string.error_compose_character_limit)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mediaPick = (ImageButton) findViewById(R.id.compose_photo_pick); |
|
|
|
|
|
|
|
mediaPick.setOnClickListener(new View.OnClickListener() { |
|
|
|
|
|
|
|
@Override |
|
|
|
|
|
|
|
public void onClick(View v) { |
|
|
|
|
|
|
|
onMediaPick(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
markSensitive = (CheckBox) findViewById(R.id.compose_mark_sensitive); |
|
|
|
|
|
|
|
markSensitive.setVisibility(View.GONE); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private void onSendSuccess() { |
|
|
|
private void onSendSuccess() { |
|
|
|
Toast.makeText(this, "Toot!", Toast.LENGTH_SHORT).show(); |
|
|
|
Toast.makeText(this, "Toot!", Toast.LENGTH_SHORT).show(); |
|
|
|
finish(); |
|
|
|
finish(); |
|
|
@ -165,6 +324,9 @@ public class ComposeActivity extends AppCompatActivity { |
|
|
|
parameters.put("status", content); |
|
|
|
parameters.put("status", content); |
|
|
|
parameters.put("visibility", visibility); |
|
|
|
parameters.put("visibility", visibility); |
|
|
|
parameters.put("sensitive", sensitive); |
|
|
|
parameters.put("sensitive", sensitive); |
|
|
|
|
|
|
|
if (inReplyToId != null) { |
|
|
|
|
|
|
|
parameters.put("in_reply_to_id", inReplyToId); |
|
|
|
|
|
|
|
} |
|
|
|
JSONArray media_ids = new JSONArray(); |
|
|
|
JSONArray media_ids = new JSONArray(); |
|
|
|
for (QueuedMedia item : mediaQueued) { |
|
|
|
for (QueuedMedia item : mediaQueued) { |
|
|
|
media_ids.put(item.getId()); |
|
|
|
media_ids.put(item.getId()); |
|
|
@ -257,81 +419,6 @@ public class ComposeActivity extends AppCompatActivity { |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
|
|
|
public void onCreate(Bundle savedInstanceState) { |
|
|
|
|
|
|
|
super.onCreate(savedInstanceState); |
|
|
|
|
|
|
|
setContentView(R.layout.activity_compose); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SharedPreferences preferences = getSharedPreferences( |
|
|
|
|
|
|
|
getString(R.string.preferences_file_key), Context.MODE_PRIVATE); |
|
|
|
|
|
|
|
domain = preferences.getString("domain", null); |
|
|
|
|
|
|
|
accessToken = preferences.getString("accessToken", null); |
|
|
|
|
|
|
|
assert(domain != null); |
|
|
|
|
|
|
|
assert(accessToken != null); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
textEditor = (EditText) findViewById(R.id.field_status); |
|
|
|
|
|
|
|
final TextView charactersLeft = (TextView) findViewById(R.id.characters_left); |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
public void afterTextChanged(Editable s) {} |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
textEditor.addTextChangedListener(textEditorWatcher); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mediaPreviewBar = (LinearLayout) findViewById(R.id.compose_media_preview_bar); |
|
|
|
|
|
|
|
mediaQueued = new ArrayList<>(); |
|
|
|
|
|
|
|
waitForMediaLatch = new CountUpDownLatch(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
final RadioGroup radio = (RadioGroup) findViewById(R.id.radio_visibility); |
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
|
|
int id = radio.getCheckedRadioButtonId(); |
|
|
|
|
|
|
|
String visibility; |
|
|
|
|
|
|
|
switch (id) { |
|
|
|
|
|
|
|
default: |
|
|
|
|
|
|
|
case R.id.radio_public: { |
|
|
|
|
|
|
|
visibility = "public"; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
case R.id.radio_unlisted: { |
|
|
|
|
|
|
|
visibility = "unlisted"; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
case R.id.radio_private: { |
|
|
|
|
|
|
|
visibility = "private"; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
readyStatus(editable.toString(), visibility, markSensitive.isChecked()); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
textEditor.setError(getString(R.string.error_compose_character_limit)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mediaPick = (ImageButton) findViewById(R.id.compose_photo_pick); |
|
|
|
|
|
|
|
mediaPick.setOnClickListener(new View.OnClickListener() { |
|
|
|
|
|
|
|
@Override |
|
|
|
|
|
|
|
public void onClick(View v) { |
|
|
|
|
|
|
|
onMediaPick(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
markSensitive = (CheckBox) findViewById(R.id.compose_mark_sensitive); |
|
|
|
|
|
|
|
markSensitive.setVisibility(View.GONE); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private void onMediaPick() { |
|
|
|
private void onMediaPick() { |
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && |
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && |
|
|
|
ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) |
|
|
|
ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) |
|
|
@ -468,11 +555,10 @@ public class ComposeActivity extends AppCompatActivity { |
|
|
|
|
|
|
|
|
|
|
|
private void downsizeMedia(final QueuedMedia item) { |
|
|
|
private void downsizeMedia(final QueuedMedia item) { |
|
|
|
item.setReadyStage(QueuedMedia.ReadyStage.DOWNSIZING); |
|
|
|
item.setReadyStage(QueuedMedia.ReadyStage.DOWNSIZING); |
|
|
|
InputStream stream = null; |
|
|
|
InputStream stream; |
|
|
|
try { |
|
|
|
try { |
|
|
|
stream = getContentResolver().openInputStream(item.getUri()); |
|
|
|
stream = getContentResolver().openInputStream(item.getUri()); |
|
|
|
} catch (FileNotFoundException e) { |
|
|
|
} catch (FileNotFoundException e) { |
|
|
|
IOUtils.closeQuietly(stream); |
|
|
|
|
|
|
|
onMediaDownsizeFailure(item); |
|
|
|
onMediaDownsizeFailure(item); |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
@ -567,11 +653,10 @@ public class ComposeActivity extends AppCompatActivity { |
|
|
|
public DataItem getData() { |
|
|
|
public DataItem getData() { |
|
|
|
byte[] content = item.getContent(); |
|
|
|
byte[] content = item.getContent(); |
|
|
|
if (content == null) { |
|
|
|
if (content == null) { |
|
|
|
InputStream stream = null; |
|
|
|
InputStream stream; |
|
|
|
try { |
|
|
|
try { |
|
|
|
stream = getContentResolver().openInputStream(item.getUri()); |
|
|
|
stream = getContentResolver().openInputStream(item.getUri()); |
|
|
|
} catch (FileNotFoundException e) { |
|
|
|
} catch (FileNotFoundException e) { |
|
|
|
IOUtils.closeQuietly(stream); |
|
|
|
|
|
|
|
return null; |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
content = inputStreamGetBytes(stream); |
|
|
|
content = inputStreamGetBytes(stream); |
|
|
@ -638,11 +723,10 @@ public class ComposeActivity extends AppCompatActivity { |
|
|
|
break; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
case "image": { |
|
|
|
case "image": { |
|
|
|
InputStream stream = null; |
|
|
|
InputStream stream; |
|
|
|
try { |
|
|
|
try { |
|
|
|
stream = contentResolver.openInputStream(uri); |
|
|
|
stream = contentResolver.openInputStream(uri); |
|
|
|
} catch (FileNotFoundException e) { |
|
|
|
} catch (FileNotFoundException e) { |
|
|
|
IOUtils.closeQuietly(stream); |
|
|
|
|
|
|
|
displayTransientError(R.string.error_media_upload_opening); |
|
|
|
displayTransientError(R.string.error_media_upload_opening); |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|