Initial implementation of donations
This commit is contained in:
@@ -24,7 +24,6 @@ import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
@@ -45,8 +44,8 @@ import me.grishka.appkit.utils.WorkerThread;
|
||||
public class CacheController{
|
||||
private static final String TAG="CacheController";
|
||||
private static final int DB_VERSION=3;
|
||||
private static final WorkerThread databaseThread=new WorkerThread("databaseThread");
|
||||
private static final Handler uiHandler=new Handler(Looper.getMainLooper());
|
||||
public static final WorkerThread databaseThread=new WorkerThread("databaseThread");
|
||||
public static final Handler uiHandler=new Handler(Looper.getMainLooper());
|
||||
|
||||
private final String accountID;
|
||||
private DatabaseHelper db;
|
||||
@@ -467,9 +466,4 @@ public class CacheController{
|
||||
db.execSQL("ALTER TABLE `notifications_mentions` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface DatabaseRunnable{
|
||||
void run(SQLiteDatabase db) throws IOException;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.joinmastodon.android.api;
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface DatabaseRunnable{
|
||||
void run(SQLiteDatabase db) throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.joinmastodon.android.api.requests.catalog;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.donations.DonationCampaign;
|
||||
|
||||
public class GetDonationCampaigns extends MastodonAPIRequest<DonationCampaign>{
|
||||
private final String locale, seed;
|
||||
|
||||
public GetDonationCampaigns(String locale, String seed){
|
||||
super(HttpMethod.GET, null, DonationCampaign.class);
|
||||
this.locale=locale;
|
||||
this.seed=seed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getURL(){
|
||||
Uri.Builder builder=new Uri.Builder()
|
||||
.scheme("https")
|
||||
.authority("api.joinmastodon.org")
|
||||
.path("/donations/campaigns")
|
||||
.appendQueryParameter("platform", "android")
|
||||
.appendQueryParameter("locale", locale)
|
||||
.appendQueryParameter("seed", seed);
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
@@ -276,4 +275,12 @@ public class AccountSession{
|
||||
public void setNotificationsMentionsOnly(boolean mentionsOnly){
|
||||
getRawLocalPreferences().edit().putBoolean("notificationsMentionsOnly", mentionsOnly).apply();
|
||||
}
|
||||
|
||||
public boolean isEligibleForDonations(){
|
||||
return "mastodon.social".equalsIgnoreCase(domain) || "mastodon.online".equalsIgnoreCase(domain);
|
||||
}
|
||||
|
||||
public int getDonationSeed(){
|
||||
return Math.abs(getFullUsername().hashCode())%100;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,16 @@ package org.joinmastodon.android.api.session;
|
||||
import android.app.Activity;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
@@ -18,11 +23,13 @@ import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.CacheController;
|
||||
import org.joinmastodon.android.api.DatabaseRunnable;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
|
||||
import org.joinmastodon.android.api.requests.filters.GetLegacyFilters;
|
||||
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
|
||||
import org.joinmastodon.android.events.EmojiUpdatedEvent;
|
||||
@@ -30,9 +37,10 @@ import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiCategory;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
@@ -60,6 +68,7 @@ public class AccountSessionManager{
|
||||
private static final String TAG="AccountSessionManager";
|
||||
public static final String SCOPE="read write follow push";
|
||||
public static final String REDIRECT_URI="mastodon-android-auth://callback";
|
||||
private static final int DB_VERSION=1;
|
||||
|
||||
private static final AccountSessionManager instance=new AccountSessionManager();
|
||||
|
||||
@@ -73,6 +82,8 @@ public class AccountSessionManager{
|
||||
private String lastActiveAccountID;
|
||||
private SharedPreferences prefs;
|
||||
private boolean loadedInstances;
|
||||
private DatabaseHelper db;
|
||||
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
||||
|
||||
public static AccountSessionManager getInstance(){
|
||||
return instance;
|
||||
@@ -442,6 +453,68 @@ public class AccountSessionManager{
|
||||
}
|
||||
}
|
||||
|
||||
private void closeDelayed(){
|
||||
CacheController.databaseThread.postRunnable(databaseCloseRunnable, 10_000);
|
||||
}
|
||||
|
||||
public void closeDatabase(){
|
||||
if(db!=null){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.d(TAG, "closeDatabase");
|
||||
db.close();
|
||||
db=null;
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelDelayedClose(){
|
||||
if(db!=null){
|
||||
CacheController.databaseThread.handler.removeCallbacks(databaseCloseRunnable);
|
||||
}
|
||||
}
|
||||
|
||||
private SQLiteDatabase getOrOpenDatabase(){
|
||||
if(db==null)
|
||||
db=new DatabaseHelper();
|
||||
return db.getWritableDatabase();
|
||||
}
|
||||
|
||||
private void runOnDbThread(DatabaseRunnable r){
|
||||
cancelDelayedClose();
|
||||
CacheController.databaseThread.postRunnable(()->{
|
||||
try{
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
r.run(db);
|
||||
}catch(SQLiteException|IOException x){
|
||||
Log.w(TAG, x);
|
||||
}finally{
|
||||
closeDelayed();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public void runIfDonationCampaignNotDismissed(String id, Runnable action){
|
||||
runOnDbThread(db->{
|
||||
try(Cursor cursor=db.query("dismissed_donation_campaigns", null, "id=?", new String[]{id}, null, null, null)){
|
||||
if(!cursor.moveToFirst()){
|
||||
UiUtils.runOnUiThread(action);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void markDonationCampaignAsDismissed(String id){
|
||||
runOnDbThread(db->{
|
||||
ContentValues values=new ContentValues();
|
||||
values.put("id", id);
|
||||
values.put("dismissed_at", System.currentTimeMillis());
|
||||
db.insert("dismissed_donation_campaigns", null, values);
|
||||
});
|
||||
}
|
||||
|
||||
public void clearDismissedDonationCampaigns(){
|
||||
runOnDbThread(db->db.delete("dismissed_donation_campaigns", null, null));
|
||||
}
|
||||
|
||||
private static class SessionsStorageWrapper{
|
||||
public List<AccountSession> accounts;
|
||||
}
|
||||
@@ -451,4 +524,24 @@ public class AccountSessionManager{
|
||||
public List<Emoji> emojis;
|
||||
public long lastUpdated;
|
||||
}
|
||||
|
||||
private static class DatabaseHelper extends SQLiteOpenHelper{
|
||||
public DatabaseHelper(){
|
||||
super(MastodonApp.context, "accounts.db", null, DB_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db){
|
||||
db.execSQL("""
|
||||
CREATE TABLE `dismissed_donation_campaigns` (
|
||||
`id` text PRIMARY KEY,
|
||||
`dismissed_at` bigint
|
||||
)""");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class DismissDonationCampaignBannerEvent{
|
||||
public final String campaignID;
|
||||
|
||||
public DismissDonationCampaignBannerEvent(String campaignID){
|
||||
this.campaignID=campaignID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.webkit.WebResourceRequest;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.DismissDonationCampaignBannerEvent;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
|
||||
public class DonationWebViewFragment extends WebViewFragment{
|
||||
public static final String SUCCESS_URL="https://sponsor.joinmastodon.org/donation/success";
|
||||
public static final String FAILURE_URL="https://sponsor.joinmastodon.org/donation/failure";
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
webView.loadUrl(Objects.requireNonNull(getArguments().getString("url")));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldOverrideUrlLoading(WebResourceRequest req){
|
||||
String url=req.getUrl().buildUpon().clearQuery().fragment(null).build().toString();
|
||||
if(url.equalsIgnoreCase(SUCCESS_URL)){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle("Success")
|
||||
.setMessage("Some sort of UI that would tell the user that their payment was successful")
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setOnDismissListener(dlg->Nav.finish(this))
|
||||
.show();
|
||||
String campaignID=getArguments().getString("campaignID");
|
||||
AccountSessionManager.getInstance().markDonationCampaignAsDismissed(campaignID);
|
||||
E.post(new DismissDonationCampaignBannerEvent(campaignID));
|
||||
return true;
|
||||
}else if(url.equalsIgnoreCase(FAILURE_URL)){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle("Failure")
|
||||
.setMessage("Some sort of UI that would tell the user that their payment didn't go through")
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setOnDismissListener(dlg->Nav.finish(this))
|
||||
.show();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,19 @@ import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.text.style.UnderlineSpan;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewStub;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.Button;
|
||||
@@ -29,11 +35,13 @@ import com.squareup.otto.Subscribe;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.catalog.GetDonationCampaigns;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.DismissDonationCampaignBannerEvent;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
@@ -41,8 +49,10 @@ import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.model.donations.DonationCampaign;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.sheets.DonationSheet;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.ui.viewcontrollers.HomeTimelineMenuController;
|
||||
import org.joinmastodon.android.ui.viewcontrollers.ToolbarDropdownMenuController;
|
||||
@@ -53,6 +63,7 @@ import org.parceler.Parcels;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -81,9 +92,12 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||
private FollowList currentList;
|
||||
private MergeRecyclerAdapter mergeAdapter;
|
||||
private DiscoverInfoBannerHelper localTimelineBannerHelper;
|
||||
private View donationBanner;
|
||||
private boolean donationBannerDismissing;
|
||||
|
||||
private String maxID;
|
||||
private String lastSavedMarkerID;
|
||||
private DonationCampaign currentDonationCampaign;
|
||||
|
||||
public HomeTimelineFragment(){
|
||||
setListLayoutId(R.layout.fragment_timeline);
|
||||
@@ -93,6 +107,23 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
localTimelineBannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE, accountID);
|
||||
|
||||
// TODO how often do we do this request? Maybe cache something somewhere?
|
||||
if(AccountSessionManager.get(accountID).isEligibleForDonations()){
|
||||
new GetDonationCampaigns(Locale.getDefault().toLanguageTag().replace('-', '_'), String.valueOf(AccountSessionManager.get(accountID).getDonationSeed()))
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(DonationCampaign result){
|
||||
if(result==null)
|
||||
return;
|
||||
AccountSessionManager.getInstance().runIfDonationCampaignNotDismissed(result.id, ()->showDonationBanner(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){}
|
||||
})
|
||||
.execNoAuth("");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -599,6 +630,12 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||
updateUpdateState(ev.state);
|
||||
}
|
||||
|
||||
public void onDismissDonationCampaignBanner(DismissDonationCampaignBannerEvent ev){
|
||||
if(currentDonationCampaign!=null && ev.campaignID.equals(currentDonationCampaign.id)){
|
||||
dismissDonationBanner();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
|
||||
return true;
|
||||
@@ -661,6 +698,75 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||
};
|
||||
}
|
||||
|
||||
private void showDonationBanner(DonationCampaign campaign){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
currentDonationCampaign=campaign;
|
||||
if(donationBanner==null){
|
||||
ViewStub stub=contentView.findViewById(R.id.donation_banner);
|
||||
donationBanner=stub.inflate();
|
||||
donationBanner.findViewById(R.id.banner_dismiss).setOnClickListener(v->{
|
||||
AccountSessionManager.getInstance().markDonationCampaignAsDismissed(currentDonationCampaign.id);
|
||||
dismissDonationBanner();
|
||||
});
|
||||
donationBanner.setOnClickListener(v->openDonationSheet());
|
||||
}else{
|
||||
donationBanner.setVisibility(View.VISIBLE);
|
||||
}
|
||||
TextView text=donationBanner.findViewById(R.id.banner_text);
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(campaign.bannerMessage);
|
||||
ssb.append(' ');
|
||||
int start=ssb.length();
|
||||
ssb.append(campaign.bannerButtonText);
|
||||
ssb.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.masterialDark_colorGoldenrodContainer, getActivity().getTheme())), start, ssb.length(), 0);
|
||||
ssb.setSpan(new UnderlineSpan(), start, ssb.length(), 0);
|
||||
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), start, ssb.length(), 0);
|
||||
text.setText(ssb);
|
||||
donationBanner.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
donationBanner.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(donationBanner, View.TRANSLATION_Y, donationBanner.getHeight(), 0),
|
||||
ObjectAnimator.ofFloat(fab, View.TRANSLATION_Y, -donationBanner.getHeight())
|
||||
);
|
||||
set.setDuration(250);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.start();
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void dismissDonationBanner(){
|
||||
if(donationBanner==null || donationBannerDismissing)
|
||||
return;
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(donationBanner, View.TRANSLATION_Y, donationBanner.getHeight()),
|
||||
ObjectAnimator.ofFloat(fab, View.TRANSLATION_Y, 0)
|
||||
);
|
||||
set.setDuration(250);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
donationBanner.setVisibility(View.GONE);
|
||||
donationBannerDismissing=false;
|
||||
}
|
||||
});
|
||||
donationBannerDismissing=true;
|
||||
set.start();
|
||||
currentDonationCampaign=null;
|
||||
}
|
||||
|
||||
private void openDonationSheet(){
|
||||
new DonationSheet(getActivity(), currentDonationCampaign, accountID).show();
|
||||
}
|
||||
|
||||
private enum ListMode{
|
||||
FOLLOWING,
|
||||
LOCAL,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebResourceError;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.fragments.LoaderFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
|
||||
public abstract class WebViewFragment extends LoaderFragment implements OnBackPressedListener{
|
||||
protected WebView webView;
|
||||
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
webView=new WebView(getActivity());
|
||||
webView.setWebChromeClient(new WebChromeClient(){
|
||||
@Override
|
||||
public void onReceivedTitle(WebView view, String title){
|
||||
setTitle(title);
|
||||
}
|
||||
});
|
||||
webView.setWebViewClient(new WebViewClient(){
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url){
|
||||
dataLoaded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error){
|
||||
onError(new MastodonErrorResponse(error.getDescription().toString(), -1, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request){
|
||||
return WebViewFragment.this.shouldOverrideUrlLoading(request);
|
||||
}
|
||||
});
|
||||
webView.getSettings().setJavaScriptEnabled(true);
|
||||
return webView;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh(){
|
||||
webView.reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(webView.canGoBack()){
|
||||
webView.goBack();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToolbarNavigationClick(){
|
||||
Nav.finish(this);
|
||||
}
|
||||
|
||||
protected abstract boolean shouldOverrideUrlLoading(WebResourceRequest req);
|
||||
}
|
||||
@@ -28,7 +28,8 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
|
||||
selfUpdateItem=new ListItem<>("Force self-update", null, this::onForceSelfUpdateClick),
|
||||
resetUpdateItem=new ListItem<>("Reset self-updater", null, this::onResetUpdaterClick),
|
||||
new ListItem<>("Reset search info banners", null, this::onResetDiscoverBannersClick),
|
||||
new ListItem<>("Reset pre-reply sheets", null, this::onResetPreReplySheetsClick)
|
||||
new ListItem<>("Reset pre-reply sheets", null, this::onResetPreReplySheetsClick),
|
||||
new ListItem<>("Clear dismissed donation campaigns", null, this::onClearDismissedCampaignsClick)
|
||||
));
|
||||
if(!GithubSelfUpdater.needSelfUpdating()){
|
||||
resetUpdateItem.isEnabled=selfUpdateItem.isEnabled=false;
|
||||
@@ -70,6 +71,11 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
|
||||
Toast.makeText(getActivity(), "Pre-reply sheets were reset", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void onClearDismissedCampaignsClick(ListItem<?> item){
|
||||
AccountSessionManager.getInstance().clearDismissedDonationCampaigns();
|
||||
Toast.makeText(getActivity(), "Dismissed campaigns cleared. Restart app to see your current campaign, if any", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void restartUI(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.joinmastodon.android.model.donations;
|
||||
|
||||
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.util.Map;
|
||||
|
||||
@AllFieldsAreRequired
|
||||
public class DonationCampaign extends BaseModel{
|
||||
public String id;
|
||||
public String bannerMessage;
|
||||
public String bannerButtonText;
|
||||
public String donationMessage;
|
||||
public String donationButtonText;
|
||||
public Amounts amounts;
|
||||
public String defaultCurrency;
|
||||
public String donationUrl;
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
super.postprocess();
|
||||
amounts.postprocess();
|
||||
}
|
||||
|
||||
public static class Amounts extends BaseModel{
|
||||
public Map<String, long[]> oneTime;
|
||||
@RequiredField
|
||||
public Map<String, long[]> monthly;
|
||||
public Map<String, long[]> yearly;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
package org.joinmastodon.android.ui.sheets;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.ToggleButton;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.DonationWebViewFragment;
|
||||
import org.joinmastodon.android.model.donations.DonationCampaign;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.CurrencyAmountInput;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Currency;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.BottomSheet;
|
||||
|
||||
public class DonationSheet extends BottomSheet{
|
||||
private final DonationCampaign campaign;
|
||||
private final String accountID;
|
||||
private DonationFrequency frequency=DonationFrequency.MONTHLY;
|
||||
|
||||
private View onceTab, monthlyTab, yearlyTab;
|
||||
private int currentTab;
|
||||
private CurrencyAmountInput amountField;
|
||||
private ToggleButton[] suggestedAmountButtons=new ToggleButton[6];
|
||||
private View button;
|
||||
private TextView buttonText;
|
||||
private Activity activity;
|
||||
|
||||
public DonationSheet(@NonNull Activity activity, DonationCampaign campaign, String accountID){
|
||||
super(activity);
|
||||
this.campaign=campaign;
|
||||
this.accountID=accountID;
|
||||
this.activity=activity;
|
||||
Context context=activity;
|
||||
|
||||
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_donation, null);
|
||||
setContentView(content);
|
||||
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
|
||||
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
|
||||
|
||||
TextView text=findViewById(R.id.text);
|
||||
text.setText(campaign.donationMessage);
|
||||
|
||||
onceTab=findViewById(R.id.once);
|
||||
monthlyTab=findViewById(R.id.monthly);
|
||||
yearlyTab=findViewById(R.id.yearly);
|
||||
onceTab.setOnClickListener(this::onTabClick);
|
||||
monthlyTab.setOnClickListener(this::onTabClick);
|
||||
yearlyTab.setOnClickListener(this::onTabClick);
|
||||
|
||||
if(campaign.amounts.yearly==null)
|
||||
yearlyTab.setVisibility(View.GONE);
|
||||
if(campaign.amounts.oneTime==null)
|
||||
onceTab.setVisibility(View.GONE);
|
||||
if(campaign.amounts.monthly==null){
|
||||
monthlyTab.setVisibility(View.GONE);
|
||||
if(campaign.amounts.oneTime!=null){
|
||||
onceTab.setSelected(true);
|
||||
currentTab=R.id.once;
|
||||
frequency=DonationFrequency.ONCE;
|
||||
}else if(campaign.amounts.yearly!=null){
|
||||
yearlyTab.setSelected(true);
|
||||
currentTab=R.id.yearly;
|
||||
frequency=DonationFrequency.YEARLY;
|
||||
}else{
|
||||
Toast.makeText(context, "Amounts object is empty", Toast.LENGTH_SHORT).show();
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
}else{
|
||||
monthlyTab.setSelected(true);
|
||||
currentTab=R.id.monthly;
|
||||
}
|
||||
|
||||
|
||||
View tabBarItself=findViewById(R.id.tabbar_inner);
|
||||
tabBarItself.setOutlineProvider(OutlineProviders.roundedRect(20));
|
||||
tabBarItself.setClipToOutline(true);
|
||||
|
||||
amountField=findViewById(R.id.amount);
|
||||
List<String> availableCurrencies=campaign.amounts.monthly.keySet().stream().sorted().collect(Collectors.toList());
|
||||
amountField.setCurrencies(availableCurrencies);
|
||||
try{
|
||||
amountField.setSelectedCurrency(campaign.defaultCurrency);
|
||||
}catch(IllegalArgumentException x){
|
||||
new M3AlertDialogBuilder(context)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage("Default currency "+campaign.defaultCurrency+" not in list of available currencies "+availableCurrencies)
|
||||
.show();
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
amountField.setChangeListener(new CurrencyAmountInput.ChangeListener(){
|
||||
@Override
|
||||
public void onCurrencyChanged(String code){
|
||||
updateSuggestedAmounts(code);
|
||||
button.setEnabled(amountField.getAmount()>=getMinimumChargeAmount(code));
|
||||
updateSuggestedButtonsState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAmountChanged(long amount){
|
||||
button.setEnabled(amount>=getMinimumChargeAmount(amountField.getCurrency()));
|
||||
updateSuggestedButtonsState();
|
||||
}
|
||||
});
|
||||
button=findViewById(R.id.button);
|
||||
buttonText=findViewById(R.id.button_text);
|
||||
|
||||
LinearLayout suggestedAmounts=findViewById(R.id.suggested_amounts);
|
||||
for(int i=0;i<suggestedAmountButtons.length;i++){
|
||||
ToggleButton btn=new ToggleButton(context);
|
||||
btn.setBackgroundResource(R.drawable.bg_filter_chip);
|
||||
btn.setTextAppearance(R.style.m3_label_large);
|
||||
btn.setTextColor(context.getResources().getColorStateList(R.color.filter_chip_text, context.getTheme()));
|
||||
btn.setMinWidth(V.dp(64));
|
||||
btn.setMinimumWidth(0);
|
||||
int pad=V.dp(16);
|
||||
btn.setPadding(pad, 0, pad, 0);
|
||||
btn.setStateListAnimator(null);
|
||||
btn.setTextOff(null);
|
||||
btn.setTextOn(null);
|
||||
btn.setOnClickListener(this::onSuggestedAmountClick);
|
||||
btn.setTag(i);
|
||||
suggestedAmountButtons[i]=btn;
|
||||
LinearLayout.LayoutParams lp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
lp.rightMargin=V.dp(8);
|
||||
suggestedAmounts.addView(btn, lp);
|
||||
}
|
||||
updateSuggestedAmounts(campaign.defaultCurrency);
|
||||
button.setEnabled(false);
|
||||
buttonText.setText(campaign.bannerButtonText);
|
||||
button.setOnClickListener(v->openWebView());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
Window window=getWindow();
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
|
||||
}
|
||||
|
||||
private void onTabClick(View v){
|
||||
if(v.getId()==currentTab)
|
||||
return;
|
||||
findViewById(currentTab).setSelected(false);
|
||||
v.setSelected(true);
|
||||
currentTab=v.getId();
|
||||
if(currentTab==R.id.once)
|
||||
frequency=DonationFrequency.ONCE;
|
||||
else if(currentTab==R.id.monthly)
|
||||
frequency=DonationFrequency.MONTHLY;
|
||||
else if(currentTab==R.id.yearly)
|
||||
frequency=DonationFrequency.YEARLY;
|
||||
updateSuggestedAmounts(amountField.getCurrency());
|
||||
}
|
||||
|
||||
private long[] getCurrentSuggestedAmounts(String currency){
|
||||
long[] amounts=(switch(frequency){
|
||||
case ONCE -> campaign.amounts.oneTime;
|
||||
case MONTHLY -> campaign.amounts.monthly;
|
||||
case YEARLY -> campaign.amounts.yearly;
|
||||
}).get(currency);
|
||||
if(amounts==null){
|
||||
amounts=new long[0];
|
||||
}
|
||||
return amounts;
|
||||
}
|
||||
|
||||
private void updateSuggestedAmounts(String currency){
|
||||
NumberFormat format=NumberFormat.getCurrencyInstance();
|
||||
try{
|
||||
format.setCurrency(Currency.getInstance(currency));
|
||||
}catch(IllegalArgumentException ignore){}
|
||||
int defaultFractionDigits=format.getMinimumFractionDigits();
|
||||
long[] amounts=getCurrentSuggestedAmounts(currency);
|
||||
for(int i=0;i<suggestedAmountButtons.length;i++){
|
||||
ToggleButton btn=suggestedAmountButtons[i];
|
||||
if(i>=amounts.length){
|
||||
btn.setVisibility(View.GONE);
|
||||
continue;
|
||||
}
|
||||
btn.setVisibility(View.VISIBLE);
|
||||
long amount=amounts[i];
|
||||
format.setMinimumFractionDigits(amount%100==0 ? 0 : defaultFractionDigits);
|
||||
btn.setText(format.format(amount/100.0));
|
||||
}
|
||||
updateSuggestedButtonsState();
|
||||
}
|
||||
|
||||
private void onSuggestedAmountClick(View v){
|
||||
int index=(int) v.getTag();
|
||||
long[] amounts=getCurrentSuggestedAmounts(amountField.getCurrency());
|
||||
amountField.setAmount(amounts[index]);
|
||||
}
|
||||
|
||||
private void updateSuggestedButtonsState(){
|
||||
long amount=amountField.getAmount();
|
||||
long[] amounts=getCurrentSuggestedAmounts(amountField.getCurrency());
|
||||
for(int i=0;i<Math.min(amounts.length, suggestedAmountButtons.length);i++){
|
||||
ToggleButton btn=suggestedAmountButtons[i];
|
||||
btn.setChecked(amounts[i]==amount);
|
||||
}
|
||||
}
|
||||
|
||||
private void openWebView(){
|
||||
Uri.Builder builder=Uri.parse(campaign.donationUrl).buildUpon();
|
||||
builder.appendQueryParameter("locale", Locale.getDefault().toLanguageTag().replace('-', '_'))
|
||||
.appendQueryParameter("platform", "android")
|
||||
.appendQueryParameter("currency", amountField.getCurrency())
|
||||
.appendQueryParameter("amount", String.valueOf(amountField.getAmount()))
|
||||
.appendQueryParameter("source", "campaign")
|
||||
.appendQueryParameter("campaign_id", campaign.id)
|
||||
.appendQueryParameter("frequency", switch(frequency){
|
||||
case ONCE -> "one_time";
|
||||
case MONTHLY -> "monthly";
|
||||
case YEARLY -> "yearly";
|
||||
})
|
||||
.appendQueryParameter("success_callback_url", DonationWebViewFragment.SUCCESS_URL)
|
||||
.appendQueryParameter("failure_callback_url", DonationWebViewFragment.FAILURE_URL);
|
||||
Bundle args=new Bundle();
|
||||
args.putString("url", builder.build().toString());
|
||||
args.putString("account", accountID);
|
||||
args.putString("campaignID", campaign.id);
|
||||
Nav.go(activity, DonationWebViewFragment.class, args);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
private static long getMinimumChargeAmount(String currency){
|
||||
// https://docs.stripe.com/currencies#minimum-and-maximum-charge-amounts
|
||||
// values are in cents
|
||||
return switch(currency){
|
||||
case "USD" -> 50;
|
||||
case "AED" -> 2_00;
|
||||
case "AUD" -> 50;
|
||||
case "BGN" -> 1_00;
|
||||
case "BRL" -> 50;
|
||||
case "CAD" -> 50;
|
||||
case "CHF" -> 50;
|
||||
case "CZK" -> 15_00;
|
||||
case "DKK" -> 2_50;
|
||||
case "EUR" -> 50;
|
||||
case "GBP" -> 30;
|
||||
case "HKD" -> 4_00;
|
||||
case "HUF" -> 175_00;
|
||||
case "INR" -> 50;
|
||||
case "JPY" -> 50_00;
|
||||
case "MXN" -> 10_00;
|
||||
case "MYR" -> 2_00;
|
||||
case "NOK" -> 3_00;
|
||||
case "NZD" -> 50;
|
||||
case "PLN" -> 2_00;
|
||||
case "RON" -> 2_00;
|
||||
case "SEK" -> 3_00;
|
||||
case "SGD" -> 50;
|
||||
case "THB" -> 10_00;
|
||||
|
||||
default -> 50;
|
||||
};
|
||||
}
|
||||
|
||||
private enum DonationFrequency{
|
||||
ONCE,
|
||||
MONTHLY,
|
||||
YEARLY
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.text.Editable;
|
||||
import android.text.InputFilter;
|
||||
import android.text.InputType;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.method.DigitsKeyListener;
|
||||
import android.text.style.ReplacementSpan;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.text.ParseException;
|
||||
import java.util.Currency;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.CustomViewHelper;
|
||||
|
||||
public class CurrencyAmountInput extends LinearLayout implements CustomViewHelper{
|
||||
private ActualEditText edit;
|
||||
private Button currencyBtn;
|
||||
private List<CurrencyInfo> currencies;
|
||||
private CurrencyInfo currentCurrency;
|
||||
private boolean spanAdded;
|
||||
private CurrencySymbolSpan symbolSpan;
|
||||
private boolean symbolBeforeAmount;
|
||||
private ChangeListener changeListener;
|
||||
private long lastAmount=0;
|
||||
private NumberFormat numberFormat=NumberFormat.getNumberInstance();
|
||||
private boolean allowSymbolToBeDeleted;
|
||||
|
||||
public CurrencyAmountInput(Context context){
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public CurrencyAmountInput(Context context, AttributeSet attrs){
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public CurrencyAmountInput(Context context, AttributeSet attrs, int defStyle){
|
||||
super(context, attrs, defStyle);
|
||||
setForeground(getResources().getDrawable(R.drawable.fg_currency_input, context.getTheme()));
|
||||
setAddStatesFromChildren(true);
|
||||
|
||||
if(!isInEditMode())
|
||||
setOutlineProvider(OutlineProviders.roundedRect(8));
|
||||
setClipToOutline(true);
|
||||
|
||||
currencyBtn=new Button(context);
|
||||
currencyBtn.setTextAppearance(R.style.m3_label_large);
|
||||
currencyBtn.setSingleLine();
|
||||
currencyBtn.setTextColor(UiUtils.getThemeColor(context, R.attr.colorM3OnSurfaceVariant));
|
||||
int pad=dp(12);
|
||||
currencyBtn.setPadding(pad, 0, pad, 0);
|
||||
currencyBtn.setBackgroundColor(UiUtils.getThemeColor(context, R.attr.colorM3SurfaceVariant));
|
||||
currencyBtn.setMinimumWidth(0);
|
||||
currencyBtn.setMinWidth(0);
|
||||
currencyBtn.setOnClickListener(v->showCurrencySelector());
|
||||
addView(currencyBtn, new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
|
||||
edit=new ActualEditText(context);
|
||||
edit.setBackgroundColor(UiUtils.getThemeColor(context, R.attr.colorM3Surface));
|
||||
pad=dp(16);
|
||||
edit.setPadding(pad, 0, pad, 0);
|
||||
edit.setSingleLine();
|
||||
edit.setTextAppearance(R.style.m3_title_large);
|
||||
edit.setTextColor(UiUtils.getThemeColor(context, R.attr.colorM3OnSurface));
|
||||
edit.setGravity(Gravity.END |Gravity.CENTER_VERTICAL);
|
||||
edit.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
|
||||
InputFilter[] filters=edit.getText().getFilters();
|
||||
for(int i=0;i<filters.length;i++){
|
||||
if(filters[i] instanceof DigitsKeyListener){
|
||||
filters[i]=new FormattingFriendlyDigitsKeyListener();
|
||||
edit.getText().setFilters(filters);
|
||||
break;
|
||||
}
|
||||
}
|
||||
addView(edit, new LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
||||
symbolSpan=new CurrencySymbolSpan(edit.getPaint());
|
||||
|
||||
NumberFormat format=NumberFormat.getInstance();
|
||||
String one=format.format(1);
|
||||
format=NumberFormat.getCurrencyInstance();
|
||||
format.setCurrency(Currency.getInstance("USD"));
|
||||
symbolBeforeAmount=format.format(1).indexOf(one)>0;
|
||||
|
||||
edit.addTextChangedListener(new TextWatcher(){
|
||||
private boolean ignore;
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable e){
|
||||
if(ignore)
|
||||
return;
|
||||
ignore=true;
|
||||
if(e.length()>0 && !spanAdded){
|
||||
SpannableString ss=new SpannableString(" ");
|
||||
ss.setSpan(symbolSpan, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
if(symbolBeforeAmount)
|
||||
e.insert(0, ss);
|
||||
else
|
||||
e.append(ss);
|
||||
spanAdded=true;
|
||||
}else if(spanAdded && e.length()<=1){
|
||||
spanAdded=false;
|
||||
if(e.length()>0){
|
||||
allowSymbolToBeDeleted=true;
|
||||
e.clear();
|
||||
allowSymbolToBeDeleted=false;
|
||||
}
|
||||
}
|
||||
ignore=false;
|
||||
|
||||
updateAmount();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setCurrencies(List<String> currencies){
|
||||
this.currencies=currencies.stream().map(CurrencyInfo::new).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public void setSelectedCurrency(String code){
|
||||
CurrencyInfo info=null;
|
||||
for(CurrencyInfo c:currencies){
|
||||
if(c.code.equals(code)){
|
||||
info=c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(info==null)
|
||||
throw new IllegalArgumentException();
|
||||
setCurrency(info);
|
||||
}
|
||||
|
||||
private void setCurrency(CurrencyInfo info){
|
||||
currencyBtn.setText(info.code);
|
||||
currentCurrency=info;
|
||||
edit.invalidate();
|
||||
if(changeListener!=null)
|
||||
changeListener.onCurrencyChanged(info.code);
|
||||
}
|
||||
|
||||
private void showCurrencySelector(){
|
||||
ArrayAdapter<CurrencyInfo> adapter=new ArrayAdapter<>(getContext(), R.layout.item_alert_single_choice_2lines_but_different, R.id.text, currencies){
|
||||
@Override
|
||||
public boolean hasStableIds(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent){
|
||||
View view=super.getView(position, convertView, parent);
|
||||
TextView subtitle=view.findViewById(R.id.subtitle);
|
||||
CurrencyInfo item=getItem(position);
|
||||
if(item.jCurrency==null || item.jCurrency.getDisplayName().equals(item.code)){
|
||||
subtitle.setVisibility(View.GONE);
|
||||
}else{
|
||||
subtitle.setVisibility(View.VISIBLE);
|
||||
subtitle.setText(item.jCurrency.getDisplayName());
|
||||
}
|
||||
return view;
|
||||
}
|
||||
};
|
||||
new M3AlertDialogBuilder(getContext())
|
||||
.setTitle(R.string.currency)
|
||||
.setSingleChoiceItems(adapter, currencies.indexOf(currentCurrency), (dlg, item)->{
|
||||
setCurrency(currencies.get(item));
|
||||
dlg.dismiss();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
public void setChangeListener(ChangeListener changeListener){
|
||||
this.changeListener=changeListener;
|
||||
}
|
||||
|
||||
private void updateAmount(){
|
||||
long newAmount;
|
||||
try{
|
||||
Number n=numberFormat.parse(edit.getText().toString().trim());
|
||||
if(n instanceof Long l){
|
||||
newAmount=l*100L;
|
||||
}else if(n instanceof Double d){
|
||||
newAmount=(long)(d*100);
|
||||
}else{
|
||||
newAmount=0;
|
||||
}
|
||||
}catch(ParseException x){
|
||||
newAmount=0;
|
||||
}
|
||||
if(newAmount!=lastAmount){
|
||||
lastAmount=newAmount;
|
||||
if(changeListener!=null)
|
||||
changeListener.onAmountChanged(lastAmount);
|
||||
}
|
||||
}
|
||||
|
||||
public long getAmount(){
|
||||
return lastAmount;
|
||||
}
|
||||
|
||||
public String getCurrency(){
|
||||
return currentCurrency.code;
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public void setAmount(long amount){
|
||||
String value;
|
||||
if(amount%100==0)
|
||||
value=String.valueOf(amount/100);
|
||||
else
|
||||
value=String.format("%.2f", amount/100.0);
|
||||
int start=spanAdded ? 1 : 0;
|
||||
edit.getText().replace(start, edit.length(), value);
|
||||
}
|
||||
|
||||
private class ActualEditText extends EditText{
|
||||
public ActualEditText(Context context){
|
||||
super(context);
|
||||
setClipToPadding(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSelectionChanged(int selStart, int selEnd){
|
||||
super.onSelectionChanged(selStart, selEnd);
|
||||
// Adjust the selection to prevent the symbol span being selected
|
||||
if(spanAdded){
|
||||
int newSelStart=symbolBeforeAmount ? Math.max(selStart, 1) : Math.min(selStart, length()-1);
|
||||
int newSelEnd=symbolBeforeAmount ? Math.max(selEnd, 1) : Math.min(selEnd, length()-1);
|
||||
if(newSelStart!=selStart || newSelEnd!=selEnd){
|
||||
setSelection(newSelStart, newSelEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class CurrencyInfo{
|
||||
public String code;
|
||||
public String symbol;
|
||||
public Currency jCurrency;
|
||||
|
||||
public CurrencyInfo(String code){
|
||||
this.code=code;
|
||||
try{
|
||||
jCurrency=Currency.getInstance(code);
|
||||
symbol=jCurrency.getSymbol();
|
||||
}catch(IllegalArgumentException x){
|
||||
symbol=code;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString(){
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
private class CurrencySymbolSpan extends ReplacementSpan{
|
||||
private Paint paint;
|
||||
public CurrencySymbolSpan(Paint paint){
|
||||
this.paint=new Paint(paint);
|
||||
this.paint.setTextSize(paint.getTextSize()*0.66f);
|
||||
this.paint.setAlpha(77);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm){
|
||||
return Math.round(this.paint.measureText(currentCurrency.symbol))+dp(2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint){
|
||||
if(!symbolBeforeAmount)
|
||||
x+=dp(2);
|
||||
canvas.drawText(currentCurrency.symbol, x, top+dp(1.5f)-this.paint.ascent(), this.paint);
|
||||
}
|
||||
}
|
||||
|
||||
private class FormattingFriendlyDigitsKeyListener extends DigitsKeyListener{
|
||||
public FormattingFriendlyDigitsKeyListener(){
|
||||
super(false, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend){
|
||||
// Allow the currency symbol to be inserted (always done as a separate insertion operation)
|
||||
if(source instanceof Spannable s && s.getSpans(start, end, CurrencySymbolSpan.class).length>0){
|
||||
return source;
|
||||
}
|
||||
// Don't allow the currency symbol to be deleted
|
||||
if(!allowSymbolToBeDeleted && end-start<dend-dstart && dest.getSpans(dstart, dend, CurrencySymbolSpan.class).length>0){
|
||||
return dest.subSequence(dstart, dend);
|
||||
}
|
||||
return super.filter(source, start, end, dest, dstart, dend);
|
||||
}
|
||||
}
|
||||
|
||||
public interface ChangeListener{
|
||||
void onCurrencyChanged(String code);
|
||||
void onAmountChanged(long amount);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user