From 1b4afe7ba927c2178c0ccaf65c8392fc0661b2ca Mon Sep 17 00:00:00 2001 From: Grishka Date: Wed, 5 Jun 2024 04:28:50 +0300 Subject: [PATCH 1/8] Fix #845 --- .../joinmastodon/android/fragments/ComposeFragment.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java index 519d3cc7..028d2b1b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java @@ -720,8 +720,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr if(!pollViewController.isEmpty()){ req.poll=pollViewController.getPollForRequest(); } - if(hasSpoiler && spoilerEdit.length()>0){ - req.spoilerText=spoilerEdit.getText().toString(); + if(hasSpoiler){ + if(spoilerEdit.length()>0) + req.spoilerText=spoilerEdit.getText().toString(); + else + req.sensitive=true; } if(postLang!=null){ req.language=postLang.locale.toLanguageTag(); From bf44f7ef13e51c7f93ef7bf6b20a37bd6c2a4fc9 Mon Sep 17 00:00:00 2001 From: Grishka Date: Sat, 8 Jun 2024 14:49:39 +0300 Subject: [PATCH 2/8] VQA fixes part 1 --- .../onboarding/InstanceCatalogFragment.java | 4 +- .../InstanceCatalogSignupFragment.java | 109 ++++++------------ .../InstanceChooserLoginFragment.java | 18 ++- .../fragments/onboarding/SignupFragment.java | 83 +++++++++---- .../android/ui/utils/UiUtils.java | 16 +++ .../ui/views/FloatingHintEditTextLayout.java | 6 + .../color/m3_on_surface_variant_alpha40.xml | 4 + .../src/main/res/layout/alert_invite_link.xml | 5 +- .../res/layout/fragment_onboarding_signup.xml | 33 +++--- .../res/layout/header_onboarding_login.xml | 5 +- mastodon/src/main/res/values/strings.xml | 9 +- 11 files changed, 168 insertions(+), 124 deletions(-) create mode 100644 mastodon/src/main/res/color/m3_on_surface_variant_alpha40.xml diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java index 78365f6f..da6df80f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java @@ -73,8 +73,6 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment onError){ - if(TextUtils.isEmpty(_domain)) + if(TextUtils.isEmpty(_domain) || _domain.indexOf('.')==-1) return; String domain=normalizeInstanceDomain(_domain); Instance cachedInstance=instancesCache.get(domain); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogSignupFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogSignupFragment.java index 56afdd3c..37cb5b84 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogSignupFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogSignupFragment.java @@ -5,8 +5,9 @@ import android.app.AlertDialog; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import android.content.DialogInterface; import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; import android.net.Uri; import android.os.Bundle; import android.text.Editable; @@ -26,20 +27,17 @@ import android.widget.PopupMenu; import android.widget.RadioButton; import android.widget.RelativeLayout; import android.widget.TextView; -import android.widget.Toast; import org.joinmastodon.android.R; -import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.MastodonErrorResponse; import org.joinmastodon.android.api.requests.accounts.CheckInviteLink; -import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories; import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances; import org.joinmastodon.android.model.Instance; -import org.joinmastodon.android.model.catalog.CatalogCategory; import org.joinmastodon.android.model.catalog.CatalogInstance; import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.text.HtmlParser; +import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter; import org.joinmastodon.android.ui.utils.SimpleTextWatcher; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.views.FilterChipView; @@ -49,11 +47,9 @@ import org.parceler.Parcels; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Objects; -import java.util.Random; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -71,9 +67,6 @@ import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; public class InstanceCatalogSignupFragment extends InstanceCatalogFragment implements OnBackPressedListener{ - private MastodonAPIRequest getCategoriesRequest; - private String currentCategory="all"; - private List categories=new ArrayList<>(); private View topBar; private List languages=Collections.emptyList(); @@ -149,58 +142,16 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple } }) .execNoAuth(""); - getCategoriesRequest=new GetCatalogCategories(null) - .setCallback(new Callback<>(){ - @Override - public void onSuccess(List result){ - getCategoriesRequest=null; - CatalogCategory all=new CatalogCategory(); - all.category="all"; - categories.add(all); - result.stream().sorted(Comparator.comparingInt((CatalogCategory cc)->cc.serversCount).reversed()).forEach(categories::add); - updateCategories(); - } - - @Override - public void onError(ErrorResponse error){ - getCategoriesRequest=null; - error.showToast(getActivity()); - CatalogCategory all=new CatalogCategory(); - all.category="all"; - categories.add(all); - updateCategories(); - } - }) - .execNoAuth(""); - } - - private void updateCategories(){ -// categoriesList.removeAllTabs(); -// for(CatalogCategory cat:categories){ -// int titleRes=getTitleForCategory(cat.category); -// TabLayout.Tab tab=categoriesList.newTab().setText(titleRes!=0 ? getString(titleRes) : cat.category).setCustomView(R.layout.item_instance_category); -// ImageView emoji=tab.getCustomView().findViewById(R.id.emoji); -// emoji.setImageResource(getEmojiForCategory(cat.category)); -// categoriesList.addTab(tab); -// } } @Override public void onDestroy(){ super.onDestroy(); - if(getCategoriesRequest!=null) - getCategoriesRequest.cancel(); } @Override protected RecyclerView.Adapter getAdapter(){ - View headerView=new View(getActivity()); - headerView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)); - - mergeAdapter=new MergeRecyclerAdapter(); - mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView)); - mergeAdapter.addAdapter(adapter=new InstancesAdapter()); - return mergeAdapter; + return adapter=new InstancesAdapter(); } @SuppressLint("ClickableViewAccessibility") @@ -222,7 +173,16 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple setStatusBarColor(0); topBar=view.findViewById(R.id.top_bar); - list.addOnScrollListener(new ElevationOnScrollListener(null, topBar, buttonBar)); + list.addOnScrollListener(new ElevationOnScrollListener(null, topBar)); + if(buttonBar.getBackground() instanceof LayerDrawable ld){ + ld=(LayerDrawable) ld.mutate(); + buttonBar.setBackground(ld); + Drawable overlay=ld.findDrawableByLayerId(R.id.color_overlay); + if(overlay!=null){ + overlay.setAlpha(20); + } + } + buttonBar.setElevation(V.dp(3)); searchEdit=view.findViewById(R.id.search_edit); searchEdit.setOnEditorActionListener(this::onSearchEnterPressed); @@ -572,6 +532,9 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple filteredData.add(instance); } } + setEmptyText(getString(R.string.no_servers_found, currentSearchQuery)); + }else{ + setEmptyText(""); } }else{ for(CatalogInstance instance:data){ @@ -591,27 +554,29 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple } } } - DiffUtil.calculateDiff(new DiffUtil.Callback(){ - @Override - public int getOldListSize(){ - return prevData.size(); - } + UiUtils.updateRecyclerViewKeepingAbsoluteScrollPosition(list, ()->{ + DiffUtil.calculateDiff(new DiffUtil.Callback(){ + @Override + public int getOldListSize(){ + return prevData.size(); + } - @Override - public int getNewListSize(){ - return filteredData.size(); - } + @Override + public int getNewListSize(){ + return filteredData.size(); + } - @Override - public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){ - return prevData.get(oldItemPosition)==filteredData.get(newItemPosition); - } + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){ + return prevData.get(oldItemPosition)==filteredData.get(newItemPosition); + } - @Override - public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){ - return prevData.get(oldItemPosition)==filteredData.get(newItemPosition); - } - }).dispatchUpdatesTo(adapter); + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){ + return prevData.get(oldItemPosition)==filteredData.get(newItemPosition); + } + }).dispatchUpdatesTo(adapter); + }); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceChooserLoginFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceChooserLoginFragment.java index e4ce6401..5f810fd3 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceChooserLoginFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceChooserLoginFragment.java @@ -1,12 +1,11 @@ package org.joinmastodon.android.fragments.onboarding; -import android.graphics.Canvas; import android.graphics.Outline; import android.graphics.Rect; -import android.graphics.RectF; import android.os.Build; import android.os.Bundle; import android.text.Editable; +import android.text.TextUtils; import android.text.TextWatcher; import android.view.View; import android.view.ViewGroup; @@ -65,7 +64,7 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{ protected void updateFilteredList(){ ArrayList prevData=new ArrayList<>(filteredData); filteredData.clear(); - if(currentSearchQuery.length()>0){ + if(!TextUtils.isEmpty(currentSearchQuery)){ boolean foundExactMatch=false; for(CatalogInstance inst:data){ if(inst.normalizedDomain.contains(currentSearchQuery)){ @@ -74,9 +73,16 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{ foundExactMatch=true; } } - if(!foundExactMatch) + if(!foundExactMatch && currentSearchQuery.indexOf('.')!=-1) filteredData.add(0, fakeInstance); } + if(filteredData.isEmpty()){ + for(CatalogInstance inst:data){ + if(inst.normalizedDomain.equals("mastodon.social") || inst.normalizedDomain.equals("mastodon.online")){ + filteredData.add(inst); + } + } + } UiUtils.updateList(prevData, filteredData, list, adapter, Objects::equals); for(int i=0;i result){ data.clear(); data.addAll(sortInstances(result)); + updateFilteredList(); } @Override @@ -112,6 +119,9 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{ Toolbar toolbar=getToolbar(); toolbar.setElevation(0); toolbar.setBackground(null); + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){ + toolbar.setContentInsetStartWithNavigation(V.dp(80)); + } } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/SignupFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/SignupFragment.java index 26d30ab6..ea008cbf 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/SignupFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/SignupFragment.java @@ -50,6 +50,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; import androidx.annotation.Nullable; @@ -62,6 +63,7 @@ import me.grishka.appkit.views.FragmentRootLinearLayout; public class SignupFragment extends ToolbarFragment{ private static final String TAG="SignupFragment"; + private final Pattern emailRegex=Pattern.compile("^[^@]+@[^@]+\\.[^@]{2,}$"); private Instance instance; @@ -97,6 +99,7 @@ public class SignupFragment extends ToolbarFragment{ View view=inflater.inflate(R.layout.fragment_onboarding_signup, container, false); TextView domain=view.findViewById(R.id.domain); + TextView atSign=view.findViewById(R.id.at_sign); displayName=view.findViewById(R.id.display_name); username=view.findViewById(R.id.username); email=view.findViewById(R.id.email); @@ -118,7 +121,7 @@ public class SignupFragment extends ToolbarFragment{ @Override public boolean onPreDraw(){ username.getViewTreeObserver().removeOnPreDrawListener(this); - username.setPadding(username.getPaddingLeft(), username.getPaddingTop(), domain.getWidth(), username.getPaddingBottom()); + username.setPadding(atSign.getWidth(), username.getPaddingTop(), domain.getWidth(), username.getPaddingBottom()); return true; } }); @@ -145,6 +148,10 @@ public class SignupFragment extends ToolbarFragment{ reasonExplain.setVisibility(View.GONE); } + password.setOnFocusChangeListener(this::onPasswordFieldFocusChange); + passwordConfirm.setOnFocusChangeListener(this::onPasswordFieldFocusChange); + email.setOnFocusChangeListener(this::onEmailFieldFocusChange); + return view; } @@ -281,34 +288,44 @@ public class SignupFragment extends ToolbarFragment{ .exec(instance.uri, apiToken); } + private CharSequence makeLinkInErrorMessage(String source, LinkSpan.OnLinkClickListener onClick){ + SpannableStringBuilder ssb=new SpannableStringBuilder(); + Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){ + private int spanStart; + @Override + public void head(Node node, int depth){ + if(node instanceof TextNode tn){ + ssb.append(tn.text()); + }else if(node instanceof Element){ + spanStart=ssb.length(); + } + } + + @Override + public void tail(Node node, int depth){ + if(node instanceof Element){ + ssb.setSpan(new LinkSpan("", onClick, LinkSpan.Type.CUSTOM, null, null, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + }); + return ssb; + } + private CharSequence getErrorDescription(MastodonDetailedErrorResponse.FieldError error, String fieldName){ return switch(fieldName){ case "email" -> switch(error.error){ case "ERR_BLOCKED" -> { String emailAddr=email.getText().toString(); String s=getResources().getString(R.string.signup_email_domain_blocked, TextUtils.htmlEncode(instance.uri), TextUtils.htmlEncode(emailAddr.substring(emailAddr.lastIndexOf('@')+1))); - SpannableStringBuilder ssb=new SpannableStringBuilder(); - Jsoup.parseBodyFragment(s).body().traverse(new NodeVisitor(){ - private int spanStart; - @Override - public void head(Node node, int depth){ - if(node instanceof TextNode tn){ - ssb.append(tn.text()); - }else if(node instanceof Element){ - spanStart=ssb.length(); - } - } - - @Override - public void tail(Node node, int depth){ - if(node instanceof Element){ - ssb.setSpan(new LinkSpan("", SignupFragment.this::onGoBackLinkClick, LinkSpan.Type.CUSTOM, null, null, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - }); - yield ssb; + yield makeLinkInErrorMessage(s, this::onGoBackLinkClick); } + case "ERR_INVALID" -> getString(R.string.signup_email_invalid); + case "ERR_TAKEN" -> makeLinkInErrorMessage(getString(R.string.signup_email_taken), this::onForgotPasswordLinkClick); + default -> error.description; + }; + case "username" -> switch(error.error){ + case "ERR_TAKEN" -> makeLinkInErrorMessage(getString(R.string.signup_username_taken), this::onGoBackLinkClick); default -> error.description; }; default -> error.description; @@ -345,7 +362,9 @@ public class SignupFragment extends ToolbarFragment{ } private void updateButtonState(){ - btn.setEnabled(username.length()>0 && email.length()>0 && email.getText().toString().contains("@") && password.length()>=8 && passwordConfirm.length()>=8 && (!instance.approvalRequired || reason.length()>0)); + btn.setEnabled(username.length()>0 && email.length()>0 && emailRegex.matcher(email.getText()).find() + && password.length()>=8 && passwordConfirm.length()>=8 && password.getText().toString().equals(passwordConfirm.getText().toString()) + && (!instance.approvalRequired || reason.length()>0)); } private void createAppAndGetToken(){ @@ -406,6 +425,24 @@ public class SignupFragment extends ToolbarFragment{ Nav.finish(this); } + private void onForgotPasswordLinkClick(LinkSpan span){ + UiUtils.launchWebBrowser(getActivity(), "https://"+instance.uri+"/auth/password/new"); + } + + private void onPasswordFieldFocusChange(View v, boolean hasFocus){ + if(hasFocus || password.length()==0 || passwordConfirm.length()==0) + return; + if(!password.getText().toString().equals(passwordConfirm.getText().toString())){ + passwordConfirmWrap.setErrorState(getString(R.string.signup_passwords_dont_match)); + } + } + + private void onEmailFieldFocusChange(View v, boolean hasFocus){ + if(!hasFocus && email.length()>0 && !emailRegex.matcher(email.getText()).find()){ + emailWrap.setErrorState(getString(R.string.signup_email_invalid)); + } + } + private class ErrorClearingListener implements TextWatcher{ public final EditText editText; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index a9c4cb57..ce7e6f44 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -1040,4 +1040,20 @@ public class UiUtils{ button.setTextColor(origColor); } } + + public static void updateRecyclerViewKeepingAbsoluteScrollPosition(RecyclerView rv, Runnable onUpdate){ + int topItem=-1; + int topItemOffset=0; + if(rv.getChildCount()>0){ + View item=rv.getChildAt(0); + topItem=rv.getChildAdapterPosition(item); + topItemOffset=item.getTop(); + } + onUpdate.run(); + int newCount=rv.getAdapter().getItemCount(); + if(newCount>=topItem){ + rv.scrollToPosition(topItem); + rv.scrollBy(0, -topItemOffset); + } + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java index cec3f1af..8d230ce1 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java @@ -158,6 +158,9 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie }else{ transY=edit.getHeight()/2f-edit.getLineHeight()/2f+(edit.getTop()-label.getTop())-(label.getHeight()/2f-label.getLineHeight()/2f); } + int labelX=label.getLeft(); + int editX=edit.getLeft()+edit.getPaddingLeft(); + float xOffset=editX-labelX; AnimatorSet anim=new AnimatorSet(); if(hintVisible){ @@ -166,6 +169,7 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie ObjectAnimator.ofFloat(label, SCALE_X, scale), ObjectAnimator.ofFloat(label, SCALE_Y, scale), ObjectAnimator.ofFloat(label, TRANSLATION_Y, transY), + ObjectAnimator.ofFloat(label, TRANSLATION_X, xOffset), ObjectAnimator.ofFloat(FloatingHintEditTextLayout.this, "animProgress", 0f) ); edit.setHintTextColor(0); @@ -173,11 +177,13 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie label.setScaleX(scale); label.setScaleY(scale); label.setTranslationY(transY); + label.setTranslationX(xOffset); anim.playTogether( ObjectAnimator.ofFloat(edit, TRANSLATION_Y, offsetY), ObjectAnimator.ofFloat(label, SCALE_X, 1f), ObjectAnimator.ofFloat(label, SCALE_Y, 1f), ObjectAnimator.ofFloat(label, TRANSLATION_Y, 0f), + ObjectAnimator.ofFloat(label, TRANSLATION_X, 0f), ObjectAnimator.ofFloat(FloatingHintEditTextLayout.this, "animProgress", 1f) ); } diff --git a/mastodon/src/main/res/color/m3_on_surface_variant_alpha40.xml b/mastodon/src/main/res/color/m3_on_surface_variant_alpha40.xml new file mode 100644 index 00000000..f95ac660 --- /dev/null +++ b/mastodon/src/main/res/color/m3_on_surface_variant_alpha40.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/alert_invite_link.xml b/mastodon/src/main/res/layout/alert_invite_link.xml index a188985b..8b1f9d43 100644 --- a/mastodon/src/main/res/layout/alert_invite_link.xml +++ b/mastodon/src/main/res/layout/alert_invite_link.xml @@ -39,14 +39,15 @@ android:layout_height="56dp" android:background="@drawable/bg_m3_filled_text_field" android:paddingHorizontal="16dp" - android:textColorHint="?colorM3OnSurfaceVariant" + android:textColorHint="@color/m3_on_surface_variant_alpha40" android:textColor="?colorM3OnSurface" android:gravity="start|bottom" android:paddingBottom="8dp" android:singleLine="true" android:inputType="textUri" android:textAppearance="@style/m3_body_large" - android:hint="example.social/invite/AbC123"/> + android:hint="example.social/invite/AbC123" + tools:ignore="HardcodedText" /> + android:foreground="@drawable/bg_m3_outlined_text_field" + tools:ignore="RtlHardcoded"> + + - - - @@ -38,7 +38,7 @@ android:textColorHint="?colorM3OnSurfaceVariant" android:inputType="textUri" android:importantForAutofill="no" - android:paddingStart="48dp" + android:paddingStart="64dp" android:layout_marginEnd="52dp" android:drawablePadding="16dp" android:textAppearance="@style/m3_body_large" @@ -47,6 +47,7 @@ Email Password Confirm password - Include capital letters, special characters, and numbers to increase your password strength. General Check your inbox @@ -321,9 +320,9 @@ Log in with the server where you created your account. Server URL Any language - Instant sign-up + Instant sign up Manual review - Any sign-up speed + Any sign up speed Europe North America South America @@ -759,4 +758,8 @@ The request timed out. Check your connection and try again? Something went wrong talking with your server. It’s probably not your fault. Try again? It could’ve been deleted, or maybe it never existed at all. + No servers found for “%s” + This username is taken. Try a different one or <a>pick a different server</a>. + That doesn’t look like a valid email address. + Email address is already in use. Did you <a>forget your password</a>? \ No newline at end of file From 1352f884cb24414381c99c3e8ccd7c58ec85253c Mon Sep 17 00:00:00 2001 From: "Evil.2000" Date: Sat, 8 Jun 2024 15:27:04 +0200 Subject: [PATCH 3/8] Added network security config --- mastodon/src/main/AndroidManifest.xml | 1 + mastodon/src/main/res/xml/network_security_config.xml | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 mastodon/src/main/res/xml/network_security_config.xml diff --git a/mastodon/src/main/AndroidManifest.xml b/mastodon/src/main/AndroidManifest.xml index 1cc532fd..24fe33dd 100644 --- a/mastodon/src/main/AndroidManifest.xml +++ b/mastodon/src/main/AndroidManifest.xml @@ -31,6 +31,7 @@ android:allowBackup="true" android:label="@string/app_name" android:supportsRtl="true" + android:networkSecurityConfig="@xml/network_security_config" android:icon="@mipmap/ic_launcher" android:theme="@style/Theme.Mastodon.AutoLightDark" android:largeHeap="true"> diff --git a/mastodon/src/main/res/xml/network_security_config.xml b/mastodon/src/main/res/xml/network_security_config.xml new file mode 100644 index 00000000..cfac8d81 --- /dev/null +++ b/mastodon/src/main/res/xml/network_security_config.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file From c3e48d20f39c8e74e80b391c0fb131009b6ee012 Mon Sep 17 00:00:00 2001 From: Grishka Date: Sat, 8 Jun 2024 18:58:04 +0300 Subject: [PATCH 4/8] VQA fixes part 2 --- mastodon/build.gradle | 2 +- .../org/joinmastodon/android/MainActivity.java | 11 +++++++++++ .../requests/catalog/GetCatalogInstances.java | 6 +++++- .../api/session/AccountSessionManager.java | 16 ++++++++++++---- .../fragments/BaseStatusListFragment.java | 15 +++++++++++++++ .../fragments/HashtagTimelineFragment.java | 4 ++++ .../android/fragments/ProfileFragment.java | 2 +- .../InstanceCatalogSignupFragment.java | 5 +---- .../onboarding/InstanceChooserLoginFragment.java | 2 +- .../android/model/catalog/CatalogInstance.java | 7 ++++++- .../joinmastodon/android/ui/utils/UiUtils.java | 13 +++++++++++++ .../main/res/drawable/ic_explore_foreground.xml | 7 +++++++ .../mipmap-anydpi-v26/ic_shortcut_explore.xml | 10 ++++++++++ 13 files changed, 87 insertions(+), 13 deletions(-) create mode 100644 mastodon/src/main/res/drawable/ic_explore_foreground.xml create mode 100644 mastodon/src/main/res/mipmap-anydpi-v26/ic_shortcut_explore.xml diff --git a/mastodon/build.gradle b/mastodon/build.gradle index 18684e10..f119b73a 100644 --- a/mastodon/build.gradle +++ b/mastodon/build.gradle @@ -13,7 +13,7 @@ android { applicationId "org.joinmastodon.android" minSdk 23 targetSdk 33 - versionCode 97 + versionCode 99 versionName "2.5.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java b/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java index 02cc15b4..0fc11997 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java +++ b/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java @@ -61,6 +61,7 @@ public class MainActivity extends FragmentStackActivity{ @Override protected void onNewIntent(Intent intent){ super.onNewIntent(intent); + setIntent(intent); if(intent.getBooleanExtra("fromNotification", false)){ String accountID=intent.getStringExtra("accountID"); AccountSession accountSession; @@ -85,6 +86,8 @@ public class MainActivity extends FragmentStackActivity{ showCompose(); }else if(Intent.ACTION_VIEW.equals(intent.getAction())){ handleURL(intent.getData(), null); + }else if(intent.getBooleanExtra("explore", false)){ + restartHomeFragment(); }/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){ GithubSelfUpdater.getInstance().handleIntentFromInstaller(intent, this); }*/ @@ -211,6 +214,8 @@ public class MainActivity extends FragmentStackActivity{ } }else if(intent.getBooleanExtra("compose", false)){ showCompose(); + }else if(intent.getBooleanExtra("explore", false) && fragment instanceof HomeFragment hf){ + getWindow().getDecorView().post(()->hf.setCurrentTab(R.id.tab_search)); }else if(Intent.ACTION_VIEW.equals(intent.getAction())){ handleURL(intent.getData(), null); }else{ @@ -218,4 +223,10 @@ public class MainActivity extends FragmentStackActivity{ } } } + + public Fragment getTopmostFragment(){ + if(fragmentContainers.isEmpty()) + return null; + return getFragmentManager().findFragmentById(fragmentContainers.get(fragmentContainers.size()-1).getId()); + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/catalog/GetCatalogInstances.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/catalog/GetCatalogInstances.java index 54a55df5..a1f6a159 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/catalog/GetCatalogInstances.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/catalog/GetCatalogInstances.java @@ -13,11 +13,13 @@ import java.util.List; public class GetCatalogInstances extends MastodonAPIRequest>{ private String lang, category; + private boolean includeClosedSignups; - public GetCatalogInstances(String lang, String category){ + public GetCatalogInstances(String lang, String category, boolean includeClosedSignups){ super(HttpMethod.GET, null, new TypeToken<>(){}); this.lang=lang; this.category=category; + this.includeClosedSignups=includeClosedSignups; } @Override @@ -30,6 +32,8 @@ public class GetCatalogInstances extends MastodonAPIRequest exten protected HashMap relationships=new HashMap<>(); protected Rect tmpRect=new Rect(); protected TypedObjectPool attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView); + private SpringAnimation listShakeAnimation; public BaseStatusListFragment(){ super(20); @@ -675,6 +679,17 @@ public abstract class BaseStatusListFragment exten protected void onModifyItemViewHolder(BindableViewHolder holder){} + public void shakeListView(){ + if(listShakeAnimation!=null) + listShakeAnimation.cancel(); + SpringAnimation anim=new SpringAnimation(list, DynamicAnimation.TRANSLATION_X, 0); + anim.setStartVelocity(V.dp(-500)); + anim.getSpring().setStiffness(500).setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY); + listShakeAnimation=anim; + anim.addEndListener((animation, canceled, value, velocity)->listShakeAnimation=null); + anim.start(); + } + protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter> implements ImageLoaderRecyclerAdapter{ public DisplayItemsAdapter(){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java index 8fdbb49a..dc7a76c5 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java @@ -275,4 +275,8 @@ public class HashtagTimelineFragment extends StatusListFragment{ }) .exec(accountID); } + + public String getHashtagName(){ + return hashtagName; + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java index 9602a1a9..deb45ae7 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java @@ -1149,7 +1149,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList startImagePicker(COVER_RESULT); }else{ Drawable drawable=cover.getDrawable(); - if(drawable==null || drawable instanceof ColorDrawable) + if(drawable==null || drawable instanceof ColorDrawable || account.headerStatic.endsWith("/missing.png")) return; currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.header, drawable), 0, null, accountID, new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0))); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogSignupFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogSignupFragment.java index 37cb5b84..8c18bee9 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogSignupFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogSignupFragment.java @@ -37,7 +37,6 @@ import org.joinmastodon.android.model.catalog.CatalogInstance; import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.text.HtmlParser; -import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter; import org.joinmastodon.android.ui.utils.SimpleTextWatcher; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.views.FilterChipView; @@ -61,8 +60,6 @@ import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.utils.BindableViewHolder; -import me.grishka.appkit.utils.MergeRecyclerAdapter; -import me.grishka.appkit.utils.SingleViewRecyclerAdapter; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; @@ -106,7 +103,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple @Override protected void doLoadData(int offset, int count){ - currentRequest=new GetCatalogInstances(null, null) + currentRequest=new GetCatalogInstances(null, null, false) .setCallback(new Callback<>(){ @Override public void onSuccess(List result){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceChooserLoginFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceChooserLoginFragment.java index 5f810fd3..38ed1c53 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceChooserLoginFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceChooserLoginFragment.java @@ -96,7 +96,7 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{ private void loadAutocompleteServers(){ loadedAutocomplete=true; - new GetCatalogInstances(null, null) + new GetCatalogInstances(null, null, true) .setCallback(new Callback<>(){ @Override public void onSuccess(List result){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/catalog/CatalogInstance.java b/mastodon/src/main/java/org/joinmastodon/android/model/catalog/CatalogInstance.java index d6f8a7db..0d916cae 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/catalog/CatalogInstance.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/catalog/CatalogInstance.java @@ -7,6 +7,7 @@ import com.google.gson.annotations.SerializedName; import org.joinmastodon.android.api.AllFieldsAreRequired; import org.joinmastodon.android.api.ObjectValidationException; +import org.joinmastodon.android.api.RequiredField; import org.joinmastodon.android.model.BaseModel; import java.net.IDN; @@ -15,14 +16,18 @@ import java.util.List; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.utils.V; -@AllFieldsAreRequired public class CatalogInstance extends BaseModel{ + @RequiredField public String domain; + @RequiredField public String version; + @RequiredField public String description; + @RequiredField public List languages; @SerializedName("region") private String _region; + @RequiredField public List categories; public String proxiedThumbnail; public int totalUsers; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index ce7e6f44..2c2d6eee 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -52,6 +52,7 @@ import android.widget.Toast; import org.joinmastodon.android.E; import org.joinmastodon.android.FileProvider; import org.joinmastodon.android.GlobalUserPreferences; +import org.joinmastodon.android.MainActivity; import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked; @@ -368,6 +369,8 @@ public class UiUtils{ } public static void openHashtagTimeline(Context context, String accountID, Hashtag hashtag){ + if(checkIfAlreadyDisplayingSameHashtag(context, hashtag.name)) + return; Bundle args=new Bundle(); args.putString("account", accountID); args.putParcelable("hashtag", Parcels.wrap(hashtag)); @@ -375,12 +378,22 @@ public class UiUtils{ } public static void openHashtagTimeline(Context context, String accountID, String hashtag){ + if(checkIfAlreadyDisplayingSameHashtag(context, hashtag)) + return; Bundle args=new Bundle(); args.putString("account", accountID); args.putString("hashtagName", hashtag); Nav.go((Activity)context, HashtagTimelineFragment.class, args); } + private static boolean checkIfAlreadyDisplayingSameHashtag(Context context, String hashtag){ + if(context instanceof MainActivity ma && ma.getTopmostFragment() instanceof HashtagTimelineFragment htf && htf.getHashtagName().equalsIgnoreCase(hashtag)){ + htf.shakeListView(); + return true; + } + return false; + } + public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, Runnable onConfirmed){ showConfirmationAlert(context, context.getString(title), message==0 ? null : context.getString(message), context.getString(confirmButton), onConfirmed); } diff --git a/mastodon/src/main/res/drawable/ic_explore_foreground.xml b/mastodon/src/main/res/drawable/ic_explore_foreground.xml new file mode 100644 index 00000000..8b28b47b --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_explore_foreground.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/mastodon/src/main/res/mipmap-anydpi-v26/ic_shortcut_explore.xml b/mastodon/src/main/res/mipmap-anydpi-v26/ic_shortcut_explore.xml new file mode 100644 index 00000000..d8aec538 --- /dev/null +++ b/mastodon/src/main/res/mipmap-anydpi-v26/ic_shortcut_explore.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file From 0dabe89bcde755226230a2ec1246ad9d38befea3 Mon Sep 17 00:00:00 2001 From: Grishka Date: Sat, 8 Jun 2024 20:43:38 +0300 Subject: [PATCH 5/8] Push notification improvements --- mastodon/src/main/AndroidManifest.xml | 1 + .../NotificationActionHandlerService.java | 171 ++++++++++++++++++ .../android/PushNotificationReceiver.java | 103 ++++++++++- 3 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 mastodon/src/main/java/org/joinmastodon/android/NotificationActionHandlerService.java diff --git a/mastodon/src/main/AndroidManifest.xml b/mastodon/src/main/AndroidManifest.xml index 1cc532fd..ecd97cd3 100644 --- a/mastodon/src/main/AndroidManifest.xml +++ b/mastodon/src/main/AndroidManifest.xml @@ -79,6 +79,7 @@ + diff --git a/mastodon/src/main/java/org/joinmastodon/android/NotificationActionHandlerService.java b/mastodon/src/main/java/org/joinmastodon/android/NotificationActionHandlerService.java new file mode 100644 index 00000000..74dbf160 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/NotificationActionHandlerService.java @@ -0,0 +1,171 @@ +package org.joinmastodon.android; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.RemoteInput; +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.service.notification.StatusBarNotification; + +import org.joinmastodon.android.api.requests.statuses.CreateStatus; +import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited; +import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged; +import org.joinmastodon.android.events.StatusCountersUpdatedEvent; +import org.joinmastodon.android.events.StatusCreatedEvent; +import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.model.StatusPrivacy; + +import java.util.UUID; + +import androidx.annotation.Nullable; +import me.grishka.appkit.api.Callback; +import me.grishka.appkit.api.ErrorResponse; + +public class NotificationActionHandlerService extends Service{ + private static final String TAG="NotificationActionHandl"; + private int runningRequestCount=0; + + @Nullable + @Override + public IBinder onBind(Intent intent){ + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId){ + String action=intent.getStringExtra("action"); + String account=intent.getStringExtra("account"); + String postID=intent.getStringExtra("post"); + String notificationTag=intent.getStringExtra("notificationTag"); + if(action==null || account==null || postID==null || notificationTag==null){ + maybeStopSelf(); + return START_NOT_STICKY; + } + NotificationManager nm=getSystemService(NotificationManager.class); + StatusBarNotification notification=findNotification(notificationTag); + if("reply".equals(action)){ + CharSequence replyText=RemoteInput.getResultsFromIntent(intent).getCharSequence("replyText"); + if(replyText==null){ + maybeStopSelf(); + return START_NOT_STICKY; + } + CreateStatus.Request req=new CreateStatus.Request(); + req.inReplyToId=postID; + req.status=intent.getStringExtra("replyPrefix")+replyText; + req.visibility=StatusPrivacy.valueOf(intent.getStringExtra("visibility")); + runningRequestCount++; + new CreateStatus(req, UUID.randomUUID().toString()) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(Status result){ + E.post(new StatusCreatedEvent(result, account)); + if(notification!=null){ + Notification n=notification.getNotification(); + nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n); + } + runningRequestCount--; + maybeStopSelf(); + } + + @Override + public void onError(ErrorResponse error){ + error.showToast(NotificationActionHandlerService.this); + if(notification!=null){ + Notification n=notification.getNotification(); + nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n); + } + runningRequestCount--; + maybeStopSelf(); + } + }) + .exec(account); + }else if("favorite".equals(action)){ + PendingIntent prevActionIntent; + if(notification!=null){ + Notification n=notification.getNotification(); + prevActionIntent=n.actions[1].actionIntent; + n.actions[1].actionIntent=null; + n.actions[1].title=getString(R.string.button_favorited); + nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n); + }else{ + prevActionIntent=null; + } + runningRequestCount++; + new SetStatusFavorited(postID, true) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(Status result){ + E.post(new StatusCountersUpdatedEvent(result)); + runningRequestCount--; + maybeStopSelf(); + } + + @Override + public void onError(ErrorResponse error){ + if(notification!=null){ + Notification n=notification.getNotification(); + n.actions[1].actionIntent=prevActionIntent; + n.actions[1].title=getString(R.string.button_favorite); + nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n); + } + error.showToast(NotificationActionHandlerService.this); + runningRequestCount--; + maybeStopSelf(); + } + }) + .exec(account); + }else if("boost".equals(action)){ + PendingIntent prevActionIntent; + if(notification!=null){ + Notification n=notification.getNotification(); + prevActionIntent=n.actions[2].actionIntent; + n.actions[2].actionIntent=null; + n.actions[2].title=getString(R.string.button_reblogged); + nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n); + }else{ + prevActionIntent=null; + } + runningRequestCount++; + new SetStatusReblogged(postID, true) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(Status result){ + E.post(new StatusCountersUpdatedEvent(result)); + runningRequestCount--; + maybeStopSelf(); + } + + @Override + public void onError(ErrorResponse error){ + if(notification!=null){ + Notification n=notification.getNotification(); + n.actions[2].actionIntent=prevActionIntent; + n.actions[2].title=getString(R.string.button_reblog); + nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n); + } + error.showToast(NotificationActionHandlerService.this); + runningRequestCount--; + maybeStopSelf(); + } + }) + .exec(account); + } + return START_NOT_STICKY; + } + + private void maybeStopSelf(){ + if(runningRequestCount==0) + stopSelf(); + } + + private StatusBarNotification findNotification(String tag){ + for(StatusBarNotification sbn:getSystemService(NotificationManager.class).getActiveNotifications()){ + if(tag.equals(sbn.getTag())){ + return sbn; + } + } + return null; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java b/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java index af3ca352..85243137 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java +++ b/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java @@ -5,14 +5,15 @@ import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.NotificationManager; import android.app.PendingIntent; +import android.app.RemoteInput; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.Build; import android.os.Bundle; +import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.util.Log; @@ -21,10 +22,13 @@ import org.joinmastodon.android.api.requests.notifications.GetNotificationByID; import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.Mention; import org.joinmastodon.android.model.PushNotification; +import org.joinmastodon.android.model.StatusPrivacy; import org.joinmastodon.android.ui.utils.UiUtils; import org.parceler.Parcels; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -144,19 +148,110 @@ public class PushNotificationReceiver extends BroadcastReceiver{ .setContentText(pn.body) .setStyle(new Notification.BigTextStyle().bigText(pn.body)) .setSmallIcon(R.drawable.ic_ntf_logo) - .setContentIntent(PendingIntent.getActivity(context, accountID.hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT)) + .setContentIntent(PendingIntent.getActivity(context, (accountID+pn.notificationId).hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT)) .setWhen(notification==null ? System.currentTimeMillis() : notification.createdAt.toEpochMilli()) .setShowWhen(true) .setCategory(Notification.CATEGORY_SOCIAL) .setAutoCancel(true) + .setOnlyAlertOnce(true) .setLights(context.getColor(R.color.primary_700), 500, 1000) - .setColor(context.getColor(R.color.primary_700)); + .setColor(context.getColor(R.color.primary_700)) + .setGroup(accountID); if(avatar!=null){ builder.setLargeIcon(UiUtils.getBitmapFromDrawable(avatar)); } if(AccountSessionManager.getInstance().getLoggedInAccounts().size()>1){ builder.setSubText(accountName); } - nm.notify(accountID, NOTIFICATION_ID, builder.build()); + String notificationTag=accountID+"_"+(notification==null ? 0 : notification.id); + if(notification!=null && (notification.type==org.joinmastodon.android.model.Notification.Type.MENTION)){ + ArrayList mentions=new ArrayList<>(); + String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id; + if(!notification.status.account.id.equals(ownID)) + mentions.add('@'+notification.status.account.acct); + for(Mention mention:notification.status.mentions){ + if(mention.id.equals(ownID)) + continue; + String m='@'+mention.acct; + if(!mentions.contains(m)) + mentions.add(m); + } + String replyPrefix=mentions.isEmpty() ? "" : TextUtils.join(" ", mentions)+" "; + + Intent replyIntent=new Intent(context, NotificationActionHandlerService.class); + replyIntent.putExtra("action", "reply"); + replyIntent.putExtra("account", accountID); + replyIntent.putExtra("post", notification.status.id); + replyIntent.putExtra("notificationTag", notificationTag); + replyIntent.putExtra("visibility", notification.status.visibility.toString()); + replyIntent.putExtra("replyPrefix", replyPrefix); + builder.addAction(new Notification.Action.Builder(Icon.createWithResource(context, R.drawable.ic_reply_24px), + context.getString(R.string.button_reply), PendingIntent.getService(context, (accountID+pn.notificationId+"reply").hashCode(), replyIntent, PendingIntent.FLAG_UPDATE_CURRENT)) + .addRemoteInput(new RemoteInput.Builder("replyText").build()) + .build()); + + Intent favIntent=new Intent(context, NotificationActionHandlerService.class); + favIntent.putExtra("action", "favorite"); + favIntent.putExtra("account", accountID); + favIntent.putExtra("post", notification.status.id); + favIntent.putExtra("notificationTag", notificationTag); + builder.addAction(new Notification.Action.Builder(Icon.createWithResource(context, R.drawable.ic_star_24px), + context.getString(R.string.button_favorite), PendingIntent.getService(context, (accountID+pn.notificationId+"favorite").hashCode(), favIntent, PendingIntent.FLAG_UPDATE_CURRENT)).build()); + + PendingIntent boostActionIntent; + if(notification.status.visibility!=StatusPrivacy.DIRECT){ + Intent boostIntent=new Intent(context, NotificationActionHandlerService.class); + boostIntent.putExtra("action", "boost"); + boostIntent.putExtra("account", accountID); + boostIntent.putExtra("post", notification.status.id); + boostIntent.putExtra("notificationTag", notificationTag); + boostActionIntent=PendingIntent.getService(context, (accountID+pn.notificationId+"boost").hashCode(), boostIntent, PendingIntent.FLAG_UPDATE_CURRENT); + }else{ + boostActionIntent=null; + } + builder.addAction(new Notification.Action.Builder(Icon.createWithResource(context, R.drawable.ic_boost_24px), + context.getString(R.string.button_reblog), boostActionIntent).build()); + } + nm.notify(notificationTag, NOTIFICATION_ID, builder.build()); + + StatusBarNotification[] activeNotifications=nm.getActiveNotifications(); + ArrayList summaryLines=new ArrayList<>(); + for(StatusBarNotification sbn:activeNotifications){ + String tag=sbn.getTag(); + if(tag!=null && tag.startsWith(accountID+"_")){ + if((sbn.getNotification().flags & Notification.FLAG_GROUP_SUMMARY)==0){ + summaryLines.add(sbn.getNotification().extras.getString("android.title")); + if(summaryLines.size()==5) + break; + } + } + } + + if(summaryLines.size()>1){ + Notification.Builder summaryBuilder; + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){ + summaryBuilder=new Notification.Builder(context, accountID+"_"+pn.notificationType); + }else{ + summaryBuilder=new Notification.Builder(context) + .setPriority(Notification.PRIORITY_DEFAULT); + } + Notification.InboxStyle inboxStyle=new Notification.InboxStyle(); + for(String line:summaryLines){ + inboxStyle.addLine(line); + } + summaryBuilder.setContentTitle("content title") + .setContentText("content text") + .setSmallIcon(R.drawable.ic_ntf_logo) + .setColor(context.getColor(R.color.primary_700)) + .setContentIntent(PendingIntent.getActivity(context, accountID.hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT)) + .setWhen(notification==null ? System.currentTimeMillis() : notification.createdAt.toEpochMilli()) + .setShowWhen(true) + .setCategory(Notification.CATEGORY_SOCIAL) + .setAutoCancel(true) + .setGroup(accountID) + .setGroupSummary(true) + .setStyle(inboxStyle.setSummaryText(accountName)); + nm.notify(accountID+"_summary", NOTIFICATION_ID, summaryBuilder.build()); + } } } From 7e1f63348a1f066d9dcf2a98268c62bcc66c6fe8 Mon Sep 17 00:00:00 2001 From: FineFindus Date: Sun, 9 Jun 2024 08:46:05 +0200 Subject: [PATCH 6/8] fix: disable GroupDivider on Honor's MagicOS When enabled on MagicOS, GroupDivider are not displayed and causes other menu items to be invisble. Similar bug to https://github.com/mastodon/mastodon-android/pull/732. --- .../org/joinmastodon/android/fragments/ProfileFragment.java | 2 +- .../android/ui/displayitems/HeaderStatusDisplayItem.java | 2 +- .../main/java/org/joinmastodon/android/ui/utils/UiUtils.java | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java index deb45ae7..2662d4f5 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java @@ -687,7 +687,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList notifications.setTitle(getString(relationship.notifying ? R.string.disable_new_post_notifications : R.string.enable_new_post_notifications, account.getDisplayUsername())); } - if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI()){ + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI() && !UiUtils.isMagic()){ menu.setGroupDividerEnabled(true); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java index c998434e..b5034f57 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java @@ -140,7 +140,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ optionsMenu=new PopupMenu(activity, more); optionsMenu.inflate(R.menu.post); - if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI()) + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI() && !UiUtils.isMagic()) optionsMenu.getMenu().setGroupDividerEnabled(true); optionsMenu.setOnMenuItemClickListener(menuItem->{ Account account=item.user; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index 2c2d6eee..c9a7e110 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -809,6 +809,10 @@ public class UiUtils{ return !TextUtils.isEmpty(getSystemProperty("ro.build.version.emui")); } + public static boolean isMagic() { + return !TextUtils.isEmpty(getSystemProperty("ro.build.version.magic")); + } + public static int alphaBlendColors(int color1, int color2, float alpha){ float alpha0=1f-alpha; int r=Math.round(((color1 >> 16) & 0xFF)*alpha0+((color2 >> 16) & 0xFF)*alpha); From 513b29f57d0700f760f9461b7fbeefb3fc56a648 Mon Sep 17 00:00:00 2001 From: Grishka Date: Sun, 9 Jun 2024 21:17:01 +0300 Subject: [PATCH 7/8] Add support for predictive back navigation --- mastodon/build.gradle | 2 +- mastodon/src/main/AndroidManifest.xml | 3 +- .../android/fragments/ComposeFragment.java | 44 ++++++++++++------- .../ComposeImageDescriptionFragment.java | 7 ++- .../CreateListAddMembersFragment.java | 16 ++----- .../android/fragments/HomeFragment.java | 14 +----- .../fragments/ListMembersFragment.java | 18 +++----- .../android/fragments/ProfileFragment.java | 33 +++++++------- .../fragments/ProfileQrCodeFragment.java | 18 +++++--- .../fragments/discover/DiscoverFragment.java | 15 ++----- .../discover/SearchQueryFragment.java | 16 +++---- .../InstanceCatalogSignupFragment.java | 18 +++----- .../settings/EditFilterFragment.java | 32 +++++++++++--- .../settings/FilterContextFragment.java | 8 ++-- .../settings/FilterWordsFragment.java | 13 +++--- .../android/ui/photoviewer/PhotoViewer.java | 7 ++- .../ToolbarDropdownMenuController.java | 13 +++++- 17 files changed, 144 insertions(+), 133 deletions(-) diff --git a/mastodon/build.gradle b/mastodon/build.gradle index f119b73a..8fc25ecc 100644 --- a/mastodon/build.gradle +++ b/mastodon/build.gradle @@ -90,7 +90,7 @@ dependencies { implementation 'me.grishka.litex:viewpager:1.0.0' implementation 'me.grishka.litex:viewpager2:1.0.0' implementation 'me.grishka.litex:palette:1.0.0' - implementation 'me.grishka.appkit:appkit:1.2.17' + implementation 'me.grishka.appkit:appkit:1.3.0' implementation 'com.google.code.gson:gson:2.8.9' implementation 'org.jsoup:jsoup:1.14.3' implementation 'com.squareup:otto:1.3.8' diff --git a/mastodon/src/main/AndroidManifest.xml b/mastodon/src/main/AndroidManifest.xml index ecd97cd3..0e6c31f0 100644 --- a/mastodon/src/main/AndroidManifest.xml +++ b/mastodon/src/main/AndroidManifest.xml @@ -33,7 +33,8 @@ android:supportsRtl="true" android:icon="@mipmap/ic_launcher" android:theme="@style/Theme.Mastodon.AutoLightDark" - android:largeHeap="true"> + android:largeHeap="true" + android:enableOnBackInvokedCallback="true"> updateCharCounter())); @@ -621,6 +631,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr if(publishButton==null) return; publishButton.setEnabled((trimmedCharCount>0 || !mediaViewController.isEmpty()) && charCount<=charLimit && mediaViewController.getNonDoneAttachmentCount()==0 && (pollViewController.isEmpty() || pollViewController.getNonEmptyOptionsCount()>1)); + updateDraftState(); } private void onCustomEmojiClick(Emoji emoji){ @@ -696,6 +707,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr overlayParams.softInputMode=WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED; overlayParams.token=mainEditText.getWindowToken(); wm.addView(sendingOverlay, overlayParams); + addBackCallback(sendingBackButtonBlocker); publishButton.setEnabled(false); V.setVisibilityAnimated(sendProgress, View.VISIBLE); @@ -737,6 +749,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr public void onSuccess(Status result){ wm.removeView(sendingOverlay); sendingOverlay=null; + removeBackCallback(sendingBackButtonBlocker); if(editingStatus==null){ E.post(new StatusCreatedEvent(result, accountID)); if(replyTo!=null){ @@ -769,6 +782,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private void handlePublishError(ErrorResponse error){ wm.removeView(sendingOverlay); sendingOverlay=null; + removeBackCallback(sendingBackButtonBlocker); V.setVisibilityAnimated(sendProgress, View.GONE); publishButton.setEnabled(true); if(error instanceof MastodonErrorResponse me){ @@ -796,19 +810,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr return (mainEditText.length()>0 && !mainEditText.getText().toString().equals(initialText)) || !mediaViewController.isEmpty() || pollFieldsHaveContent; } - @Override - public boolean onBackPressed(){ - if(emojiKeyboard.isVisible()){ - emojiKeyboard.hide(); - return true; + private void updateDraftState(){ + boolean hasDraft=hasDraft(); + if(hasDraft!=prevHadDraft){ + prevHadDraft=hasDraft; + if(hasDraft){ + addBackCallback(discardConfirmationCallback); + }else{ + removeBackCallback(discardConfirmationCallback); + } } - if(hasDraft()){ - confirmDiscardDraftAndFinish(); - return true; - } - if(sendingOverlay!=null) - return true; - return false; } @Override @@ -842,7 +853,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private void confirmDiscardDraftAndFinish(){ new M3AlertDialogBuilder(getActivity()) .setTitle(editingStatus==null ? R.string.discard_draft : R.string.discard_changes) - .setPositiveButton(R.string.discard, (dialog, which)->Nav.finish(this)) + .setPositiveButton(R.string.discard, (dialog, which)->{ + removeBackCallback(discardConfirmationCallback); + Nav.finish(this); + }) .setNegativeButton(R.string.cancel, null) .show(); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeImageDescriptionFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeImageDescriptionFragment.java index 5e3cc22c..efb6aa31 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeImageDescriptionFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeImageDescriptionFragment.java @@ -32,12 +32,11 @@ import java.util.Collections; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.imageloader.ViewImageLoader; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.utils.V; -public class ComposeImageDescriptionFragment extends MastodonToolbarFragment implements OnBackPressedListener{ +public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{ private static final String TAG="ComposeImageDescription"; private String accountID, attachmentID; @@ -138,9 +137,9 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp } @Override - public boolean onBackPressed(){ + public void onStop(){ + super.onStop(); deliverResult(); - return false; } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/CreateListAddMembersFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/CreateListAddMembersFragment.java index afd92a0c..a5406726 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/CreateListAddMembersFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/CreateListAddMembersFragment.java @@ -42,13 +42,11 @@ import me.grishka.appkit.Nav; import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.SimpleCallback; -import me.grishka.appkit.fragments.OnBackPressedListener; -import me.grishka.appkit.fragments.WindowInsetsAwareFragment; import me.grishka.appkit.utils.CubicBezierInterpolator; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.FragmentRootLinearLayout; -public class CreateListAddMembersFragment extends BaseAccountListFragment implements OnBackPressedListener, AddNewListMembersFragment.Listener{ +public class CreateListAddMembersFragment extends BaseAccountListFragment implements AddNewListMembersFragment.Listener{ private FollowList followList; private Button nextButton; private View buttonBar; @@ -59,6 +57,7 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem private WindowInsets lastInsets; private boolean dismissingSearchFragment; private HashSet accountIDsInList=new HashSet<>(); + private Runnable searchFragmentDismisser=this::dismissSearchFragment; @Override public void onCreate(Bundle savedInstanceState){ @@ -156,6 +155,7 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem searchFragmentContainer.animate().translationX(0).alpha(1).setDuration(300).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{ rootView.setVisibility(View.GONE); }).start(); + addBackCallback(searchFragmentDismisser); return true; } @@ -183,6 +183,7 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem private void dismissSearchFragment(){ if(searchFragment==null || dismissingSearchFragment) return; + removeBackCallback(searchFragmentDismisser); dismissingSearchFragment=true; rootView.setVisibility(View.VISIBLE); searchFragmentContainer.animate().translationX(V.dp(100)).alpha(0).setDuration(200).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{ @@ -201,15 +202,6 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem Nav.finish(this); } - @Override - public boolean onBackPressed(){ - if(searchFragment!=null){ - dismissSearchFragment(); - return true; - } - return false; - } - @Override public boolean isAccountInList(AccountViewModel account){ return accountIDsInList.contains(account.account.id); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java index f40283f0..7329a484 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java @@ -30,8 +30,8 @@ import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestions import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.PaginatedResponse; -import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet; import org.joinmastodon.android.ui.OutlineProviders; +import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.views.TabBar; import org.joinmastodon.android.utils.ObjectIdComparator; @@ -48,13 +48,12 @@ import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.fragments.AppKitFragment; import me.grishka.appkit.fragments.LoaderFragment; -import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.imageloader.ViewImageLoader; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.FragmentRootLinearLayout; -public class HomeFragment extends AppKitFragment implements OnBackPressedListener{ +public class HomeFragment extends AppKitFragment{ private FragmentRootLinearLayout content; private HomeTimelineFragment homeTimelineFragment; private NotificationsListFragment notificationsFragment; @@ -272,15 +271,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene return false; } - @Override - public boolean onBackPressed(){ - if(currentTab==R.id.tab_profile) - return profileFragment.onBackPressed(); - if(currentTab==R.id.tab_search) - return searchFragment.onBackPressed(); - return false; - } - @Override public void onSaveInstanceState(Bundle outState){ super.onSaveInstanceState(outState); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ListMembersFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ListMembersFragment.java index f6a5b364..13f5aba9 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ListMembersFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ListMembersFragment.java @@ -44,12 +44,11 @@ import java.util.stream.Collectors; import me.grishka.appkit.Nav; import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; -import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.utils.CubicBezierInterpolator; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.FragmentRootLinearLayout; -public class ListMembersFragment extends PaginatedAccountListFragment implements AddNewListMembersFragment.Listener, OnBackPressedListener{ +public class ListMembersFragment extends PaginatedAccountListFragment implements AddNewListMembersFragment.Listener{ private ImageButton fab; private FollowList followList; private boolean inSelectionMode; @@ -63,6 +62,8 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements private WindowInsets lastInsets; private HashSet accountIDsInList=new HashSet<>(); private boolean dismissingSearchFragment; + private Runnable searchFragmentDismisser=this::dismissSearchFragment;; + private Runnable actionModeDismisser=()->actionMode.finish(); public ListMembersFragment(){ setListLayoutId(R.layout.recycler_fragment_with_fab); @@ -214,6 +215,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements searchFragmentContainer.animate().translationX(0).alpha(1).setDuration(300).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{ rootView.setVisibility(View.GONE); }).start(); + addBackCallback(searchFragmentDismisser); } private void onItemClick(AccountViewHolder holder){ @@ -293,9 +295,11 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements selectedAccounts.clear(); updateItemsForSelectionModeTransition(); V.setVisibilityAnimated(fab, View.VISIBLE); + removeBackCallback(actionModeDismisser); } }); updateActionModeTitle(); + addBackCallback(actionModeDismisser); } private void updateActionModeTitle(){ @@ -371,15 +375,6 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements removeAccounts(Set.of(account.account.id), onDone); } - @Override - public boolean onBackPressed(){ - if(searchFragment!=null){ - dismissSearchFragment(); - return true; - } - return false; - } - private void dismissSearchFragment(){ if(searchFragment==null || dismissingSearchFragment) return; @@ -393,6 +388,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements searchFragment=null; dismissingSearchFragment=false; }).start(); + removeBackCallback(searchFragmentDismisser); getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java index 2662d4f5..0045291f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java @@ -100,14 +100,13 @@ import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.fragments.BaseRecyclerFragment; import me.grishka.appkit.fragments.LoaderFragment; -import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.imageloader.ViewImageLoader; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.utils.CubicBezierInterpolator; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.FragmentRootLinearLayout; -public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop{ +public class ProfileFragment extends LoaderFragment implements ScrollableToTop{ private static final int AVATAR_RESULT=722; private static final int COVER_RESULT=343; @@ -158,6 +157,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList private Animator tabBarColorAnim; private MenuItem editSaveMenuItem; private boolean savingEdits; + private Runnable editModeBackCallback=this::onEditModeBackCallback; @Override public void onCreate(Bundle savedInstanceState){ @@ -983,12 +983,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList refreshLayout.setEnabled(false); editDirty=false; V.setVisibilityAnimated(fab, View.GONE); + addBackCallback(editModeBackCallback); } private void exitEditMode(){ if(!isInEditMode) throw new IllegalStateException(); isInEditMode=false; + removeBackCallback(editModeBackCallback); invalidateOptionsMenu(); actionButton.setText(R.string.edit_profile); @@ -1098,23 +1100,18 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList updateRelationship(); } - @Override - public boolean onBackPressed(){ - if(isInEditMode){ - if(savingEdits) - return true; - if(editDirty || aboutFragment.isEditDirty()){ - new M3AlertDialogBuilder(getActivity()) - .setTitle(R.string.discard_changes) - .setPositiveButton(R.string.discard, (dlg, btn)->exitEditMode()) - .setNegativeButton(R.string.cancel, null) - .show(); - }else{ - exitEditMode(); - } - return true; + private void onEditModeBackCallback(){ + if(savingEdits) + return; + if(editDirty || aboutFragment.isEditDirty()){ + new M3AlertDialogBuilder(getActivity()) + .setTitle(R.string.discard_changes) + .setPositiveButton(R.string.discard, (dlg, btn)->exitEditMode()) + .setNegativeButton(R.string.cancel, null) + .show(); + }else{ + exitEditMode(); } - return false; } private List createFakeAttachments(String url, Drawable drawable){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileQrCodeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileQrCodeFragment.java index 919b13a7..32094a64 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileQrCodeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileQrCodeFragment.java @@ -53,6 +53,8 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; +import android.window.OnBackInvokedCallback; +import android.window.OnBackInvokedDispatcher; import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHintType; @@ -144,12 +146,16 @@ public class ProfileQrCodeFragment extends AppKitFragment{ if(!isTablet){ getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } - dlg.setOnKeyListener((dialog, keyCode, event)->{ - if(keyCode==KeyEvent.KEYCODE_BACK && event.getAction()==KeyEvent.ACTION_DOWN){ - dismiss(); - } - return true; - }); + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){ + dlg.getOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, this::dismiss); + }else{ + dlg.setOnKeyListener((dialog, keyCode, event)->{ + if(keyCode==KeyEvent.KEYCODE_BACK && event.getAction()==KeyEvent.ACTION_DOWN){ + dismiss(); + } + return true; + }); + } } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverFragment.java index a1934c14..ca93bdd4 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverFragment.java @@ -36,10 +36,9 @@ import androidx.viewpager2.widget.ViewPager2; import me.grishka.appkit.Nav; import me.grishka.appkit.fragments.AppKitFragment; import me.grishka.appkit.fragments.BaseRecyclerFragment; -import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.utils.V; -public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener{ +public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{ private static final int QUERY_RESULT=937; private static final int SCAN_RESULT=456; @@ -62,6 +61,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, private String accountID; private String currentQuery; private Intent scannerIntent; + private Runnable searchExitCallback=this::exitSearch; @Override public void onCreate(Bundle savedInstanceState){ @@ -232,6 +232,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, searchBack.setEnabled(true); searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); tabsDivider.setVisibility(View.GONE); + addBackCallback(searchExitCallback); } } @@ -248,6 +249,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); tabsDivider.setVisibility(View.VISIBLE); currentQuery=null; + removeBackCallback(searchExitCallback); } private Fragment getFragmentForPage(int page){ @@ -260,15 +262,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, }; } - @Override - public boolean onBackPressed(){ - if(searchActive){ - exitSearch(); - return true; - } - return false; - } - @Override public void onFragmentResult(int reqCode, boolean success, Bundle result){ if(reqCode==QUERY_RESULT && success){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchQueryFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchQueryFragment.java index 3ff7fc79..16768a38 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchQueryFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchQueryFragment.java @@ -57,14 +57,13 @@ import androidx.recyclerview.widget.RecyclerView; import me.grishka.appkit.Nav; import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.fragments.CustomTransitionsFragment; -import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.utils.MergeRecyclerAdapter; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; -public class SearchQueryFragment extends MastodonRecyclerFragment implements CustomTransitionsFragment, OnBackPressedListener{ +public class SearchQueryFragment extends MastodonRecyclerFragment implements CustomTransitionsFragment{ private static final Pattern HASHTAG_REGEX=Pattern.compile("^(\\w*[a-zA-Z·]\\w*)$", Pattern.CASE_INSENSITIVE); private static final Pattern USERNAME_REGEX=Pattern.compile("^@?([a-z0-9_-]+)(@[^\\s]+)?$", Pattern.CASE_INSENSITIVE); @@ -371,6 +370,11 @@ public class SearchQueryFragment extends MastodonRecyclerFragment languages=Collections.emptyList(); @@ -84,6 +83,8 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple private String inviteCode, inviteCodeHost; private AlertDialog currentInviteLinkAlert; + private Runnable exitQueryModeCallback=()->setSearchQueryMode(false); + public InstanceCatalogSignupFragment(){ super(R.layout.fragment_onboarding_common, 10); } @@ -582,19 +583,13 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom())); } - @Override - public boolean onBackPressed(){ - if(searchQueryMode){ - setSearchQueryMode(false); - return true; - } - return false; - } - private void setSearchQueryMode(boolean enabled){ + if(searchQueryMode==enabled) + return; searchQueryMode=enabled; RelativeLayout.LayoutParams lp=(RelativeLayout.LayoutParams) searchEdit.getLayoutParams(); if(searchQueryMode){ + addBackCallback(exitQueryModeCallback); filtersScroll.setVisibility(View.GONE); lp.removeRule(RelativeLayout.END_OF); backBtn.setScaleX(0.83333333f); @@ -602,6 +597,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple backBtn.setTranslationX(V.dp(8)); searchEdit.setCompoundDrawableTintList(ColorStateList.valueOf(0)); }else{ + removeBackCallback(exitQueryModeCallback); filtersScroll.setVisibility(View.VISIBLE); focusThing.requestFocus(); searchEdit.setText(""); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/EditFilterFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/EditFilterFragment.java index 49a96320..83f1f43f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/EditFilterFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/EditFilterFragment.java @@ -24,6 +24,7 @@ import org.joinmastodon.android.model.FilterKeyword; import org.joinmastodon.android.model.viewmodel.CheckableListItem; import org.joinmastodon.android.model.viewmodel.ListItem; import org.joinmastodon.android.ui.M3AlertDialogBuilder; +import org.joinmastodon.android.ui.utils.SimpleTextWatcher; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout; import org.parceler.Parcels; @@ -44,11 +45,10 @@ import androidx.recyclerview.widget.RecyclerView; import me.grishka.appkit.Nav; import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; -import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.utils.MergeRecyclerAdapter; import me.grishka.appkit.utils.SingleViewRecyclerAdapter; -public class EditFilterFragment extends BaseSettingsFragment implements OnBackPressedListener{ +public class EditFilterFragment extends BaseSettingsFragment{ private static final int WORDS_RESULT=370; private static final int CONTEXT_RESULT=651; @@ -63,6 +63,13 @@ public class EditFilterFragment extends BaseSettingsFragment implements On private ArrayList deletedWordIDs=new ArrayList<>(); private EnumSet context=EnumSet.allOf(FilterContext.class); private boolean dirty; + private boolean wasDirty; + + private Runnable confirmCallback=()->{ + if(isDirty()){ + UiUtils.showConfirmationAlert(getActivity(), R.string.discard_changes, 0, R.string.discard, ()->Nav.finish(this)); + } + }; @Override public void onCreate(Bundle savedInstanceState){ @@ -101,6 +108,7 @@ public class EditFilterFragment extends BaseSettingsFragment implements On titleEditLayout.updateHint(); if(filter!=null) titleEdit.setText(filter.title); + titleEdit.addTextChangedListener(new SimpleTextWatcher(e->updateBackCallback())); MergeRecyclerAdapter adapter=new MergeRecyclerAdapter(); adapter.addAdapter(new SingleViewRecyclerAdapter(titleEditLayout)); @@ -158,6 +166,7 @@ public class EditFilterFragment extends BaseSettingsFragment implements On } a.dismiss(); } + updateBackCallback(); }) .show(); alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); @@ -309,6 +318,7 @@ public class EditFilterFragment extends BaseSettingsFragment implements On } deletedWordIDs.addAll(result.getStringArrayList("deleted")); } + updateBackCallback(); } } @@ -317,11 +327,19 @@ public class EditFilterFragment extends BaseSettingsFragment implements On } @Override - public boolean onBackPressed(){ - if(isDirty()){ - UiUtils.showConfirmationAlert(getActivity(), R.string.discard_changes, 0, R.string.discard, ()->Nav.finish(this)); - return true; + protected void toggleCheckableItem(ListItem item){ + super.toggleCheckableItem(item); + updateBackCallback(); + } + + private void updateBackCallback(){ + boolean dirty=isDirty(); + if(dirty!=wasDirty){ + wasDirty=dirty; + if(dirty) + addBackCallback(confirmCallback); + else + removeBackCallback(confirmCallback); } - return false; } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterContextFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterContextFragment.java index 37d91107..a9850352 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterContextFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterContextFragment.java @@ -11,9 +11,7 @@ import java.util.Arrays; import java.util.EnumSet; import java.util.stream.Collectors; -import me.grishka.appkit.fragments.OnBackPressedListener; - -public class FilterContextFragment extends BaseSettingsFragment implements OnBackPressedListener{ +public class FilterContextFragment extends BaseSettingsFragment{ private EnumSet context; @Override @@ -33,7 +31,8 @@ public class FilterContextFragment extends BaseSettingsFragment i protected void doLoadData(int offset, int count){} @Override - public boolean onBackPressed(){ + public void onStop(){ + super.onStop(); context=EnumSet.noneOf(FilterContext.class); for(ListItem item:data){ if(((CheckableListItem) item).checked) @@ -42,6 +41,5 @@ public class FilterContextFragment extends BaseSettingsFragment i Bundle args=new Bundle(); args.putSerializable("context", context); setResult(true, args); - return false; } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterWordsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterWordsFragment.java index 0dec195b..943050a9 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterWordsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterWordsFragment.java @@ -1,7 +1,6 @@ package org.joinmastodon.android.fragments.settings; import android.app.AlertDialog; -import android.os.Build; import android.os.Bundle; import android.os.Parcelable; import android.text.InputType; @@ -11,11 +10,9 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup; import android.view.WindowInsets; import android.widget.Button; import android.widget.EditText; -import android.widget.ImageButton; import org.joinmastodon.android.R; import org.joinmastodon.android.model.FilterKeyword; @@ -33,15 +30,15 @@ import java.util.Collections; import java.util.List; import java.util.stream.Collectors; -import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.utils.V; -public class FilterWordsFragment extends BaseSettingsFragment implements OnBackPressedListener{ +public class FilterWordsFragment extends BaseSettingsFragment{ private Button fab; private ActionMode actionMode; private ArrayList> selectedItems=new ArrayList<>(); private ArrayList deletedItemIDs=new ArrayList<>(); private MenuItem deleteItem; + private Runnable actionModeDismisser=()->actionMode.finish(); public FilterWordsFragment(){ setListLayoutId(R.layout.recycler_fragment_with_text_fab); @@ -80,12 +77,12 @@ public class FilterWordsFragment extends BaseSettingsFragment imp } @Override - public boolean onBackPressed(){ + public void onStop(){ + super.onStop(); Bundle result=new Bundle(); result.putParcelableArrayList("words", (ArrayList) data.stream().map(i->i.parentObject).map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new))); result.putStringArrayList("deleted", deletedItemIDs); setResult(true, result); - return false; } @Override @@ -259,6 +256,7 @@ public class FilterWordsFragment extends BaseSettingsFragment imp } itemsAdapter.notifyItemRangeChanged(0, data.size()); updateActionModeTitle(); + addBackCallback(actionModeDismisser); } private void leaveSelectionMode(boolean fromActionMode){ @@ -280,6 +278,7 @@ public class FilterWordsFragment extends BaseSettingsFragment imp data.set(i, newItem); } itemsAdapter.notifyItemRangeChanged(0, data.size()); + removeBackCallback(actionModeDismisser); } private void updateActionModeTitle(){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewer.java b/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewer.java index 4d22f5cb..19cec6b7 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewer.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewer.java @@ -54,6 +54,7 @@ import android.widget.SeekBar; import android.widget.TextView; import android.widget.Toast; import android.widget.Toolbar; +import android.window.OnBackInvokedDispatcher; import org.joinmastodon.android.R; import org.joinmastodon.android.api.MastodonAPIController; @@ -169,7 +170,7 @@ public class PhotoViewer implements ZoomPanView.Listener{ windowView=new FrameLayout(activity){ @Override public boolean dispatchKeyEvent(KeyEvent event){ - if(event.getKeyCode()==KeyEvent.KEYCODE_BACK){ + if(Build.VERSION.SDK_INT=30 ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS : WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; windowView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); wm.addView(windowView, wlp); + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){ + // TODO make use of the progress callback for nicer animation + windowView.findOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, ()->onStartSwipeToDismissTransition(0)); + } windowView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){ @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ToolbarDropdownMenuController.java b/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ToolbarDropdownMenuController.java index 520ef51e..e2e2b73e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ToolbarDropdownMenuController.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ToolbarDropdownMenuController.java @@ -10,6 +10,7 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.PixelFormat; import android.graphics.Rect; +import android.os.Build; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; @@ -19,6 +20,7 @@ import android.view.ViewTreeObserver; import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.Toolbar; +import android.window.OnBackInvokedDispatcher; import org.joinmastodon.android.R; import org.joinmastodon.android.ui.OutlineProviders; @@ -83,6 +85,15 @@ public class ToolbarDropdownMenuController{ .withLayer() .start(); controllerStack.add(initialSubmenu); + + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){ + windowView.findOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, ()->{ + if(controllerStack.size()>1) + popSubmenuController(); + else + dismiss(); + }); + } } public void dismiss(){ @@ -243,7 +254,7 @@ public class ToolbarDropdownMenuController{ @Override public boolean dispatchKeyEvent(KeyEvent event){ - if(event.getKeyCode()==KeyEvent.KEYCODE_BACK){ + if(Build.VERSION.SDK_INT1) popSubmenuController(); From e66751dc067ff5a4885139c4132da82f6e71a5ff Mon Sep 17 00:00:00 2001 From: Grishka Date: Tue, 11 Jun 2024 11:35:23 +0300 Subject: [PATCH 8/8] Notifications fixes --- .../NotificationActionHandlerService.java | 8 +++- .../android/PushNotificationReceiver.java | 40 ++++++++++--------- mastodon/src/main/res/values/strings.xml | 4 ++ 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/mastodon/src/main/java/org/joinmastodon/android/NotificationActionHandlerService.java b/mastodon/src/main/java/org/joinmastodon/android/NotificationActionHandlerService.java index 74dbf160..6250d391 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/NotificationActionHandlerService.java +++ b/mastodon/src/main/java/org/joinmastodon/android/NotificationActionHandlerService.java @@ -6,6 +6,7 @@ import android.app.PendingIntent; import android.app.RemoteInput; import android.app.Service; import android.content.Intent; +import android.os.Bundle; import android.os.IBinder; import android.service.notification.StatusBarNotification; @@ -46,7 +47,12 @@ public class NotificationActionHandlerService extends Service{ NotificationManager nm=getSystemService(NotificationManager.class); StatusBarNotification notification=findNotification(notificationTag); if("reply".equals(action)){ - CharSequence replyText=RemoteInput.getResultsFromIntent(intent).getCharSequence("replyText"); + Bundle remoteInputResults=RemoteInput.getResultsFromIntent(intent); + if(remoteInputResults==null){ + maybeStopSelf(); + return START_NOT_STICKY; + } + CharSequence replyText=remoteInputResults.getCharSequence("replyText"); if(replyText==null){ maybeStopSelf(); return START_NOT_STICKY; diff --git a/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java b/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java index 85243137..d7d18c3d 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java +++ b/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java @@ -178,17 +178,19 @@ public class PushNotificationReceiver extends BroadcastReceiver{ } String replyPrefix=mentions.isEmpty() ? "" : TextUtils.join(" ", mentions)+" "; - Intent replyIntent=new Intent(context, NotificationActionHandlerService.class); - replyIntent.putExtra("action", "reply"); - replyIntent.putExtra("account", accountID); - replyIntent.putExtra("post", notification.status.id); - replyIntent.putExtra("notificationTag", notificationTag); - replyIntent.putExtra("visibility", notification.status.visibility.toString()); - replyIntent.putExtra("replyPrefix", replyPrefix); - builder.addAction(new Notification.Action.Builder(Icon.createWithResource(context, R.drawable.ic_reply_24px), - context.getString(R.string.button_reply), PendingIntent.getService(context, (accountID+pn.notificationId+"reply").hashCode(), replyIntent, PendingIntent.FLAG_UPDATE_CURRENT)) - .addRemoteInput(new RemoteInput.Builder("replyText").build()) - .build()); + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){ + Intent replyIntent=new Intent(context, NotificationActionHandlerService.class); + replyIntent.putExtra("action", "reply"); + replyIntent.putExtra("account", accountID); + replyIntent.putExtra("post", notification.status.id); + replyIntent.putExtra("notificationTag", notificationTag); + replyIntent.putExtra("visibility", notification.status.visibility.toString()); + replyIntent.putExtra("replyPrefix", replyPrefix); + builder.addAction(new Notification.Action.Builder(Icon.createWithResource(context, R.drawable.ic_reply_24px), + context.getString(R.string.button_reply), PendingIntent.getService(context, (accountID+pn.notificationId+"reply").hashCode(), replyIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE)) + .addRemoteInput(new RemoteInput.Builder("replyText").setLabel(context.getString(R.string.button_reply)).build()) + .build()); + } Intent favIntent=new Intent(context, NotificationActionHandlerService.class); favIntent.putExtra("action", "favorite"); @@ -196,7 +198,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{ favIntent.putExtra("post", notification.status.id); favIntent.putExtra("notificationTag", notificationTag); builder.addAction(new Notification.Action.Builder(Icon.createWithResource(context, R.drawable.ic_star_24px), - context.getString(R.string.button_favorite), PendingIntent.getService(context, (accountID+pn.notificationId+"favorite").hashCode(), favIntent, PendingIntent.FLAG_UPDATE_CURRENT)).build()); + context.getString(R.string.button_favorite), PendingIntent.getService(context, (accountID+pn.notificationId+"favorite").hashCode(), favIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)).build()); PendingIntent boostActionIntent; if(notification.status.visibility!=StatusPrivacy.DIRECT){ @@ -205,7 +207,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{ boostIntent.putExtra("account", accountID); boostIntent.putExtra("post", notification.status.id); boostIntent.putExtra("notificationTag", notificationTag); - boostActionIntent=PendingIntent.getService(context, (accountID+pn.notificationId+"boost").hashCode(), boostIntent, PendingIntent.FLAG_UPDATE_CURRENT); + boostActionIntent=PendingIntent.getService(context, (accountID+pn.notificationId+"boost").hashCode(), boostIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); }else{ boostActionIntent=null; } @@ -216,13 +218,15 @@ public class PushNotificationReceiver extends BroadcastReceiver{ StatusBarNotification[] activeNotifications=nm.getActiveNotifications(); ArrayList summaryLines=new ArrayList<>(); + int notificationCount=0; for(StatusBarNotification sbn:activeNotifications){ String tag=sbn.getTag(); if(tag!=null && tag.startsWith(accountID+"_")){ if((sbn.getNotification().flags & Notification.FLAG_GROUP_SUMMARY)==0){ - summaryLines.add(sbn.getNotification().extras.getString("android.title")); - if(summaryLines.size()==5) - break; + if(summaryLines.size()<5){ + summaryLines.add(sbn.getNotification().extras.getString("android.title")); + } + notificationCount++; } } } @@ -239,8 +243,8 @@ public class PushNotificationReceiver extends BroadcastReceiver{ for(String line:summaryLines){ inboxStyle.addLine(line); } - summaryBuilder.setContentTitle("content title") - .setContentText("content text") + summaryBuilder.setContentTitle(context.getString(R.string.app_name)) + .setContentText(context.getResources().getQuantityString(R.plurals.x_new_notifications, notificationCount, notificationCount)) .setSmallIcon(R.drawable.ic_ntf_logo) .setColor(context.getColor(R.color.primary_700)) .setContentIntent(PendingIntent.getActivity(context, accountID.hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT)) diff --git a/mastodon/src/main/res/values/strings.xml b/mastodon/src/main/res/values/strings.xml index efc4b9d2..a7967c88 100644 --- a/mastodon/src/main/res/values/strings.xml +++ b/mastodon/src/main/res/values/strings.xml @@ -762,4 +762,8 @@ This username is taken. Try a different one or <a>pick a different server</a>. That doesn’t look like a valid email address. Email address is already in use. Did you <a>forget your password</a>? + + %,d new notification + %,d new notifications + \ No newline at end of file