Merge branch '#136' of https://github.com/torrentcome/Tusky into torrentcome-#136

This commit is contained in:
Vavassor 2017-06-02 20:32:36 -04:00
commit 733fa6e53a
22 changed files with 523 additions and 168 deletions

View File

@ -50,6 +50,7 @@ dependencies {
compile 'com.github.arimorty:floatingsearchview:2.0.4' compile 'com.github.arimorty:floatingsearchview:2.0.4'
compile 'com.theartofdev.edmodo:android-image-cropper:2.4.3' compile 'com.theartofdev.edmodo:android-image-cropper:2.4.3'
compile 'com.jakewharton:butterknife:8.5.1' compile 'com.jakewharton:butterknife:8.5.1'
compile 'org.jsoup:jsoup:1.10.2'
compile 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0' compile 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0'
compile('org.eclipse.paho:org.eclipse.paho.android.service:1.1.1') { compile('org.eclipse.paho:org.eclipse.paho.android.service:1.1.1') {
exclude module: 'support-v4' exclude module: 'support-v4'

View File

@ -83,7 +83,7 @@
android:name="com.theartofdev.edmodo.cropper.CropImageActivity" android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
android:theme="@style/Base.Theme.AppCompat" /> android:theme="@style/Base.Theme.AppCompat" />
<receiver android:name=".util.NotificationClearBroadcastReceiver" /> <receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
<service android:name="org.eclipse.paho.android.service.MqttService" /> <service android:name="org.eclipse.paho.android.service.MqttService" />
<service <service

View File

@ -51,9 +51,9 @@ import com.keylesspalace.tusky.fragment.SFragment;
import com.keylesspalace.tusky.interfaces.LinkListener; import com.keylesspalace.tusky.interfaces.LinkListener;
import com.keylesspalace.tusky.interfaces.StatusRemoveListener; import com.keylesspalace.tusky.interfaces.StatusRemoveListener;
import com.keylesspalace.tusky.pager.AccountPagerAdapter; import com.keylesspalace.tusky.pager.AccountPagerAdapter;
import com.keylesspalace.tusky.receiver.TimelineReceiver;
import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.Assert; import com.keylesspalace.tusky.util.Assert;
import com.keylesspalace.tusky.util.TimelineReceiver;
import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
import com.pkmmte.view.CircularImageView; import com.pkmmte.view.CircularImageView;
import com.squareup.picasso.Picasso; import com.squareup.picasso.Picasso;

View File

@ -24,7 +24,6 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.AssetFileDescriptor; import android.content.res.AssetFileDescriptor;
import android.content.res.Resources; import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
@ -39,10 +38,8 @@ import android.os.Environment;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.support.annotation.AttrRes; import android.support.annotation.AttrRes;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar; import android.support.design.widget.Snackbar;
import android.support.v13.view.inputmethod.InputConnectionCompat; import android.support.v13.view.inputmethod.InputConnectionCompat;
@ -56,6 +53,7 @@ import android.support.v7.widget.Toolbar;
import android.text.Editable; import android.text.Editable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.text.style.URLSpan; import android.text.style.URLSpan;
import android.util.Log; import android.util.Log;
@ -73,14 +71,17 @@ import android.widget.TextView;
import com.keylesspalace.tusky.entity.Media; import com.keylesspalace.tusky.entity.Media;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.fragment.ComposeOptionsFragment; import com.keylesspalace.tusky.fragment.ComposeOptionsFragment;
import com.keylesspalace.tusky.util.DownsizeImageTask;
import com.keylesspalace.tusky.util.EditTextTyped;
import com.keylesspalace.tusky.util.CountUpDownLatch; import com.keylesspalace.tusky.util.CountUpDownLatch;
import com.keylesspalace.tusky.util.DownsizeImageTask;
import com.keylesspalace.tusky.util.IOUtils; import com.keylesspalace.tusky.util.IOUtils;
import com.keylesspalace.tusky.util.MediaUtils;
import com.keylesspalace.tusky.util.ParserUtils;
import com.keylesspalace.tusky.util.SpanUtils; import com.keylesspalace.tusky.util.SpanUtils;
import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.view.EditTextTyped;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
@ -92,7 +93,6 @@ import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Random;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
@ -103,17 +103,43 @@ import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
import retrofit2.Response; import retrofit2.Response;
public class ComposeActivity extends BaseActivity implements ComposeOptionsFragment.Listener { import static com.keylesspalace.tusky.util.MediaUtils.MEDIA_SIZE_UNKNOWN;
import static com.keylesspalace.tusky.util.MediaUtils.getMediaSize;
import static com.keylesspalace.tusky.util.MediaUtils.inputStreamGetBytes;
import static com.keylesspalace.tusky.util.StringUtils.carriageReturn;
import static com.keylesspalace.tusky.util.StringUtils.randomAlphanumericString;
public class ComposeActivity extends BaseActivity implements ComposeOptionsFragment.Listener, ParserUtils.ParserListener {
private static final String TAG = "ComposeActivity"; // logging tag private static final String TAG = "ComposeActivity"; // logging tag
private static final int STATUS_CHARACTER_LIMIT = 500; private static final int STATUS_CHARACTER_LIMIT = 500;
private static final int STATUS_MEDIA_SIZE_LIMIT = 4000000; // 4MB private static final int STATUS_MEDIA_SIZE_LIMIT = 4000000; // 4MB
private static final int MEDIA_PICK_RESULT = 1; private static final int MEDIA_PICK_RESULT = 1;
private static final int MEDIA_TAKE_PHOTO_RESULT = 2; private static final int MEDIA_TAKE_PHOTO_RESULT = 2;
private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1; private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1;
private static final int MEDIA_SIZE_UNKNOWN = -1;
private static final int COMPOSE_SUCCESS = -1; private static final int COMPOSE_SUCCESS = -1;
private static final int THUMBNAIL_SIZE = 128; // pixels private static final int THUMBNAIL_SIZE = 128; // pixels
@BindView(R.id.compose_edit_field)
EditTextTyped textEditor;
@BindView(R.id.compose_media_preview_bar)
LinearLayout mediaPreviewBar;
@BindView(R.id.compose_content_warning_bar)
View contentWarningBar;
@BindView(R.id.field_content_warning)
EditText contentWarningEditor;
@BindView(R.id.characters_left)
TextView charactersLeft;
@BindView(R.id.floating_btn)
Button floatingBtn;
@BindView(R.id.compose_photo_pick)
ImageButton pickBtn;
@BindView(R.id.compose_photo_take)
ImageButton takeBtn;
@BindView(R.id.action_toggle_nsfw)
Button nsfwBtn;
@BindView(R.id.postProgress)
ProgressBar postProgress;
@BindView(R.id.action_toggle_visibility)
ImageButton visibilityBtn;
private String inReplyToId; private String inReplyToId;
private ArrayList<QueuedMedia> mediaQueued; private ArrayList<QueuedMedia> mediaQueued;
private CountUpDownLatch waitForMediaLatch; private CountUpDownLatch waitForMediaLatch;
@ -127,93 +153,12 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
private Uri photoUploadUri; private Uri photoUploadUri;
// this only exists when a status is trying to be sent, but uploads are still occurring // this only exists when a status is trying to be sent, but uploads are still occurring
private ProgressDialog finishingUploadDialog; private ProgressDialog finishingUploadDialog;
@BindView(R.id.compose_edit_field)
EditTextTyped textEditor;
@BindView(R.id.compose_media_preview_bar) LinearLayout mediaPreviewBar;
@BindView(R.id.compose_content_warning_bar) View contentWarningBar;
@BindView(R.id.field_content_warning) EditText contentWarningEditor;
@BindView(R.id.characters_left) TextView charactersLeft;
@BindView(R.id.floating_btn) Button floatingBtn;
@BindView(R.id.compose_photo_pick) ImageButton pickBtn;
@BindView(R.id.compose_photo_take) ImageButton takeBtn;
@BindView(R.id.action_toggle_nsfw) Button nsfwBtn;
@BindView(R.id.postProgress) ProgressBar postProgress;
@BindView(R.id.action_toggle_visibility) ImageButton visibilityBtn;
private static class QueuedMedia { /**
enum Type { * The Target object must be stored as a member field or method and cannot be an anonymous class otherwise this won't work as expected. The reason is that Picasso accepts this parameter as a weak memory reference. Because anonymous classes are eligible for garbage collection when there are no more references, the network request to fetch the image may finish after this anonymous class has already been reclaimed. See this Stack Overflow discussion for more details.
IMAGE, */
VIDEO @SuppressWarnings("FieldCanBeLocal")
} private Target target;
enum ReadyStage {
DOWNSIZING,
UPLOADING
}
Type type;
ImageView preview;
Uri uri;
String id;
Call<Media> uploadRequest;
URLSpan uploadUrl;
ReadyStage readyStage;
byte[] content;
long mediaSize;
QueuedMedia(Type type, Uri uri, ImageView preview, long mediaSize) {
this.type = type;
this.uri = uri;
this.preview = preview;
this.mediaSize = mediaSize;
}
}
/**This saves enough information to re-enqueue an attachment when restoring the activity. */
private static class SavedQueuedMedia implements Parcelable {
QueuedMedia.Type type;
Uri uri;
Bitmap preview;
long mediaSize;
SavedQueuedMedia(QueuedMedia.Type type, Uri uri, ImageView view, long mediaSize) {
this.type = type;
this.uri = uri;
this.preview = ((BitmapDrawable) view.getDrawable()).getBitmap();
this.mediaSize = mediaSize;
}
SavedQueuedMedia(Parcel parcel) {
type = (QueuedMedia.Type) parcel.readSerializable();
uri = parcel.readParcelable(Uri.class.getClassLoader());
preview = parcel.readParcelable(Bitmap.class.getClassLoader());
mediaSize = parcel.readLong();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeSerializable(type);
dest.writeParcelable(uri, flags);
dest.writeParcelable(preview, flags);
dest.writeLong(mediaSize);
}
public static final Parcelable.Creator<SavedQueuedMedia> CREATOR
= new Parcelable.Creator<SavedQueuedMedia>() {
public SavedQueuedMedia createFromParcel(Parcel in) {
return new SavedQueuedMedia(in);
}
public SavedQueuedMedia[] newArray(int size) {
return new SavedQueuedMedia[size];
}
};
}
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@ -339,6 +284,8 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
postProgress.setVisibility(View.INVISIBLE); postProgress.setVisibility(View.INVISIBLE);
updateNsfwButtonColor(); updateNsfwButtonColor();
final ParserUtils parser = new ParserUtils(this);
// Setup the main text field. // Setup the main text field.
setEditTextMimeTypes(null); // new String[] { "image/gif", "image/webp" } setEditTextMimeTypes(null); // new String[] { "image/gif", "image/webp" }
final int mentionColour = ThemeUtils.getColor(this, R.attr.compose_mention_color); final int mentionColour = ThemeUtils.getColor(this, R.attr.compose_mention_color);
@ -350,7 +297,8 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
} }
@Override @Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {} public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override @Override
public void afterTextChanged(Editable editable) { public void afterTextChanged(Editable editable) {
@ -358,6 +306,13 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
} }
}); });
textEditor.addOnPasteListener(new EditTextTyped.OnPasteListener() {
@Override
public void onPaste() {
parser.getPastedURLText(ComposeActivity.this);
}
});
// Add any mentions to the text field when a reply is first composed. // Add any mentions to the text field when a reply is first composed.
if (mentionedUsernames != null) { if (mentionedUsernames != null) {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
@ -373,7 +328,8 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
// Initialise the content warning editor. // Initialise the content warning editor.
contentWarningEditor.addTextChangedListener(new TextWatcher() { contentWarningEditor.addTextChangedListener(new TextWatcher() {
@Override @Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {} public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override @Override
public void onTextChanged(CharSequence s, int start, int before, int count) { public void onTextChanged(CharSequence s, int start, int before, int count) {
@ -381,10 +337,11 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
} }
@Override @Override
public void afterTextChanged(Editable s) {} public void afterTextChanged(Editable s) {
}
}); });
showContentWarning(startingHideText); showContentWarning(startingHideText);
if(startingContentWarning != null){ if (startingContentWarning != null) {
contentWarningEditor.setText(startingContentWarning); contentWarningEditor.setText(startingContentWarning);
} }
@ -441,6 +398,9 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
int left = Math.min(start, end); int left = Math.min(start, end);
int right = Math.max(start, end); int right = Math.max(start, end);
textEditor.getText().replace(left, right, text, 0, text.length()); textEditor.getText().replace(left, right, text, 0, text.length());
parser.putInClipboardManager(this, text);
textEditor.onPaste();
} }
} }
} }
@ -639,7 +599,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
} }
private boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, private boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags,
String[] mimeTypes) { String[] mimeTypes) {
try { try {
if (currentInputContentInfo != null) { if (currentInputContentInfo != null) {
currentInputContentInfo.releasePermission(); currentInputContentInfo.releasePermission();
@ -819,9 +779,9 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
private void onMediaPick() { private void onMediaPick() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) { != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, ActivityCompat.requestPermissions(this,
new String[] { Manifest.permission.READ_EXTERNAL_STORAGE }, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE); PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE);
} else { } else {
initiateMediaPicking(); initiateMediaPicking();
@ -830,7 +790,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
@Override @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
@NonNull int[] grantResults) { @NonNull int[] grantResults) {
switch (requestCode) { switch (requestCode) {
case PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE: { case PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE: {
if (grantResults.length > 0 if (grantResults.length > 0
@ -891,7 +851,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
intent.setType("image/* video/*"); intent.setType("image/* video/*");
} else { } else {
String[] mimeTypes = new String[] { "image/*", "video/*" }; String[] mimeTypes = new String[]{"image/*", "video/*"};
intent.setType("*/*"); intent.setType("*/*");
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes); intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
} }
@ -989,7 +949,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
} }
private void removeAllMediaFromQueue() { private void removeAllMediaFromQueue() {
for (Iterator<QueuedMedia> it = mediaQueued.iterator(); it.hasNext();) { for (Iterator<QueuedMedia> it = mediaQueued.iterator(); it.hasNext(); ) {
QueuedMedia item = it.next(); QueuedMedia item = it.next();
it.remove(); it.remove();
removeMediaFromQueue(item); removeMediaFromQueue(item);
@ -1011,7 +971,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
public void onFailure() { public void onFailure() {
onMediaDownsizeFailure(item); onMediaDownsizeFailure(item);
} }
}).execute(item.uri); }).execute(item.uri);
} }
private void onMediaDownsizeFailure(QueuedMedia item) { private void onMediaDownsizeFailure(QueuedMedia item) {
@ -1019,33 +979,6 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
removeMediaFromQueue(item); 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) {
Log.d(TAG, Log.getStackTraceString(e));
return null;
}
return buffer.toByteArray();
}
private void uploadMedia(final QueuedMedia item) { private void uploadMedia(final QueuedMedia item) {
item.readyStage = QueuedMedia.ReadyStage.UPLOADING; item.readyStage = QueuedMedia.ReadyStage.UPLOADING;
@ -1141,20 +1074,6 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
} }
} }
private static long getMediaSize(ContentResolver contentResolver, Uri uri) {
long mediaSize;
Cursor cursor = contentResolver.query(uri, null, null, null, null);
if (cursor != null) {
int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
cursor.moveToFirst();
mediaSize = cursor.getLong(sizeIndex);
cursor.close();
} else {
mediaSize = MEDIA_SIZE_UNKNOWN;
}
return mediaSize;
}
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
@ -1233,12 +1152,12 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
void showMarkSensitive(boolean show) { void showMarkSensitive(boolean show) {
showMarkSensitive = show; showMarkSensitive = show;
if(!showMarkSensitive) { if (!showMarkSensitive) {
statusMarkSensitive = false; statusMarkSensitive = false;
nsfwBtn.setTextColor(ThemeUtils.getColor(this, R.attr.compose_nsfw_button_color)); nsfwBtn.setTextColor(ThemeUtils.getColor(this, R.attr.compose_nsfw_button_color));
} }
if(show) { if (show) {
nsfwBtn.setVisibility(View.VISIBLE); nsfwBtn.setVisibility(View.VISIBLE);
} else { } else {
nsfwBtn.setVisibility(View.GONE); nsfwBtn.setVisibility(View.GONE);
@ -1265,4 +1184,131 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@Override
public void onReceiveHeaderInfo(ParserUtils.HeaderInfo headerInfo) {
if (!TextUtils.isEmpty(headerInfo.title)) {
cleanBaseUrl(headerInfo);
textEditor.append(headerInfo.title);
textEditor.append(carriageReturn);
textEditor.append(headerInfo.baseUrl);
}
if (!TextUtils.isEmpty(headerInfo.image)) {
Picasso.Builder builder = new Picasso.Builder(getApplicationContext());
builder.listener(new Picasso.Listener() {
@Override
public void onImageLoadFailed(Picasso picasso, Uri uri, Exception exception) {
exception.printStackTrace();
}
});
target = MediaUtils.picassoImageTarget(ComposeActivity.this, new MediaUtils.MediaListener() {
@Override
public void onCallback(final Uri headerInfo) {
if (headerInfo != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
long mediaSize = getMediaSize(getContentResolver(), headerInfo);
pickMedia(headerInfo, mediaSize);
}
});
}
}
});
Picasso.with(this).load(headerInfo.image).into(target);
}
}
// remove the precedent paste from the edit text
private void cleanBaseUrl(ParserUtils.HeaderInfo headerInfo) {
int lengthBaseUrl = headerInfo.baseUrl.length();
int total = textEditor.getText().length();
int indexSubString = total - lengthBaseUrl;
String text = textEditor.getText().toString();
text = text.substring(0, indexSubString);
textEditor.setText(text);
}
@Override
public void onErrorHeaderInfo() {
displayTransientError(R.string.error_generic);
}
private static class QueuedMedia {
Type type;
ImageView preview;
Uri uri;
String id;
Call<Media> uploadRequest;
URLSpan uploadUrl;
ReadyStage readyStage;
byte[] content;
long mediaSize;
QueuedMedia(Type type, Uri uri, ImageView preview, long mediaSize) {
this.type = type;
this.uri = uri;
this.preview = preview;
this.mediaSize = mediaSize;
}
enum Type {
IMAGE,
VIDEO
}
enum ReadyStage {
DOWNSIZING,
UPLOADING
}
}
/**
* This saves enough information to re-enqueue an attachment when restoring the activity.
*/
private static class SavedQueuedMedia implements Parcelable {
public static final Parcelable.Creator<SavedQueuedMedia> CREATOR
= new Parcelable.Creator<SavedQueuedMedia>() {
public SavedQueuedMedia createFromParcel(Parcel in) {
return new SavedQueuedMedia(in);
}
public SavedQueuedMedia[] newArray(int size) {
return new SavedQueuedMedia[size];
}
};
QueuedMedia.Type type;
Uri uri;
Bitmap preview;
long mediaSize;
SavedQueuedMedia(QueuedMedia.Type type, Uri uri, ImageView view, long mediaSize) {
this.type = type;
this.uri = uri;
this.preview = ((BitmapDrawable) view.getDrawable()).getBitmap();
this.mediaSize = mediaSize;
}
SavedQueuedMedia(Parcel parcel) {
type = (QueuedMedia.Type) parcel.readSerializable();
uri = parcel.readParcelable(Uri.class.getClassLoader());
preview = parcel.readParcelable(Bitmap.class.getClassLoader());
mediaSize = parcel.readLong();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeSerializable(type);
dest.writeParcelable(uri, flags);
dest.writeParcelable(preview, flags);
dest.writeLong(mediaSize);
}
}
} }

View File

@ -28,7 +28,7 @@ import android.widget.TextView;
import android.widget.ToggleButton; import android.widget.ToggleButton;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.util.RoundedTransformation; import com.keylesspalace.tusky.view.RoundedTransformation;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.util.DateUtils; import com.keylesspalace.tusky.util.DateUtils;

View File

@ -43,8 +43,8 @@ import com.keylesspalace.tusky.entity.Relationship;
import com.keylesspalace.tusky.interfaces.AccountActionListener; import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.keylesspalace.tusky.network.MastodonAPI; import com.keylesspalace.tusky.network.MastodonAPI;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.util.EndlessOnScrollListener;
import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
import java.util.List; import java.util.List;

View File

@ -40,8 +40,8 @@ import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.interfaces.StatusRemoveListener; import com.keylesspalace.tusky.interfaces.StatusRemoveListener;
import com.keylesspalace.tusky.util.EndlessOnScrollListener;
import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
import java.util.List; import java.util.List;

View File

@ -39,9 +39,9 @@ import com.keylesspalace.tusky.adapter.TimelineAdapter;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.interfaces.StatusRemoveListener; import com.keylesspalace.tusky.interfaces.StatusRemoveListener;
import com.keylesspalace.tusky.util.EndlessOnScrollListener; import com.keylesspalace.tusky.receiver.TimelineReceiver;
import com.keylesspalace.tusky.util.TimelineReceiver;
import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;

View File

@ -39,8 +39,8 @@ import com.keylesspalace.tusky.network.MastodonAPI;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.interfaces.StatusRemoveListener; import com.keylesspalace.tusky.interfaces.StatusRemoveListener;
import com.keylesspalace.tusky.util.ConversationLineItemDecoration;
import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.view.ConversationLineItemDecoration;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;

View File

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not, * You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */ * see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util; package com.keylesspalace.tusky.receiver;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;

View File

@ -1,4 +1,4 @@
package com.keylesspalace.tusky.util; package com.keylesspalace.tusky.receiver;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;

View File

@ -0,0 +1,117 @@
package com.keylesspalace.tusky.util;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Environment;
import android.provider.OpenableColumns;
import android.support.annotation.Nullable;
import android.support.v4.content.FileProvider;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Class who will have all the code link with Media
* <p>
* Motivation : try to keep the ComposeActivity "smaller" and make modular method
*/
public class MediaUtils {
public static final int MEDIA_SIZE_UNKNOWN = -1;
@Nullable
public 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();
}
public static long getMediaSize(ContentResolver contentResolver, Uri uri) {
long mediaSize;
Cursor cursor = contentResolver.query(uri, null, null, null, null);
if (cursor != null) {
int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
cursor.moveToFirst();
mediaSize = cursor.getLong(sizeIndex);
cursor.close();
} else {
mediaSize = MEDIA_SIZE_UNKNOWN;
}
return mediaSize;
}
// Download an image with picasso
public static Target picassoImageTarget(final Context context, final MediaListener mediaListener) {
final String imageName = "temp";
return new Target() {
@Override
public void onBitmapLoaded(final Bitmap bitmap, Picasso.LoadedFrom from) {
new Thread(new Runnable() {
@Override
public void run() {
FileOutputStream fos = null;
Uri uriForFile;
try {
// we download only a "temp" file
File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File tempFile = File.createTempFile(
imageName,
".jpg",
storageDir
);
fos = new FileOutputStream(tempFile);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
uriForFile = FileProvider.getUriForFile(context,
"com.keylesspalace.tusky.fileprovider",
tempFile);
// giving to the activity the URI callback
mediaListener.onCallback(uriForFile);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
}
@Override
public void onBitmapFailed(Drawable errorDrawable) {
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
}
};
}
public interface MediaListener {
void onCallback(Uri headerInfo);
}
}

View File

@ -33,6 +33,8 @@ import android.util.Log;
import com.keylesspalace.tusky.MainActivity; import com.keylesspalace.tusky.MainActivity;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver;
import com.keylesspalace.tusky.view.RoundedTransformation;
import com.squareup.picasso.Picasso; import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target; import com.squareup.picasso.Target;

View File

@ -0,0 +1,124 @@
package com.keylesspalace.tusky.util;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.URLUtil;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.helper.HttpConnection;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
import java.util.List;
import static com.keylesspalace.tusky.util.StringUtils.QUOTE;
/**
* Inspect and Get the information from an URL
*/
public class ParserUtils {
private static final String TAG = "ParserUtils";
private ParserListener parserListener;
public ParserUtils(ParserListener parserListener) {
this.parserListener = parserListener;
}
// ComposeActivity : EditText inside the onTextChanged
public String getPastedURLText(Context context) {
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
String pasteData;
if (clipboard.hasPrimaryClip()) {
// get what is in the clipboard
ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0);
pasteData = item.getText().toString();
// If we share with an app, it's not only an url
List<String> strings = StringUtils.extractUrl(pasteData);
String url = strings.get(0); // we assume that the first url is the good one
if (strings.size() > 0) {
if (URLUtil.isValidUrl(url)) {
new ThreadHeaderInfo().execute(url);
}
}
}
return null;
}
public void putInClipboardManager(Context context, String string) {
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("", string);
clipboard.setPrimaryClip(clip);
}
// parse the HTML page
private HeaderInfo parsePageHeaderInfo(String urlStr) throws Exception {
Connection con = Jsoup.connect(urlStr);
HeaderInfo headerInfo = new HeaderInfo();
con.userAgent(HttpConnection.DEFAULT_UA);
Document doc = con.get();
// get info
String text;
Elements metaOgTitle = doc.select("meta[property=og:title]");
if (metaOgTitle != null) {
text = metaOgTitle.attr("content");
} else {
text = doc.title();
}
String imageUrl = null;
Elements metaOgImage = doc.select("meta[property=og:image]");
if (metaOgImage != null) {
imageUrl = metaOgImage.attr("content");
}
// set info
headerInfo.baseUrl = urlStr;
if (!TextUtils.isEmpty(text)) {
headerInfo.title = QUOTE + text.toUpperCase() + QUOTE;
}
if (!TextUtils.isEmpty(imageUrl)) {
headerInfo.image = (imageUrl);
}
return headerInfo;
}
public interface ParserListener {
void onReceiveHeaderInfo(HeaderInfo headerInfo);
void onErrorHeaderInfo();
}
public class HeaderInfo {
public String baseUrl;
public String title;
public String image;
}
private class ThreadHeaderInfo extends AsyncTask<String, Void, HeaderInfo> {
protected HeaderInfo doInBackground(String... urls) {
try {
String url = urls[0];
return parsePageHeaderInfo(url);
} catch (Exception e) {
Log.e(TAG, "ThreadHeaderInfo#parsePageHeaderInfo() failed." + e.getMessage());
return null;
}
}
protected void onPostExecute(HeaderInfo headerInfo) {
if (headerInfo != null) {
Log.i(TAG, "ThreadHeaderInfo#parsePageHeaderInfo() success." + headerInfo.title + " " + headerInfo.image);
parserListener.onReceiveHeaderInfo(headerInfo);
} else {
parserListener.onErrorHeaderInfo();
}
}
}
}

View File

@ -0,0 +1,34 @@
package com.keylesspalace.tusky.util;
import android.util.Patterns;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.regex.Matcher;
public class StringUtils {
public final static String carriageReturn = System.getProperty("line.separator");
final static String QUOTE = "\"";
public 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);
}
static List<String> extractUrl(String text) {
List<String> links = new ArrayList<>();
Matcher m = Patterns.WEB_URL.matcher(text);
while (m.find()) {
String url = m.group();
links.add(url);
}
return links;
}
}

View File

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not, * You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */ * see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util; package com.keylesspalace.tusky.view;
import android.content.Context; import android.content.Context;
import android.graphics.Canvas; import android.graphics.Canvas;

View File

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not, * You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */ * see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util; package com.keylesspalace.tusky.view;
import android.content.Context; import android.content.Context;
import android.support.v13.view.inputmethod.EditorInfoCompat; import android.support.v13.view.inputmethod.EditorInfoCompat;
@ -23,9 +23,13 @@ import android.util.AttributeSet;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnection;
import com.keylesspalace.tusky.util.Assert;
public class EditTextTyped extends AppCompatEditText { public class EditTextTyped extends AppCompatEditText {
InputConnectionCompat.OnCommitContentListener onCommitContentListener; InputConnectionCompat.OnCommitContentListener onCommitContentListener;
String[] mimeTypes; String[] mimeTypes;
private OnPasteListener mOnPasteListener;
public EditTextTyped(Context context) { public EditTextTyped(Context context) {
super(context); super(context);
@ -35,6 +39,10 @@ public class EditTextTyped extends AppCompatEditText {
super(context, attributeSet); super(context, attributeSet);
} }
public void addOnPasteListener(OnPasteListener mOnPasteListener) {
this.mOnPasteListener = mOnPasteListener;
}
public void setMimeTypes(String[] types, public void setMimeTypes(String[] types,
InputConnectionCompat.OnCommitContentListener listener) { InputConnectionCompat.OnCommitContentListener listener) {
mimeTypes = types; mimeTypes = types;
@ -53,4 +61,26 @@ public class EditTextTyped extends AppCompatEditText {
return connection; return connection;
} }
} }
@Override
public boolean onTextContextMenuItem(int id) {
boolean consumed = super.onTextContextMenuItem(id);
switch (id) {
case android.R.id.paste:
onPaste();
}
return consumed;
}
/**
* Text was pasted into the EditText.
*/
public void onPaste() {
if (mOnPasteListener != null)
mOnPasteListener.onPaste();
}
public interface OnPasteListener {
void onPaste();
}
} }

View File

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not, * You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */ * see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util; package com.keylesspalace.tusky.view;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;

View File

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not, * You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */ * see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util; package com.keylesspalace.tusky.view;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray; import android.content.res.TypedArray;
@ -22,6 +22,7 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.util.Assert;
public class FlowLayout extends ViewGroup { public class FlowLayout extends ViewGroup {
private int paddingHorizontal; // internal padding between child views private int paddingHorizontal; // internal padding between child views

View File

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not, * You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */ * see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util; package com.keylesspalace.tusky.view;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapShader; import android.graphics.BitmapShader;

View File

@ -54,7 +54,7 @@
android:paddingLeft="16dp" android:paddingLeft="16dp"
android:paddingRight="16dp"> android:paddingRight="16dp">
<com.keylesspalace.tusky.util.EditTextTyped <com.keylesspalace.tusky.view.EditTextTyped
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:id="@+id/compose_edit_field" android:id="@+id/compose_edit_field"

View File

@ -95,7 +95,7 @@
</RelativeLayout> </RelativeLayout>
<com.keylesspalace.tusky.util.FlowLayout <com.keylesspalace.tusky.view.FlowLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:id="@+id/status_content_warning_bar" android:id="@+id/status_content_warning_bar"
@ -127,7 +127,7 @@
android:textAllCaps="true" android:textAllCaps="true"
android:background="?attr/content_warning_button" /> android:background="?attr/content_warning_button" />
</com.keylesspalace.tusky.util.FlowLayout> </com.keylesspalace.tusky.view.FlowLayout>
<TextView <TextView
android:id="@+id/status_content" android:id="@+id/status_content"