Attaching media to toots is now possible. Images over the upload limit are automatically downsized, videos are not.

This commit is contained in:
Vavassor 2017-01-16 13:15:42 -05:00
commit 6b684bceff
17 changed files with 865 additions and 25 deletions

View file

@ -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);
}
}
}
}

View file

@ -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();
}
}
}

View file

@ -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();
}
}

View 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;
}
}

View file

@ -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);
}

View file

@ -54,6 +54,10 @@ public class VolleySingleton {
getRequestQueue().add(request);
}
public void cancelRequest(String tag) {
getRequestQueue().cancelAll(tag);
}
public ImageLoader getImageLoader() {
return imageLoader;
}