package com.lemenzo.messenger;

import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Typeface;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.Telephony;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.Gravity;
import android.view.View;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.BaseAdapter;
import android.view.ViewGroup;
import android.widget.TextView;
import com.lemenzo.messenger.data.DraftRepository;
import com.lemenzo.messenger.model.BroadcastRecord;
import com.lemenzo.messenger.data.BroadcastStore;
import com.lemenzo.messenger.data.MessageRepository;
import com.lemenzo.messenger.data.PendingMessageStore;
import com.lemenzo.messenger.data.SmsContentObserver;
import com.lemenzo.messenger.model.ConversationSummary;
import com.lemenzo.messenger.sms.SmsRoleHelper;
import com.lemenzo.messenger.ui.Ui;
import com.lemenzo.messenger.ui.SwipeRevealTouchListener;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public final class MainActivity extends Activity {
    private MessageRepository repo;
    private ListView list;
    private ConversationAdapter conversationAdapter;
    private View emptyState;
    private EditText search;
    private LinearLayout roleCard;
    private DraftRepository drafts;
    private PendingMessageStore pendingStore;
    private BroadcastStore broadcastStore;
    private final ExecutorService executor=Executors.newSingleThreadExecutor();
    private final ExecutorService avatarExecutor=Executors.newFixedThreadPool(2);
    private final android.util.LruCache<String,android.graphics.Bitmap> avatarCache=
            new android.util.LruCache<String,android.graphics.Bitmap>(8*1024){
                @Override protected int sizeOf(String k,android.graphics.Bitmap v){
                    return Math.max(1,v.getAllocationByteCount()/1024);
                }
            };
    private SmsContentObserver smsObserver;
    private final Handler mainHandler = new Handler(Looper.getMainLooper());
    private final android.content.SharedPreferences.OnSharedPreferenceChangeListener pendingListener =
            (prefs,key) -> refresh(true);
    private final android.content.SharedPreferences.OnSharedPreferenceChangeListener broadcastListener =
            (prefs,key) -> refresh(false);
    private Runnable pendingSearch;
    private int searchGeneration;
    private final Object refreshLock = new Object();
    private boolean refreshInFlight;
    private boolean refreshRequested;
    private boolean providerDirty = true;
    private List<ConversationSummary> cachedConversations = java.util.Collections.emptyList();
    private String latestRefreshQuery = "";
    private int latestRefreshGeneration;
    private long lastRenderSignature = Long.MIN_VALUE;
    private View openSwipeForeground;
    private View openSwipeAction;
    private final DateFormat conversationTimeFormat = DateFormat.getTimeInstance(DateFormat.SHORT);
    private final DateFormat conversationDateFormat = DateFormat.getDateInstance(DateFormat.SHORT);
    private final java.text.SimpleDateFormat conversationWeekdayFormat =
            new java.text.SimpleDateFormat("EEE", java.util.Locale.getDefault());

    private static final class InboxItem {
        final ConversationSummary conversation;
        final BroadcastRecord broadcast;
        InboxItem(ConversationSummary conversation, BroadcastRecord broadcast){
            this.conversation=conversation;this.broadcast=broadcast;
        }
        boolean isBroadcast(){return broadcast!=null;}
        long stableId(){
            return isBroadcast()
                    ? (Long.MIN_VALUE | (broadcast.id.hashCode() & 0x7fffffffL))
                    : conversation.threadId;
        }
        long date(){return isBroadcast()?broadcast.date:conversation.date;}
        String name(){return isBroadcast()?broadcast.displayName():conversation.displayName;}
        String address(){return isBroadcast()?"":conversation.address;}
        String snippet(){return isBroadcast()?broadcast.body:conversation.snippet;}
        int unread(){return isBroadcast()?0:conversation.unreadCount;}
        String photo(){return isBroadcast()?"":conversation.photoUri;}
    }

    @Override public void onCreate(Bundle b){
        super.onCreate(b);
        Ui.prepareEdgeToEdge(this);
        repo=new MessageRepository(this);
        drafts=new DraftRepository(this);
        pendingStore=new PendingMessageStore(this);
        broadcastStore=new BroadcastStore(this);
        build();
        ensureSmsRoleThenPermissions();
    }

    @Override protected void onResume(){
        super.onResume();
        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);
        broadcastStore.preferences().registerOnSharedPreferenceChangeListener(broadcastListener);
        repo.clearContactCache();
        lastRenderSignature = Long.MIN_VALUE;
        updateRoleCard();
        refresh(true);
    }

    @Override protected void onPause(){
        if(smsObserver!=null){
            getContentResolver().unregisterContentObserver(smsObserver);
            smsObserver.dispose();
            smsObserver=null;
        }
        if(pendingStore!=null)pendingStore.preferences().unregisterOnSharedPreferenceChangeListener(pendingListener);
        if(broadcastStore!=null)broadcastStore.preferences().unregisterOnSharedPreferenceChangeListener(broadcastListener);
        super.onPause();
    }

    private void build(){
        FrameLayout frame=new FrameLayout(this);
        frame.setBackgroundColor(getColor(R.color.lemenzo_background));

        LinearLayout root=new LinearLayout(this);
        root.setOrientation(LinearLayout.VERTICAL);
        int p=Ui.dp(root,20);
        root.setPadding(p,Ui.dp(root,12),p,Ui.dp(root,22));
        Ui.applySafeInsets(root);
        frame.addView(root,new FrameLayout.LayoutParams(-1,-1));

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

        TextView title=Ui.text(this,getString(R.string.app_title),27,true);
        header.addView(title,new LinearLayout.LayoutParams(0,Ui.dp(root,56),1f));

        ImageButton searchButton=Ui.iconButton(this,R.drawable.ic_search,getString(R.string.search_conversations));
        searchButton.setOnClickListener(v->{ search.requestFocus(); search.setSelection(search.length()); });
        header.addView(searchButton,new LinearLayout.LayoutParams(Ui.dp(root,48),Ui.dp(root,48)));

        ImageButton menu=Ui.iconButton(this,R.drawable.ic_more,getString(R.string.more_options));
        menu.setOnClickListener(v->startActivity(new Intent(this,SettingsActivity.class)));
        header.addView(menu,new LinearLayout.LayoutParams(Ui.dp(root,48),Ui.dp(root,48)));
        root.addView(header);

        search=new EditText(this);
        search.setHint(R.string.search_conversations);
        search.setHintTextColor(getColor(R.color.lemenzo_text_muted));
        search.setTextColor(getColor(R.color.lemenzo_text_primary));
        search.setSingleLine();
        search.setTextSize(15);
        search.setBackgroundResource(R.drawable.search_background);
        search.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_search,0,0,0);
        search.setCompoundDrawablePadding(Ui.dp(root,10));
        search.setPadding(Ui.dp(root,16),0,Ui.dp(root,16),0);
        LinearLayout.LayoutParams sp=new LinearLayout.LayoutParams(-1,Ui.dp(root,54));
        sp.setMargins(0,0,0,Ui.dp(root,16));
        root.addView(search,sp);

        roleCard=new LinearLayout(this);
        roleCard.setOrientation(LinearLayout.VERTICAL);
        roleCard.setBackgroundResource(R.drawable.role_card_background);
        roleCard.setPadding(Ui.dp(root,16),Ui.dp(root,14),Ui.dp(root,16),Ui.dp(root,14));
        TextView roleTitle=Ui.text(this,getString(R.string.default_sms_required),15.5f,true);
        roleCard.addView(roleTitle);
        TextView roleSummary=Ui.text(this,getString(R.string.default_sms_required_summary),13,false);
        roleSummary.setTextColor(getColor(R.color.lemenzo_text_secondary));
        LinearLayout.LayoutParams rslp=new LinearLayout.LayoutParams(-1,-2);rslp.setMargins(0,Ui.dp(root,5),0,Ui.dp(root,10));
        roleCard.addView(roleSummary,rslp);
        TextView setDefault=Ui.text(this,getString(R.string.set_default),13,true);
        setDefault.setTextColor(0xffffffff);Ui.center(setDefault);setDefault.setBackgroundResource(R.drawable.action_button_blue);
        setDefault.setOnClickListener(v->SmsRoleHelper.requestRole(this));
        LinearLayout.LayoutParams sdp=new LinearLayout.LayoutParams(Ui.dp(root,112),Ui.dp(root,48));sdp.gravity=Gravity.END;roleCard.addView(setDefault,sdp);
        LinearLayout.LayoutParams rcp=new LinearLayout.LayoutParams(-1,-2);rcp.setMargins(0,0,0,Ui.dp(root,14));root.addView(roleCard,rcp);

        FrameLayout conversationArea = new FrameLayout(this);
        conversationArea.setBackgroundResource(R.drawable.group_background);

        list = new ListView(this);
        list.setDivider(null);
        list.setDividerHeight(0);
        list.setClipToPadding(false);
        list.setPadding(0,Ui.dp(root,4),0,Ui.dp(root,4));
        list.setCacheColorHint(0x00000000);
        list.setScrollingCacheEnabled(false);
        list.setSmoothScrollbarEnabled(true);
        list.setVerticalScrollBarEnabled(true);
        conversationAdapter = new ConversationAdapter();
        list.setAdapter(conversationAdapter);
        list.setOnScrollListener(new android.widget.AbsListView.OnScrollListener(){
            @Override public void onScrollStateChanged(android.widget.AbsListView view,int state){
                if(state==android.widget.AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL
                        && openSwipeForeground!=null && openSwipeAction!=null){
                    SwipeRevealTouchListener.close(openSwipeForeground,openSwipeAction,null);
                    openSwipeForeground=null;openSwipeAction=null;
                }
            }
            @Override public void onScroll(android.widget.AbsListView view,int first,int visible,int total){}
        });
        conversationArea.addView(list,new FrameLayout.LayoutParams(-1,-1));

        LinearLayout empty=new LinearLayout(this);
        empty.setOrientation(LinearLayout.VERTICAL);
        empty.setGravity(Gravity.CENTER);
        TextView emptyTitle=Ui.text(this,getString(R.string.no_conversations),17,true);
        emptyTitle.setTextColor(getColor(R.color.lemenzo_text_primary));
        Ui.center(emptyTitle);
        empty.addView(emptyTitle);
        TextView emptyHint=Ui.text(this,getString(R.string.empty_conversations_hint),13.5f,false);
        emptyHint.setTextColor(getColor(R.color.lemenzo_blue));
        Ui.center(emptyHint);
        LinearLayout.LayoutParams emptyHintLp=new LinearLayout.LayoutParams(-2,-2);
        emptyHintLp.setMargins(0,Ui.dp(root,8),0,0);
        empty.addView(emptyHint,emptyHintLp);
        emptyState=empty;
        FrameLayout.LayoutParams emptyLp=new FrameLayout.LayoutParams(-1,-2,Gravity.CENTER);
        conversationArea.addView(empty,emptyLp);
        list.setEmptyView(empty);
        root.addView(conversationArea,new LinearLayout.LayoutParams(-1,0,1f));

        ImageButton compose=Ui.iconButton(this,R.drawable.ic_compose,getString(R.string.new_message));
        compose.setBackgroundResource(R.drawable.round_blue_button);
        compose.setPadding(Ui.dp(root,16),Ui.dp(root,16),Ui.dp(root,16),Ui.dp(root,16));
        compose.setOnClickListener(v->startActivity(new Intent(this,ComposeActivity.class)));
        FrameLayout.LayoutParams cp=new FrameLayout.LayoutParams(Ui.dp(root,62),Ui.dp(root,62),Gravity.END|Gravity.BOTTOM);
        cp.setMargins(0,0,Ui.dp(root,24),Ui.dp(root,34));
        frame.addView(compose,cp);

        search.addTextChangedListener(new TextWatcher(){
            public void beforeTextChanged(CharSequence s,int a,int c,int d){}
            public void onTextChanged(CharSequence s,int a,int b,int c){scheduleRefresh();}
            public void afterTextChanged(Editable e){}
        });
        setContentView(frame);
    }

    private void updateRoleCard(){
        if(roleCard==null)return;
        roleCard.setVisibility(SmsRoleHelper.isSmsRoleAvailable(this)&&!SmsRoleHelper.isDefaultSmsApp(this)?View.VISIBLE:View.GONE);
    }

    private void ensureSmsRoleThenPermissions(){
        if(SmsRoleHelper.isSmsRoleAvailable(this) && !SmsRoleHelper.isDefaultSmsApp(this)){
            SmsRoleHelper.requestRole(this); return;
        }
        requestRuntimePermissions();
    }

    private void requestRuntimePermissions(){
        String[] ps={Manifest.permission.READ_SMS,Manifest.permission.RECEIVE_SMS,Manifest.permission.RECEIVE_MMS,
                Manifest.permission.RECEIVE_WAP_PUSH,Manifest.permission.SEND_SMS,Manifest.permission.READ_CONTACTS,
                Manifest.permission.READ_PHONE_STATE,Manifest.permission.POST_NOTIFICATIONS};
        ArrayList<String> missing=new ArrayList<>();
        for(String p:ps)if(checkSelfPermission(p)!=PackageManager.PERMISSION_GRANTED)missing.add(p);
        if(!missing.isEmpty())requestPermissions(missing.toArray(new String[0]),100);else refresh();
    }

    @Override public void onActivityResult(int requestCode,int resultCode,Intent data){
        super.onActivityResult(requestCode,resultCode,data);
        if(requestCode==SmsRoleHelper.REQUEST_ROLE_SMS){updateRoleCard();requestRuntimePermissions();}
    }

    @Override public void onRequestPermissionsResult(int r,String[]p,int[]g){super.onRequestPermissionsResult(r,p,g);refresh();}

    private void scheduleRefresh(){
        if (pendingSearch != null) mainHandler.removeCallbacks(pendingSearch);
        pendingSearch = () -> refresh(false);
        mainHandler.postDelayed(pendingSearch, 250L);
    }

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

    private void refresh(boolean providerChanged){
        if(list==null||checkSelfPermission(Manifest.permission.READ_SMS)!=PackageManager.PERMISSION_GRANTED)return;
        String query=search==null?"":search.getText().toString();
        int generation=++searchGeneration;
        synchronized(refreshLock){
            providerDirty |= providerChanged;
            latestRefreshQuery=query;
            latestRefreshGeneration=generation;
            refreshRequested=true;
            if(refreshInFlight)return;
            refreshInFlight=true;
        }
        executor.execute(this::drainRefreshRequests);
    }

    private void drainRefreshRequests(){
        while(!Thread.currentThread().isInterrupted()){
            final String query;
            final int generation;
            final boolean reloadProvider;
            synchronized(refreshLock){
                if(!refreshRequested){
                    refreshInFlight=false;
                    return;
                }
                refreshRequested=false;
                reloadProvider=providerDirty;
                providerDirty=false;
                query=latestRefreshQuery;
                generation=latestRefreshGeneration;
            }
            if(reloadProvider) cachedConversations=repo.conversations("");
            List<InboxItem> xs=filterCachedConversations(query);
            long signature=conversationSignature(xs);
            runOnUiThread(()->{
                if(generation==searchGeneration && signature!=lastRenderSignature){
                    lastRenderSignature=signature;
                    render(xs);
                }
            });
        }
        synchronized(refreshLock){refreshInFlight=false;}
    }

    private List<InboxItem> filterCachedConversations(String query){
        String q=query==null?"":query.trim().toLowerCase(Locale.ROOT);
        ArrayList<InboxItem> out=new ArrayList<>();
        for(ConversationSummary x:cachedConversations){
            if(q.isEmpty()||containsIgnoreCase(x.address,q)||containsIgnoreCase(x.displayName,q)||containsIgnoreCase(x.snippet,q))
                out.add(new InboxItem(x,null));
        }
        if(broadcastStore!=null){
            for(BroadcastRecord b:broadcastStore.all()){
                if(q.isEmpty()||containsIgnoreCase(b.displayName(),q)||containsIgnoreCase(b.body,q))
                    out.add(new InboxItem(null,b));
            }
        }
        out.sort((a,b)->Long.compare(b.date(),a.date()));
        return out;
    }

    private static boolean containsIgnoreCase(String value,String lowerQuery){
        return value!=null&&value.toLowerCase(Locale.ROOT).contains(lowerQuery);
    }

    private static long conversationSignature(List<InboxItem> items){
        long h=1125899906842597L;
        for(InboxItem x:items){
            h=31*h+x.stableId();
            h=31*h+x.date();
            h=31*h+x.unread();
            h=31*h+(x.name()==null?0:x.name().hashCode());
            h=31*h+(x.snippet()==null?0:x.snippet().hashCode());
            if(x.isBroadcast()){
                h=31*h+x.broadcast.sent;
                h=31*h+x.broadcast.failed;
            }
        }
        return h;
    }

    private void render(List<InboxItem> xs){
        if(isFinishing()||list==null||conversationAdapter==null)return;
        conversationAdapter.replace(xs);
    }

    /** Framework ListView keeps only visible rows alive, so inbox startup and scrolling stay
     * bounded even with a very large conversation history. */
    private final class ConversationAdapter extends BaseAdapter {
        private final ArrayList<InboxItem> items = new ArrayList<>();

        void replace(List<InboxItem> data) {
            items.clear();
            if(data!=null) items.addAll(data);
            notifyDataSetChanged();
        }
        @Override public int getCount(){ return items.size(); }
        @Override public InboxItem getItem(int position){ return items.get(position); }
        @Override public long getItemId(int position){ return items.get(position).stableId(); }
        @Override public boolean hasStableIds(){ return true; }
        @Override public View getView(int position, View convertView, ViewGroup parent){
            ConversationRowHolder holder;
            if (convertView == null || !(convertView.getTag() instanceof ConversationRowHolder)) {
                convertView = createConversationRow();
                holder = (ConversationRowHolder) convertView.getTag();
            } else {
                holder = (ConversationRowHolder) convertView.getTag();
            }
            bindConversationRow(holder, getItem(position));
            return convertView;
        }
    }

    private String conversationTimeLabel(long when){
        Calendar now=Calendar.getInstance();
        Calendar then=Calendar.getInstance();
        then.setTimeInMillis(when);
        if(now.get(Calendar.YEAR)==then.get(Calendar.YEAR) && now.get(Calendar.DAY_OF_YEAR)==then.get(Calendar.DAY_OF_YEAR))
            return conversationTimeFormat.format(new Date(when));
        Calendar yesterday=(Calendar)now.clone();
        yesterday.add(Calendar.DAY_OF_YEAR,-1);
        if(yesterday.get(Calendar.YEAR)==then.get(Calendar.YEAR) && yesterday.get(Calendar.DAY_OF_YEAR)==then.get(Calendar.DAY_OF_YEAR))
            return getString(R.string.yesterday);
        long age=now.getTimeInMillis()-when;
        if(age>=0 && age<7L*24L*60L*60L*1000L)
            return conversationWeekdayFormat.format(new Date(when));
        return conversationDateFormat.format(new Date(when));
    }

    @Override protected void onDestroy(){
        if(pendingSearch!=null)mainHandler.removeCallbacks(pendingSearch);
        executor.shutdownNow();
        avatarExecutor.shutdownNow();
        avatarCache.evictAll();
        super.onDestroy();
    }

    private static final class ConversationRowHolder {
        FrameLayout container;
        LinearLayout root;
        LinearLayout deleteAction;
        ImageView avatarImage;
        TextView avatarInitial;
        TextView name;
        TextView snippet;
        TextView time;
        TextView badge;
    }

    private View createConversationRow(){
        FrameLayout container=new FrameLayout(this);
        container.setMinimumHeight(Ui.dp(container,82));

        LinearLayout deleteAction=new LinearLayout(this);
        deleteAction.setOrientation(LinearLayout.VERTICAL);
        deleteAction.setGravity(Gravity.CENTER);
        deleteAction.setBackgroundResource(R.drawable.swipe_delete_background);
        deleteAction.setClickable(true);
        deleteAction.setFocusable(true);
        deleteAction.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
        ImageView trash=new ImageView(this);
        trash.setImageResource(R.drawable.ic_delete);
        trash.setContentDescription(null);
        deleteAction.addView(trash,new LinearLayout.LayoutParams(Ui.dp(container,30),Ui.dp(container,30)));
        TextView deleteLabel=Ui.text(this,getString(R.string.delete),12.5f,true);
        deleteLabel.setTextColor(0xffffffff);
        Ui.center(deleteLabel);
        LinearLayout.LayoutParams dlpText=new LinearLayout.LayoutParams(-2,-2);dlpText.setMargins(0,Ui.dp(container,2),0,0);
        deleteAction.addView(deleteLabel,dlpText);
        FrameLayout.LayoutParams deleteLp=new FrameLayout.LayoutParams(Ui.dp(container,92),-1,Gravity.END);
        deleteLp.setMargins(0,Ui.dp(container,5),0,Ui.dp(container,5));
        container.addView(deleteAction,deleteLp);

        LinearLayout r=new LinearLayout(this);
        r.setGravity(Gravity.CENTER_VERTICAL);
        r.setPadding(Ui.dp(r,14),Ui.dp(r,12),Ui.dp(r,14),Ui.dp(r,12));
        r.setBackgroundColor(getColor(R.color.lemenzo_surface));
        r.setClickable(true);
        r.setFocusable(true);
        r.setMinimumHeight(Ui.dp(r,82));
        container.addView(r,new FrameLayout.LayoutParams(-1,-2));

        ConversationRowHolder h=new ConversationRowHolder();
        h.container=container;
        h.root=r;
        h.deleteAction=deleteAction;

        FrameLayout avatarHost=new FrameLayout(this);
        h.avatarImage=new ImageView(this);
        h.avatarImage.setScaleType(ImageView.ScaleType.CENTER_CROP);
        h.avatarImage.setBackgroundResource(R.drawable.avatar_background);
        h.avatarImage.setClipToOutline(true);
        h.avatarImage.setContentDescription(getString(R.string.contact_avatar));
        avatarHost.addView(h.avatarImage,new FrameLayout.LayoutParams(-1,-1));
        h.avatarInitial=Ui.text(this,"?",19,true);
        h.avatarInitial.setTextColor(0xffffffff);
        Ui.center(h.avatarInitial);
        h.avatarInitial.setBackgroundResource(R.drawable.avatar_background);
        h.avatarInitial.setContentDescription(getString(R.string.contact_avatar));
        avatarHost.addView(h.avatarInitial,new FrameLayout.LayoutParams(-1,-1));
        r.addView(avatarHost,new LinearLayout.LayoutParams(Ui.dp(r,52),Ui.dp(r,52)));

        LinearLayout c=new LinearLayout(this);
        c.setOrientation(LinearLayout.VERTICAL);
        c.setPadding(Ui.dp(r,14),0,Ui.dp(r,8),0);
        h.name=Ui.text(this,"",16.5f,true);
        h.name.setSingleLine();
        c.addView(h.name);
        h.snippet=Ui.text(this,"",SettingsActivity.conversationPreviewTextSize(this),false);
        h.snippet.setTextColor(getColor(R.color.lemenzo_text_secondary));
        h.snippet.setSingleLine();
        LinearLayout.LayoutParams snp=new LinearLayout.LayoutParams(-1,-2);
        snp.setMargins(0,Ui.dp(r,4),0,0);
        c.addView(h.snippet,snp);
        r.addView(c,new LinearLayout.LayoutParams(0,-2,1f));

        LinearLayout end=new LinearLayout(this);
        end.setOrientation(LinearLayout.VERTICAL);
        end.setGravity(Gravity.END|Gravity.CENTER_VERTICAL);
        h.time=Ui.text(this,"",12,false);
        end.addView(h.time);
        h.badge=Ui.text(this,"",11,true);
        h.badge.setTextColor(0xffffffff);
        Ui.center(h.badge);
        h.badge.setBackgroundResource(R.drawable.unread_badge);
        LinearLayout.LayoutParams bp=new LinearLayout.LayoutParams(Ui.dp(r,24),Ui.dp(r,24));
        bp.gravity=Gravity.END;
        bp.setMargins(0,Ui.dp(r,7),0,0);
        end.addView(h.badge,bp);
        r.addView(end,new LinearLayout.LayoutParams(-2,-2));

        container.setTag(h);
        return container;
    }

    private void bindConversationRow(ConversationRowHolder h, InboxItem x){
        LinearLayout r=h.root;
        // Recycled rows may still carry a swipe translation from a previous item.
        if(openSwipeForeground==r){openSwipeForeground=null;openSwipeAction=null;}
        r.animate().cancel();
        r.setTranslationX(0f);
        r.setAlpha(1f);
        r.setOnTouchListener(null);
        SwipeRevealTouchListener.reset(r,h.deleteAction);

        String name=x.name();
        if(name==null||name.trim().isEmpty())name=x.address()==null?getString(R.string.generic_unknown):x.address();
        String initial=name.isEmpty()?"?":name.substring(0,1).toUpperCase(Locale.getDefault());
        h.avatarImage.setImageDrawable(null);
        h.avatarImage.setVisibility(View.GONE);
        h.avatarInitial.setText(initial);
        h.avatarInitial.setVisibility(View.VISIBLE);
        final String avatarKey = x.photo();
        h.avatarImage.setTag(avatarKey);
        if(avatarKey!=null&&!avatarKey.isEmpty()){
            android.graphics.Bitmap cached=avatarCache.get(avatarKey);
            if(cached!=null){
                h.avatarImage.setImageBitmap(cached);
                h.avatarImage.setVisibility(View.VISIBLE);
                h.avatarInitial.setVisibility(View.GONE);
            }else{
                avatarExecutor.execute(() -> {
                    android.graphics.Bitmap bmp=null;
                    try{bmp=getContentResolver().loadThumbnail(android.net.Uri.parse(avatarKey),new android.util.Size(96,96),null);}catch(Exception ignored){}
                    if(bmp==null)return;
                    avatarCache.put(avatarKey,bmp);
                    final android.graphics.Bitmap ready=bmp;
                    runOnUiThread(() -> {
                        if(isFinishing()||isDestroyed()||!avatarKey.equals(h.avatarImage.getTag()))return;
                        h.avatarImage.setImageBitmap(ready);
                        h.avatarImage.setVisibility(View.VISIBLE);
                        h.avatarInitial.setVisibility(View.GONE);
                    });
                });
            }
        }

        h.name.setText(name);
        h.snippet.setText(x.snippet()==null?"":x.snippet());
        h.snippet.setTextSize(SettingsActivity.conversationPreviewTextSize(this));
        h.time.setText(conversationTimeLabel(x.date()));
        h.time.setTextColor(getColor(x.unread()>0?R.color.lemenzo_blue:R.color.lemenzo_text_muted));
        if(x.isBroadcast()){
            h.badge.setText(Integer.toString(x.broadcast.total));
            h.badge.setVisibility(View.VISIBLE);
        }else if(x.unread()>0){
            h.badge.setText(x.unread()>99?"99+":Integer.toString(x.unread()));
            h.badge.setVisibility(View.VISIBLE);
        }else{
            h.badge.setText("");
            h.badge.setVisibility(View.GONE);
        }

        String finalName=name;
        r.setContentDescription(getString(R.string.open_conversation)+" "+finalName);
        r.setOnClickListener(v->{
            if(x.isBroadcast()){
                startActivity(new Intent(this,BroadcastActivity.class)
                        .putExtra(BroadcastActivity.EXTRA_BROADCAST_ID,x.broadcast.id));
            }else{
                Intent i=new Intent(this,ConversationActivity.class);
                i.putExtra("thread_id",x.conversation.threadId);
                i.putExtra("address",x.conversation.address);
                startActivity(i);
            }
        });
        enableConversationSwipe(h,x);
    }

    private void enableConversationSwipe(ConversationRowHolder holder, InboxItem conversation) {
        View row=holder.root;
        View action=holder.deleteAction;
        SwipeRevealTouchListener.StateListener state=new SwipeRevealTouchListener.StateListener(){
            @Override public void onOpened(View foreground){
                if(openSwipeForeground!=null && openSwipeForeground!=foreground && openSwipeAction!=null){
                    SwipeRevealTouchListener.close(openSwipeForeground,openSwipeAction,null);
                }
                openSwipeForeground=foreground;
                openSwipeAction=action;
            }
            @Override public void onClosed(View foreground){
                if(openSwipeForeground==foreground){openSwipeForeground=null;openSwipeAction=null;}
            }
        };
        row.setOnTouchListener(new SwipeRevealTouchListener(row,action,state));
        action.setOnClickListener(v->{
            v.setEnabled(false);
            executor.execute(()->{
                boolean deleted;
                if(conversation.isBroadcast()){
                    broadcastStore.remove(conversation.broadcast.id);
                    deleted=true;
                }else{
                    boolean hadPending=pendingStore!=null && pendingStore.hasForAddress(conversation.conversation.address);
                    boolean providerDeleted=repo.deleteConversation(conversation.conversation.threadId);
                    if(pendingStore!=null)pendingStore.removeForAddress(conversation.conversation.address);
                    if(drafts!=null)drafts.clear(conversation.conversation.address);
                    deleted=providerDeleted||hadPending;
                }
                runOnUiThread(()->{
                    v.setEnabled(true);
                    SwipeRevealTouchListener.reset(row,action);
                    if(openSwipeForeground==row){openSwipeForeground=null;openSwipeAction=null;}
                    if(deleted){
                        lastRenderSignature=Long.MIN_VALUE;
                        refresh(true);
                    }else{
                        android.widget.Toast.makeText(this,R.string.conversation_delete_failed,android.widget.Toast.LENGTH_LONG).show();
                    }
                });
            });
        });
    }

}
