package com.lemenzo.messenger;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.util.Size;
import android.provider.Telephony;
import android.provider.ContactsContract;
import android.provider.OpenableColumns;
import android.database.Cursor;
import android.view.Gravity;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.ListView;
import android.widget.BaseAdapter;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.TextView;
import android.widget.Toast;
import com.lemenzo.messenger.data.ContactResolver;
import com.lemenzo.messenger.data.DraftRepository;
import com.lemenzo.messenger.data.MessageRepository;
import com.lemenzo.messenger.data.MessagePage;
import com.lemenzo.messenger.data.PendingMessageStore;
import com.lemenzo.messenger.data.SmsContentObserver;
import com.lemenzo.messenger.model.MessageItem;
import com.lemenzo.messenger.model.PendingMessage;
import com.lemenzo.messenger.mms.MmsSender;
import com.lemenzo.messenger.sms.SmsSender;
import com.lemenzo.messenger.sms.RetryMessage;
import com.lemenzo.messenger.sms.SubscriptionHelper;
import com.lemenzo.messenger.ui.Ui;
import com.lemenzo.messenger.ui.SwipeDeleteTouchListener;
import java.text.DateFormat;
import java.util.Date;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public final class ConversationActivity extends Activity {
    private static final int REQUEST_ATTACHMENT = 2001;

    private long threadId;
    private String address;
    private MessageRepository repo;
    private ContactResolver contactResolver;
    private DraftRepository drafts;
    private PendingMessageStore pendingStore;
    private ListView messages;
    private TimelineAdapter timelineAdapter;
    private boolean firstTimelineRender = true;
    private EditText input;
    private ImageButton sendButton;
    private boolean sendInFlight;
    private Uri pendingAttachment;
    private LinearLayout attachmentPreview;
    private TextView attachmentName;
    private ImageView attachmentThumb;
    private FrameLayout headerAvatar;
    private TextView headerName;
    private TextView headerAddress;
    private final ExecutorService executor = Executors.newSingleThreadExecutor();
    private final ExecutorService sendExecutor = Executors.newSingleThreadExecutor();
    private final ExecutorService mediaExecutor = Executors.newFixedThreadPool(2);
    private final android.util.LruCache<String, android.graphics.Bitmap> thumbnailCache =
            new android.util.LruCache<String, android.graphics.Bitmap>(16 * 1024) {
                @Override protected int sizeOf(String key, android.graphics.Bitmap value) {
                    return Math.max(1, value.getAllocationByteCount() / 1024);
                }
            };
    private SmsContentObserver smsObserver;
    private final Object refreshLock = new Object();
    private boolean refreshInFlight;
    private boolean refreshRequested;
    private boolean providerDirty = true;
    private static final int MESSAGE_PAGE_SIZE = 300;
    private final ArrayList<MessageItem> cachedMessages = new ArrayList<>();
    private boolean hasOlderMessages = true;
    private boolean loadingOlderMessages;
    // Stable keyset cursor for older history. Date alone is insufficient because many
    // imported SMS rows and second-granular MMS rows can share a timestamp.
    private long olderBeforeDate = Long.MAX_VALUE;
    private long olderBeforeSmsId = Long.MAX_VALUE;
    private long olderBeforeMmsId = Long.MAX_VALUE;
    private long lastRenderSignature = Long.MIN_VALUE;
    private final DateFormat messageTimeFormat = DateFormat.getTimeInstance(DateFormat.SHORT);
    private final DateFormat separatorDateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM);
    private final SharedPreferences.OnSharedPreferenceChangeListener pendingListener =
            (prefs, key) -> runOnUiThread(() -> refresh(false));

    @Override
    public void onCreate(Bundle state) {
        super.onCreate(state);
        threadId = getIntent().getLongExtra("thread_id", -1);
        address = getIntent().getStringExtra("address");
        repo = new MessageRepository(this);
        contactResolver = new ContactResolver(this);
        if (address == null || address.trim().isEmpty() || "Unknown".equalsIgnoreCase(address.trim())) {
            String resolvedAddress = repo.addressForThread(threadId);
            if (resolvedAddress != null && !resolvedAddress.trim().isEmpty()) address = resolvedAddress.trim();
        }
        drafts = new DraftRepository(this);
        pendingStore = new PendingMessageStore(this);
        build();
        input.setText(drafts.load(address));
        executor.execute(() -> repo.markThreadRead(threadId));
        refresh(true);
    }

    @Override
    protected void onResume() {
        super.onResume();
        contactResolver.clearCache();
        updateContactHeader();
        if (smsObserver == null) {
            smsObserver = new SmsContentObserver(() -> refresh(true));
            getContentResolver().registerContentObserver(
                    Telephony.Sms.CONTENT_URI,
                    true,
                    smsObserver);
            getContentResolver().registerContentObserver(
                    Telephony.Mms.CONTENT_URI,
                    true,
                    smsObserver);
        }
        pendingStore.preferences().registerOnSharedPreferenceChangeListener(pendingListener);
        if (input != null) input.setTextSize(SettingsActivity.messageTextSize(this));
        lastRenderSignature = Long.MIN_VALUE;
        refresh(true);
    }

    @Override
    protected void onPause() {
        drafts.save(address, input == null ? "" : input.getText().toString());
        if (smsObserver != null) {
            getContentResolver().unregisterContentObserver(smsObserver);
            smsObserver.dispose();
            smsObserver = null;
        }
        pendingStore.preferences().unregisterOnSharedPreferenceChangeListener(pendingListener);
        super.onPause();
    }

    @Override
    protected void onDestroy() {
        executor.shutdownNow();
        sendExecutor.shutdownNow();
        mediaExecutor.shutdownNow();
        thumbnailCache.evictAll();
        super.onDestroy();
    }

    private void build() {
        LinearLayout root = new LinearLayout(this);
        root.setOrientation(LinearLayout.VERTICAL);
        root.setBackgroundColor(getColor(R.color.lemenzo_background));
        int hp = Ui.dp(root, 14);
        root.setPadding(hp, Ui.dp(root, 6), hp, Ui.dp(root, 8));
        Ui.applyImeAwareInsets(this, root);

        LinearLayout header = new LinearLayout(this);
        header.setGravity(Gravity.CENTER_VERTICAL);
        header.setPadding(0, Ui.dp(root, 4), 0, Ui.dp(root, 8));

        ImageButton back = Ui.iconButton(this, R.drawable.ic_back, getString(R.string.navigate_back));
        back.setOnClickListener(v -> finish());
        header.addView(back, new LinearLayout.LayoutParams(Ui.dp(root, 48), Ui.dp(root, 52)));

        headerAvatar = new FrameLayout(this);
        header.addView(headerAvatar, new LinearLayout.LayoutParams(Ui.dp(root, 48), Ui.dp(root, 48)));

        LinearLayout identity = new LinearLayout(this);
        identity.setOrientation(LinearLayout.VERTICAL);
        identity.setPadding(Ui.dp(root, 11), 0, Ui.dp(root, 4), 0);
        headerName = Ui.text(this, "", 17.5f, true);
        headerName.setSingleLine();
        identity.addView(headerName);
        headerAddress = Ui.text(this, "", 11.5f, false);
        headerAddress.setTextColor(getColor(R.color.lemenzo_text_muted));
        headerAddress.setSingleLine();
        identity.addView(headerAddress);
        header.addView(identity, new LinearLayout.LayoutParams(0, -2, 1f));

        View.OnClickListener contactAction = v -> openContactOrAdd();
        headerAvatar.setClickable(true);
        headerAvatar.setFocusable(true);
        headerAvatar.setOnClickListener(contactAction);
        identity.setClickable(true);
        identity.setFocusable(true);
        identity.setOnClickListener(contactAction);
        updateContactHeader();

        ImageButton call = Ui.iconButton(this, R.drawable.ic_phone, getString(R.string.call));
        call.setOnClickListener(v -> {
            if (address != null && !address.isEmpty()) {
                Intent callIntent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + Uri.encode(address)));
                try {
                    callIntent.setPackage("com.android.dialer");
                    startActivity(callIntent);
                } catch (android.content.ActivityNotFoundException e) {
                    callIntent.setPackage(null);
                    startActivity(callIntent);
                }
            }
        });
        header.addView(call, new LinearLayout.LayoutParams(Ui.dp(root, 48), Ui.dp(root, 48)));

        ImageButton more = Ui.iconButton(this, R.drawable.ic_more, getString(R.string.more_options));
        more.setOnClickListener(this::showConversationMenu);
        header.addView(more, new LinearLayout.LayoutParams(Ui.dp(root, 48), Ui.dp(root, 48)));
        root.addView(header);

        messages = new ListView(this);
        messages.setDivider(null);
        messages.setDividerHeight(0);
        messages.setClipToPadding(false);
        messages.setPadding(Ui.dp(root, 4), Ui.dp(root, 10), Ui.dp(root, 4), Ui.dp(root, 12));
        messages.setCacheColorHint(0x00000000);
        messages.setScrollingCacheEnabled(false);
        messages.setSmoothScrollbarEnabled(true);
        messages.setStackFromBottom(true);
        messages.setTranscriptMode(AbsListView.TRANSCRIPT_MODE_NORMAL);
        timelineAdapter = new TimelineAdapter();
        messages.setAdapter(timelineAdapter);
        messages.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override public void onScrollStateChanged(AbsListView view, int state) {
                if (state == AbsListView.OnScrollListener.SCROLL_STATE_IDLE
                        && view.getFirstVisiblePosition() <= 2) loadOlderMessages();
            }
            @Override public void onScroll(AbsListView view, int first, int visible, int total) {
                if (first <= 1 && visible > 0 && total > 0 && !firstTimelineRender) loadOlderMessages();
            }
        });
        root.addView(messages, new LinearLayout.LayoutParams(-1, 0, 1f));

        attachmentPreview = new LinearLayout(this);
        attachmentPreview.setGravity(Gravity.CENTER_VERTICAL);
        attachmentPreview.setBackgroundResource(R.drawable.card_background);
        attachmentPreview.setPadding(Ui.dp(root,12),Ui.dp(root,7),Ui.dp(root,6),Ui.dp(root,7));
        attachmentPreview.setVisibility(View.GONE);
        attachmentThumb=new ImageView(this);
        attachmentThumb.setScaleType(ImageView.ScaleType.CENTER_CROP);
        attachmentThumb.setBackgroundResource(R.drawable.circle_surface);
        attachmentThumb.setPadding(Ui.dp(root,8),Ui.dp(root,8),Ui.dp(root,8),Ui.dp(root,8));
        LinearLayout.LayoutParams atp=new LinearLayout.LayoutParams(Ui.dp(root,42),Ui.dp(root,42));atp.setMargins(0,0,Ui.dp(root,10),0);
        attachmentPreview.addView(attachmentThumb,atp);
        attachmentName=Ui.text(this,"",13.5f,false);attachmentName.setTextColor(getColor(R.color.lemenzo_text_secondary));
        attachmentPreview.addView(attachmentName,new LinearLayout.LayoutParams(0,-2,1f));
        ImageButton removeAttachment=Ui.iconButton(this,R.drawable.ic_close,getString(R.string.remove_attachment));
        removeAttachment.setOnClickListener(v->clearAttachment());
        attachmentPreview.addView(removeAttachment,new LinearLayout.LayoutParams(Ui.dp(root,48),Ui.dp(root,48)));
        LinearLayout.LayoutParams app=new LinearLayout.LayoutParams(-1,-2);app.setMargins(0,0,0,Ui.dp(root,6));root.addView(attachmentPreview,app);

        LinearLayout composer = new LinearLayout(this);
        composer.setGravity(Gravity.BOTTOM);
        composer.setPadding(0, Ui.dp(root, 7), 0, Ui.dp(root, 2));

        ImageButton attach = Ui.iconButton(this, R.drawable.ic_add, getString(R.string.attach));
        attach.setBackgroundResource(R.drawable.circle_surface);
        attach.setPadding(Ui.dp(root, 12), Ui.dp(root, 12), Ui.dp(root, 12), Ui.dp(root, 12));
        attach.setOnClickListener(v -> pickAttachment());
        composer.addView(attach, new LinearLayout.LayoutParams(Ui.dp(root, 48), Ui.dp(root, 48)));

        LinearLayout shell = new LinearLayout(this);
        shell.setGravity(Gravity.BOTTOM);
        shell.setMinimumHeight(Ui.dp(root,52));
        shell.setBackgroundResource(R.drawable.composer_background);
        shell.setPadding(Ui.dp(root, 8), Ui.dp(root,4), Ui.dp(root, 4), Ui.dp(root,4));
        LinearLayout.LayoutParams shellParams = new LinearLayout.LayoutParams(0, -2, 1f);
        shellParams.setMargins(Ui.dp(root, 8), 0, Ui.dp(root, 8), 0);
        composer.addView(shell, shellParams);

        input = new EditText(this);
        input.setHint(R.string.message_hint);
        input.setTextColor(getColor(R.color.lemenzo_text_primary));
        input.setHintTextColor(getColor(R.color.lemenzo_text_muted));
        input.setTextSize(SettingsActivity.messageTextSize(this));
        input.setSingleLine(false);
        input.setMaxLines(5);
        input.setMaxHeight(Ui.dp(root,124));
        input.setMinHeight(Ui.dp(root,44));
        input.setGravity(Gravity.CENTER_VERTICAL);
        input.setBackgroundColor(0x00000000);
        input.setPadding(Ui.dp(root, 6), 0, Ui.dp(root, 3), 0);
        shell.addView(input, new LinearLayout.LayoutParams(0, -2, 1f));

        sendButton = Ui.iconButton(this, R.drawable.ic_send, getString(R.string.send_message));
        sendButton.setBackgroundResource(R.drawable.round_blue_button);
        sendButton.setPadding(Ui.dp(root, 14), Ui.dp(root, 14), Ui.dp(root, 14), Ui.dp(root, 14));
        sendButton.setOnClickListener(v -> send());
        composer.addView(sendButton, new LinearLayout.LayoutParams(Ui.dp(root, 52), Ui.dp(root, 52)));

        root.addView(composer);
        setContentView(root);
    }

    private void refresh() { refresh(true); }

    private void refresh(boolean providerChanged) {
        synchronized (refreshLock) {
            providerDirty |= providerChanged;
            refreshRequested = true;
            if (refreshInFlight) return;
            refreshInFlight = true;
        }
        executor.execute(this::drainRefreshRequests);
    }

    private void drainRefreshRequests() {
        while (!Thread.currentThread().isInterrupted()) {
            boolean reloadProvider;
            synchronized (refreshLock) {
                if (!refreshRequested) {
                    refreshInFlight = false;
                    return;
                }
                refreshRequested = false;
                reloadProvider = providerDirty;
                providerDirty = false;
            }
            List<MessageItem> data;
            if (reloadProvider) {
                // Provider notifications usually mean a new/sent message. Query only the newest
                // page and merge it into the active history window. Never throw away pages the
                // user has already loaded merely because a new SMS arrived while reading history.
                MessagePage newest = repo.messagesPage(threadId, MESSAGE_PAGE_SIZE);
                synchronized (cachedMessages) {
                    if (cachedMessages.isEmpty()) {
                        cachedMessages.addAll(newest.items);
                        hasOlderMessages = newest.hasOlder;
                        olderBeforeDate = newest.nextBeforeDate;
                        olderBeforeSmsId = newest.nextSmsId;
                        olderBeforeMmsId = newest.nextMmsId;
                    } else {
                        mergeNewestPageLocked(newest.items);
                    }
                    data = new ArrayList<>(cachedMessages);
                }
            } else {
                synchronized (cachedMessages) { data = new ArrayList<>(cachedMessages); }
            }
            List<PendingMessage> pending = pendingStore.forAddress(address);
            long signature = messageSignature(data, pending);
            runOnUiThread(() -> {
                if (!isFinishing() && signature != lastRenderSignature) {
                    lastRenderSignature = signature;
                    render(data, pending);
                }
            });
        }
        synchronized (refreshLock) { refreshInFlight = false; }
    }

    /** Caller must hold cachedMessages. */
    private void mergeNewestPageLocked(List<MessageItem> newest) {
        if (newest == null || newest.isEmpty()) return;
        java.util.HashMap<String, MessageItem> replacements = new java.util.HashMap<>();
        for (MessageItem item : newest) replacements.put(messageKey(item), item);

        for (int i = 0; i < cachedMessages.size(); i++) {
            MessageItem replacement = replacements.remove(messageKey(cachedMessages.get(i)));
            if (replacement != null) cachedMessages.set(i, replacement);
        }
        if (!replacements.isEmpty()) cachedMessages.addAll(replacements.values());
        cachedMessages.sort((a,b) -> {
            int byDate = Long.compare(a.date,b.date);
            if (byDate != 0) return byDate;
            int byProtocol = Integer.compare(a.protocol,b.protocol);
            if (byProtocol != 0) return byProtocol;
            return Long.compare(a.id,b.id);
        });
    }

    private void loadOlderMessages() {
        synchronized (refreshLock) {
            if (loadingOlderMessages || !hasOlderMessages || olderBeforeDate == Long.MAX_VALUE) return;
            loadingOlderMessages = true;
        }
        final int oldTimelineCount = timelineAdapter == null ? 0 : timelineAdapter.getCount();
        final int oldFirst = messages == null ? 0 : messages.getFirstVisiblePosition();
        final View firstChild = messages == null ? null : messages.getChildAt(0);
        final int oldTop = firstChild == null ? 0 : firstChild.getTop();
        final long boundaryDate = olderBeforeDate;
        final long boundarySmsId = olderBeforeSmsId;
        final long boundaryMmsId = olderBeforeMmsId;

        executor.execute(() -> {
            MessagePage page = repo.messagesPage(
                    threadId, boundaryDate, boundarySmsId, boundaryMmsId, MESSAGE_PAGE_SIZE);
            int added = 0;
            synchronized (cachedMessages) {
                java.util.HashSet<String> existing = new java.util.HashSet<>();
                for (MessageItem m : cachedMessages) existing.add(messageKey(m));
                ArrayList<MessageItem> prepend = new ArrayList<>();
                for (MessageItem m : page.items) {
                    if (existing.add(messageKey(m))) prepend.add(m);
                }
                if (!prepend.isEmpty()) {
                    cachedMessages.addAll(0, prepend);
                    cachedMessages.sort((a,b) -> {
                        int byDate = Long.compare(a.date,b.date);
                        if (byDate != 0) return byDate;
                        int byProtocol = Integer.compare(a.protocol,b.protocol);
                        if (byProtocol != 0) return byProtocol;
                        return Long.compare(a.id,b.id);
                    });
                    added = prepend.size();
                }
                hasOlderMessages = page.hasOlder && !page.items.isEmpty();
                olderBeforeDate = page.nextBeforeDate;
                olderBeforeSmsId = page.nextSmsId;
                olderBeforeMmsId = page.nextMmsId;
            }
            List<MessageItem> data;
            synchronized (cachedMessages) { data = new ArrayList<>(cachedMessages); }
            List<PendingMessage> pending = pendingStore.forAddress(address);
            final int addedMessages = added;
            runOnUiThread(() -> {
                synchronized (refreshLock) { loadingOlderMessages = false; }
                if (isFinishing()) return;
                lastRenderSignature = messageSignature(data, pending);
                render(data, pending);
                if (timelineAdapter != null && messages != null && addedMessages > 0) {
                    int delta = Math.max(0, timelineAdapter.getCount() - oldTimelineCount);
                    messages.setSelectionFromTop(Math.max(0, oldFirst + delta), oldTop);
                }
            });
        });
    }

    private static String messageKey(MessageItem m) {
        return m.protocol + ":" + m.id;
    }

    private static long messageSignature(List<MessageItem> data, List<PendingMessage> pending) {
        long h = 1125899906842597L;
        for (MessageItem m : data) {
            h = 31*h + m.id;
            h = 31*h + m.date;
            h = 31*h + m.protocol;
            h = 31*h + (m.body == null ? 0 : m.body.hashCode());
            h = 31*h + (m.mediaUri == null ? 0 : m.mediaUri.hashCode());
        }
        for (PendingMessage p : pending) {
            h = 31*h + p.date;
            h = 31*h + p.state;
            h = 31*h + (p.token == null ? 0 : p.token.hashCode());
            h = 31*h + (p.body == null ? 0 : p.body.hashCode());
        }
        return h;
    }

    private void render(List<MessageItem> data, List<PendingMessage> pending) {
        if (isFinishing() || timelineAdapter == null) return;

        boolean stayAtBottom = firstTimelineRender || isTimelineNearBottom();
        ArrayList<TimelineItem> timeline = new ArrayList<>();
        long lastDay = Long.MIN_VALUE;
        for (MessageItem m : data) {
            long day = dayKey(m.date);
            if (day != lastDay) {
                timeline.add(TimelineItem.separator(m.date));
                lastDay = day;
            }
            timeline.add(TimelineItem.message(m));
        }
        for (PendingMessage p : pending) timeline.add(TimelineItem.pending(p));

        timelineAdapter.replace(timeline);
        if (stayAtBottom && !timeline.isEmpty()) {
            messages.post(() -> messages.setSelection(timelineAdapter.getCount() - 1));
        }
        firstTimelineRender = false;
    }

    private boolean isTimelineNearBottom() {
        if (messages == null || timelineAdapter == null || timelineAdapter.getCount() == 0) return true;
        int lastVisible = messages.getLastVisiblePosition();
        return lastVisible >= timelineAdapter.getCount() - 3;
    }

    private long dayKey(long when){java.util.Calendar c=java.util.Calendar.getInstance();c.setTimeInMillis(when);return ((long)c.get(java.util.Calendar.YEAR))*400L+c.get(java.util.Calendar.DAY_OF_YEAR);}

    private String dateSeparatorLabel(long when){
        java.util.Calendar now=java.util.Calendar.getInstance(), then=java.util.Calendar.getInstance();then.setTimeInMillis(when);
        if(now.get(java.util.Calendar.YEAR)==then.get(java.util.Calendar.YEAR)&&now.get(java.util.Calendar.DAY_OF_YEAR)==then.get(java.util.Calendar.DAY_OF_YEAR)) return getString(R.string.today);
        java.util.Calendar y=(java.util.Calendar)now.clone();y.add(java.util.Calendar.DAY_OF_YEAR,-1);
        if(y.get(java.util.Calendar.YEAR)==then.get(java.util.Calendar.YEAR)&&y.get(java.util.Calendar.DAY_OF_YEAR)==then.get(java.util.Calendar.DAY_OF_YEAR)) return getString(R.string.yesterday);
        return separatorDateFormat.format(new Date(when));
    }

    private View buildDateSeparator(long when, ViewGroup parent){
        TextView t=Ui.text(this,dateSeparatorLabel(when),12.5f,false);
        t.setTextColor(getColor(R.color.lemenzo_text_muted));
        Ui.center(t);
        t.setPadding(0,Ui.dp(t,10),0,Ui.dp(t,8));
        return t;
    }

    private void loadMediaThumbnail(ImageView image, Uri uri, int width, int height) {
        if (image == null || uri == null) return;
        final String key = uri.toString() + "@" + width + "x" + height;
        image.setTag(key);
        android.graphics.Bitmap cached = thumbnailCache.get(key);
        if (cached != null && !cached.isRecycled()) {
            image.setImageBitmap(cached);
            return;
        }
        image.setImageDrawable(null);
        mediaExecutor.execute(() -> {
            try {
                android.graphics.Bitmap bitmap = getContentResolver().loadThumbnail(
                        uri, new Size(Math.max(1,width), Math.max(1,height)), null);
                if (bitmap != null) thumbnailCache.put(key, bitmap);
                runOnUiThread(() -> {
                    if (!isFinishing() && !isDestroyed() && key.equals(image.getTag())) image.setImageBitmap(bitmap);
                });
            } catch (java.io.IOException | RuntimeException ignored) {
                // Keep the styled placeholder instead of blocking/crashing the conversation UI.
            }
        });
    }

    private static final class TimelineItem {
        static final int TYPE_SEPARATOR=0, TYPE_MESSAGE=1, TYPE_PENDING=2;
        final int type;
        final long date;
        final MessageItem message;
        final PendingMessage pending;
        private TimelineItem(int type,long date,MessageItem message,PendingMessage pending){this.type=type;this.date=date;this.message=message;this.pending=pending;}
        static TimelineItem separator(long date){return new TimelineItem(TYPE_SEPARATOR,date,null,null);}
        static TimelineItem message(MessageItem m){return new TimelineItem(TYPE_MESSAGE,m.date,m,null);}
        static TimelineItem pending(PendingMessage p){return new TimelineItem(TYPE_PENDING,p.date,null,p);}
    }

    private static final class SeparatorHolder {
        TextView text;
    }

    private static final class MessageRowHolder {
        LinearLayout root;
        LinearLayout bubble;
        ImageView image;
        TextView attachment;
        TextView body;
        TextView time;
    }

    private static final class PendingRowHolder {
        LinearLayout root;
        LinearLayout bubble;
        LinearLayout statusRow;
        TextView body;
        TextView time;
        TextView status;
        TextView retry;
    }

    private View createSeparatorRow() {
        TextView text=Ui.text(this,"",12.5f,false);
        text.setTextColor(getColor(R.color.lemenzo_text_muted));
        Ui.center(text);
        text.setPadding(0,Ui.dp(text,10),0,Ui.dp(text,8));
        SeparatorHolder holder=new SeparatorHolder();
        holder.text=text;
        text.setTag(holder);
        return text;
    }

    private View createMessageRow() {
        int maxBubbleWidth=(int)(getResources().getDisplayMetrics().widthPixels*0.78f);
        LinearLayout wrap=new LinearLayout(this);
        wrap.setPadding(0,Ui.dp(wrap,4),0,Ui.dp(wrap,4));

        LinearLayout bubble=new LinearLayout(this);
        bubble.setOrientation(LinearLayout.VERTICAL);
        bubble.setMinimumWidth(Ui.dp(wrap,56));

        ImageView image=new ImageView(this);
        image.setAdjustViewBounds(true);
        image.setScaleType(ImageView.ScaleType.CENTER_CROP);
        image.setBackgroundResource(R.drawable.circle_surface);
        image.setContentDescription(getString(R.string.mms_photo));
        int imageWidth=Math.min(maxBubbleWidth,Ui.dp(wrap,280));
        int imageHeight=Ui.dp(wrap,220);
        bubble.addView(image,new LinearLayout.LayoutParams(imageWidth,imageHeight));

        TextView attachment=Ui.text(this,getString(R.string.mms_attachment),14,true);
        bubble.addView(attachment);

        TextView body=Ui.text(this,"",SettingsActivity.messageTextSize(this),false);
        body.setMaxWidth(maxBubbleWidth);
        bubble.addView(body);

        TextView time=Ui.text(this,"",11,false);
        time.setGravity(Gravity.END);
        bubble.addView(time,new LinearLayout.LayoutParams(-1,-2));

        wrap.addView(bubble,new LinearLayout.LayoutParams(-2,-2));

        MessageRowHolder holder=new MessageRowHolder();
        holder.root=wrap;
        holder.bubble=bubble;
        holder.image=image;
        holder.attachment=attachment;
        holder.body=body;
        holder.time=time;
        wrap.setTag(holder);
        return wrap;
    }

    private void bindMessageRow(MessageRowHolder holder, MessageItem message) {
        LinearLayout wrap=holder.root;
        LinearLayout bubble=holder.bubble;
        bubble.animate().cancel();
        bubble.setTranslationX(0f);
        bubble.setAlpha(1f);
        bubble.setOnTouchListener(null);
        bubble.setOnClickListener(null);
        bubble.setOnLongClickListener(null);
        bubble.setContentDescription(null);

        wrap.setGravity(message.outgoing?Gravity.END:Gravity.START);
        boolean hasMedia=message.protocol==MessageItem.PROTOCOL_MMS && message.mediaUri!=null;
        boolean imageMedia=hasMedia && message.mediaType!=null && message.mediaType.startsWith("image/");

        bubble.setPadding(
                Ui.dp(wrap,hasMedia?8:14),Ui.dp(wrap,hasMedia?8:9),
                Ui.dp(wrap,hasMedia?8:14),Ui.dp(wrap,8));
        bubble.setBackgroundResource(message.outgoing?R.drawable.bubble_outgoing:R.drawable.bubble_incoming);

        holder.image.setTag(null);
        holder.image.setImageDrawable(null);
        holder.image.setVisibility(imageMedia?View.VISIBLE:View.GONE);
        if(imageMedia){
            android.view.ViewGroup.LayoutParams lp=holder.image.getLayoutParams();
            int width=lp==null?Ui.dp(wrap,280):lp.width;
            int height=lp==null?Ui.dp(wrap,220):lp.height;
            loadMediaThumbnail(holder.image,message.mediaUri,width,height);
        }

        holder.attachment.setVisibility(hasMedia&&!imageMedia?View.VISIBLE:View.GONE);
        holder.attachment.setTextColor(message.outgoing?0xffffffff:getColor(R.color.lemenzo_blue));

        String bodyText=message.body==null?"":message.body;
        holder.body.setText(bodyText);
        holder.body.setTextSize(SettingsActivity.messageTextSize(this));
        holder.body.setPadding(0,hasMedia&&!bodyText.isEmpty()?Ui.dp(wrap,8):0,0,0);
        holder.body.setVisibility(bodyText.isEmpty()?View.GONE:View.VISIBLE);

        holder.time.setText(messageTimeFormat.format(new Date(message.date)));
        holder.time.setTextColor(message.outgoing?0xd9ffffff:getColor(R.color.lemenzo_text_muted));

        bubble.setOnLongClickListener(v -> {
            showMessageActions(bubble, message);
            return true;
        });
        enableMessageSwipe(bubble,message);
    }

    private View createPendingRow() {
        int maxBubbleWidth=(int)(getResources().getDisplayMetrics().widthPixels*0.78f);
        LinearLayout wrap=new LinearLayout(this);
        wrap.setOrientation(LinearLayout.VERTICAL);
        wrap.setGravity(Gravity.END);
        wrap.setPadding(0,Ui.dp(wrap,4),0,Ui.dp(wrap,4));

        LinearLayout bubble=new LinearLayout(this);
        bubble.setOrientation(LinearLayout.VERTICAL);
        bubble.setPadding(Ui.dp(wrap,14),Ui.dp(wrap,9),Ui.dp(wrap,14),Ui.dp(wrap,8));
        bubble.setBackgroundResource(R.drawable.bubble_outgoing);
        bubble.setMinimumWidth(Ui.dp(wrap,56));

        TextView body=Ui.text(this,"",SettingsActivity.messageTextSize(this),false);
        body.setMaxWidth(maxBubbleWidth);
        bubble.addView(body);

        TextView time=Ui.text(this,"",11,false);
        time.setTextColor(0xd9ffffff);
        time.setGravity(Gravity.END);
        bubble.addView(time,new LinearLayout.LayoutParams(-1,-2));
        wrap.addView(bubble,new LinearLayout.LayoutParams(-2,-2));

        LinearLayout statusRow=new LinearLayout(this);
        statusRow.setGravity(Gravity.CENTER_VERTICAL|Gravity.END);
        statusRow.setMinimumHeight(Ui.dp(wrap,42));

        TextView status=Ui.text(this,"",13.5f,true);
        statusRow.addView(status,new LinearLayout.LayoutParams(-2,-2));

        TextView retry=Ui.text(this,getString(R.string.retry_action),14,true);
        retry.setTextColor(getColor(R.color.lemenzo_text_primary));
        retry.setGravity(Gravity.CENTER);
        retry.setMinHeight(Ui.dp(wrap,40));
        retry.setMinWidth(Ui.dp(wrap,72));
        retry.setPadding(Ui.dp(wrap,14),Ui.dp(wrap,8),Ui.dp(wrap,14),Ui.dp(wrap,8));
        retry.setBackgroundResource(R.drawable.retry_button_background);
        retry.setContentDescription(getString(R.string.retry_message));
        LinearLayout.LayoutParams retryParams=new LinearLayout.LayoutParams(-2,-2);
        retryParams.setMargins(Ui.dp(wrap,10),0,0,0);
        statusRow.addView(retry,retryParams);

        LinearLayout.LayoutParams statusParams=new LinearLayout.LayoutParams(-2,-2);
        statusParams.setMargins(0,Ui.dp(wrap,3),0,0);
        wrap.addView(statusRow,statusParams);

        PendingRowHolder holder=new PendingRowHolder();
        holder.root=wrap;
        holder.bubble=bubble;
        holder.statusRow=statusRow;
        holder.body=body;
        holder.time=time;
        holder.status=status;
        holder.retry=retry;
        wrap.setTag(holder);
        return wrap;
    }

    private void bindPendingRow(PendingRowHolder holder, PendingMessage pending) {
        holder.bubble.animate().cancel();
        holder.bubble.setTranslationX(0f);
        holder.bubble.setAlpha(1f);
        holder.bubble.setOnTouchListener(null);
        holder.bubble.setOnLongClickListener(null);
        holder.bubble.setOnClickListener(null);
        holder.bubble.setContentDescription(null);
        holder.retry.setOnClickListener(null);
        holder.statusRow.setOnClickListener(null);

        String body=pending.body==null?"":pending.body;
        if(body.isEmpty()&&pending.protocol==PendingMessage.PROTOCOL_MMS&&pending.attachmentUri!=null)
            body=getString(R.string.mms_attachment);
        holder.body.setText(body);
        holder.body.setTextSize(SettingsActivity.messageTextSize(this));
        holder.time.setText(messageTimeFormat.format(new Date(pending.date)));

        boolean failed=pending.state==PendingMessage.STATE_FAILED;
        boolean sent=pending.state==PendingMessage.STATE_SENT;
        holder.status.setText(failed?R.string.status_failed:(sent?R.string.status_sent:R.string.status_sending));
        holder.status.setTextSize(failed?13.5f:12.5f);
        holder.status.setTypeface(null,failed?android.graphics.Typeface.BOLD:android.graphics.Typeface.NORMAL);
        holder.status.setTextColor(getColor(failed?R.color.lemenzo_danger:R.color.lemenzo_blue));
        holder.retry.setVisibility(failed&&pending.token!=null?View.VISIBLE:View.GONE);

        if(failed&&pending.token!=null){
            final String retryToken=pending.token;
            holder.retry.setTag(retryToken);
            View.OnClickListener retryAction=v->{
                v.setEnabled(false);
                v.setAlpha(0.65f);
                // MMS retry can preprocess an attachment. Never perform that work on the UI thread.
                executor.execute(()->{
                    boolean accepted=RetryMessage.retry(this,retryToken);
                    runOnUiThread(()->{
                        if(accepted){
                            // PendingMessageStore now carries SENDING state; rerender from source of truth.
                            refresh(false);
                        }else{
                            // The ListView may have recycled this holder while retry was running.
                            // Only mutate it when it still represents the same pending message.
                            if(retryToken.equals(holder.retry.getTag())){
                                holder.retry.setEnabled(true);
                                holder.retry.setAlpha(1f);
                            }
                            Toast.makeText(this,R.string.send_failed,Toast.LENGTH_LONG).show();
                        }
                    });
                });
            };
            holder.retry.setEnabled(true);
            holder.retry.setAlpha(1f);
            holder.retry.setOnClickListener(retryAction);
            holder.statusRow.setContentDescription(getString(R.string.retry_message));
        }else{
            holder.retry.setTag(null);
            holder.retry.setEnabled(true);
            holder.retry.setAlpha(1f);
            holder.statusRow.setContentDescription(getString(sent?R.string.status_sent:R.string.status_sending));
        }
    }

    private final class TimelineAdapter extends BaseAdapter {
        private final ArrayList<TimelineItem> items=new ArrayList<>();
        void replace(List<TimelineItem> data){items.clear();if(data!=null)items.addAll(data);notifyDataSetChanged();}
        @Override public int getCount(){return items.size();}
        @Override public TimelineItem getItem(int position){return items.get(position);}
        @Override public long getItemId(int position){TimelineItem i=getItem(position);return i.type==TimelineItem.TYPE_MESSAGE?((i.message.protocol+1L)<<60)^i.message.id:(i.date^(i.type*31L));}
        @Override public boolean hasStableIds(){return true;}
        @Override public int getViewTypeCount(){return 3;}
        @Override public int getItemViewType(int position){return getItem(position).type;}
        @Override public boolean isEnabled(int position){return getItem(position).type!=TimelineItem.TYPE_SEPARATOR;}
        @Override public View getView(int position,View convertView,ViewGroup parent){
            TimelineItem item=getItem(position);
            if(item.type==TimelineItem.TYPE_SEPARATOR){
                SeparatorHolder holder;
                if(convertView==null||!(convertView.getTag() instanceof SeparatorHolder)){
                    convertView=createSeparatorRow();
                }
                holder=(SeparatorHolder)convertView.getTag();
                holder.text.setText(dateSeparatorLabel(item.date));
                return convertView;
            }
            if(item.type==TimelineItem.TYPE_MESSAGE){
                MessageRowHolder holder;
                if(convertView==null||!(convertView.getTag() instanceof MessageRowHolder)){
                    convertView=createMessageRow();
                }
                holder=(MessageRowHolder)convertView.getTag();
                bindMessageRow(holder,item.message);
                return convertView;
            }
            PendingRowHolder holder;
            if(convertView==null||!(convertView.getTag() instanceof PendingRowHolder)){
                convertView=createPendingRow();
            }
            holder=(PendingRowHolder)convertView.getTag();
            bindPendingRow(holder,item.pending);
            return convertView;
        }
    }

    private void updateContactHeader() {
        if (headerAvatar == null || headerName == null || headerAddress == null || contactResolver == null) return;
        final String targetAddress = address;
        // Paint a cheap immediate fallback, then resolve contact data/photo away from UI thread.
        String fallback = targetAddress == null || targetAddress.trim().isEmpty()
                ? getString(R.string.generic_unknown) : targetAddress;
        headerName.setText(fallback);
        headerAddress.setText("");
        headerAddress.setVisibility(View.GONE);
        headerAvatar.removeAllViews();
        TextView fallbackAvatar = Ui.text(this, fallback.isEmpty()?"?":fallback.substring(0,1).toUpperCase(),16,true);
        fallbackAvatar.setTextColor(0xffffffff);
        Ui.center(fallbackAvatar);
        fallbackAvatar.setBackgroundResource(R.drawable.avatar_background);
        fallbackAvatar.setContentDescription(getString(R.string.contact_avatar));
        headerAvatar.addView(fallbackAvatar,new FrameLayout.LayoutParams(-1,-1));

        mediaExecutor.execute(() -> {
            String displayName = contactResolver.displayName(targetAddress);
            if(displayName==null||displayName.trim().isEmpty())displayName=fallback;
            Uri photo = contactResolver.photoUri(targetAddress);
            android.graphics.Bitmap bitmap=null;
            if(photo!=null){
                String key="contact:"+photo;
                bitmap=thumbnailCache.get(key);
                if(bitmap==null){
                    try{bitmap=getContentResolver().loadThumbnail(photo,new Size(96,96),null);}catch(Exception ignored){}
                    if(bitmap!=null)thumbnailCache.put(key,bitmap);
                }
            }
            final String resolvedName=displayName;
            final android.graphics.Bitmap resolvedBitmap=bitmap;
            runOnUiThread(() -> {
                if(isFinishing()||isDestroyed()||!java.util.Objects.equals(address,targetAddress))return;
                headerName.setText(resolvedName);
                if(targetAddress!=null&&!targetAddress.trim().isEmpty()&&!targetAddress.equals(resolvedName)){
                    headerAddress.setText(targetAddress);
                    headerAddress.setVisibility(View.VISIBLE);
                }else{
                    headerAddress.setText("");
                    headerAddress.setVisibility(View.GONE);
                }
                headerAvatar.removeAllViews();
                View avatarView;
                if(resolvedBitmap!=null){
                    ImageView avatar=new ImageView(this);
                    avatar.setScaleType(ImageView.ScaleType.CENTER_CROP);
                    avatar.setImageBitmap(resolvedBitmap);
                    avatar.setBackgroundResource(R.drawable.avatar_background);
                    avatar.setClipToOutline(true);
                    avatar.setContentDescription(getString(R.string.contact_avatar));
                    avatarView=avatar;
                }else{
                    TextView avatar=Ui.text(this,resolvedName.isEmpty()?"?":resolvedName.substring(0,1).toUpperCase(),16,true);
                    avatar.setTextColor(0xffffffff);
                    Ui.center(avatar);
                    avatar.setBackgroundResource(R.drawable.avatar_background);
                    avatar.setContentDescription(getString(R.string.contact_avatar));
                    avatarView=avatar;
                }
                headerAvatar.addView(avatarView,new FrameLayout.LayoutParams(-1,-1));
            });
        });
    }

    private void showConversationMenu(View anchor) {
        PopupMenu menu = new PopupMenu(this, anchor);
        boolean saved = contactResolver != null && contactResolver.isSavedContact(address);
        menu.getMenu().add(saved ? R.string.contact_info : R.string.add_contact);
        menu.getMenu().add(R.string.delete_conversation);
        menu.getMenu().add(R.string.settings);
        menu.setOnMenuItemClickListener(item -> {
            CharSequence title = item.getTitle();
            if (title.equals(getString(saved ? R.string.contact_info : R.string.add_contact))) {
                openContactOrAdd();
                return true;
            }
            if (title.equals(getString(R.string.delete_conversation))) {
                confirmDeleteConversation();
                return true;
            }
            if (title.equals(getString(R.string.settings))) {
                startActivity(new Intent(this, SettingsActivity.class));
                return true;
            }
            return false;
        });
        menu.show();
    }

    private void openContactOrAdd() {
        if (address == null || address.trim().isEmpty() || address.contains(",") || address.contains(";")) return;
        Uri existing = contactResolver == null ? null : contactResolver.contactUri(address);
        Intent intent;
        if (existing != null) {
            intent = new Intent(Intent.ACTION_VIEW);
            intent.setDataAndType(existing, ContactsContract.Contacts.CONTENT_ITEM_TYPE);
        } else {
            intent = new Intent(Intent.ACTION_INSERT);
            intent.setType(ContactsContract.Contacts.CONTENT_TYPE);
            intent.putExtra(ContactsContract.Intents.Insert.PHONE, address);
        }
        try {
            intent.setPackage("com.lemenzo.contacts");
            startActivity(intent);
        } catch (android.content.ActivityNotFoundException first) {
            try {
                intent.setPackage(null);
                startActivity(intent);
            } catch (android.content.ActivityNotFoundException second) {
                Toast.makeText(this, R.string.add_contact_failed, Toast.LENGTH_LONG).show();
            }
        }
    }

    private void showMessageActions(View anchor, MessageItem message) {
        if (anchor == null || message == null) return;

        PopupMenu menu = new PopupMenu(this, anchor);
        final int actionCopy = 1;
        final int actionDelete = 2;

        String body = message.body == null ? "" : message.body.trim();
        if (!body.isEmpty()) {
            menu.getMenu().add(0, actionCopy, 0, R.string.copy);
        }
        menu.getMenu().add(0, actionDelete, 1, R.string.delete_message);

        menu.setOnMenuItemClickListener(item -> {
            if (item.getItemId() == actionCopy) {
                copyMessageText(message);
                return true;
            }
            if (item.getItemId() == actionDelete) {
                confirmDeleteMessage(message);
                return true;
            }
            return false;
        });
        menu.show();
    }

    private void copyMessageText(MessageItem message) {
        if (message == null) return;

        String body = message.body == null ? "" : message.body;
        if (body.isEmpty()) return;

        ClipboardManager clipboard = getSystemService(ClipboardManager.class);
        if (clipboard == null) return;

        clipboard.setPrimaryClip(
                ClipData.newPlainText(getString(R.string.app_title), body));
        Toast.makeText(this, R.string.message_copied, Toast.LENGTH_SHORT).show();
    }

    private void enableMessageSwipe(View bubble, MessageItem message) {
        bubble.setOnTouchListener(new SwipeDeleteTouchListener(bubble,64,92,()->
                showDeleteConfirmation(
                        getString(R.string.delete_message),
                        getString(R.string.delete_message_confirm),
                        () -> executor.execute(() -> {
                            boolean deleted = repo.deleteMessage(message);
                            if (deleted) removeCachedMessage(message);
                            runOnUiThread(() -> {
                                SwipeDeleteTouchListener.reset(bubble);
                                if (deleted) {
                                    lastRenderSignature = Long.MIN_VALUE;
                                    refresh(false);
                                } else {
                                    Toast.makeText(this,R.string.delete_failed,Toast.LENGTH_LONG).show();
                                }
                            });
                        }),
                        () -> SwipeDeleteTouchListener.reset(bubble))));
    }

    private void removeCachedMessage(MessageItem message) {
        if (message == null) return;
        String key = messageKey(message);
        synchronized (cachedMessages) {
            cachedMessages.removeIf(item -> key.equals(messageKey(item)));
        }
    }

    private void confirmDeleteMessage(MessageItem message) {
        showDeleteConfirmation(
                getString(R.string.delete_message),
                getString(R.string.delete_message_confirm),
                () -> executor.execute(() -> {
                    boolean deleted = repo.deleteMessage(message);
                    if (deleted) removeCachedMessage(message);
                    runOnUiThread(() -> {
                        if (deleted) { lastRenderSignature = Long.MIN_VALUE; refresh(false); }
                        else Toast.makeText(this, R.string.delete_failed, Toast.LENGTH_LONG).show();
                    });
                }),
                null);
    }

    private void confirmDeleteConversation() {
        showDeleteConfirmation(
                getString(R.string.delete_conversation),
                getString(R.string.delete_conversation_confirm),
                () -> executor.execute(() -> {
                    boolean deleted = repo.deleteConversation(threadId);
                    if (deleted) {
                        pendingStore.removeForAddress(address);
                        drafts.clear(address);
                    }
                    runOnUiThread(() -> {
                        if (deleted) finish();
                        else Toast.makeText(this, R.string.conversation_delete_failed, Toast.LENGTH_LONG).show();
                    });
                }),
                null);
    }

    private void showDeleteConfirmation(String title, String message, Runnable confirm, Runnable cancel) {
        AlertDialog dialog = new AlertDialog.Builder(this)
                .setTitle(title)
                .setMessage(message)
                .setNegativeButton(android.R.string.cancel, (d, w) -> { if (cancel != null) cancel.run(); })
                .setPositiveButton(R.string.delete, (d, w) -> { if (confirm != null) confirm.run(); })
                .create();
        dialog.setOnCancelListener(d -> { if (cancel != null) cancel.run(); });
        dialog.setOnShowListener(d -> {
            if (dialog.getWindow() != null) dialog.getWindow().setBackgroundDrawableResource(R.drawable.confirm_dialog_background);
            dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(getColor(R.color.lemenzo_danger));
            dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(getColor(R.color.lemenzo_blue));
        });
        dialog.show();
    }

    private void pickAttachment() {
        Intent picker = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        picker.addCategory(Intent.CATEGORY_OPENABLE);
        picker.setType("*/*");
        picker.putExtra(
                Intent.EXTRA_MIME_TYPES,
                new String[]{"image/*", "video/*", "audio/*", "text/x-vcard"});
        startActivityForResult(picker, REQUEST_ATTACHMENT);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_ATTACHMENT && resultCode == RESULT_OK && data != null) {
            pendingAttachment = data.getData();
            if (pendingAttachment != null) {
                try {
                    getContentResolver().takePersistableUriPermission(
                            pendingAttachment,
                            data.getFlags()
                                    & (Intent.FLAG_GRANT_READ_URI_PERMISSION
                                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION));
                } catch (SecurityException ignored) {
                }
                showAttachmentPreview(pendingAttachment);
            }
        }
    }

    private void showAttachmentPreview(Uri uri){
        if(attachmentPreview!=null)attachmentPreview.setVisibility(View.VISIBLE);
        if(attachmentName!=null)attachmentName.setText(R.string.mms_attachment);
        if(attachmentThumb!=null){
            attachmentThumb.setPadding(Ui.dp(attachmentThumb,8),Ui.dp(attachmentThumb,8),Ui.dp(attachmentThumb,8),Ui.dp(attachmentThumb,8));
            attachmentThumb.setImageResource(R.drawable.ic_attachment_file);
            attachmentThumb.setContentDescription(getString(R.string.mms_attachment));
        }
        mediaExecutor.execute(() -> {
            String label=getString(R.string.mms_attachment);
            String type=null;
            android.graphics.Bitmap thumb=null;
            try(Cursor c=getContentResolver().query(uri,new String[]{OpenableColumns.DISPLAY_NAME},null,null,null)){
                if(c!=null&&c.moveToFirst()&&c.getString(0)!=null)label=c.getString(0);
            }catch(RuntimeException ignored){}
            try { type=getContentResolver().getType(uri); } catch (RuntimeException ignored) {}
            if(type!=null&&type.startsWith("image/")){
                try { thumb=getContentResolver().loadThumbnail(uri,new Size(96,96),null); }
                catch (Exception ignored) {}
            }
            final String finalLabel=label;
            final android.graphics.Bitmap finalThumb=thumb;
            runOnUiThread(() -> {
                if(isFinishing()||isDestroyed()||pendingAttachment==null||!pendingAttachment.equals(uri))return;
                if(attachmentName!=null)attachmentName.setText(finalLabel);
                if(attachmentThumb!=null&&finalThumb!=null){
                    attachmentThumb.setPadding(0,0,0,0);
                    attachmentThumb.setImageBitmap(finalThumb);
                    attachmentThumb.setContentDescription(getString(R.string.attachment_image));
                }
            });
        });
    }

    private void clearAttachment(){pendingAttachment=null;if(attachmentPreview!=null)attachmentPreview.setVisibility(View.GONE);}

    private void send() {
        if (sendInFlight) return;
        String text = input.getText().toString().trim();
        if (address == null || address.isEmpty()) return;
        if (text.isEmpty() && pendingAttachment == null) return;

        final Uri attachment = pendingAttachment;
        SubscriptionHelper.choose(this, subscriptionId -> {
            sendInFlight = true;
            if (sendButton != null) { sendButton.setEnabled(false); sendButton.setAlpha(0.55f); }
            sendExecutor.execute(() -> {
                boolean accepted = attachment != null
                        ? MmsSender.send(getApplicationContext(), address, text, attachment, subscriptionId)
                        : SmsSender.send(getApplicationContext(), address, text, subscriptionId);
                runOnUiThread(() -> {
                    sendInFlight = false;
                    if (isFinishing() || isDestroyed()) return;
                    if (sendButton != null) { sendButton.setEnabled(true); sendButton.setAlpha(1f); }
                    if (accepted) {
                        drafts.clear(address);
                        input.setText("");
                        if (attachment != null && attachment.equals(pendingAttachment)) clearAttachment();
                        refresh();
                    } else {
                        Toast.makeText(this,
                                attachment != null ? R.string.mms_send_failed : R.string.send_failed,
                                Toast.LENGTH_LONG).show();
                    }
                });
            });
        });
    }

}