replaced direct media access, added image caching, bug fix

This commit is contained in:
nuclearfog 2022-01-13 20:51:31 +01:00
parent b27d77b937
commit 33260ae84e
No known key found for this signature in database
GPG Key ID: AA0271FBE406DB98
20 changed files with 530 additions and 403 deletions

View File

@ -21,8 +21,6 @@ import android.content.ActivityNotFoundException;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
@ -31,7 +29,6 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.widget.Toast;
import androidx.annotation.NonNull;
@ -44,6 +41,7 @@ import org.nuclearfog.twidda.backend.ImageSaver;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
/**
@ -72,11 +70,6 @@ public abstract class MediaActivity extends AppCompatActivity implements Locatio
*/
private static final String[] TYPE_ALL = {MIME_IMAGE_READ, MIME_VIDEO_READ};
/**
* Cursor mode to get the full path to the image
*/
private static final String[] GET_MEDIA = {MediaStore.MediaColumns.DATA};
/**
* request code to check permissions
*/
@ -109,9 +102,9 @@ public abstract class MediaActivity extends AppCompatActivity implements Locatio
@Nullable
private ImageSaver imageTask;
private Bitmap image;
private String filename;
private boolean locationPending = false;
private Uri selectedImage;
private String imageName;
@Override
@ -153,17 +146,7 @@ public abstract class MediaActivity extends AppCompatActivity implements Locatio
protected final void onActivityResult(int reqCode, int returnCode, @Nullable Intent intent) {
super.onActivityResult(reqCode, returnCode, intent);
if (returnCode == RESULT_OK && intent != null && intent.getData() != null) {
Cursor cursor = getContentResolver().query(intent.getData(), GET_MEDIA, null, null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
String path = cursor.getString(0);
if (path != null) {
onMediaFetched(reqCode, path);
// todo add error handling if no media returned
}
}
cursor.close();
}
onMediaFetched(reqCode, intent.getData());
}
}
@ -200,22 +183,23 @@ public abstract class MediaActivity extends AppCompatActivity implements Locatio
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// store images directly
File imageFolder = Environment.getExternalStoragePublicDirectory(DIRECTORY_PICTURES);
File imageFile = new File(imageFolder, filename);
OutputStream fileStream = new FileOutputStream(imageFile);
File imageFile = new File(imageFolder, imageName);
InputStream src = getContentResolver().openInputStream(selectedImage);
OutputStream dest = new FileOutputStream(imageFile);
imageTask = new ImageSaver(this);
imageTask.execute(image, fileStream);
imageTask.execute(src, dest);
} else {
// use scoped storage
ContentValues values = new ContentValues();
values.put(DISPLAY_NAME, filename);
values.put(DISPLAY_NAME, imageName);
values.put(DATE_TAKEN, System.currentTimeMillis());
values.put(RELATIVE_PATH, DIRECTORY_PICTURES);
values.put(MIME_TYPE, MIME_IMAGE_WRITE);
Uri imageUri = getContentResolver().insert(EXTERNAL_CONTENT_URI, values);
if (imageUri != null) {
OutputStream fileStream = getContentResolver().openOutputStream(imageUri);
OutputStream dest = getContentResolver().openOutputStream(imageUri);
imageTask = new ImageSaver(this);
imageTask.execute(image, fileStream);
imageTask.execute(selectedImage, dest);
}
}
}
@ -231,8 +215,7 @@ public abstract class MediaActivity extends AppCompatActivity implements Locatio
Toast.makeText(this, R.string.info_image_saved, LENGTH_SHORT).show();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// start media scanner to scan for new image
String[] fileName = {filename};
MediaScannerConnection.scanFile(this, fileName, null, null);
MediaScannerConnection.scanFile(this, new String[]{imageName}, null, null);
}
}
@ -283,15 +266,11 @@ public abstract class MediaActivity extends AppCompatActivity implements Locatio
/**
* store image bitmap into storage
*
* @param filename name of the file
* @param image image bitmap to store
* @param uri Uri of the image
*/
protected void storeImage(Bitmap image, String filename) {
this.image = image;
this.filename = filename;
if (!filename.endsWith(".jpg")) {
this.filename += ".jpg";
}
protected void storeImage(Uri uri) {
selectedImage = uri;
imageName = "shitter_" + uri.getLastPathSegment() + "jpg";
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|| checkSelfPermission(PERMISSIONS[2][0]) == PERMISSION_GRANTED) {
saveImage();
@ -352,9 +331,8 @@ public abstract class MediaActivity extends AppCompatActivity implements Locatio
/**
* called when a media file path was successfully fetched
*
* @param resultType type of media call
* @param path local path to the media file
* @param resultType type of media call
* @param uri Uri of the file
*/
protected abstract void onMediaFetched(int resultType, @NonNull String path);
protected abstract void onMediaFetched(int resultType, @NonNull Uri uri);
}

View File

@ -3,21 +3,14 @@ package org.nuclearfog.twidda.activities;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.location.Location;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener;
import android.media.MediaPlayer.OnInfoListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ProgressBar;
@ -37,26 +30,21 @@ import org.nuclearfog.twidda.adapter.ImageAdapter;
import org.nuclearfog.twidda.adapter.ImageAdapter.OnImageClickListener;
import org.nuclearfog.twidda.backend.ImageLoader;
import org.nuclearfog.twidda.backend.SeekbarUpdater;
import org.nuclearfog.twidda.backend.holder.ImageHolder;
import org.nuclearfog.twidda.backend.utils.AppStyles;
import org.nuclearfog.twidda.backend.utils.ErrorHandler;
import org.nuclearfog.twidda.backend.utils.StringTools;
import org.nuclearfog.twidda.database.GlobalSettings;
import org.nuclearfog.zoomview.ZoomView;
import static android.media.MediaPlayer.MEDIA_ERROR_UNKNOWN;
import static android.media.MediaPlayer.MEDIA_INFO_BUFFERING_END;
import static android.media.MediaPlayer.MEDIA_INFO_BUFFERING_START;
import static android.media.MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START;
import static android.os.AsyncTask.Status.RUNNING;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_UP;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_SHORT;
import static android.media.MediaPlayer.*;
import static android.os.AsyncTask.Status.*;
import static android.view.MotionEvent.*;
import static android.view.View.*;
import static android.widget.Toast.*;
import static androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL;
import java.io.File;
/**
* Media viewer activity for images and videos
*
@ -66,15 +54,25 @@ public class MediaViewer extends MediaActivity implements OnImageClickListener,
OnPreparedListener, OnInfoListener, OnErrorListener, OnClickListener, OnTouchListener {
/**
* Key for the media URL, local or online, required
* Key for online media files
*/
public static final String KEY_MEDIA_LINK = "media_link";
/**
* key for local media files
*/
public static final String KEY_MEDIA_URI = "media_uri";
/**
* Key for the media type, required
* {@link #MEDIAVIEWER_IMAGE}, {@link #MEDIAVIEWER_VIDEO} or {@link #MEDIAVIEWER_ANGIF}
*/
public static final String KEY_MEDIA_TYPE = "media_type";
/**
* cache folder name
*/
public static final String CACHE_FOLDER = "imagecache";
/**
* setup media viewer for images from twitter
*/
@ -124,11 +122,12 @@ public class MediaViewer extends MediaActivity implements OnImageClickListener,
private ZoomView zoomImage;
private ViewGroup controlPanel;
private String[] mediaLinks;
private Uri[] mediaLinks;
private int type;
private PlayStat playStat = PlayStat.IDLE;
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(AppStyles.setFontScale(newBase));
@ -163,43 +162,55 @@ public class MediaViewer extends MediaActivity implements OnImageClickListener,
GlobalSettings settings = GlobalSettings.getInstance(this);
AppStyles.setProgressColor(loadingCircle, settings.getHighlightColor());
AppStyles.setTheme(controlPanel, settings.getBackgroundColor());
adapter = new ImageAdapter(settings, this);
adapter = new ImageAdapter(getApplicationContext(), this);
// check if link is a local file
Parcelable[] links = getIntent().getParcelableArrayExtra(KEY_MEDIA_URI);
if (links != null) {
mediaLinks = new Uri[links.length];
for (int i = 0; i < mediaLinks.length ; i++) {
mediaLinks[i] = (Uri) links[i];
}
}
// check if links are http:// links
else {
String[] strLinks = getIntent().getStringArrayExtra(KEY_MEDIA_LINK);
mediaLinks = new Uri[strLinks.length];
for (int i = 0; i < mediaLinks.length ; i++) {
mediaLinks[i] = Uri.parse(strLinks[i]);
}
}
// get intent data and type
mediaLinks = getIntent().getStringArrayExtra(KEY_MEDIA_LINK);
type = getIntent().getIntExtra(KEY_MEDIA_TYPE, 0);
if (mediaLinks != null && mediaLinks.length > 0) {
switch (type) {
case MEDIAVIEWER_IMAGE:
zoomImage.setVisibility(VISIBLE);
imageListContainer.setVisibility(VISIBLE);
if (!mediaLinks[0].startsWith("http"))
adapter.disableSaveButton();
imageList.setLayoutManager(new LinearLayoutManager(this, HORIZONTAL, false));
imageList.setAdapter(adapter);
switch (type) {
case MEDIAVIEWER_IMAGE:
zoomImage.setVisibility(VISIBLE);
imageListContainer.setVisibility(VISIBLE);
if (!mediaLinks[0].getScheme().startsWith("http")) {
adapter.disableSaveButton();
for (Uri uri : mediaLinks)
setImage(uri);
adapter.disableLoading();
} else {
imageAsync = new ImageLoader(this);
imageAsync.execute(mediaLinks);
break;
}
imageList.setLayoutManager(new LinearLayoutManager(this, HORIZONTAL, false));
imageList.setAdapter(adapter);
break;
case MEDIAVIEWER_VIDEO:
controlPanel.setVisibility(VISIBLE);
if (mediaLinks[0].startsWith("/"))
share.setVisibility(GONE); // local image
else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && mediaLinks[0].startsWith("https://")) {
// for any reason VideoView ignores TLS 1.2 setup, so we have to use http:// instead
// todo find a solution to add TLS 1.2 support for pre lollipop devices
mediaLinks[0] = "http://" + mediaLinks[0].substring(8);
}
seekUpdate = new SeekbarUpdater(this, PROGRESS_UPDATE);
// fall through
case MEDIAVIEWER_ANGIF:
videoView.setVisibility(VISIBLE);
videoView.setZOrderMediaOverlay(true); // disable black background
videoView.getHolder().setFormat(PixelFormat.TRANSPARENT);
videoView.setVideoURI(Uri.parse(mediaLinks[0]));
break;
}
case MEDIAVIEWER_VIDEO:
controlPanel.setVisibility(VISIBLE);
if (mediaLinks[0].getScheme().startsWith("http"))
share.setVisibility(GONE); // local image
seekUpdate = new SeekbarUpdater(this, PROGRESS_UPDATE);
// fall through
case MEDIAVIEWER_ANGIF:
videoView.setVisibility(VISIBLE);
videoView.setZOrderMediaOverlay(true); // disable black background
videoView.getHolder().setFormat(PixelFormat.TRANSPARENT);
videoView.setVideoURI(mediaLinks[0]);
break;
}
share.setOnClickListener(this);
play.setOnClickListener(this);
@ -231,6 +242,7 @@ public class MediaViewer extends MediaActivity implements OnImageClickListener,
imageAsync.cancel(true);
if (seekUpdate != null)
seekUpdate.shutdown();
clearCache();
super.onDestroy();
}
@ -254,7 +266,7 @@ public class MediaViewer extends MediaActivity implements OnImageClickListener,
// open link with another app
else if (v.getId() == R.id.controller_share) {
if (mediaLinks != null && mediaLinks.length > 0) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(mediaLinks[0]));
Intent intent = new Intent(Intent.ACTION_VIEW, mediaLinks[0]);
try {
startActivity(intent);
} catch (ActivityNotFoundException err) {
@ -320,24 +332,20 @@ public class MediaViewer extends MediaActivity implements OnImageClickListener,
@Override
protected void onMediaFetched(int resultType, @NonNull String path) {
protected void onMediaFetched(int resultType, @NonNull Uri uri) {
}
@Override
public void onImageClick(Bitmap image) {
public void onImageClick(Uri uri) {
zoomImage.reset();
zoomImage.setImageBitmap(image);
zoomImage.setImageURI(uri);
}
@Override
public void onImageSave(Bitmap image, int pos) {
if (mediaLinks != null && pos < mediaLinks.length && mediaLinks[pos] != null) {
String link = mediaLinks[pos];
String name = "shitter_" + link.substring(link.lastIndexOf('/') + 1);
storeImage(image, name);
}
public void onImageSave(Uri uri) {
storeImage(uri);
}
@ -439,15 +447,15 @@ public class MediaViewer extends MediaActivity implements OnImageClickListener,
/**
* set downloaded image into preview list
*
* @param image Image container
* @param imageUri Image Uri
*/
public void setImage(ImageHolder image) {
public void setImage(Uri imageUri) {
if (adapter.isEmpty()) {
zoomImage.reset();
zoomImage.setImageBitmap(image.reducedImage);
zoomImage.setImageURI(imageUri);
loadingCircle.setVisibility(INVISIBLE);
}
adapter.addLast(image);
adapter.addLast(imageUri);
}
/**
@ -490,4 +498,15 @@ public class MediaViewer extends MediaActivity implements OnImageClickListener,
break;
}
}
/**
* clear the image cache
*/
private void clearCache() {
File cacheFolder = new File(getExternalCacheDir(), CACHE_FOLDER);
File[] files = cacheFolder.listFiles();
for (File file : files) {
file.delete();
}
}
}

View File

@ -1,20 +1,17 @@
package org.nuclearfog.twidda.activities;
import static android.os.AsyncTask.Status.RUNNING;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.view.View.*;
import static android.widget.Toast.LENGTH_SHORT;
import static org.nuclearfog.twidda.activities.MediaViewer.KEY_MEDIA_LINK;
import static org.nuclearfog.twidda.activities.MediaViewer.KEY_MEDIA_TYPE;
import static org.nuclearfog.twidda.activities.MediaViewer.MEDIAVIEWER_IMAGE;
import static org.nuclearfog.twidda.activities.MediaViewer.*;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageButton;
@ -26,6 +23,7 @@ import androidx.annotation.Nullable;
import org.nuclearfog.twidda.R;
import org.nuclearfog.twidda.backend.MessageUpdater;
import org.nuclearfog.twidda.backend.holder.DirectmessageHolder;
import org.nuclearfog.twidda.backend.utils.AppStyles;
import org.nuclearfog.twidda.backend.utils.ErrorHandler;
import org.nuclearfog.twidda.dialog.ConfirmDialog;
@ -53,7 +51,7 @@ public class MessageEditor extends MediaActivity implements OnClickListener, OnC
private Dialog loadingCircle;
private ConfirmDialog errorDialog, leaveDialog;
@Nullable
private String mediaPath;
private Uri mediaUri;
@Override
protected void attachBaseContext(Context newBase) {
@ -95,7 +93,7 @@ public class MessageEditor extends MediaActivity implements OnClickListener, OnC
@Override
public void onBackPressed() {
if (receiver.getText().length() == 0 && message.getText().length() == 0 && mediaPath == null) {
if (receiver.getText().length() == 0 && message.getText().length() == 0 && mediaUri == null) {
super.onBackPressed();
} else if (!leaveDialog.isShowing()) {
leaveDialog.show();
@ -117,11 +115,11 @@ public class MessageEditor extends MediaActivity implements OnClickListener, OnC
@Override
protected void onMediaFetched(int resultType, @NonNull String path) {
protected void onMediaFetched(int resultType, @NonNull Uri uri) {
if (resultType == REQUEST_IMAGE) {
preview.setVisibility(VISIBLE);
media.setVisibility(GONE);
mediaPath = path;
mediaUri = uri;
}
}
@ -140,9 +138,8 @@ public class MessageEditor extends MediaActivity implements OnClickListener, OnC
}
// open media
else if (v.getId() == R.id.dm_preview) {
String[] link = {mediaPath};
Intent image = new Intent(this, MediaViewer.class);
image.putExtra(KEY_MEDIA_LINK, link);
image.putExtra(KEY_MEDIA_URI, new Uri[]{mediaUri});
image.putExtra(KEY_MEDIA_TYPE, MEDIAVIEWER_IMAGE);
startActivity(image);
}
@ -199,9 +196,12 @@ public class MessageEditor extends MediaActivity implements OnClickListener, OnC
private void sendMessage() {
String username = receiver.getText().toString();
String message = this.message.getText().toString();
if (!username.trim().isEmpty() && (!message.trim().isEmpty() || mediaPath != null)) {
messageAsync = new MessageUpdater(this);
messageAsync.execute(username, message, mediaPath);
if (!username.trim().isEmpty() && (!message.trim().isEmpty() || mediaUri != null)) {
DirectmessageHolder holder = new DirectmessageHolder(username, message);
if (mediaUri != null)
holder.addMedia(getApplicationContext(), mediaUri);
messageAsync = new MessageUpdater(this, holder);
messageAsync.execute();
if (!loadingCircle.isShowing()) {
loadingCircle.show();
}

View File

@ -12,10 +12,9 @@ import static org.nuclearfog.twidda.database.GlobalSettings.PROFILE_IMG_HIGH_RES
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.view.Gravity;
import android.view.Menu;
@ -38,6 +37,7 @@ import com.squareup.picasso.Picasso;
import org.nuclearfog.twidda.R;
import org.nuclearfog.twidda.backend.UserUpdater;
import org.nuclearfog.twidda.backend.holder.ProfileHolder;
import org.nuclearfog.twidda.model.User;
import org.nuclearfog.twidda.backend.utils.AppStyles;
import org.nuclearfog.twidda.backend.utils.ErrorHandler;
@ -49,8 +49,6 @@ import org.nuclearfog.twidda.dialog.ConfirmDialog.OnConfirmListener;
import org.nuclearfog.twidda.dialog.ProgressDialog;
import org.nuclearfog.twidda.dialog.ProgressDialog.OnProgressStopListener;
import java.io.File;
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation;
/**
@ -76,7 +74,8 @@ public class ProfileEditor extends MediaActivity implements OnClickListener, OnP
private ConfirmDialog errorDialog, closeDialog;
private User user;
private String profileLink, bannerLink;
private Uri profileLink, bannerLink;
@Override
protected void attachBaseContext(Context newBase) {
@ -182,22 +181,20 @@ public class ProfileEditor extends MediaActivity implements OnClickListener, OnP
@Override
protected void onMediaFetched(int resultType, @NonNull String path) {
protected void onMediaFetched(int resultType, @NonNull Uri uri) {
// Add image as profile image
if (resultType == REQUEST_PROFILE) {
Bitmap image = BitmapFactory.decodeFile(path);
profile_image.setImageBitmap(image);
profileLink = path;
profile_image.setImageURI(uri);
profileLink = uri;
}
// Add image as banner image
else if (resultType == REQUEST_BANNER) {
File img = new File(path);
Point displaySize = new Point();
getWindowManager().getDefaultDisplay().getSize(displaySize);
picasso.load(img).resize(displaySize.x, displaySize.x / 3).centerCrop(Gravity.TOP).into(profile_banner, this);
picasso.load(uri).resize(displaySize.x, displaySize.x / 3).centerCrop(Gravity.TOP).into(profile_banner, this);
addBannerBtn.setVisibility(INVISIBLE);
changeBannerBtn.setVisibility(VISIBLE);
bannerLink = path;
bannerLink = uri;
}
}
@ -289,8 +286,13 @@ public class ProfileEditor extends MediaActivity implements OnClickListener, OnP
String errMsg = getString(R.string.error_invalid_link);
link.setError(errMsg);
} else if (editorAsync == null || editorAsync.getStatus() != RUNNING) {
editorAsync = new UserUpdater(this);
editorAsync.execute(username, userLink, userLoc, userBio, profileLink, bannerLink);
ProfileHolder holder = new ProfileHolder(username, userLink, userBio, userLoc);
if (profileLink != null)
holder.addImageUri(getApplicationContext(), profileLink);
if (bannerLink != null)
holder.addBannerUri(getApplicationContext(), bannerLink);
editorAsync = new UserUpdater(this, holder);
editorAsync.execute();
if (!loadingCircle.isShowing()) {
loadingCircle.show();
}

View File

@ -6,15 +6,13 @@ import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_LONG;
import static android.widget.Toast.LENGTH_SHORT;
import static org.nuclearfog.twidda.activities.MediaViewer.KEY_MEDIA_LINK;
import static org.nuclearfog.twidda.activities.MediaViewer.KEY_MEDIA_TYPE;
import static org.nuclearfog.twidda.activities.MediaViewer.MEDIAVIEWER_IMAGE;
import static org.nuclearfog.twidda.activities.MediaViewer.MEDIAVIEWER_VIDEO;
import static org.nuclearfog.twidda.activities.MediaViewer.*;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
@ -70,6 +68,10 @@ public class TweetEditor extends MediaActivity implements OnClickListener, OnPro
*/
public static final String KEY_TWEETPOPUP_TEXT = "tweet_text";
private static final String MIME_GIF = "image/gif";
private static final String MIME_IMAGE_ALL = "image/";
private static final String MIME_VIDEO_ALL = "video/";
/**
* max amount of images (limited to 4 by twitter)
*/
@ -90,7 +92,7 @@ public class TweetEditor extends MediaActivity implements OnClickListener, OnPro
private View locationPending;
private Location location;
private List<String> mediaPath = new LinkedList<>();
private List<Uri> mediaPath = new LinkedList<>();
private MediaType selectedFormat = MediaType.NONE;
@Override
@ -203,7 +205,8 @@ public class TweetEditor extends MediaActivity implements OnClickListener, OnPro
// open media preview
else if (v.getId() == R.id.tweet_prev_media) {
Intent image = new Intent(this, MediaViewer.class);
image.putExtra(KEY_MEDIA_LINK, mediaPath.toArray(new String[0]));
Uri[] uris = mediaPath.toArray(new Uri[0]);
image.putExtra(KEY_MEDIA_URI, uris);
if (selectedFormat == MediaType.VIDEO) {
image.putExtra(KEY_MEDIA_TYPE, MEDIAVIEWER_VIDEO);
startActivity(image);
@ -235,29 +238,32 @@ public class TweetEditor extends MediaActivity implements OnClickListener, OnPro
@Override
protected void onMediaFetched(int resultType, @NonNull String path) {
String mime = StringTools.getMimeType(path);
protected void onMediaFetched(int resultType, @NonNull Uri uri) {
String mime = getContentResolver().getType(uri);
if (mime == null) {
Toast.makeText(this, R.string.error_file_format, LENGTH_SHORT).show();
}
// check if file is a gif image
if (mime.equals("image/gif")) {
else if (mime.equals(MIME_GIF)) {
if (selectedFormat == MediaType.NONE) {
selectedFormat = MediaType.GIF;
previewBtn.setImageResource(R.drawable.gif);
AppStyles.setDrawableColor(previewBtn, settings.getIconColor());
previewBtn.setVisibility(VISIBLE);
mediaBtn.setVisibility(GONE);
mediaPath.add(path);
mediaPath.add(uri);
} else {
Toast.makeText(this, R.string.info_cant_add_video, LENGTH_SHORT).show();
}
}
// check if file is an image with supported extension
else if (mime.startsWith("image")) {
else if (mime.startsWith(MIME_IMAGE_ALL)) {
if (selectedFormat == MediaType.NONE)
selectedFormat = MediaType.IMAGE;
if (selectedFormat == MediaType.IMAGE) {
// add up to 4 images
if (mediaPath.size() < MAX_IMAGES) {
mediaPath.add(path);
mediaPath.add(uri);
previewBtn.setVisibility(VISIBLE);
// if limit reached, remove mediaselect button
if (mediaPath.size() == MAX_IMAGES) {
@ -269,14 +275,14 @@ public class TweetEditor extends MediaActivity implements OnClickListener, OnPro
}
}
// check if file is a video with supported extension
else if (mime.startsWith("video")) {
else if (mime.startsWith(MIME_VIDEO_ALL)) {
if (selectedFormat == MediaType.NONE) {
selectedFormat = MediaType.VIDEO;
previewBtn.setImageResource(R.drawable.video);
AppStyles.setDrawableColor(previewBtn, settings.getIconColor());
previewBtn.setVisibility(VISIBLE);
mediaBtn.setVisibility(GONE);
mediaPath.add(path);
mediaPath.add(uri);
}
}
// file type is not supported
@ -349,9 +355,9 @@ public class TweetEditor extends MediaActivity implements OnClickListener, OnPro
TweetHolder tweet = new TweetHolder(tweetStr, inReplyId);
// add media
if (selectedFormat == MediaType.IMAGE || selectedFormat == MediaType.GIF)
tweet.addMedia(mediaPath.toArray(new String[0]), TweetHolder.MediaType.IMAGE);
tweet.addMedia(getApplicationContext(), mediaPath);
else if (selectedFormat == MediaType.VIDEO)
tweet.addMedia(mediaPath.toArray(new String[0]), TweetHolder.MediaType.VIDEO);
tweet.addMedia(getApplicationContext(), mediaPath);
// add location
if (location != null)
tweet.addLocation(location);

View File

@ -1,6 +1,7 @@
package org.nuclearfog.twidda.adapter;
import android.graphics.Bitmap;
import android.content.Context;
import android.net.Uri;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
@ -12,15 +13,17 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import org.nuclearfog.twidda.adapter.holder.Footer;
import org.nuclearfog.twidda.adapter.holder.ImageItem;
import org.nuclearfog.twidda.backend.holder.ImageHolder;
import org.nuclearfog.twidda.backend.utils.PicassoBuilder;
import org.nuclearfog.twidda.database.GlobalSettings;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;
import static android.view.View.VISIBLE;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import com.squareup.picasso.Picasso;
/**
* Adapter class for image previews
*
@ -42,29 +45,32 @@ public class ImageAdapter extends Adapter<ViewHolder> {
private GlobalSettings settings;
private List<ImageHolder> images = new LinkedList<>();
private Picasso picasso;
private List<Uri> imageUri = new ArrayList<>(5);
private boolean loading = false;
private boolean saveImg = true;
/**
* @param itemClickListener click listener
*/
public ImageAdapter(GlobalSettings settings, OnImageClickListener itemClickListener) {
public ImageAdapter(Context context, OnImageClickListener itemClickListener) {
this.itemClickListener = itemClickListener;
this.settings = settings;
this.settings = GlobalSettings.getInstance(context);
picasso = PicassoBuilder.get(context);
}
/**
* add new image at the last position
*
* @param imageItem image to add
* @param uri Uri of the image
*/
@MainThread
public void addLast(@NonNull ImageHolder imageItem) {
int imagePos = images.size();
public void addLast(@NonNull Uri uri) {
int imagePos = imageUri.size();
if (imagePos == 0)
loading = true;
images.add(imageItem);
imageUri.add(uri);
notifyItemInserted(imagePos);
}
@ -74,7 +80,7 @@ public class ImageAdapter extends Adapter<ViewHolder> {
@MainThread
public void disableLoading() {
loading = false;
int circlePos = images.size();
int circlePos = imageUri.size();
notifyItemRemoved(circlePos);
}
@ -91,13 +97,13 @@ public class ImageAdapter extends Adapter<ViewHolder> {
* @return true if there isn't any image
*/
public boolean isEmpty() {
return images.isEmpty();
return imageUri.isEmpty();
}
@Override
public int getItemViewType(int position) {
if (loading && position == images.size())
if (loading && position == imageUri.size())
return LOADING;
return PICTURE;
}
@ -106,8 +112,8 @@ public class ImageAdapter extends Adapter<ViewHolder> {
@Override
public int getItemCount() {
if (loading)
return images.size() + 1;
return images.size();
return imageUri.size() + 1;
return imageUri.size();
}
@ -121,8 +127,7 @@ public class ImageAdapter extends Adapter<ViewHolder> {
public void onClick(View v) {
int pos = item.getLayoutPosition();
if (pos != NO_POSITION) {
Bitmap img = images.get(pos).reducedImage;
itemClickListener.onImageClick(img);
itemClickListener.onImageClick(imageUri.get(pos));
}
}
});
@ -133,8 +138,7 @@ public class ImageAdapter extends Adapter<ViewHolder> {
public void onClick(View v) {
int pos = item.getLayoutPosition();
if (pos != NO_POSITION) {
Bitmap img = images.get(pos).fullImage;
itemClickListener.onImageSave(img, pos);
itemClickListener.onImageSave(imageUri.get(pos));
}
}
});
@ -150,8 +154,8 @@ public class ImageAdapter extends Adapter<ViewHolder> {
public void onBindViewHolder(@NonNull ViewHolder vh, int index) {
if (vh instanceof ImageItem) {
ImageItem item = (ImageItem) vh;
Bitmap image = images.get(index).preview;
item.preview.setImageBitmap(image);
Uri uri = imageUri.get(index);
picasso.load(uri).into(item.preview);
}
}
@ -163,16 +167,13 @@ public class ImageAdapter extends Adapter<ViewHolder> {
/**
* called to select an image
*
* @param image selected image bitmap
* @param uri selected image uri
*/
void onImageClick(Bitmap image);
void onImageClick(Uri uri);
/**
* called to save image to storage
*
* @param image selected image bitmap
* @param index current image index
*/
void onImageSave(Bitmap image, int index);
void onImageSave(Uri uri);
}
}

View File

@ -1,7 +1,6 @@
package org.nuclearfog.twidda.backend;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import androidx.annotation.NonNull;
@ -10,25 +9,27 @@ import androidx.annotation.Nullable;
import org.nuclearfog.twidda.activities.MediaViewer;
import org.nuclearfog.twidda.backend.api.Twitter;
import org.nuclearfog.twidda.backend.api.TwitterException;
import org.nuclearfog.twidda.backend.holder.ImageHolder;
import org.nuclearfog.twidda.backend.utils.ErrorHandler;
import org.nuclearfog.twidda.backend.utils.StringTools;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.ref.WeakReference;
/**
* Background task to load images from twitter and storage
* todo: cache files to prevent out of memory error
* background async to download images to a cache folder
*
* @author nuclearfog
* @see MediaViewer
*/
public class ImageLoader extends AsyncTask<String, ImageHolder, Boolean> {
public class ImageLoader extends AsyncTask<Uri, Uri, Boolean> {
@Nullable
private ErrorHandler.TwitterError err;
private Twitter twitter;
private WeakReference<MediaViewer> callback;
private File cache;
/**
* initialize image loader
@ -39,23 +40,32 @@ public class ImageLoader extends AsyncTask<String, ImageHolder, Boolean> {
super();
callback = new WeakReference<>(activity);
twitter = Twitter.get(activity);
cache = new File(activity.getExternalCacheDir(), MediaViewer.CACHE_FOLDER);
cache.mkdirs();
}
@Override
protected Boolean doInBackground(String[] links) {
protected Boolean doInBackground(Uri[] links) {
try {
for (String link : links) {
Bitmap image;
if (link.startsWith("https://")) {
image = twitter.downloadImage(link);
} else {
image = BitmapFactory.decodeFile(link);
}
if (image != null) {
ImageHolder images = new ImageHolder(image);
publishProgress(images);
// create cache folder if not exists
// download imaged to a local cache folder
for (Uri link : links) {
File file = new File(cache, StringTools.getRandomString());
file.createNewFile();
FileOutputStream os = new FileOutputStream(file);
InputStream input = twitter.downloadImage(link.toString());
// copy image to cache folder
int length;
byte[] buffer = new byte[4096];
while ((length = input.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
input.close();
os.close();
// create a new uri
publishProgress(Uri.fromFile(file));
}
return true;
} catch (TwitterException err) {
@ -68,9 +78,9 @@ public class ImageLoader extends AsyncTask<String, ImageHolder, Boolean> {
@Override
protected void onProgressUpdate(ImageHolder[] images) {
protected void onProgressUpdate(Uri[] uris) {
if (callback.get() != null) {
callback.get().setImage(images[0]);
callback.get().setImage(uris[0]);
}
}

View File

@ -1,10 +1,10 @@
package org.nuclearfog.twidda.backend;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import org.nuclearfog.twidda.activities.MediaActivity;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
@ -16,10 +16,6 @@ import java.lang.ref.WeakReference;
*/
public class ImageSaver extends AsyncTask<Object, Void, Boolean> {
/**
* Quality of the saved jpeg images
*/
private static final int JPEG_QUALITY = 90;
private WeakReference<MediaActivity> callback;
@ -34,12 +30,19 @@ public class ImageSaver extends AsyncTask<Object, Void, Boolean> {
protected Boolean doInBackground(Object... data) {
try {
if (data != null && data.length == 2) {
if (data[0] instanceof Bitmap && data[1] instanceof OutputStream) {
Bitmap image = (Bitmap) data[0];
OutputStream fileStream = (OutputStream) data[1];
boolean imageSaved = image.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, fileStream);
fileStream.close();
return imageSaved;
if (data[0] instanceof InputStream && data[1] instanceof OutputStream) {
InputStream source = (InputStream) data[0];
OutputStream destiny = (OutputStream) data[1];
// copy file from cache to the destiny folder
int length;
byte[] buffer = new byte[4096];
while ((length = source.read(buffer)) > 0) {
destiny.write(buffer, 0, length);
}
source.close();
destiny.close();
return true;
}
}
} catch (Exception err) {

View File

@ -72,7 +72,7 @@ public class ListAction extends AsyncTask<Long, Void, UserList> {
return twitter.unfollowUserlist(listId);
case DELETE:
return twitter.destroyUserlist(listId);
return twitter.deleteUserlist(listId);
}
} catch (TwitterException err) {
this.err = err;

View File

@ -7,6 +7,7 @@ import androidx.annotation.NonNull;
import org.nuclearfog.twidda.activities.MessageEditor;
import org.nuclearfog.twidda.backend.api.Twitter;
import org.nuclearfog.twidda.backend.api.TwitterException;
import org.nuclearfog.twidda.backend.holder.DirectmessageHolder;
import org.nuclearfog.twidda.backend.utils.ErrorHandler;
import java.lang.ref.WeakReference;
@ -17,38 +18,41 @@ import java.lang.ref.WeakReference;
* @author nuclearfog
* @see MessageEditor
*/
public class MessageUpdater extends AsyncTask<String, Void, Boolean> {
public class MessageUpdater extends AsyncTask<Void, Void, Boolean> {
private ErrorHandler.TwitterError twException;
private WeakReference<MessageEditor> callback;
private Twitter twitter;
private DirectmessageHolder message;
/**
* send direct message
*
* @param activity Activity context
*/
public MessageUpdater(@NonNull MessageEditor activity) {
public MessageUpdater(@NonNull MessageEditor activity, DirectmessageHolder message) {
super();
twitter = Twitter.get(activity);
callback = new WeakReference<>(activity);
this.message = message;
}
@Override
protected Boolean doInBackground(String[] param) {
protected Boolean doInBackground(Void[] v) {
try {
// first check if user exists
long id = twitter.showUser(param[0]).getId();
long id = twitter.showUser(message.getReceiver()).getId();
// upload media if any
long mediaId = -1;
String mediaPath = param[2];
if (mediaPath != null && !mediaPath.isEmpty()) {
mediaId = twitter.uploadImage(param[2]);
if (message.getMediaStream() != null) {
mediaId = twitter.uploadMedia(message.getMediaStream(), message.getMimeType());
message.getMediaStream().close();
}
// upload message and media ID if defined
if (!isCancelled()) {
twitter.sendDirectmessage(id, param[1], mediaId);
twitter.sendDirectmessage(id, message.getText(), mediaId);
}
return true;
} catch (TwitterException twException) {

View File

@ -8,6 +8,7 @@ import org.nuclearfog.twidda.backend.api.TwitterException;
import org.nuclearfog.twidda.backend.holder.TweetHolder;
import org.nuclearfog.twidda.backend.utils.ErrorHandler;
import java.io.InputStream;
import java.lang.ref.WeakReference;
/**
@ -18,9 +19,8 @@ import java.lang.ref.WeakReference;
*/
public class TweetUpdater extends AsyncTask<Void, Void, Boolean> {
private ErrorHandler.TwitterError twException;
private Twitter twitter;
private ErrorHandler.TwitterError twException;
private WeakReference<TweetEditor> callback;
private TweetHolder tweet;
@ -40,25 +40,16 @@ public class TweetUpdater extends AsyncTask<Void, Void, Boolean> {
@Override
protected Boolean doInBackground(Void[] v) {
try {
long[] mediaIds = {};
String[] mediaLinks = tweet.getMediaPaths();
if (mediaLinks != null && mediaLinks.length > 0) {
mediaIds = new long[mediaLinks.length];
String[] mimeTypes = tweet.getMimeTypes();
InputStream[] mediaStreams = tweet.getMediaStreams();
// upload image
if (tweet.getMediaType() == TweetHolder.MediaType.IMAGE) {
for (int i = 0; i < mediaLinks.length; i++) {
mediaIds[i] = twitter.uploadImage(mediaLinks[i]);
if (isCancelled()) {
break;
}
}
}
// upload video file
else if (tweet.getMediaType() == TweetHolder.MediaType.VIDEO) {// fixme
//mediaIds[0] = mTwitter.uploadVideo(mediaLinks[0]);
}
// upload media first
long[] mediaIds = new long[mediaStreams.length];
for (int pos = 0 ; pos < mediaStreams.length ; pos++) {
mediaIds[pos] = twitter.uploadMedia(mediaStreams[pos], mimeTypes[pos]);
mediaStreams[pos].close();
}
// upload tweet
if (!isCancelled()) {
double[] coordinates = null;

View File

@ -7,10 +7,12 @@ import androidx.annotation.Nullable;
import org.nuclearfog.twidda.activities.ProfileEditor;
import org.nuclearfog.twidda.backend.api.Twitter;
import org.nuclearfog.twidda.backend.api.TwitterException;
import org.nuclearfog.twidda.backend.holder.ProfileHolder;
import org.nuclearfog.twidda.backend.utils.ErrorHandler;
import org.nuclearfog.twidda.model.User;
import org.nuclearfog.twidda.database.AppDatabase;
import java.io.IOException;
import java.lang.ref.WeakReference;
/**
@ -19,7 +21,7 @@ import java.lang.ref.WeakReference;
* @author nuclearfog
* @see ProfileEditor
*/
public class UserUpdater extends AsyncTask<String, Void, User> {
public class UserUpdater extends AsyncTask<Void, Void, User> {
@Nullable
private ErrorHandler.TwitterError twException;
@ -27,31 +29,36 @@ public class UserUpdater extends AsyncTask<String, Void, User> {
private Twitter twitter;
private AppDatabase db;
private ProfileHolder profile;
public UserUpdater(ProfileEditor activity) {
public UserUpdater(ProfileEditor activity, ProfileHolder profile) {
super();
callback = new WeakReference<>(activity);
twitter = Twitter.get(activity);
db = new AppDatabase(activity);
this.profile = profile;
}
@Override
protected User doInBackground(String[] param) {
protected User doInBackground(Void[] v) {
try {
String name = param[0];
String link = param[1];
String location = param[2];
String bio = param[3];
String profileImg = param[4];
String bannerImg = param[5];
twitter.updateProfileImage(profileImg);
twitter.updateProfileBanner(bannerImg);
User user = twitter.updateProfile(name, link, location, bio);
if (profile.getProfileImageStream() != null) {
twitter.updateProfileImage(profile.getProfileImageStream());
profile.getProfileImageStream().close();
}
if (profile.getBannerImageStream() != null) {
twitter.updateProfileBanner(profile.getBannerImageStream());
profile.getBannerImageStream().close();
}
User user = twitter.updateProfile(profile.getName(), profile.getUrl(), profile.getLocation(), profile.getDescription());
db.storeUser(user);
return user;
} catch (TwitterException twException) {
this.twException = twException;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

View File

@ -1,8 +1,6 @@
package org.nuclearfog.twidda.backend.api;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
@ -27,7 +25,6 @@ import org.nuclearfog.twidda.model.User;
import org.nuclearfog.twidda.database.GlobalSettings;
import org.nuclearfog.twidda.model.UserList;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
@ -50,6 +47,8 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okio.BufferedSink;
import okio.Okio;
/**
* new API implementation to replace twitter4j and add version 2.0 support
@ -258,6 +257,8 @@ public class Twitter {
*/
public User showUser(String name) throws TwitterException {
List<String> params = new ArrayList<>(3);
if (name.startsWith("@"))
name = name.substring(1);
params.add("screen_name=" + name);
return getUser1(USER_LOOKUP, params);
}
@ -940,7 +941,7 @@ public class Twitter {
* @param listId ID of the list
* @return removed userlist
*/
public UserList destroyUserlist(long listId) throws TwitterException {
public UserList deleteUserlist(long listId) throws TwitterException {
List<String> params = new ArrayList<>(2);
params.add("list_id=" + listId);
return getUserlist(USERLIST_DESTROY, params);
@ -1064,7 +1065,7 @@ public class Twitter {
params.add("id=" + messageId);
try {
Response response = delete(DIRECTMESSAGE_DELETE, params);
if (response.code() != 200) {
if (response.code() >= 300) {
throw new TwitterException(response);
}
} catch (IOException err) {
@ -1130,20 +1131,18 @@ public class Twitter {
}
/**
* upload image file to twitter and generate a media ID
* upload medida file to twitter and generate a media ID
*
* @param filePath path to the local file
* @param uploadStream path to the local file
* @return media ID
*/
public long uploadImage(String filePath) throws TwitterException {
File file = new File(filePath);
String mime = StringTools.getMimeType(filePath);
public long uploadMedia(InputStream uploadStream, String mime) throws TwitterException {
List<String> params = new ArrayList<>(4);
try {
// step 1 INIT
List<String> params = new ArrayList<>();
params.add("command=INIT");
params.add("media_type=" + mime);
params.add("total_bytes=" + file.length());
params.add("total_bytes=" + uploadStream.available());
Response response = post(MEDIA_UPLOAD, params);
if (response.code() < 200 || response.code() >= 300 || response.body() == null)
throw new TwitterException(response);
@ -1155,7 +1154,7 @@ public class Twitter {
params.add("command=APPEND");
params.add("segment_index=0");
params.add("media_id=" + mediaId);
response = post(MEDIA_UPLOAD, params, file, "media");
response = post(MEDIA_UPLOAD, params, uploadStream, "media");
if (response.code() < 200 || response.code() >= 300)
throw new TwitterException(response);
@ -1181,14 +1180,13 @@ public class Twitter {
* @param link link to the image
* @return image bitmap
*/
public Bitmap downloadImage(String link) throws TwitterException {
public InputStream downloadImage(String link) throws TwitterException {
try {
// this type of link requires authentication
if (link.startsWith("https://ton.twitter.com/")) {
Response response = get(link, new ArrayList<>(0));
if (response.code() == 200) {
InputStream is = response.body().byteStream();
return BitmapFactory.decodeStream(is);
return response.body().byteStream();
} else {
throw new TwitterException(response);
}
@ -1196,8 +1194,7 @@ public class Twitter {
// public link, no authentication required
else {
URL imageUrl = new URL(link);
InputStream is = imageUrl.openConnection().getInputStream();
return BitmapFactory.decodeStream(is);
return imageUrl.openConnection().getInputStream();
}
} catch (IOException e) {
throw new TwitterException(e);
@ -1225,19 +1222,19 @@ public class Twitter {
/**
* update current user's profile image
*
* @param path local path to the image
* @param inputStream inputstream of the local image file
*/
public void updateProfileImage(String path) throws TwitterException {
updateImage(PROFILE_UPDATE_IMAGE, path, "image");
public void updateProfileImage(InputStream inputStream) throws TwitterException {
updateImage(PROFILE_UPDATE_IMAGE, inputStream, "image");
}
/**
* update current user's profile banner image
*
* @param path local path to the image
* @param inputStream inputstream of the local image file
*/
public void updateProfileBanner(String path) throws TwitterException {
updateImage(PROFILE_UPDATE_BANNER, path, "banner");
public void updateProfileBanner(InputStream inputStream) throws TwitterException {
updateImage(PROFILE_UPDATE_BANNER, inputStream, "banner");
}
/**
@ -1552,15 +1549,15 @@ public class Twitter {
* update profile images
*
* @param endpoint endpoint to use
* @param imagePath path to the local image
* @param input inputstream of the image file
* @param key key name used to identify the type of image
*/
private void updateImage(String endpoint, String imagePath, String key) throws TwitterException {
private void updateImage(String endpoint, InputStream input, String key) throws TwitterException {
try {
List<String> params = new ArrayList<>(3);
params.add("skip_status=true");
params.add("include_entities=false");
Response response = post(endpoint, params, new File(imagePath), key);
Response response = post(endpoint, params, input, key);
if (response.body() != null) {
JSONObject json = new JSONObject(response.body().string());
if (response.code() != 200) {
@ -1655,10 +1652,20 @@ public class Twitter {
* @param params additional http parameters
* @return http resonse
*/
private Response post(String endpoint, List<String> params, File file, String addToKey) throws IOException {
RequestBody data = RequestBody.create(MediaType.parse("application/octet-stream"), file);
private Response post(String endpoint, List<String> params, InputStream is, String addToKey) throws IOException {
RequestBody data = new RequestBody() {
@Override
public MediaType contentType() {
return MediaType.parse("application/octet-stream");
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
sink.writeAll(Okio.buffer(Okio.source(is)));
}
};
RequestBody body = new MultipartBody.Builder().setType(MultipartBody.FORM)
.addFormDataPart(addToKey, file.getName(), data).build();
.addFormDataPart(addToKey, "", data).build();
return post(endpoint, params, body);
}

View File

@ -0,0 +1,81 @@
package org.nuclearfog.twidda.backend.holder;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.io.InputStream;
/**
* this class holds information about a directmessage
*
* @author nuclearfog
*/
public class DirectmessageHolder {
private String name;
private String text;
private String mimeType = "";
private InputStream fileStream;
public DirectmessageHolder(String name, String text) {
this.name = name;
this.text = text;
}
/**
* add media uri and create input stream
*
* @param context context used to create inputstream and mime type
* @param uri uri of a local media file
*/
public void addMedia(Context context, @NonNull Uri uri) {
ContentResolver resolver = context.getContentResolver();
try {
fileStream = resolver.openInputStream(uri);
mimeType = resolver.getType(uri);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* get name of the receiver
*
* @return screen name
*/
public String getReceiver() {
return name;
}
/**
* get message text
*
* @return message text
*/
public String getText() {
return text;
}
/**
* get inputstream of the media file
*
* @return input stream
*/
public InputStream getMediaStream() {
return fileStream;
}
/**
* get MIME type of the media file
*
* @return mime type string
*/
public String getMimeType() {
return mimeType;
}
}

View File

@ -1,63 +0,0 @@
package org.nuclearfog.twidda.backend.holder;
import android.graphics.Bitmap;
import androidx.annotation.NonNull;
/**
* Container class for Bitmap images and previews
*
* @author nuclearfog
*/
public class ImageHolder {
/**
* maximum height of the smallest preview in pixels
*/
private static final float PREVIEW_HEIGHT = 320.0f;
/**
* maximum height of the image preview in pixels
*/
private static final float REDUCED_HEIGHT = 1200.0f;
/**
* preview image bitmap
*/
public final Bitmap preview;
/**
* downscaled image bitmap
*/
public final Bitmap reducedImage;
/**
* full image bitmap
*/
public final Bitmap fullImage;
/**
* @param fullImage Full size image
*/
public ImageHolder(@NonNull Bitmap fullImage) {
this.fullImage = fullImage;
float reducedRatio = fullImage.getHeight() / REDUCED_HEIGHT;
float previewRatio = fullImage.getHeight() / PREVIEW_HEIGHT;
if (reducedRatio > 1.0f) {
int height = (int) REDUCED_HEIGHT;
int width = (int) (fullImage.getWidth() / reducedRatio);
reducedImage = Bitmap.createScaledBitmap(fullImage, width, height, false);
} else {
reducedImage = fullImage;
}
if (previewRatio > 1.0f) {
int height = (int) PREVIEW_HEIGHT;
int width = (int) (fullImage.getWidth() / previewRatio);
preview = Bitmap.createScaledBitmap(fullImage, width, height, false);
} else {
preview = fullImage;
}
}
}

View File

@ -0,0 +1,103 @@
package org.nuclearfog.twidda.backend.holder;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
/**
* holder class for profile update information
*
* @author nuclearfog
*/
public class ProfileHolder {
private String name;
private String url;
private String description;
private String location;
private InputStream profileImgStream;
private InputStream bannerImgStream;
public ProfileHolder(String name, String url, String description, String location) {
this.name = name;
this.url = url;
this.description = description;
this.location = location;
}
/**
* add profile image Uri
*
* @param context context used to resolve Uri
* @param profileImgUri Uri of the local image file
*/
public void addImageUri(Context context, @Nullable Uri profileImgUri) {
try {
profileImgStream = context.getContentResolver().openInputStream(profileImgUri);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* add banner image Uri
*
* @param context context used to resolve Uri
* @param bannerImgUri Uri of the local image file
*/
public void addBannerUri(Context context, @Nullable Uri bannerImgUri) {
try {
bannerImgStream = context.getContentResolver().openInputStream(bannerImgUri);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* @return screen name of the user
*/
public String getName() {
return name;
}
/**
* @return profile description (bio)
*/
public String getDescription() {
return description;
}
/**
* @return location name
*/
public String getLocation() {
return location;
}
/**
* @return profile url
*/
public String getUrl() {
return url;
}
/**
* @return filestream of the profile image
*/
public InputStream getProfileImageStream() {
return profileImgStream;
}
/**
* @return filestream of the banner image
*/
public InputStream getBannerImageStream() {
return bannerImgStream;
}
}

View File

@ -1,9 +1,16 @@
package org.nuclearfog.twidda.backend.holder;
import android.content.ContentResolver;
import android.content.Context;
import android.location.Location;
import android.net.Uri;
import androidx.annotation.NonNull;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* TweetHolder keeps information about a written tweet such as text, media files and location
*
@ -11,19 +18,13 @@ import androidx.annotation.NonNull;
*/
public class TweetHolder {
public enum MediaType {
IMAGE,
VIDEO,
NONE
}
private final String text;
private final long replyId;
private String[] mediaPaths;
private double longitude;
private double latitude;
private InputStream[] mediaStreams;
private MediaType mType = MediaType.NONE;
private String[] mimeTypes = {};
private boolean hasLocation = false;
@ -41,12 +42,25 @@ public class TweetHolder {
/**
* Add media paths to the holder
*
* @param mediaLinks array of media paths from storage
* @param mType type of media
* @param context context to resolve Uri links
* @param mediaUri array of media paths from storage
*/
public void addMedia(String[] mediaLinks, MediaType mType) {
this.mediaPaths = mediaLinks;
this.mType = mType;
public void addMedia(Context context, List<Uri> mediaUri) {
if (!mediaUri.isEmpty()) {
List<InputStream> iss = new ArrayList<>();
List<String> mimeTypes = new ArrayList<>();
ContentResolver resolver = context.getContentResolver();
try {
for (Uri uri : mediaUri) {
iss.add(resolver.openInputStream(uri));
mimeTypes.add(resolver.getType(uri));
}
} catch (Exception e) {
e.printStackTrace();
}
this.mediaStreams = iss.toArray(new InputStream[0]);
this.mimeTypes = mimeTypes.toArray(new String[0]);
}
}
/**
@ -83,8 +97,8 @@ public class TweetHolder {
*
* @return media type
*/
public MediaType getMediaType() {
return mType;
public String[] getMimeTypes() {
return mimeTypes;
}
/**
@ -92,8 +106,8 @@ public class TweetHolder {
*
* @return array of media paths
*/
public String[] getMediaPaths() {
return mediaPaths;
public InputStream[] getMediaStreams() {
return mediaStreams;
}
/**
@ -123,15 +137,6 @@ public class TweetHolder {
return hasLocation;
}
/**
* return if tweet is a reply
*
* @return true if tweet is a reply
*/
public boolean isReply() {
return replyId > 0;
}
@NonNull
@Override
public String toString() {

View File

@ -2,7 +2,6 @@ package org.nuclearfog.twidda.backend.proxy;
import org.nuclearfog.twidda.database.GlobalSettings;
import java.io.IOException;
import java.net.Authenticator;
import java.net.PasswordAuthentication;

View File

@ -108,36 +108,6 @@ public final class StringTools {
return result;
}
/**
* get MIME type of a media file
* if file type is not supported, return {@link #MIME_ALL}
*
* @param filename file name or path with extension
* @return MIME type
*/
public static String getMimeType(String filename) {
int end = filename.lastIndexOf('.');
if (end < 0)
return MIME_ALL;
String extension = filename.substring(end + 1).toLowerCase();
switch (extension) {
case "jpg":
case "jpeg":
case "webp":
case "png":
case "gif":
return "image/" + extension;
case "mp4":
case "3gp":
return "video/" + extension;
default:
return MIME_ALL;
}
}
/**
* convert Twitter ISO 8601 date time to long format
*

View File

@ -5,7 +5,11 @@ import android.database.Cursor;
import org.nuclearfog.twidda.model.DirectMessage;
import org.nuclearfog.twidda.model.User;
/**
* database implementation of a directmessage
*
* @author nuclearfog
*/
class DirectMessageImpl implements DirectMessage {
private long id;