Attaching media to toots is now possible. Images over the upload limit are automatically downsized, videos are not.
This commit is contained in:
parent
bab33a0715
commit
6b684bceff
17 changed files with 865 additions and 25 deletions
|
@ -3,6 +3,7 @@
|
|||
package="com.keylesspalace.tusky">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
|
|
@ -1,14 +1,39 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
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;
|
||||
import android.os.Bundle;
|
||||
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;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.View;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
@ -19,18 +44,107 @@ import com.android.volley.Response;
|
|||
import com.android.volley.VolleyError;
|
||||
import com.android.volley.toolbox.JsonObjectRequest;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
public class ComposeActivity extends AppCompatActivity {
|
||||
private static 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 MEDIA_PICK_RESULT = 1;
|
||||
private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1;
|
||||
|
||||
private String domain;
|
||||
private String accessToken;
|
||||
private EditText textEditor;
|
||||
private ImageButton mediaPick;
|
||||
private CheckBox markSensitive;
|
||||
private LinearLayout mediaPreviewBar;
|
||||
private List<QueuedMedia> mediaQueued;
|
||||
private CountUpDownLatch waitForMediaLatch;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private void onSendSuccess() {
|
||||
Toast.makeText(this, "Toot!", Toast.LENGTH_SHORT).show();
|
||||
|
@ -41,13 +155,21 @@ public class ComposeActivity extends AppCompatActivity {
|
|||
textEditor.setError(getString(R.string.error_sending_status));
|
||||
}
|
||||
|
||||
private void sendStatus(String content, String visibility) {
|
||||
private void sendStatus(String content, String visibility, boolean sensitive) {
|
||||
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);
|
||||
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) {
|
||||
onSendFailure(e);
|
||||
return;
|
||||
|
@ -74,6 +196,48 @@ public class ComposeActivity extends AppCompatActivity {
|
|||
VolleySingleton.getInstance(this).addToRequestQueue(request);
|
||||
}
|
||||
|
||||
private void onReadyFailure(Exception exception, final String content,
|
||||
final String visibility, final boolean sensitive) {
|
||||
doErrorDialog(R.string.error_media_upload_sending, R.string.action_retry,
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
readyStatus(content, visibility, sensitive);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void readyStatus(final String content, final String visibility,
|
||||
final boolean sensitive) {
|
||||
final ProgressDialog dialog = ProgressDialog.show(this, "Finishing Media Upload",
|
||||
"Uploading...", true);
|
||||
new AsyncTask<Void, Void, Boolean>() {
|
||||
private Exception exception;
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
try {
|
||||
waitForMediaLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
exception = e;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean successful) {
|
||||
super.onPostExecute(successful);
|
||||
dialog.dismiss();
|
||||
if (successful) {
|
||||
sendStatus(content, visibility, sensitive);
|
||||
} else {
|
||||
onReadyFailure(exception, content, visibility, sensitive);
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
@ -103,6 +267,10 @@ public class ComposeActivity extends AppCompatActivity {
|
|||
};
|
||||
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() {
|
||||
|
@ -127,11 +295,346 @@ public class ComposeActivity extends AppCompatActivity {
|
|||
break;
|
||||
}
|
||||
}
|
||||
sendStatus(editable.toString(), visibility);
|
||||
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() {
|
||||
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);
|
||||
}
|
||||
|
||||
/** A replacement for View.setPaddingRelative to use under API level 16. */
|
||||
private static void setPaddingRelative(View view, int left, int top, int right, int bottom) {
|
||||
view.setPadding(
|
||||
view.getPaddingLeft() + left,
|
||||
view.getPaddingTop() + top,
|
||||
view.getPaddingRight() + right,
|
||||
view.getPaddingBottom() + bottom);
|
||||
}
|
||||
|
||||
private void enableMediaPicking() {
|
||||
mediaPick.setEnabled(true);
|
||||
}
|
||||
|
||||
private void disableMediaPicking() {
|
||||
mediaPick.setEnabled(false);
|
||||
}
|
||||
|
||||
private void addMediaToQueue(QueuedMedia.Type type, Bitmap preview, Uri uri, long mediaSize) {
|
||||
assert(mediaQueued.size() < Status.MAX_MEDIA_ATTACHMENTS);
|
||||
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;
|
||||
setPaddingRelative(textEditor, 0, 0, 0, totalHeight);
|
||||
// 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) {
|
||||
markSensitive.setVisibility(View.VISIBLE);
|
||||
}
|
||||
waitForMediaLatch.countUp();
|
||||
if (mediaSize > STATUS_MEDIA_SIZE_LIMIT && type == QueuedMedia.Type.IMAGE) {
|
||||
downsizeMedia(item);
|
||||
} else {
|
||||
uploadMedia(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeMediaFromQueue(QueuedMedia item) {
|
||||
int moveBottom = mediaPreviewBar.getMeasuredHeight();
|
||||
mediaPreviewBar.removeView(item.getPreview());
|
||||
mediaQueued.remove(item);
|
||||
if (mediaQueued.size() == 0) {
|
||||
markSensitive.setVisibility(View.GONE);
|
||||
/* 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. */
|
||||
setPaddingRelative(textEditor, 0, 0, 0, moveBottom);
|
||||
}
|
||||
enableMediaPicking();
|
||||
cancelReadyingMedia(item);
|
||||
}
|
||||
|
||||
private void downsizeMedia(final QueuedMedia item) {
|
||||
item.setReadyStage(QueuedMedia.ReadyStage.DOWNSIZING);
|
||||
InputStream stream;
|
||||
try {
|
||||
stream = getContentResolver().openInputStream(item.getUri());
|
||||
} catch (FileNotFoundException e) {
|
||||
onMediaDownsizeFailure(item);
|
||||
return;
|
||||
}
|
||||
Bitmap bitmap = BitmapFactory.decodeStream(stream);
|
||||
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) {
|
||||
onUploadFailure(item, e);
|
||||
return;
|
||||
}
|
||||
waitForMediaLatch.countDown();
|
||||
}
|
||||
}, new Response.ErrorListener() {
|
||||
@Override
|
||||
public void onErrorResponse(VolleyError error) {
|
||||
onUploadFailure(item, error);
|
||||
}
|
||||
}) {
|
||||
@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) {
|
||||
InputStream stream;
|
||||
try {
|
||||
stream = getContentResolver().openInputStream(item.getUri());
|
||||
} catch (FileNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
content = inputStreamGetBytes(stream);
|
||||
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);
|
||||
}
|
||||
|
||||
private void onUploadFailure(QueuedMedia item, @Nullable Exception exception) {
|
||||
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);
|
||||
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": {
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
public class CountUpDownLatch {
|
||||
private int count;
|
||||
|
||||
public CountUpDownLatch() {
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
public synchronized void countDown() {
|
||||
count--;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
public synchronized void countUp() {
|
||||
count++;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
public synchronized void await() throws InterruptedException {
|
||||
while (count != 0) {
|
||||
wait();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DownsizeImageTask extends AsyncTask<Bitmap, Void, Boolean> {
|
||||
private Listener listener;
|
||||
private int sizeLimit;
|
||||
private List<byte[]> resultList;
|
||||
|
||||
public DownsizeImageTask(int sizeLimit, Listener listener) {
|
||||
this.listener = listener;
|
||||
this.sizeLimit = sizeLimit;
|
||||
}
|
||||
|
||||
public static Bitmap scaleDown(Bitmap source, float maxImageSize, boolean filter) {
|
||||
float ratio = Math.min(maxImageSize / source.getWidth(), maxImageSize / source.getHeight());
|
||||
int width = Math.round(ratio * source.getWidth());
|
||||
int height = Math.round(ratio * source.getHeight());
|
||||
return Bitmap.createScaledBitmap(source, width, height, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Bitmap... bitmaps) {
|
||||
final int count = bitmaps.length;
|
||||
resultList = new ArrayList<>(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
/* Unfortunately, there isn't a determined worst case compression ratio for image
|
||||
* formats. So, the only way to tell if they're too big is to compress them and
|
||||
* test, and keep trying at smaller sizes. The initial estimate should be good for
|
||||
* many cases, so it should only iterate once, but the loop is used to be absolutely
|
||||
* sure it gets downsized to below the limit. */
|
||||
int iterations = 0;
|
||||
int scaledImageSize = 4096;
|
||||
do {
|
||||
stream.reset();
|
||||
Bitmap bitmap = scaleDown(bitmaps[i], scaledImageSize, true);
|
||||
Bitmap.CompressFormat format;
|
||||
/* It's not likely the user will give transparent images over the upload limit, but
|
||||
* if they do, make sure the transparency is retained. */
|
||||
if (!bitmap.hasAlpha()) {
|
||||
format = Bitmap.CompressFormat.JPEG;
|
||||
} else {
|
||||
format = Bitmap.CompressFormat.PNG;
|
||||
}
|
||||
bitmap.compress(format, 75, stream);
|
||||
scaledImageSize /= 2;
|
||||
iterations++;
|
||||
} while (stream.size() > sizeLimit);
|
||||
assert(iterations < 3);
|
||||
resultList.add(stream.toByteArray());
|
||||
if (isCancelled()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean successful) {
|
||||
if (successful) {
|
||||
listener.onSuccess(resultList);
|
||||
} else {
|
||||
listener.onFailure();
|
||||
}
|
||||
super.onPostExecute(successful);
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onSuccess(List<byte[]> contentList);
|
||||
void onFailure();
|
||||
}
|
||||
}
|
102
app/src/main/java/com/keylesspalace/tusky/MultipartRequest.java
Normal file
102
app/src/main/java/com/keylesspalace/tusky/MultipartRequest.java
Normal file
|
@ -0,0 +1,102 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import com.android.volley.NetworkResponse;
|
||||
import com.android.volley.ParseError;
|
||||
import com.android.volley.Request;
|
||||
import com.android.volley.Response;
|
||||
import com.android.volley.toolbox.HttpHeaderParser;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
public class MultipartRequest extends Request<JSONObject> {
|
||||
private static final String CHARSET = "utf-8";
|
||||
private final String boundary = "something-" + System.currentTimeMillis();
|
||||
|
||||
private JSONObject parameters;
|
||||
private Response.Listener<JSONObject> listener;
|
||||
|
||||
public MultipartRequest(int method, String url, JSONObject parameters,
|
||||
Response.Listener<JSONObject> listener, Response.ErrorListener errorListener) {
|
||||
super(method, url, errorListener);
|
||||
this.parameters = parameters;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBodyContentType() {
|
||||
return "multipart/form-data;boundary=" + boundary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBody() {
|
||||
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
|
||||
DataOutputStream stream = new DataOutputStream(byteStream);
|
||||
try {
|
||||
// Write the JSON parameters first.
|
||||
if (parameters != null) {
|
||||
stream.writeBytes(String.format("--%s\r\n", boundary));
|
||||
stream.writeBytes("Content-Disposition: form-data; name=\"parameters\"\r\n");
|
||||
stream.writeBytes(String.format(
|
||||
"Content-Type: application/json; charset=%s\r\n", CHARSET));
|
||||
stream.writeBytes("\r\n");
|
||||
stream.writeBytes(parameters.toString());
|
||||
}
|
||||
|
||||
// Write the binary data.
|
||||
DataItem data = getData();
|
||||
if (data != null) {
|
||||
stream.writeBytes(String.format("--%s\r\n", boundary));
|
||||
stream.writeBytes(String.format(
|
||||
"Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n",
|
||||
data.name, data.filename));
|
||||
stream.writeBytes(String.format("Content-Type: %s\r\n", data.mimeType));
|
||||
stream.writeBytes(String.format("Content-Length: %s\r\n",
|
||||
String.valueOf(data.content.length)));
|
||||
stream.writeBytes("\r\n");
|
||||
stream.write(data.content);
|
||||
}
|
||||
|
||||
// Close the multipart form data.
|
||||
stream.writeBytes(String.format("--%s--\r\n", boundary));
|
||||
|
||||
return byteStream.toByteArray();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
|
||||
try {
|
||||
String jsonString = new String(response.data,
|
||||
HttpHeaderParser.parseCharset(response.headers));
|
||||
return Response.success(new JSONObject(jsonString),
|
||||
HttpHeaderParser.parseCacheHeaders(response));
|
||||
} catch (JSONException|UnsupportedEncodingException e) {
|
||||
return Response.error(new ParseError(e));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deliverResponse(JSONObject response) {
|
||||
listener.onResponse(response);
|
||||
}
|
||||
|
||||
public DataItem getData() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class DataItem {
|
||||
public String name;
|
||||
public String filename;
|
||||
public String mimeType;
|
||||
public byte[] content;
|
||||
}
|
||||
}
|
|
@ -55,9 +55,12 @@ public class TimelineAdapter extends RecyclerView.Adapter {
|
|||
} else {
|
||||
holder.setRebloggedByUsername(rebloggedByUsername);
|
||||
}
|
||||
Status.MediaAttachment[] attachments = status.getAttachments();
|
||||
boolean sensitive = status.getSensitive();
|
||||
holder.setMediaPreviews(status.getAttachments(), sensitive, listener);
|
||||
if (!sensitive) {
|
||||
holder.setMediaPreviews(attachments, sensitive, listener);
|
||||
/* A status without attachments is sometimes still marked sensitive, so it's necessary
|
||||
* to check both whether there are any attachments and if it's marked sensitive. */
|
||||
if (!sensitive || attachments.length == 0) {
|
||||
holder.hideSensitiveMediaWarning();
|
||||
}
|
||||
holder.setupButtons(listener, position);
|
||||
|
@ -144,6 +147,10 @@ public class TimelineAdapter extends RecyclerView.Adapter {
|
|||
mediaPreview1 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_1);
|
||||
mediaPreview2 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_2);
|
||||
mediaPreview3 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_3);
|
||||
mediaPreview0.setDefaultImageResId(R.drawable.media_preview_unloaded);
|
||||
mediaPreview1.setDefaultImageResId(R.drawable.media_preview_unloaded);
|
||||
mediaPreview2.setDefaultImageResId(R.drawable.media_preview_unloaded);
|
||||
mediaPreview3.setDefaultImageResId(R.drawable.media_preview_unloaded);
|
||||
sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning);
|
||||
}
|
||||
|
||||
|
|
|
@ -54,6 +54,10 @@ public class VolleySingleton {
|
|||
getRequestQueue().add(request);
|
||||
}
|
||||
|
||||
public void cancelRequest(String tag) {
|
||||
getRequestQueue().cancelAll(tag);
|
||||
}
|
||||
|
||||
public ImageLoader getImageLoader() {
|
||||
return imageLoader;
|
||||
}
|
||||
|
|
31
app/src/main/res/drawable/ic_media.xml
Normal file
31
app/src/main/res/drawable/ic_media.xml
Normal file
|
@ -0,0 +1,31 @@
|
|||
<vector android:height="48dp" android:viewportHeight="1133.894"
|
||||
android:viewportWidth="1134.6519" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="1" android:fillColor="#00000000"
|
||||
android:pathData="M52.7,262.2L1081.9,262.2A38.6,38.6 0,0 1,1120.5 300.8L1120.5,833.1A38.6,38.6 0,0 1,1081.9 871.7L52.7,871.7A38.6,38.6 0,0 1,14.2 833.1L14.2,300.8A38.6,38.6 0,0 1,52.7 262.2z"
|
||||
android:strokeAlpha="1" android:strokeColor="#000000"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="28.34645653"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#00000000"
|
||||
android:pathData="m19.6,458.3c104.2,-9.7 76.2,61 365.2,125.3 61.9,13.8 50,40.6 96.2,58 105.8,39.9 376.7,15.8 639.8,33.5"
|
||||
android:strokeAlpha="1" android:strokeColor="#000000"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="28.34645653"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#00000000"
|
||||
android:pathData="m1011.8,494c0,0 -130.5,-8 -158.9,-39.2 -142.4,-156.4 -193.3,0.9 -217,-9.7 -74.7,-33.3 -65,21.3 -199.8,103.2 -20.5,12.4 -8.8,16.9 39.1,18.1 143.3,3.8 -74.6,16.2 24.6,18.4l115.8,2.6"
|
||||
android:strokeAlpha="1" android:strokeColor="#000000"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="28.34645653"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#00000000"
|
||||
android:pathData="m1121.2,496.7c0,0 -254.5,-33.7 -505.7,90.3 -98.7,48.8 350.1,80.7 350.1,80.7"
|
||||
android:strokeAlpha="1" android:strokeColor="#000000"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="28.34645653"/>
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="m459,531.9 l-245.5,-0" android:strokeAlpha="1"
|
||||
android:strokeColor="#000000" android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter" android:strokeWidth="28.34645653"/>
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M14.2,639C390.1,602 473.1,743.8 1118.5,752.1"
|
||||
android:strokeAlpha="1" android:strokeColor="#000000"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="35.43307114"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#00000000"
|
||||
android:pathData="M277.5,425.5m-62.9,0a62.9,62.9 0,1 1,125.7 0a62.9,62.9 0,1 1,-125.7 0"
|
||||
android:strokeAlpha="1" android:strokeColor="#000000"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="35.43307114"/>
|
||||
</vector>
|
11
app/src/main/res/drawable/ic_media_disabled.xml
Normal file
11
app/src/main/res/drawable/ic_media_disabled.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector android:height="48dp" android:viewportHeight="1133.894"
|
||||
android:viewportWidth="1134.6519" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="1" android:fillColor="#9d9d9d"
|
||||
android:pathData="m277.5,344.9c-44.3,0 -80.6,36.3 -80.6,80.6 -0,44.3 36.3,80.6 80.6,80.6 44.3,0 80.6,-36.3 80.6,-80.6 -0,-44.3 -36.3,-80.6 -80.6,-80.6zM277.5,380.3c25.1,0 45.1,20 45.1,45.1 0,25.1 -20,45.1 -45.1,45.1 -25.1,0 -45.1,-20 -45.1,-45.1 0,-25.1 20,-45.1 45.1,-45.1zM135.7,615.2c-38,-0.1 -78.6,1.8 -123.2,6.2l-0.1,35.3 306.3,31.4 2.1,-20.4c4.8,0.8 9.6,1.5 14.4,2.4l6.3,-34.9c-65,-11.8 -130.2,-19.8 -205.8,-20zM549.6,676.1 L549.2,679.4 542.6,712.8c135.7,27.1 307.8,53.6 575.7,57.1l2,-35.3 -570.7,-58.5zM213.5,517.8 L213.5,546.1 459,546.1 459,517.8 213.5,517.8zM1034.5,478.4c-83.3,0.3 -215.7,11.2 -354.9,65.1l10.2,26.4c134.7,-52.1 263.8,-62.9 344.7,-63.2 53,-0.2 84.7,4.1 84.7,4.1l3.7,-28.1c0,0 -34,-4.5 -88.6,-4.3zM628.4,605.1 L618,631.5c43.4,17 128.1,28.6 204.3,37.2 76.1,8.6 142.3,13.3 142.3,13.3l2,-28.3c0,0 -65.7,-4.7 -141.1,-13.2 -75.4,-8.5 -161.9,-21.6 -197.1,-35.4zM828.5,411.3 L810.2,432.9c10,8.5 20.8,18.8 32.2,31.4 10.5,11.6 25.7,17.8 43,23.3 14.5,4.6 30.4,7.9 46.1,10.7l-269,63.5 6.5,27.6 346,-81.6 -2.4,-27.9c0,0 -32,-2 -67.4,-7.7 -17.7,-2.9 -36.1,-6.7 -51.3,-11.5 -15.2,-4.8 -26.9,-11.3 -30.6,-15.3 -12.2,-13.4 -23.8,-24.6 -34.9,-34zM445.8,525.5c-5.4,3.5 -11.1,7.1 -17,10.7 -5.8,3.5 -10.5,5.8 -14.4,13.3 -2,3.8 -2.8,10.6 -0.3,15.7 2.5,5.1 6.3,7.4 9.3,8.8l9.1,4.5 31.1,-31.1 -17.7,-21.9zM37.7,443.2c-6.1,-0 -12.5,0.3 -19.4,1l2.6,28.2c6.1,-0.6 11.7,-0.8 16.8,-0.8 23.5,0 37.3,5.1 55.5,15l-0.1,-0.1c86.8,47.7 182.4,93.2 288.7,110.9 6.2,1.4 11.4,2.9 15.9,4.4l9.2,-26.8c-5.7,-1.9 -11.9,-3.7 -19.1,-5.3l-0.4,-0.1 -0.4,-0.1C285.7,552.8 192.7,508.9 106.9,461.7l-0,-0 -0,-0C86.5,450.7 65.9,443.2 37.7,443.2l-0,0zM652.9,568.5 L647.6,573.8 549.5,671.9 579.7,674.6c135,12.1 340.2,1.1 540.2,14.6l3.7,-28 -470.7,-92.7zM662.3,599.2 L941.1,654.1C817.5,652.1 701.5,654.1 612.8,648.7l49.4,-49.4zM52.7,248c-29,0 -52.7,23.8 -52.7,52.7l0,532.4c0,29 23.8,52.7 52.7,52.7l66.5,0a14.2,14.2 0,0 0,10 -4.2L738.7,272.2a14.2,14.2 0,0 0,-10 -24.2l-676,0zM979.2,248a14.2,14.2 0,0 0,-10 4.2L359.7,861.7a14.2,14.2 0,0 0,10 24.2l712.2,0c29,0 52.7,-23.8 52.7,-52.7l0,-532.4c0,-29 -23.8,-52.7 -52.7,-52.7l-102.7,0zM52.7,276.4 L694.5,276.4 113.3,857.5 52.7,857.5c-13.8,0 -24.4,-10.6 -24.4,-24.4l0,-532.4c0,-13.8 10.6,-24.4 24.4,-24.4zM985.1,276.4 L1081.9,276.4c13.8,0 24.4,10.6 24.4,24.4l0,532.4c0,13.8 -10.6,24.4 -24.4,24.4l-677.9,0 581.1,-581.1z"
|
||||
android:strokeAlpha="1" android:strokeColor="#00000000"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="35.43307114"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#00000000"
|
||||
android:pathData="M915.1,130.3L985.8,201.1A38.6,38.6 58.5,0 1,985.8 255.6L238.6,1002.8A38.6,38.6 84,0 1,184.1 1002.8L113.3,932.1A38.6,38.6 92.7,0 1,113.3 877.6L860.6,130.3A38.6,38.6 79.6,0 1,915.1 130.3z"
|
||||
android:strokeAlpha="1" android:strokeColor="#9d9d9d"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="35.43307515"/>
|
||||
</vector>
|
14
app/src/main/res/drawable/media_preview_unloaded.xml
Normal file
14
app/src/main/res/drawable/media_preview_unloaded.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/media_preview_unloaded_background" />
|
||||
|
||||
<stroke
|
||||
android:dashWidth="4dp"
|
||||
android:dashGap="4dp"
|
||||
android:width="2dp"
|
||||
android:color="#AFAFAF" />
|
||||
|
||||
</shape>
|
5
app/src/main/res/drawable/media_selector.xml
Normal file
5
app/src/main/res/drawable/media_selector.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_enabled="true" android:drawable="@drawable/ic_media" />
|
||||
<item android:state_enabled="false" android:drawable="@drawable/ic_media_disabled" />
|
||||
</selector>
|
|
@ -1,17 +1,59 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/activity_compose"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<EditText
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:inputType="textMultiLine"
|
||||
android:ems="10"
|
||||
android:gravity="top|left"
|
||||
android:id="@+id/field_status"
|
||||
android:contentDescription="What's Happening?"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageButton
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
app:srcCompat="@drawable/media_selector"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:id="@+id/compose_photo_pick"
|
||||
android:layout_marginLeft="8dp" />
|
||||
|
||||
<CheckBox
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="@dimen/compose_mark_sensitive_margin"
|
||||
android:id="@+id/compose_mark_sensitive"
|
||||
android:text="@string/action_mark_sensitive" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<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:contentDescription="@string/description_compose" />
|
||||
|
||||
<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">
|
||||
|
||||
<!--This is filled at runtime with ImageView's for each preview in the upload queue.-->
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
@ -59,7 +101,7 @@
|
|||
android:text="500" />
|
||||
|
||||
<Button
|
||||
android:text="TOOT"
|
||||
android:text="@string/action_send"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/button_send" />
|
||||
|
|
|
@ -30,17 +30,15 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPersonName"
|
||||
android:text="Domain"
|
||||
android:contentDescription="@string/description_domain"
|
||||
android:ems="10"
|
||||
android:id="@+id/edit_text_domain" />
|
||||
|
||||
<Button
|
||||
android:text="LOG IN"
|
||||
android:text="@string/action_login"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/button_login"
|
||||
android:layout_centerHorizontal="false"
|
||||
android:layout_centerInParent="false" />
|
||||
android:id="@+id/button_login" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -119,7 +119,7 @@
|
|||
<com.android.volley.toolbox.NetworkImageView
|
||||
android:id="@+id/status_media_preview_0"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="@dimen/status_media_preview_height"
|
||||
android:layout_weight="1"
|
||||
android:scaleType="centerCrop"
|
||||
android:layout_marginTop="@dimen/status_media_preview_top_margin" />
|
||||
|
@ -127,7 +127,7 @@
|
|||
<com.android.volley.toolbox.NetworkImageView
|
||||
android:id="@+id/status_media_preview_1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="@dimen/status_media_preview_height"
|
||||
android:layout_weight="1"
|
||||
android:scaleType="centerCrop"
|
||||
android:layout_marginTop="@dimen/status_media_preview_top_margin" />
|
||||
|
@ -141,14 +141,14 @@
|
|||
<com.android.volley.toolbox.NetworkImageView
|
||||
android:id="@+id/status_media_preview_2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="@dimen/status_media_preview_height"
|
||||
android:layout_weight="1"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<com.android.volley.toolbox.NetworkImageView
|
||||
android:id="@+id/status_media_preview_3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="@dimen/status_media_preview_height"
|
||||
android:layout_weight="1"
|
||||
android:scaleType="centerCrop" />
|
||||
</LinearLayout>
|
||||
|
|
|
@ -6,4 +6,5 @@
|
|||
<color name="gray">#4F4F4F</color>
|
||||
<color name="view_video_background">#000000</color>
|
||||
<color name="sensitive_media_warning_background">#303030</color>
|
||||
<color name="media_preview_unloaded_background">#DFDFDF</color>
|
||||
</resources>
|
||||
|
|
|
@ -6,4 +6,9 @@
|
|||
<dimen name="status_avatar_padding">8dp</dimen>
|
||||
<dimen name="status_boost_icon_vertical_padding">5dp</dimen>
|
||||
<dimen name="status_media_preview_top_margin">4dp</dimen>
|
||||
<dimen name="status_media_preview_height">96dp</dimen>
|
||||
<dimen name="compose_media_preview_margin">8dp</dimen>
|
||||
<dimen name="compose_media_preview_margin_bottom">16dp</dimen>
|
||||
<dimen name="compose_media_preview_side">48dp</dimen>
|
||||
<dimen name="compose_mark_sensitive_margin">8dp</dimen>
|
||||
</resources>
|
||||
|
|
|
@ -42,6 +42,12 @@
|
|||
<string name="error_fetching_notifications">Notifications could not be fetched.</string>
|
||||
<string name="error_compose_character_limit">The toot is too long!</string>
|
||||
<string name="error_sending_status">The toot failed to be sent.</string>
|
||||
<string name="error_media_upload_size">The file must be less than 4MB.</string>
|
||||
<string name="error_media_upload_type">That type of file is not able to be uploaded.</string>
|
||||
<string name="error_media_upload_opening">That file could not be opened.</string>
|
||||
<string name="error_media_upload_permission">Permission to read media is required to upload it.</string>
|
||||
<string name="error_media_upload_image_or_video">Images and videos cannot both be attached to the same toot.</string>
|
||||
<string name="error_media_upload_sending">The media could not be uploaded.</string>
|
||||
|
||||
<string name="title_home">Home</string>
|
||||
<string name="title_notifications">Notifications</string>
|
||||
|
@ -57,10 +63,17 @@
|
|||
<string name="notification_follow_format">%s followed you</string>
|
||||
|
||||
<string name="action_compose">Compose</string>
|
||||
<string name="action_login">Log In</string>
|
||||
<string name="action_logout">Log Out</string>
|
||||
<string name="action_follow">Follow</string>
|
||||
<string name="action_block">Block</string>
|
||||
<string name="action_delete">Delete</string>
|
||||
<string name="action_send">TOOT</string>
|
||||
<string name="action_retry">Retry</string>
|
||||
<string name="action_mark_sensitive">Mark Sensitive</string>
|
||||
|
||||
<string name="description_domain">Domain</string>
|
||||
<string name="description_compose">What\'s Happening?</string>
|
||||
|
||||
<string name="visibility_public">Public</string>
|
||||
<string name="visibility_private">Private</string>
|
||||
|
|
Loading…
Reference in a new issue