diff --git a/app/build.gradle b/app/build.gradle index df9f0e16..bb7ffbe1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-android' android { @@ -65,13 +66,14 @@ dependencies { compile 'com.evernote:android-job:1.2.0' implementation 'com.android.support.constraint:constraint-layout:1.0.2' //room - compile 'android.arch.persistence.room:runtime:1.0.0' - annotationProcessor 'android.arch.persistence.room:compiler:1.0.0' - testCompile 'junit:junit:4.12' + implementation "android.arch.persistence.room:runtime:1.0.0" + kapt 'android.arch.persistence.room:compiler:1.0.0' + + testCompile "junit:junit:4.12" androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) - compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" } repositories { mavenCentral() diff --git a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java index aa209c4b..b0882a6e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java @@ -16,6 +16,7 @@ package com.keylesspalace.tusky; import android.Manifest; +import android.annotation.SuppressLint; import android.app.ProgressDialog; import android.content.ContentResolver; import android.content.Context; @@ -26,10 +27,7 @@ import android.content.pm.PackageManager; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.graphics.drawable.Drawable; -import android.media.MediaMetadataRetriever; -import android.media.ThumbnailUtils; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; @@ -39,9 +37,9 @@ import android.os.Parcel; import android.os.Parcelable; import android.provider.MediaStore; import android.support.annotation.AttrRes; -import android.support.annotation.LayoutRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.Px; import android.support.annotation.StringRes; import android.support.design.widget.Snackbar; import android.support.v13.view.inputmethod.InputConnectionCompat; @@ -61,16 +59,11 @@ import android.text.TextUtils; import android.text.TextWatcher; import android.text.style.URLSpan; import android.util.Log; -import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup; import android.webkit.MimeTypeMap; -import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; -import android.widget.Filter; -import android.widget.Filterable; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; @@ -80,6 +73,7 @@ import android.widget.Toast; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import com.keylesspalace.tusky.adapter.MentionAutoCompleteAdapter; import com.keylesspalace.tusky.db.TootDao; import com.keylesspalace.tusky.db.TootEntity; import com.keylesspalace.tusky.entity.Account; @@ -98,9 +92,6 @@ import com.keylesspalace.tusky.util.StringUtils; import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.view.EditTextTyped; import com.keylesspalace.tusky.view.ProgressImageView; -import com.keylesspalace.tusky.view.RoundedTransformation; -import com.squareup.picasso.Picasso; -import com.squareup.picasso.Target; import java.io.File; import java.io.FileNotFoundException; @@ -121,7 +112,8 @@ import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; -public final class ComposeActivity extends BaseActivity implements ComposeOptionsFragment.Listener { +public final class ComposeActivity extends BaseActivity + implements ComposeOptionsFragment.Listener, MentionAutoCompleteAdapter.AccountSearchProvider { private static final String TAG = "ComposeActivity"; // logging tag private static final int STATUS_CHARACTER_LIMIT = 500; private static final int STATUS_MEDIA_SIZE_LIMIT = 8388608; // 8MiB @@ -129,7 +121,8 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption private static final int MEDIA_TAKE_PHOTO_RESULT = 2; private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1; private static final int COMPOSE_SUCCESS = -1; - private static final int THUMBNAIL_SIZE = 128; // pixels + @Px + private static final int THUMBNAIL_SIZE = 128; private static final String SAVED_TOOT_UID_EXTRA = "saved_toot_uid"; private static final String SAVED_TOOT_TEXT_EXTRA = "saved_toot_text"; @@ -140,8 +133,11 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption private static final String MENTIONED_USERNAMES_EXTRA = "netnioned_usernames"; private static final String REPLYING_STATUS_AUTHOR_USERNAME_EXTRA = "replying_author_nickname_extra"; private static final String REPLYING_STATUS_CONTENT_EXTRA = "replying_status_content"; + + private static final String REMEMBERED_VISIBILITY_PREF = "rememberedVisibilityNum"; private static TootDao tootDao = TuskyApplication.getDB().tootDao(); + private TextView replyTextView; private TextView replyContentTextView; private EditTextTyped textEditor; private LinearLayout mediaPreviewBar; @@ -160,27 +156,21 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption private ArrayList mediaQueued; private CountUpDownLatch waitForMediaLatch; private boolean showMarkSensitive; - private String statusVisibility; // The current values of the options that will be applied + private Status.Visibility statusVisibility; // The current values of the options that will be applied private boolean statusMarkSensitive; // to the status being composed. - private boolean statusHideText; // + private boolean statusHideText; private boolean statusAlreadyInFlight; // to prevent duplicate sends by mashing the send button private InputContentInfoCompat currentInputContentInfo; private int currentFlags; private Uri photoUploadUri; private int savedTootUid = 0; - /** - * 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. - */ - @SuppressWarnings("FieldCanBeLocal") - private Target target; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_compose); - TextView replyTextView = findViewById(R.id.reply_tv); + replyTextView = findViewById(R.id.reply_tv); replyContentTextView = findViewById(R.id.reply_content_tv); textEditor = findViewById(R.id.compose_edit_field); mediaPreviewBar = findViewById(R.id.compose_media_preview_bar); @@ -253,13 +243,16 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption * state. */ SharedPreferences preferences = getPrivatePreferences(); - String startingVisibility; + Status.Visibility startingVisibility; boolean startingHideText; String startingContentWarning = null; ArrayList savedMediaQueued = null; if (savedInstanceState != null) { showMarkSensitive = savedInstanceState.getBoolean("showMarkSensitive"); - startingVisibility = savedInstanceState.getString("statusVisibility"); + startingVisibility = Status.Visibility.byNum( + savedInstanceState.getInt("statusVisibility", + Status.Visibility.PUBLIC.getNum()) + ); statusMarkSensitive = savedInstanceState.getBoolean("statusMarkSensitive"); startingHideText = savedInstanceState.getBoolean("statusHideText"); // Keep these until everything needed to put them in the queue is finished initializing. @@ -274,7 +267,10 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption photoUploadUri = savedInstanceState.getParcelable("photoUploadUri"); } else { showMarkSensitive = false; - startingVisibility = preferences.getString("rememberedVisibility", "public"); + startingVisibility = Status.Visibility.byNum( + preferences.getInt(REMEMBERED_VISIBILITY_PREF, + Status.Visibility.UNKNOWN.getNum()) + ); statusMarkSensitive = false; startingHideText = false; photoUploadUri = null; @@ -287,37 +283,20 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption String[] mentionedUsernames = null; ArrayList loadedDraftMediaUris = null; inReplyToId = null; + Status.Visibility replyVisibility = Status.Visibility.UNKNOWN; if (intent != null) { inReplyToId = intent.getStringExtra(IN_REPLY_TO_ID_EXTRA); - String replyVisibility = intent.getStringExtra(REPLY_VISIBILITY_EXTRA); - - if (replyVisibility != null && startingVisibility != null) { - // Lowest possible visibility setting in response - if (startingVisibility.equals("direct") || replyVisibility.equals("direct")) { - startingVisibility = "direct"; - } else if (startingVisibility.equals("private") || replyVisibility.equals("private")) { - startingVisibility = "private"; - } else if (startingVisibility.equals("unlisted") || replyVisibility.equals("unlisted")) { - startingVisibility = "unlisted"; - } else { - startingVisibility = replyVisibility; - } - } + replyVisibility = Status.Visibility.byNum( + intent.getIntExtra(REPLY_VISIBILITY_EXTRA, Status.Visibility.UNKNOWN.getNum()) + ); mentionedUsernames = intent.getStringArrayExtra(MENTIONED_USERNAMES_EXTRA); - if (inReplyToId != null) { - startingHideText = !intent.getStringExtra(CONTENT_WARNING_EXTRA).equals(""); + String contentWarning = intent.getStringExtra(CONTENT_WARNING_EXTRA); + if (contentWarning != null) { + startingHideText = !contentWarning.isEmpty(); if (startingHideText) { - startingContentWarning = intent.getStringExtra(CONTENT_WARNING_EXTRA); - } - } else { - String contentWarning = intent.getStringExtra(CONTENT_WARNING_EXTRA); - if (contentWarning != null) { - startingHideText = !contentWarning.isEmpty(); - if (startingHideText) { - startingContentWarning = contentWarning; - } + startingContentWarning = contentWarning; } } @@ -361,14 +340,12 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption } } - /* If the currently logged in account is locked, its posts should default to private. This - * should override even the reply settings, so this must be done after those are set up. */ - if (preferences.getBoolean("loggedInAccountLocked", false)) { - startingVisibility = "private"; - } + Status.Visibility pickedVisibility = pickVisibility(startingVisibility, replyVisibility, + preferences.getBoolean("loggedInAccountLocked", false)); // After the starting state is finalised, the interface can be set to reflect this state. - setStatusVisibility(startingVisibility); + setStatusVisibility(pickedVisibility); + postProgress.setVisibility(View.INVISIBLE); updateHideMediaToggleColor(); updateVisibleCharactersLeft(); @@ -393,7 +370,8 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption } }); - textEditor.setAdapter(new MentionAutoCompleteAdapter(this, R.layout.item_autocomplete)); + textEditor.setAdapter( + new MentionAutoCompleteAdapter(this, R.layout.item_autocomplete, this)); textEditor.setTokenizer(new MentionTokenizer()); // Add any mentions to the text field when a reply is first composed. @@ -442,7 +420,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption } } else if (savedMediaQueued != null) { for (SavedQueuedMedia item : savedMediaQueued) { - Bitmap preview = getImageThumbnail(getContentResolver(), item.uri); + Bitmap preview = MediaUtils.getImageThumbnail(getContentResolver(), item.uri, THUMBNAIL_SIZE); addMediaToQueue(item.type, preview, item.uri, item.mediaSize, item.readyStage); } } else if (intent != null && savedInstanceState == null) { @@ -453,25 +431,27 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption if (type != null) { if (type.startsWith("image/")) { List uriList = new ArrayList<>(); - switch (intent.getAction()) { - case Intent.ACTION_SEND: { - Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); - if (uri != null) { - uriList.add(uri); + if (intent.getAction() != null) { + switch (intent.getAction()) { + case Intent.ACTION_SEND: { + Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); + if (uri != null) { + uriList.add(uri); + } + break; } - break; - } - case Intent.ACTION_SEND_MULTIPLE: { - ArrayList list = intent.getParcelableArrayListExtra( - Intent.EXTRA_STREAM); - if (list != null) { - for (Uri uri : list) { - if (uri != null) { - uriList.add(uri); + case Intent.ACTION_SEND_MULTIPLE: { + ArrayList list = intent.getParcelableArrayListExtra( + Intent.EXTRA_STREAM); + if (list != null) { + for (Uri uri : list) { + if (uri != null) { + uriList.add(uri); + } } } + break; } - break; } } for (Uri uri : uriList) { @@ -506,7 +486,6 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption } outState.putParcelableArrayList("savedMediaQueued", savedMediaQueued); outState.putBoolean("showMarkSensitive", showMarkSensitive); - outState.putString("statusVisibility", statusVisibility); outState.putBoolean("statusMarkSensitive", statusMarkSensitive); outState.putBoolean("statusHideText", statusHideText); if (currentInputContentInfo != null) { @@ -701,6 +680,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption return c; } + @SuppressLint("StaticFieldLeak") private boolean saveTheToot(String s, @Nullable String contentWarning) { if (TextUtils.isEmpty(s)) { return false; @@ -715,14 +695,11 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption }.getType()); } - final TootEntity toot = new TootEntity(); - toot.setText(s); - toot.setContentWarning(contentWarning); + String mediaUrlsSerialized = null; if (!ListUtils.isEmpty(mediaQueued)) { List savedList = saveMedia(existingUris); if (!ListUtils.isEmpty(savedList)) { - String json = new Gson().toJson(savedList); - toot.setUrls(json); + mediaUrlsSerialized = new Gson().toJson(savedList); if (!ListUtils.isEmpty(existingUris)) { deleteMedia(setDifference(existingUris, savedList)); } @@ -734,16 +711,15 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption * can be deleted. */ deleteMedia(existingUris); } + final TootEntity toot = new TootEntity(savedTootUid, s, mediaUrlsSerialized, contentWarning, + inReplyToId, + getIntent().getStringExtra(REPLYING_STATUS_CONTENT_EXTRA), + getIntent().getStringExtra(REPLYING_STATUS_AUTHOR_USERNAME_EXTRA), statusVisibility); new AsyncTask() { @Override protected Void doInBackground(Void... params) { - if (savedTootUid != 0) { - toot.setUid(savedTootUid); - tootDao.updateToot(toot); - } else { - tootDao.insert(toot); - } + tootDao.insertOrReplace(toot); return null; } }.execute(); @@ -759,10 +735,10 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption } } - private void setStatusVisibility(String visibility) { + private void setStatusVisibility(Status.Visibility visibility) { statusVisibility = visibility; switch (visibility) { - case "public": { + case PUBLIC: { floatingBtn.setText(R.string.action_send_public); floatingBtn.setCompoundDrawables(null, null, null, null); Drawable globe = AppCompatResources.getDrawable(this, R.drawable.ic_public_24dp); @@ -771,7 +747,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption } break; } - case "private": { + case PRIVATE: { addLockToSendButton(); Drawable lock = AppCompatResources.getDrawable(this, R.drawable.ic_lock_outline_24dp); @@ -780,7 +756,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption } break; } - case "direct": { + case DIRECT: { addLockToSendButton(); Drawable envelope = AppCompatResources.getDrawable(this, R.drawable.ic_email_24dp); if (envelope != null) { @@ -788,7 +764,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption } break; } - case "unlisted": + case UNLISTED: default: { floatingBtn.setText(R.string.action_send); floatingBtn.setCompoundDrawables(null, null, null, null); @@ -808,7 +784,8 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption fragment.show(getSupportFragmentManager(), null); } - public void onVisibilityChanged(String visibility) { + @Override + public void onVisibilityChanged(Status.Visibility visibility) { setStatusVisibility(visibility); } @@ -848,18 +825,17 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption @Override protected void onStop() { super.onStop(); - if (inReplyToId != null) { - /* Don't save the visibility setting for replies because they adopt the visibility of - * the status they reply to and that behaviour needs to be kept separate. */ - return; + // Don't save the visibility setting for replies because they adopt the visibility of + // the status they reply to and that behaviour needs to be kept separate. + if (inReplyToId == null) { + getPrivatePreferences().edit() + .putInt(REMEMBERED_VISIBILITY_PREF, statusVisibility.getNum()) + .apply(); } - getPrivatePreferences().edit() - .putString("rememberedVisibility", statusVisibility) - .apply(); } private void setEditTextMimeTypes() { - final String[] mimeTypes = new String[] {"image/*"}; + final String[] mimeTypes = new String[]{"image/*"}; textEditor.setMimeTypes(mimeTypes, new InputConnectionCompat.OnCommitContentListener() { @Override public boolean onCommitContent(InputContentInfoCompat inputContentInfo, @@ -932,7 +908,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption return true; } - private void sendStatus(String content, String visibility, boolean sensitive, + private void sendStatus(String content, Status.Visibility visibility, boolean sensitive, String spoilerText) { ArrayList mediaIds = new ArrayList<>(); @@ -946,25 +922,23 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption if (response.isSuccessful()) { onSendSuccess(); } else { - onSendFailure(); + onSendFailure(response); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - onSendFailure(); + onSendFailure(null); } }; - mastodonApi.createStatus(content, inReplyToId, spoilerText, visibility, sensitive, mediaIds) - .enqueue(callback); + mastodonApi.createStatus(content, inReplyToId, spoilerText, visibility.serverString(), + sensitive, mediaIds).enqueue(callback); } private void onSendSuccess() { // If the status was loaded from a draft, delete the draft and associated media files. if (savedTootUid != 0) { - TootEntity status = new TootEntity(); - status.setUid(savedTootUid); - tootDao.delete(status); + tootDao.delete(savedTootUid); for (QueuedMedia item : mediaQueued) { try { if (getContentResolver().delete(item.uri, null, null) == 0) { @@ -983,16 +957,32 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption finish(); } - private void onSendFailure() { - textEditor.setError(getString(R.string.error_generic)); + private void onSendFailure(@Nullable Response response) { setStateToNotReadying(); + + if (response != null && inReplyToId != null && response.code() == 404) { + new AlertDialog.Builder(this) + .setMessage(R.string.dialog_reply_not_found) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + inReplyToId = null; + replyContentTextView.setVisibility(View.GONE); + replyTextView.setVisibility(View.GONE); + } + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } else { + textEditor.setError(getString(R.string.error_generic)); + } } - private void readyStatus(final String visibility, final boolean sensitive) { + private void readyStatus(final Status.Visibility visibility, final boolean sensitive) { finishingUploadDialog = ProgressDialog.show( this, getString(R.string.dialog_title_finishing_media_upload), getString(R.string.dialog_message_uploading_media), true, true); - final AsyncTask waitForMediaTask = + @SuppressLint("StaticFieldLeak") final AsyncTask waitForMediaTask = new AsyncTask() { @Override protected Boolean doInBackground(Void... params) { @@ -1035,7 +1025,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption waitForMediaTask.execute(); } - private void onReadySuccess(String visibility, boolean sensitive) { + private void onReadySuccess(Status.Visibility visibility, boolean sensitive) { /* Validate the status meets the character limit. This has to be delayed until after all * uploads finish because their links are added when the upload succeeds and that affects * whether the limit is met or not. */ @@ -1056,7 +1046,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption } } - private void onReadyFailure(final String visibility, final boolean sensitive) { + private void onReadyFailure(final Status.Visibility visibility, final boolean sensitive) { doErrorDialog(R.string.error_media_upload_sending, R.string.action_retry, new View.OnClickListener() { @Override @@ -1439,44 +1429,6 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption } } - @Nullable - private static Bitmap getImageThumbnail(ContentResolver contentResolver, Uri uri) { - InputStream stream; - try { - stream = contentResolver.openInputStream(uri); - } catch (FileNotFoundException e) { - return null; - } - Bitmap source = BitmapFactory.decodeStream(stream); - if (source == null) { - IOUtils.closeQuietly(stream); - return null; - } - Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, THUMBNAIL_SIZE, THUMBNAIL_SIZE); - source.recycle(); - try { - if (stream != null) { - stream.close(); - } - } catch (IOException e) { - bitmap.recycle(); - return null; - } - return bitmap; - } - - @Nullable - private static Bitmap getVideoThumbnail(Context context, Uri uri) { - MediaMetadataRetriever retriever = new MediaMetadataRetriever(); - retriever.setDataSource(context, uri); - Bitmap source = retriever.getFrameAtTime(); - if (source == null) { - return null; - } - Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, THUMBNAIL_SIZE, THUMBNAIL_SIZE); - source.recycle(); - return bitmap; - } private void pickMedia(Uri uri, long mediaSize) { ContentResolver contentResolver = getContentResolver(); @@ -1498,7 +1450,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption displayTransientError(R.string.error_media_upload_image_or_video); return; } - Bitmap bitmap = getVideoThumbnail(this, uri); + Bitmap bitmap = MediaUtils.getVideoThumbnail(this, uri, THUMBNAIL_SIZE); if (bitmap != null) { addMediaToQueue(QueuedMedia.Type.VIDEO, bitmap, uri, mediaSize, null); } else { @@ -1507,7 +1459,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption break; } case "image": { - Bitmap bitmap = getImageThumbnail(contentResolver, uri); + Bitmap bitmap = MediaUtils.getImageThumbnail(contentResolver, uri, THUMBNAIL_SIZE); if (bitmap != null) { addMediaToQueue(QueuedMedia.Type.IMAGE, bitmap, uri, mediaSize, null); } else { @@ -1562,10 +1514,8 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption return super.onOptionsItemSelected(item); } - /** - * Does a synchronous search request for accounts fulfilling the given partial mention text. - */ - private ArrayList autocompleteMention(String mention) { + @Override + public List searchAccounts(String mention) { ArrayList resultList = new ArrayList<>(); try { List accountList = mastodonApi.searchAccounts(mention, false, 40) @@ -1580,7 +1530,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption return resultList; } - private static class QueuedMedia { + private static final class QueuedMedia { Type type; ProgressImageView preview; Uri uri; @@ -1657,98 +1607,42 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption } } - private class MentionAutoCompleteAdapter extends ArrayAdapter implements Filterable { - private ArrayList resultList; - @LayoutRes - private int layoutId; - - MentionAutoCompleteAdapter(Context context, @LayoutRes int resource) { - super(context, resource); - layoutId = resource; - resultList = new ArrayList<>(); + /** + * Function to decide which visibility should be used for posting a status + * + * @return {@code PRIVATE} if account is locked, {@code PUBLIC} if both start and reply + * visibilities are unknown or minimal known visibility of two of them. + */ + private static Status.Visibility pickVisibility(final Status.Visibility startVisibility, + final Status.Visibility replyVisibility, + boolean isAccountLocked) { + // If the currently logged in account is locked, its posts should default to private. + // This should override even the reply settings. + if (isAccountLocked) { + return Status.Visibility.PRIVATE; } - @Override - public int getCount() { - return resultList.size(); + if (startVisibility == Status.Visibility.UNKNOWN && + replyVisibility == Status.Visibility.UNKNOWN) { + return Status.Visibility.PUBLIC; } - @Override - public Account getItem(int index) { - return resultList.get(index); + if (replyVisibility == Status.Visibility.UNKNOWN) { + return startVisibility; } - @Override - @NonNull - public Filter getFilter() { - return new Filter() { - @Override - public CharSequence convertResultToString(Object resultValue) { - return ((Account) resultValue).username; - } - - // This method is invoked in a worker thread. - @Override - protected FilterResults performFiltering(CharSequence constraint) { - FilterResults filterResults = new FilterResults(); - if (constraint != null) { - ArrayList accounts = autocompleteMention(constraint.toString()); - filterResults.values = accounts; - filterResults.count = accounts.size(); - } - return filterResults; - } - - @SuppressWarnings("unchecked") - @Override - protected void publishResults(CharSequence constraint, FilterResults results) { - if (results != null && results.count > 0) { - resultList.clear(); - ArrayList newResults = (ArrayList) results.values; - resultList.addAll(newResults); - notifyDataSetChanged(); - } else { - notifyDataSetInvalidated(); - } - } - }; + if (startVisibility == Status.Visibility.UNKNOWN) { + return replyVisibility; } - @Override - @NonNull - public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { - View view = convertView; - - Context context = getContext(); - - if (convertView == null) { - LayoutInflater layoutInflater = - (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - view = layoutInflater.inflate(layoutId, null); - } - - Account account = getItem(position); - if (account != null) { - TextView username = view.findViewById(R.id.username); - TextView displayName = view.findViewById(R.id.display_name); - ImageView avatar = view.findViewById(R.id.avatar); - String format = getContext().getString(R.string.status_username_format); - String formattedUsername = String.format(format, account.username); - username.setText(formattedUsername); - displayName.setText(account.getDisplayName()); - if (!account.avatar.isEmpty()) { - Picasso.with(context) - .load(account.avatar) - .placeholder(R.drawable.avatar_default) - .transform(new RoundedTransformation(7, 0)) - .into(avatar); - } - } - - return view; + if (startVisibility.getNum() > replyVisibility.getNum()) { + return startVisibility; + } else { + return replyVisibility; } } + @SuppressWarnings("WeakerAccess") public static final class IntentBuilder { @Nullable private Integer savedTootUid; @@ -1761,11 +1655,11 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption @Nullable private String inReplyToId; @Nullable - private String replyVisibility; + private Status.Visibility replyVisibility; @Nullable private String contentWarning; @Nullable - private Account replyingStatusAuthor; + private String replyingStatusAuthor; @Nullable private String replyingStatusContent; @@ -1794,7 +1688,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption return this; } - public IntentBuilder replyVisibility(String replyVisibility) { + public IntentBuilder replyVisibility(Status.Visibility replyVisibility) { this.replyVisibility = replyVisibility; return this; } @@ -1804,8 +1698,8 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption return this; } - public IntentBuilder repyingStatusAuthor(Account author) { - this.replyingStatusAuthor = author; + public IntentBuilder repyingStatusAuthor(String username) { + this.replyingStatusAuthor = username; return this; } @@ -1834,7 +1728,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption intent.putExtra(IN_REPLY_TO_ID_EXTRA, inReplyToId); } if (replyVisibility != null) { - intent.putExtra(REPLY_VISIBILITY_EXTRA, replyVisibility); + intent.putExtra(REPLY_VISIBILITY_EXTRA, replyVisibility.getNum()); } if (contentWarning != null) { intent.putExtra(CONTENT_WARNING_EXTRA, contentWarning); @@ -1843,8 +1737,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption intent.putExtra(REPLYING_STATUS_CONTENT_EXTRA, replyingStatusContent); } if (replyingStatusAuthor != null) { - intent.putExtra(REPLYING_STATUS_AUTHOR_USERNAME_EXTRA, - replyingStatusAuthor.localUsername); + intent.putExtra(REPLYING_STATUS_AUTHOR_USERNAME_EXTRA, replyingStatusAuthor); } return intent; } diff --git a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java index 9aa567c9..cd1d6057 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java @@ -20,6 +20,7 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; +import android.support.annotation.Nullable; import android.support.v7.app.ActionBar; import android.support.v7.widget.DividerItemDecoration; import android.support.v7.widget.LinearLayoutManager; @@ -37,6 +38,7 @@ import com.keylesspalace.tusky.db.TootDao; import com.keylesspalace.tusky.db.TootEntity; import com.keylesspalace.tusky.util.ThemeUtils; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; @@ -50,6 +52,9 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter. private SavedTootAdapter adapter; private TextView noContent; + private List toots = new ArrayList<>(); + @Nullable private AsyncTask asyncTask; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -82,9 +87,13 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter. @Override protected void onResume() { super.onResume(); + fetchToots(); + } - // req - getAllToot(); + @Override + protected void onPause() { + super.onPause(); + if (asyncTask != null) asyncTask.cancel(true); } @Override @@ -98,24 +107,9 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter. return super.onOptionsItemSelected(item); } - private void getAllToot() { - new AsyncTask>() { - @Override - protected List doInBackground(Void... params) { - return tootDao.loadAll(); - } - - @Override - protected void onPostExecute(List tootEntities) { - super.onPostExecute(tootEntities); - // set ui - setNoContent(tootEntities.size()); - if (adapter != null) { - adapter.setItems(tootEntities); - adapter.notifyDataSetChanged(); - } - } - }.execute(); + private void fetchToots() { + asyncTask = new FetchPojosTask(this) + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } private void setNoContent(int size) { @@ -140,11 +134,12 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter. } } // update DB - tootDao.delete(item); + tootDao.delete(item.getUid()); + toots.remove(position); // update adapter if (adapter != null) { adapter.removeItem(position); - setNoContent(adapter.getItemCount()); + setNoContent(toots.size()); } } @@ -155,7 +150,41 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter. .savedTootText(item.getText()) .contentWarning(item.getContentWarning()) .savedJsonUrls(item.getUrls()) + .inReplyToId(item.getInReplyToId()) + .repyingStatusAuthor(item.getInReplyToUsername()) + .replyingStatusContent(item.getInReplyToText()) + .replyVisibility(item.getVisibility()) .build(this); startActivity(intent); } + + static final class FetchPojosTask extends AsyncTask> { + + private final WeakReference activityRef; + + FetchPojosTask(SavedTootActivity activity) { + this.activityRef = new WeakReference<>(activity); + } + + @Override + protected List doInBackground(Void... voids) { + return tootDao.loadAll(); + } + + @Override + protected void onPostExecute(List pojos) { + super.onPostExecute(pojos); + SavedTootActivity activity = activityRef.get(); + if (activity == null) return; + + activity.toots.addAll(pojos); + + // set ui + activity.setNoContent(pojos.size()); + List toots = new ArrayList<>(pojos.size()); + toots.addAll(pojos); + activity.adapter.setItems(toots); + activity.adapter.notifyDataSetChanged(); + } + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java index 3bca5563..efe92c88 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java @@ -56,6 +56,7 @@ public class TuskyApplication extends Application { db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "tuskyDB") .allowMainThreadQueries() .addMigrations(AppDatabase.MIGRATION_2_3) + .addMigrations(AppDatabase.MIGRATION_3_4) .build(); JobManager.create(this).addJobCreator(new NotificationPullJobCreator(this)); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/MentionAutoCompleteAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/MentionAutoCompleteAdapter.java new file mode 100644 index 00000000..4a4ba0c6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/MentionAutoCompleteAdapter.java @@ -0,0 +1,143 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.adapter; + +import android.content.Context; +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.ImageView; +import android.widget.TextView; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Account; +import com.keylesspalace.tusky.view.RoundedTransformation; +import com.squareup.picasso.Picasso; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by charlag on 12/11/17. + */ + +public class MentionAutoCompleteAdapter extends ArrayAdapter + implements Filterable { + private ArrayList resultList; + @LayoutRes + private int layoutId; + private final AccountSearchProvider accountSearchProvider; + + public MentionAutoCompleteAdapter(Context context, @LayoutRes int resource, + AccountSearchProvider accountSearchProvider) { + super(context, resource); + layoutId = resource; + resultList = new ArrayList<>(); + this.accountSearchProvider = accountSearchProvider; + } + + @Override + public int getCount() { + return resultList.size(); + } + + @Override + public Account getItem(int index) { + return resultList.get(index); + } + + @Override + @NonNull + public Filter getFilter() { + return new Filter() { + @Override + public CharSequence convertResultToString(Object resultValue) { + return ((Account) resultValue).username; + } + + // This method is invoked in a worker thread. + @Override + protected FilterResults performFiltering(CharSequence constraint) { + FilterResults filterResults = new FilterResults(); + if (constraint != null) { + List accounts = + accountSearchProvider.searchAccounts(constraint.toString()); + filterResults.values = accounts; + filterResults.count = accounts.size(); + } + return filterResults; + } + + @SuppressWarnings("unchecked") + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + if (results != null && results.count > 0) { + resultList.clear(); + ArrayList newResults = (ArrayList) results.values; + resultList.addAll(newResults); + notifyDataSetChanged(); + } else { + notifyDataSetInvalidated(); + } + } + }; + } + + @Override + @NonNull + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view = convertView; + + Context context = getContext(); + + if (convertView == null) { + LayoutInflater layoutInflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + //noinspection ConstantConditions + view = layoutInflater.inflate(layoutId, parent, false); + } + + Account account = getItem(position); + if (account != null) { + TextView username = view.findViewById(R.id.username); + TextView displayName = view.findViewById(R.id.display_name); + ImageView avatar = view.findViewById(R.id.avatar); + String format = getContext().getString(R.string.status_username_format); + String formattedUsername = String.format(format, account.username); + username.setText(formattedUsername); + displayName.setText(account.getDisplayName()); + if (!account.avatar.isEmpty()) { + Picasso.with(context) + .load(account.avatar) + .placeholder(R.drawable.avatar_default) + .transform(new RoundedTransformation(7, 0)) + .into(avatar); + } + } + + return view; + } + + public interface AccountSearchProvider { + List searchAccounts(String mention); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index 28f69da7..9d87b547 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -1,25 +1,39 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + package com.keylesspalace.tusky.db; import android.arch.persistence.db.SupportSQLiteDatabase; import android.arch.persistence.room.Database; import android.arch.persistence.room.RoomDatabase; import android.arch.persistence.room.migration.Migration; +import android.support.annotation.NonNull; /** * DB version & declare DAO */ -@Database(entities = {TootEntity.class}, version = 3, exportSchema = false) +@Database(entities = {TootEntity.class}, version = 4, exportSchema = false) public abstract class AppDatabase extends RoomDatabase { public abstract TootDao tootDao(); public static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override - public void migrate(SupportSQLiteDatabase database) { - //this migration is necessary because of a change in the room library + public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("CREATE TABLE TootEntity2 (uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, text TEXT, urls TEXT, contentWarning TEXT);"); - database.execSQL("INSERT INTO TootEntity2 SELECT * FROM TootEntity;"); database.execSQL("DROP TABLE TootEntity;"); database.execSQL("ALTER TABLE TootEntity2 RENAME TO TootEntity;"); @@ -27,4 +41,13 @@ public abstract class AppDatabase extends RoomDatabase { } }; + public static final Migration MIGRATION_3_4 = new Migration(3, 4) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE TootEntity ADD COLUMN inReplyToId TEXT"); + database.execSQL("ALTER TABLE TootEntity ADD COLUMN inReplyToText TEXT"); + database.execSQL("ALTER TABLE TootEntity ADD COLUMN inReplyToUsername TEXT"); + database.execSQL("ALTER TABLE TootEntity ADD COLUMN visibility INTEGER"); + } + }; } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java b/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java index 3c792bce..dc1b8f7d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java @@ -1,33 +1,43 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + package com.keylesspalace.tusky.db; import android.arch.persistence.room.Dao; -import android.arch.persistence.room.Delete; import android.arch.persistence.room.Insert; +import android.arch.persistence.room.OnConflictStrategy; import android.arch.persistence.room.Query; +import android.arch.persistence.room.Transaction; import android.arch.persistence.room.Update; import java.util.List; /** * Created by cto3543 on 28/06/2017. - * crud interface on this Toot DB + * + * DAO to fetch and update toots in the DB. */ @Dao public interface TootDao { - // c - @Insert - long insert(TootEntity users); + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insertOrReplace(TootEntity users); - // r - @Query("SELECT * FROM TootEntity") + @Query("SELECT * FROM TootEntity ORDER BY uid DESC") List loadAll(); - // u - @Update - void updateToot(TootEntity toot); - - // d - @Delete - int delete(TootEntity user); + @Query("DELETE FROM TootEntity WHERE uid = :uid") + int delete(int uid); } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TootEntity.java b/app/src/main/java/com/keylesspalace/tusky/db/TootEntity.java index fb00afa8..695a5b3c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TootEntity.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/TootEntity.java @@ -1,57 +1,119 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + package com.keylesspalace.tusky.db; import android.arch.persistence.room.ColumnInfo; import android.arch.persistence.room.Entity; import android.arch.persistence.room.PrimaryKey; +import android.arch.persistence.room.TypeConverter; +import android.arch.persistence.room.TypeConverters; +import android.support.annotation.Nullable; + +import com.keylesspalace.tusky.entity.Status; /** - * toot model + * Toot model. */ @Entity +@TypeConverters(TootEntity.Converters.class) public class TootEntity { @PrimaryKey(autoGenerate = true) - private int uid; + private final int uid; @ColumnInfo(name = "text") - private String text; + private final String text; @ColumnInfo(name = "urls") - private String urls; + private final String urls; @ColumnInfo(name = "contentWarning") - private String contentWarning; + private final String contentWarning; - // getter setter - public String getText() { - return text; - } + @ColumnInfo(name = "inReplyToId") + private final String inReplyToId; + + @Nullable + @ColumnInfo(name = "inReplyToText") + private final String inReplyToText; + + @Nullable + @ColumnInfo(name = "inReplyToUsername") + private final String inReplyToUsername; + + @ColumnInfo(name = "visibility") + private final Status.Visibility visibility; - public void setText(String text) { + public TootEntity(int uid, String text, String urls, String contentWarning, String inReplyToId, + @Nullable String inReplyToText, @Nullable String inReplyToUsername, + Status.Visibility visibility) { + this.uid = uid; this.text = text; + this.urls = urls; + this.contentWarning = contentWarning; + this.inReplyToId = inReplyToId; + this.inReplyToText = inReplyToText; + this.inReplyToUsername = inReplyToUsername; + this.visibility = visibility; } - public String getContentWarning() { - return contentWarning; + public String getText() { + return text; } - public void setContentWarning(String contentWarning) { - this.contentWarning = contentWarning; + public String getContentWarning() { + return contentWarning; } public int getUid() { return uid; } - public void setUid(int uid) { - this.uid = uid; - } - public String getUrls() { return urls; } - public void setUrls(String urls) { - this.urls = urls; + public String getInReplyToId() { + return inReplyToId; + } + + @Nullable + public String getInReplyToText() { + return inReplyToText; + } + + @Nullable + public String getInReplyToUsername() { + return inReplyToUsername; + } + + public Status.Visibility getVisibility() { + return visibility; + } + + public static final class Converters { + + @TypeConverter + public Status.Visibility visibilityFromInt(int number) { + return Status.Visibility.byNum(number); + } + + @TypeConverter + public int intToVisibility(Status.Visibility visibility) { + return visibility.getNum(); + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.java b/app/src/main/java/com/keylesspalace/tusky/entity/Status.java index 6bc81c59..53e1f203 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.java +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.java @@ -50,15 +50,45 @@ public class Status { } public enum Visibility { - UNKNOWN, + UNKNOWN(0), @SerializedName("public") - PUBLIC, + PUBLIC(1), @SerializedName("unlisted") - UNLISTED, + UNLISTED(2), @SerializedName("private") - PRIVATE, + PRIVATE(3), @SerializedName("direct") - DIRECT, + DIRECT(4); + + private final int num; + + Visibility(int num) { + this.num = num; + } + + public int getNum() { + return num; + } + + public static Visibility byNum(int num) { + switch (num) { + case 4: return DIRECT; + case 3: return PRIVATE; + case 2: return UNLISTED; + case 1: return PUBLIC; + case 0: default: return UNKNOWN; + } + } + + public String serverString() { + switch (this) { + case PUBLIC: return "public"; + case UNLISTED: return "unlisted"; + case PRIVATE: return "private"; + case DIRECT: return "direct"; + case UNKNOWN: default: return "unknown"; + } + } } public String id; @@ -162,7 +192,7 @@ public class Status { } } - public static class Mention { + public static final class Mention { public String id; public String url; @@ -179,6 +209,7 @@ public class Status { public String website; } + @SuppressWarnings("unused") public static class Emoji { private String shortcode; private String url; diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ComposeOptionsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ComposeOptionsFragment.java index 2c2f28b1..6fbbd47d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ComposeOptionsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ComposeOptionsFragment.java @@ -34,11 +34,12 @@ import android.widget.RadioButton; import android.widget.RadioGroup; import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.util.ThemeUtils; public class ComposeOptionsFragment extends BottomSheetDialogFragment { public interface Listener { - void onVisibilityChanged(String visibility); + void onVisibilityChanged(Status.Visibility visibility); void onContentWarningChanged(boolean hideText); } @@ -46,10 +47,10 @@ public class ComposeOptionsFragment extends BottomSheetDialogFragment { private CheckBox hideText; private Listener listener; - public static ComposeOptionsFragment newInstance(String visibility, boolean hideText) { + public static ComposeOptionsFragment newInstance(Status.Visibility visibility, boolean hideText) { Bundle arguments = new Bundle(); ComposeOptionsFragment fragment = new ComposeOptionsFragment(); - arguments.putString("visibility", visibility); + arguments.putInt("visibilityNum", visibility.getNum()); arguments.putBoolean("hideText", hideText); fragment.setArguments(arguments); return fragment; @@ -68,18 +69,18 @@ public class ComposeOptionsFragment extends BottomSheetDialogFragment { View rootView = inflater.inflate(R.layout.fragment_compose_options, container, false); Bundle arguments = getArguments(); - String statusVisibility = arguments.getString("visibility"); + Status.Visibility visibility = Status.Visibility.byNum( + arguments.getInt("visibilityNum", 0) + ); boolean statusHideText = arguments.getBoolean("hideText"); radio = rootView.findViewById(R.id.radio_visibility); int radioCheckedId = R.id.radio_public; - if (statusVisibility != null) { - switch (statusVisibility) { - case "public": radioCheckedId = R.id.radio_public; break; - case "private": radioCheckedId = R.id.radio_private; break; - case "unlisted": radioCheckedId = R.id.radio_unlisted; break; - case "direct": radioCheckedId = R.id.radio_direct; break; - } + switch (visibility) { + case PUBLIC: radioCheckedId = R.id.radio_public; break; + case PRIVATE: radioCheckedId = R.id.radio_private; break; + case UNLISTED: radioCheckedId = R.id.radio_unlisted; break; + case DIRECT: radioCheckedId = R.id.radio_direct; break; } radio.check(radioCheckedId); @@ -104,23 +105,23 @@ public class ComposeOptionsFragment extends BottomSheetDialogFragment { radio.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int checkedId) { - String visibility; + Status.Visibility visibility; switch (checkedId) { default: case R.id.radio_public: { - visibility = "public"; + visibility = Status.Visibility.PUBLIC; break; } case R.id.radio_unlisted: { - visibility = "unlisted"; + visibility = Status.Visibility.UNLISTED; break; } case R.id.radio_private: { - visibility = "private"; + visibility = Status.Visibility.PRIVATE; break; } case R.id.radio_direct: { - visibility = "direct"; + visibility = Status.Visibility.DIRECT; break; } } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index ef77542c..eb20619b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -93,7 +93,7 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov protected void reply(Status status) { String inReplyToId = status.getActionableId(); Status actionableStatus = status.getActionableStatus(); - String replyVisibility = actionableStatus.getVisibility().toString().toLowerCase(); + Status.Visibility replyVisibility = actionableStatus.getVisibility(); String contentWarning = actionableStatus.spoilerText; Status.Mention[] mentions = actionableStatus.mentions; List mentionedUsernames = new ArrayList<>(); @@ -107,7 +107,7 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov .replyVisibility(replyVisibility) .contentWarning(contentWarning) .mentionedUsernames(mentionedUsernames) - .repyingStatusAuthor(actionableStatus.account) + .repyingStatusAuthor(actionableStatus.account.localUsername) .replyingStatusContent(actionableStatus.content.toString()) .build(getContext()); startActivityForResult(intent, COMPOSE_RESULT); diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java index aedf6dd0..fc527cbc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -320,9 +320,9 @@ public class ViewThreadFragment extends SFragment implements call.enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful()) { + StatusContext context = response.body(); + if (response.isSuccessful() && context != null) { swipeRefreshLayout.setRefreshing(false); - StatusContext context = response.body(); setContext(context.ancestors, context.descendants); } else { onThreadRequestFailure(id); diff --git a/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.java index 47e38904..5166dca3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.java @@ -19,11 +19,15 @@ import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.drawable.Drawable; +import android.media.MediaMetadataRetriever; +import android.media.ThumbnailUtils; import android.net.Uri; import android.os.Environment; import android.provider.OpenableColumns; import android.support.annotation.Nullable; +import android.support.annotation.Px; import android.support.v4.content.FileProvider; import com.squareup.picasso.Picasso; @@ -31,6 +35,7 @@ import com.squareup.picasso.Target; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -83,60 +88,43 @@ public class MediaUtils { return mediaSize; } - /** Download an image with picasso asynchronously and call the given listener when completed. */ - 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) { + @Nullable + public static Bitmap getImageThumbnail(ContentResolver contentResolver, Uri uri, + @Px int thumbnailSize) { + InputStream stream; + try { + stream = contentResolver.openInputStream(uri); + } catch (FileNotFoundException e) { + return null; + } + Bitmap source = BitmapFactory.decodeStream(stream); + if (source == null) { + IOUtils.closeQuietly(stream); + return null; + } + Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, thumbnailSize, thumbnailSize); + source.recycle(); + try { + if (stream != null) { + stream.close(); } - }; + } catch (IOException e) { + bitmap.recycle(); + return null; + } + return bitmap; } - public interface MediaListener { - void onCallback(Uri headerInfo); + @Nullable + public static Bitmap getVideoThumbnail(Context context, Uri uri, @Px int thumbnailSize) { + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(context, uri); + Bitmap source = retriever.getFrameAtTime(); + if (source == null) { + return null; + } + Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, thumbnailSize, thumbnailSize); + source.recycle(); + return bitmap; } } diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 6bc6733c..e93f3278 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -138,6 +138,7 @@ Скачать Статус запроса на подписку: ожидается ответ Отписаться от этого аккаунта? + Не удалось опубликовать статус. Статус, на который вы отвечаете, может быть недоступен. Убрать информацию об ответе? Публичный: Показать в публичных лентах Скрытый: Не показывать в лентах diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0a721be6..2f8983ed 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -146,6 +146,7 @@ Download Follow request pending: awaiting their response Unfollow this account? + Couldn\'t post this status. The status you\'re replying to might not be available. Remove reply info? Public: Post to public timelines Unlisted: Do not show in public timelines