Fix Nitter instances

This commit is contained in:
Thomas 2025-03-04 15:19:51 +01:00
parent fae30e63a8
commit 8d3621dbe7
6 changed files with 197 additions and 132 deletions

View file

@ -14,10 +14,12 @@ package app.fedilab.android.mastodon.client.entities.nitter;
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.Context;
import androidx.annotation.NonNull;
import org.jsoup.select.Elements;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.ElementList;
import org.simpleframework.xml.Namespace;
@ -29,6 +31,7 @@ import java.net.IDN;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -82,7 +85,8 @@ public class Nitter implements Serializable {
status.text = feedItem.title;
status.content = status.content.replaceAll("<img [^>]*src=\"[^\"]+\"[^>]*>", "");
status.visibility = "public";
status.created_at = Helper.stringToDateWithFormat(context, feedItem.pubDate, "EEE, dd MMM yyyy HH:mm:ss zzz");
String dateFormat = "E', 'dd' 'MMM' 'yyyy' 'hh:m:s' GMT'";
status.created_at = Helper.stringToDateWithFormat(context, feedItem.pubDate, dateFormat);
status.uri = feedItem.guid;
status.url = feedItem.link;
if (!accounts.containsKey(feedItem.creator)) {
@ -177,4 +181,83 @@ public class Nitter implements Serializable {
}
public static Status nitterHTMLParser(Context context, org.jsoup.nodes.Element timelineItem, String nitterInstance) {
if(timelineItem == null) {
return null;
}
Status status = new Status();
Account account = new Account();
String fedilabInstance = "nitter.fedilab.app";
org.jsoup.nodes.Element messageLink;
if(timelineItem.select(".quote-text").html().isEmpty()) {
status.content = timelineItem.select(".tweet-content").html();
status.text = timelineItem.select(".tweet-content").text();
status.url = "https://"+ nitterInstance +timelineItem.select(".tweet-link").attr("href");
messageLink = timelineItem.select(".tweet-link").first();
} else {
status.content = timelineItem.select(".quote-text").html();
status.text = timelineItem.select(".quote-text").text();
status.url = "https://"+ nitterInstance +timelineItem.select(".quote-link").attr("href");
messageLink = timelineItem.select(".quote-link").first();
}
status.uri = status.url;
String status_id = String.valueOf(ThreadLocalRandom.current().nextLong(10,10000000));;
if(messageLink != null){
String[] splitLink = messageLink.attr("href").split("/");
status_id = splitLink[splitLink.length-1];
}
String pubDate = timelineItem.select(".tweet-date").select("a").attr("title");
org.jsoup.nodes.Element nameElement = timelineItem.select(".fullname").first();
String name = nameElement!= null?nameElement.text():"";
org.jsoup.nodes.Element userNameElement = timelineItem.select(".username").first();
String userName = userNameElement!= null?userNameElement.text().replace("@",""):"";
String avatar = "https://" + fedilabInstance + timelineItem.select(".avatar").attr("src");
account.id = userName;
account.acct = userName;
if(timelineItem.select(".replying-to").html().isEmpty()) {
account.username = userName;
account.display_name = name;
} else {
account.display_name = timelineItem.select(".fullname").text() +"&nbsp;" +timelineItem.select(".replying-to").text();
}
account.avatar = avatar;
account.avatar_static = avatar;
account.url = "https://"+ nitterInstance +"/" + userName;
status.id = status_id;
status.account = account;
Elements imageElements = timelineItem.select(".attachments").select("img");
Elements videoElements = timelineItem.select(".attachments").select("video");
ArrayList<Attachment> attachmentList = new ArrayList<>();
for(org.jsoup.nodes.Element imageElement: imageElements) {
Attachment attachment = new Attachment();
attachment.type = "image";
attachment.url = "https://"+fedilabInstance+imageElement.attr("src");
attachment.preview_url = "https://"+fedilabInstance+imageElement.attr("src");
attachment.id = imageElement.attr("src");
attachmentList.add(attachment);
}
for(org.jsoup.nodes.Element videoElement: videoElements) {
Attachment attachment = new Attachment();
attachment.type = "video";
attachment.url = "https://"+fedilabInstance+videoElement.child(0).attr("src");
attachment.preview_url = "https://"+fedilabInstance+videoElement.attr("poster");
attachment.id = videoElement.attr("poster");
attachmentList.add(attachment);
}
status.visibility = "public";
status.media_attachments = attachmentList;
String dateFormat = "MMM d', 'yyyy' · 'h:m a' UTC'";
status.created_at = Helper.stringToDateWithFormat(context, pubDate, dateFormat);
return status;
}
}

View file

@ -61,7 +61,6 @@ import android.provider.OpenableColumns;
import android.text.Html;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
@ -647,9 +646,9 @@ public class Helper {
*/
public static Date stringToDateWithFormat(Context context, String stringDate, String format) {
if (stringDate == null)
return null;
return new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat(format, Locale.US);
Date date = null;
Date date = new Date();
try {
date = dateFormat.parse(stringDate);
} catch (java.text.ParseException ignored) {

View file

@ -58,7 +58,6 @@ import android.os.Looper;
import android.text.Html;
import android.text.SpannableString;
import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.MotionEvent;
@ -567,15 +566,19 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
}
return;
}
Intent intent = new Intent(context, ContextActivity.class);
Bundle args = new Bundle();
args.putSerializable(Helper.ARG_STATUS, statusToDeal.quote);
new CachedBundle(context).insertBundle(args, Helper.getCurrentAccount(context), bundleId -> {
Bundle bundle = new Bundle();
bundle.putLong(Helper.ARG_INTENT_ID, bundleId);
intent.putExtras(bundle);
context.startActivity(intent);
});
if (!remote) {
Intent intent = new Intent(context, ContextActivity.class);
Bundle args = new Bundle();
args.putSerializable(Helper.ARG_STATUS, statusToDeal.quote);
new CachedBundle(context).insertBundle(args, Helper.getCurrentAccount(context), bundleId -> {
Bundle bundle = new Bundle();
bundle.putLong(Helper.ARG_INTENT_ID, bundleId);
intent.putExtras(bundle);
context.startActivity(intent);
});
} else {
Helper.openBrowser(context,statusToDeal.quote.url);
}
});
holder.binding.quotedMessage.cardviewContainer.setStrokeColor(ThemeHelper.getAttColor(context, R.attr.colorPrimary));
holder.binding.quotedMessage.statusContent.setText(
@ -1453,15 +1456,24 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
//--- BOOSTER INFO ---
if (status.reblog != null) {
MastodonHelper.loadPPMastodon(holder.binding.statusBoosterAvatar, status.account);
if(status.account.avatar != null) {
MastodonHelper.loadPPMastodon(holder.binding.statusBoosterAvatar, status.account);
holder.binding.statusBoosterAvatar.setVisibility(View.VISIBLE);
} else {
holder.binding.statusBoosterAvatar.setVisibility(View.GONE);
}
holder.binding.statusBoosterDisplayName.setText(
status.account.getSpanDisplayName(context,
new WeakReference<>(holder.binding.statusBoosterDisplayName)),
TextView.BufferType.SPANNABLE);
holder.binding.statusBoosterInfo.setVisibility(View.VISIBLE);
holder.binding.statusBoosterUsername.setText(String.format("@%s", status.account.acct));
if(status.account.acct != null) {
holder.binding.statusBoosterUsername.setText(String.format("@%s", status.account.acct));
holder.binding.statusBoosterUsername.setVisibility(View.VISIBLE);
} else {
holder.binding.statusBoosterUsername.setVisibility(View.GONE);
}
} else {
holder.binding.statusBoosterInfo.setVisibility(View.GONE);
}

View file

@ -17,6 +17,7 @@ package app.fedilab.android.mastodon.ui.fragment.timeline;
import static app.fedilab.android.BaseMainActivity.currentInstance;
import static app.fedilab.android.BaseMainActivity.networkAvailable;
import static app.fedilab.android.mastodon.helper.Helper.TAG;
import android.content.BroadcastReceiver;
import android.content.Context;
@ -418,7 +419,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
search = bundle.getString(Helper.ARG_SEARCH_KEYWORD, null);
searchCache = bundle.getString(Helper.ARG_SEARCH_KEYWORD_CACHE, null);
pinnedTimeline = (PinnedTimeline) bundle.getSerializable(Helper.ARG_REMOTE_INSTANCE);
canBeFederated = true;
if (pinnedTimeline != null && pinnedTimeline.remoteInstance != null) {
if (pinnedTimeline.remoteInstance.type != RemoteInstance.InstanceType.NITTER) {
remoteInstance = pinnedTimeline.remoteInstance.host;
@ -490,7 +491,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
initialStatuses = null;
lockForResumeCall = 0;
canBeFederated = true;
retry_for_home_done = false;
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity());
boolean displayScrollBar = sharedpreferences.getBoolean(getString(R.string.SET_TIMELINE_SCROLLBAR), false);
@ -562,7 +563,12 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
}
//Update the timeline with new statuses
int insertedStatus;
if (timelineType != Timeline.TimeLineEnum.TREND_MESSAGE_PUBLIC && timelineType != Timeline.TimeLineEnum.TREND_MESSAGE ) {
if(pinnedTimeline!= null && pinnedTimeline.remoteInstance != null && pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER) {
insertedStatus = fetched_statuses.statuses.size();
int fromPosition = timelineStatuses.size();
timelineStatuses.addAll(fetched_statuses.statuses);
statusAdapter.notifyItemRangeInserted(fromPosition, insertedStatus);
} else if (timelineType != Timeline.TimeLineEnum.TREND_MESSAGE_PUBLIC && timelineType != Timeline.TimeLineEnum.TREND_MESSAGE ) {
insertedStatus = updateStatusListWith(fetched_statuses.statuses);
} else { //Trends cannot be ordered by id
insertedStatus = fetched_statuses.statuses.size();
@ -679,8 +685,8 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
if (max_id == null || (statuses.pagination.max_id != null && Helper.compareTo(statuses.pagination.max_id, max_id) < 0) || timelineType.getValue().startsWith("TREND_")) {
max_id = statuses.pagination.max_id;
}
//For Lemmy pagination
if (pinnedTimeline != null && pinnedTimeline.remoteInstance != null && pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.LEMMY) {
//For Lemmy and Nitter pagination
if (pinnedTimeline != null && pinnedTimeline.remoteInstance != null && (pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.LEMMY || pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER)) {
max_id = statuses.pagination.max_id;
}
if (min_id == null || (statuses.pagination.min_id != null && Helper.compareTo(statuses.pagination.min_id, min_id) > 0)) {
@ -1066,6 +1072,26 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
}
});
}
}//LEMMY TIMELINES
else if (pinnedTimeline != null && pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.LEMMY) {
if (direction == null) {
timelinesVM.getLemmy(remoteInstance, lemmy_post_id, null, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), this::initializeStatusesCommonView);
} else if (direction == DIRECTION.BOTTOM) {
timelinesVM.getLemmy(remoteInstance, lemmy_post_id, max_id, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false, true, fetchStatus));
} else if (direction == DIRECTION.TOP) {
flagLoading = false;
} else if (direction == DIRECTION.REFRESH || direction == DIRECTION.SCROLL_TOP) {
timelinesVM.getLemmy(remoteInstance, lemmy_post_id, null, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), statusesRefresh -> {
if (statusAdapter != null) {
dealWithPagination(statusesRefresh, direction, true, true, fetchStatus);
} else {
initializeStatusesCommonView(statusesRefresh);
}
});
}
}//MISSKEY TIMELINES
else if (pinnedTimeline != null && pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.MISSKEY) {
if (direction == null) {
@ -1087,27 +1113,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
}
});
}
} //LEMMY TIMELINES
else if (pinnedTimeline != null && pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.LEMMY) {
if (direction == null) {
timelinesVM.getLemmy(remoteInstance, lemmy_post_id, null, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), this::initializeStatusesCommonView);
} else if (direction == DIRECTION.BOTTOM) {
timelinesVM.getLemmy(remoteInstance, lemmy_post_id, max_id, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false, true, fetchStatus));
} else if (direction == DIRECTION.TOP) {
flagLoading = false;
} else if (direction == DIRECTION.REFRESH || direction == DIRECTION.SCROLL_TOP) {
timelinesVM.getLemmy(remoteInstance, lemmy_post_id, null, MastodonHelper.statusesPerCall(requireActivity()))
.observe(getViewLifecycleOwner(), statusesRefresh -> {
if (statusAdapter != null) {
dealWithPagination(statusesRefresh, direction, true, true, fetchStatus);
} else {
initializeStatusesCommonView(statusesRefresh);
}
});
}
}//PEERTUBE TIMELINES
} //PEERTUBE TIMELINES
else if (pinnedTimeline != null && pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.PEERTUBE) {
if (direction == null) {
timelinesVM.getPeertube(remoteInstance, null, MastodonHelper.statusesPerCall(requireActivity()))

View file

@ -41,15 +41,12 @@ import java.net.IDN;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import app.fedilab.android.BuildConfig;
import app.fedilab.android.R;
import app.fedilab.android.activities.MainActivity;
import app.fedilab.android.mastodon.client.endpoints.MastodonTimelinesService;
import app.fedilab.android.mastodon.client.entities.api.Account;
import app.fedilab.android.mastodon.client.entities.api.Attachment;
import app.fedilab.android.mastodon.client.entities.api.Conversation;
import app.fedilab.android.mastodon.client.entities.api.Conversations;
import app.fedilab.android.mastodon.client.entities.api.Marker;
@ -255,19 +252,12 @@ public class TimelinesVM extends AndroidViewModel {
public LiveData<Statuses> getNitterRSS(
String accountsStr,
String max_position) {
Context context = getApplication().getApplicationContext();
SharedPreferences sharedpreferences = PreferenceManager
.getDefaultSharedPreferences(context);
String instance = sharedpreferences.getString(context.getString(R.string.SET_NITTER_HOST), context.getString(R.string.DEFAULT_NITTER_HOST)).toLowerCase();
if (instance.trim().isEmpty()) {
instance = context.getString(R.string.DEFAULT_NITTER_HOST);
}
MastodonTimelinesService mastodonTimelinesService = initInstanceXMLOnly(instance);
MastodonTimelinesService mastodonTimelinesService = initInstanceXMLOnly("nitter.fedilab.app");
accountsStr = accountsStr.replaceAll("\\s", ",");
statusesMutableLiveData = new MutableLiveData<>();
String finalAccountsStr = accountsStr;
String finalInstance = instance;
new Thread(() -> {
Call<Nitter> publicTlCall = mastodonTimelinesService.getNitter(finalAccountsStr, max_position);
Statuses statuses = new Statuses();
@ -280,7 +270,7 @@ public class TimelinesVM extends AndroidViewModel {
if (rssResponse != null && rssResponse.mFeedItems != null) {
for (Nitter.FeedItem feedItem : rssResponse.mFeedItems) {
if (!feedItem.title.startsWith("RT by")) {
Status status = Nitter.convert(getApplication(), finalInstance, feedItem);
Status status = Nitter.convert(getApplication(), "nitter.fedilab.app", feedItem);
statusList.add(status);
}
}
@ -314,99 +304,75 @@ public class TimelinesVM extends AndroidViewModel {
String max_position) {
statusesMutableLiveData = new MutableLiveData<>();
Context context = getApplication().getApplicationContext();
SharedPreferences sharedpreferences = PreferenceManager
.getDefaultSharedPreferences(context);
String instance = sharedpreferences.getString(context.getString(R.string.SET_NITTER_HOST), context.getString(R.string.DEFAULT_NITTER_HOST)).toLowerCase();
if (instance.trim().isEmpty()) {
instance = context.getString(R.string.DEFAULT_NITTER_HOST);
}
//TODO: remove after tests
instance = "nitter.privacydev.net";
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context);
final String nitterInstance = sharedpreferences.getString(context.getString(R.string.SET_NITTER_HOST), context.getString(R.string.DEFAULT_NITTER_HOST)).toLowerCase();
final String fedilabInstance = "nitter.fedilab.app";
accountsStr = accountsStr.replaceAll("\\s", ",").replaceAll(",,",",");
String maxposition = max_position == null ? "" : "?max_position="+max_position;
String url = "https://" + instance + "/" + accountsStr + "/with_replies"+maxposition;
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS).build();
String cursor = max_position == null ? "" : max_position;
String url = "https://"+fedilabInstance+"/" + accountsStr + "/with_replies" +cursor;
Request request = new Request.Builder()
.header("User-Agent", context.getString(R.string.app_name) + "/" + BuildConfig.VERSION_NAME + "/" + BuildConfig.VERSION_CODE)
.url(url)
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
.header("accept-language","en-US;q=0.6")
.header("dnt","1")
.header("user-agent","Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0")
.get()
.build();
String finalInstance = instance;
String finalInstance1 = instance;
client.newCall(request).enqueue(new Callback() {
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull okhttp3.Call call, @NonNull IOException e) {
}
@Override
public void onResponse(@NonNull okhttp3.Call call, @NonNull okhttp3.Response response) throws IOException {
Statuses statuses = new Statuses();
if (response.isSuccessful()) {
try {
String data = response.body().string();
Document doc = Jsoup.parse(data);
Elements timelineItems = doc.select(".timeline-item");
List<Status> statusList = new ArrayList<>();
for(Element timelineItem: timelineItems) {
//Not a RT
if(timelineItem.select(".icon-retweet").html().trim().isEmpty()) {
Status status = new Status();
Account account = new Account();
String[] splitLink = timelineItem.select(".tweet-link").text().split("/");
String status_id = splitLink[splitLink.length-1];
String pubDate = timelineItem.select(".tweet-date").select("a").attr("title");
String name = timelineItem.select(".fullname").text();
String userName = timelineItem.select(".username").text();
String avatar = "https://"+ finalInstance + timelineItem.select(".avatar").attr("src");
account.id = userName;
account.acct = userName;
account.username = userName;
account.display_name = name;
account.avatar = avatar;
account.avatar_static = avatar;
account.url = "https://"+ finalInstance +"/" + userName;
status.id = status_id;
status.account = account;
status.url = "https://"+ finalInstance +timelineItem.select(".tweet-link").attr("href");
status.content = timelineItem.select(".tweet-content").text();
Pattern imgPattern = Pattern.compile("<img [^>]*src=\"([^\"]+)\"[^>]*>");
Matcher matcher = imgPattern.matcher(status.content);
String description = status.content;
ArrayList<Attachment> attachmentList = new ArrayList<>();
while (matcher.find()) {
description = description.replaceAll(Pattern.quote(matcher.group()), "");
Attachment attachment = new Attachment();
attachment.type = "image";
attachment.url = matcher.group(1);
attachment.preview_url = matcher.group(1);
attachment.id = matcher.group(1);
attachmentList.add(attachment);
}
status.visibility = "public";
status.media_attachments = attachmentList;
String dateformat = "MMM d', 'yyyy' · 'h:m a' UTC'";
status.created_at = Helper.stringToDateWithFormat(context, pubDate, dateformat);
statusList.add(status);
if(!timelineItem.select(".unavailable").html().isEmpty() || timelineItem.select(".tweet-link").attr("href").isEmpty()) {
continue;
}
//RT
boolean isBoosted = !timelineItem.select(".retweet-header").select(".icon-container").isEmpty();
Status status = Nitter.nitterHTMLParser(context, timelineItem, nitterInstance);
//Quoted message
if(!timelineItem.select(".quote").html().isEmpty()) {
status.quote = Nitter.nitterHTMLParser(context, timelineItem.select(".quote").first(), nitterInstance);
}
Status finalStatus;
if(isBoosted) {
finalStatus = new Status();
finalStatus.reblog = status;
finalStatus.id = status.id;
finalStatus.visibility = "public";
finalStatus.url = "https://"+ nitterInstance +timelineItem.select(".tweet-link").attr("href");
finalStatus.uri = finalStatus.url;
Account acccountOriginal = new Account();
acccountOriginal.display_name = timelineItem.select(".retweet-header").select(".icon-container").text();
finalStatus.account = acccountOriginal;
} else {
finalStatus = status;
}
statusList.add(finalStatus);
}
statuses.statuses = statusList;
String max_id = response.headers().get("min-id");
Elements elementsShow = doc.select(".show-more a");
Element showMore = null;
if(elementsShow.size() > 1) {
showMore = elementsShow.get(elementsShow.size()-1);
} else {
showMore = elementsShow.get(0);
}
String cursor = showMore.attr("href");
statuses.pagination = new Pagination();
statuses.pagination.max_id = max_id;
statuses.pagination.max_id = cursor;
} catch (Exception e) {
e.printStackTrace();
}

View file

@ -612,7 +612,7 @@
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/action_share_container"
android:layout_width="match_parent"
android:layout_height="28dp"
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="6dp"
@ -625,12 +625,11 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/action_share"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:iconGravity="textStart"
android:adjustViewBounds="true"
app:icon="@drawable/ic_baseline_share_24"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:strokeColor="?colorPrimary" />
</androidx.appcompat.widget.LinearLayoutCompat>