package com.xixun.xixunplayer;

import static android.view.View.VISIBLE;

import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.StatFs;
import android.view.Choreographer;
import android.view.View;
import android.webkit.WebView;

import androidx.activity.ComponentActivity;
import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.media3.common.util.UnstableApi;

import com.xixun.joey.aidlset.CardService;

import net.lingala.zip4j.ZipFile;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.HashSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;

import gnph.util.IOs;
import gnph.util.JSList;
import gnph.util.JSMap;
import gnph.util.NumFmts;
import gnph.util.O;

public class MainActivity extends ComponentActivity implements Choreographer.FrameCallback, Runnable {

    public static MainActivity ins;
    public Intent environIntent = new Intent();
    HashSet<IntentReceiver> environs = new HashSet<>();
    BackView backView;
    Prog progView;
    long launchMilli = System.currentTimeMillis();
    long syncMs;
    int state;

    @RequiresApi(api = Build.VERSION_CODES.R)
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        var msg = "==== MainActivity onCreate ==== UI Thread: "+Thread.currentThread().getId();
        Util.println(msg);
        ins = this;
        startService(new Intent(this, RestartService.class));

        if(ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) init();
        else {
            Util.println("---- No permission, Try again ...");
            ActivityCompat.requestPermissions(this, new String[]{
                android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
                android.Manifest.permission.READ_EXTERNAL_STORAGE,
                android.Manifest.permission.MANAGE_EXTERNAL_STORAGE,
                android.Manifest.permission.RECEIVE_BOOT_COMPLETED,
                android.Manifest.permission.INTERNET
            }, 999);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if(requestCode==999 && grantResults.length > 0 && grantResults[0]==PackageManager.PERMISSION_GRANTED && backView==null) init();
    }

    @SuppressLint("UnspecifiedRegisterReceiverFlag")
    public void init() {
        var dir = Build.VERSION.SDK_INT < Build.VERSION_CODES.R ? Environment.getExternalStorageDirectory().getAbsolutePath() + "/XixunPlayer" : getExternalFilesDir(null).getAbsolutePath();
        var msg = "---- dir "+dir;
        Util.println(msg);
        Util.programDir = dir + "/program";
        Util.backImgFile = dir + "/background";

        var program = new File(Util.programDir);
        if(program.isFile()) program.delete();
        msg = "---- mkdir: "+program.mkdirs();
        Util.println(msg);

        var conn = new ServiceConnection() {
            public void onServiceDisconnected(ComponentName name) {
                Util.println("Disconnected cardsystem aidl service");
            }

            public void onServiceConnected(ComponentName name, IBinder iBinder) {
                Util.println("Bind cardsystem aidl service success");
                var service = CardService.Stub.asInterface(iBinder);
                try {
                    Util.isScreenOn = service.isScreenOpen();
                    Util.println("isScreenOn: "+Util.isScreenOn);
                    backView = new BackView(MainActivity.this, service.getScreenWidth(), service.getScreenHeight());
                    state = 5;
                    if(Util.isScreenOn) initProg();
                    else state = 8;
                    if(progView==null) setContentView(backView);
                } catch (RemoteException e) {
                    Util.makeText(MainActivity.this, Util.toStr(e)).show();
                    Util.printStackTrace(e);
                } finally {
                    unbindService(this);
                }
            }
        };
        var intent = new Intent("com.xixun.joey.aidlset.SettingsService");
        intent.setPackage("com.xixun.joey.cardsystem");
        bindService(intent, conn, Context.BIND_AUTO_CREATE);

        var intentFilter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
        intentFilter.addDataScheme("file");
        registerReceiver(new BroadcastReceiver(){
            long lastMs;
            char [] pass = {'8','8','8'};
            @Override
            public void onReceive(Context context, Intent intent) {
                var path = intent.getData().getPath();
                Util.println("\nMEDIA_MOUNTED path: "+path);
                var ms = System.currentTimeMillis();
                if(ms-lastMs<1000) return;
                lastMs = ms;
                Util.makeText(MainActivity.this, "MEDIA_MOUNTED path: "+path).show();
                new Thread(()->{
                    try {
                        var zip = new ZipFile(path+"/program.zip");
                        if(zip.isEncrypted()) zip.setPassword(pass);
                        var json = JSMap.fromClose(zip.getInputStream(zip.getFileHeader("program")));
                        var isIns = json.bool("isIns");
                        long size = 0;
                        var headers = zip.getFileHeaders();
                        for(var header : headers) size += header.getUncompressedSize();
                        if(size==0) {
                            Util.println("zip size is 0");
                            runOnUiThread(() -> {
                                Util.makeText(MainActivity.this, "zip size is 0").show();
                            });
                            return;
                        }
                        Util.deleteFiles(size, null);
                        for(var header : headers) zip.extractFile(header, Util.programDir);
                        runOnUiThread(() -> {
                            Util.println("Import Succeed");
                            Util.makeText(MainActivity.this, "Import Succeed").show();
                            initProg();
                        });
                    } catch (Exception e) {
                        Util.printStackTrace(e);
                        runOnUiThread(() -> {
                            Util.makeText(MainActivity.this, Util.toStr(e)).show();
                        });
                    }
                }).start();
            }
        }, intentFilter);

        registerReceiver(new BroadcastReceiver(){
            @Override
            public void onReceive(Context context, Intent intent) {
                Util.println("Receive PAUSE_PLAYER");
                Util.isScreenOn = ! intent.getBooleanExtra("pause", false);
                Util.println("  isScreenOn: "+Util.isScreenOn);
                if(! Util.isScreenOn) {
                    state = 8;
                    if(progView!=null) {
                        progView.release();
                        progView = null;
                        setContentView(backView);
                    }
                } else if(progView==null) initProg();
            }
        }, new IntentFilter("com.xixun.action.PAUSE_PLAYER"));

//        registerReceiver(new BroadcastReceiver(){
//            @Override
//            public void onReceive(Context context, Intent intent) {
//            }
//        }, new IntentFilter("com.xixun.joey.CHANGE_COMPANYID"), RECEIVER_EXPORTED);

        registerReceiver(new BroadcastReceiver(){
            @Override
            public void onReceive(Context context, Intent intent) {
                MainActivity.this.environIntent = intent;
                for(var environ : environs) {
                    environ.onReceive(intent);
                    ((View)environ).invalidate();
                }
            }
        }, new IntentFilter("xixun.intent.action.TEMPERATURE_HUMIDITY"));

        registerReceiver(new BroadcastReceiver(){
            @Override
            public void onReceive(Context context, Intent intent) {
                try {
                    var code = intent.getIntExtra("code", 0);
                    Util.println("  remote_control "+code);
                    if(progView == null || code > progView.pages.size()) return;
                    if(! progView.avas.isEmpty()) page(curAva).hide();
                    var millis = (System.currentTimeMillis()+999)/1000*1000;
                    if(code > 0) {
                        progView.avas.clear();
                        progView.avas.add(code-1);
                        curAva = 0;
                        curTimes = 1;
                        waitTo = 0;
                        var page = page(0);
                        page.setMillis(millis);
                        if(state != 6) {
                            setContentView(progView);
                            state = 6;
                        }
                    } else {
                        waitTo = Long.MAX_VALUE;
                        syncProg(millis);
                    }
                } catch (Throwable e) {
                    Util.makeText(MainActivity.this, Util.toStr(e)).show();
                    Util.printStackTrace(e);
                }
            }
        }, new IntentFilter("com.xixun.yzd.REMOTE_CONTROL"));

        new Thread(this).start();
    }

    public void stopProg() {
        if(progView==null) return;
        progView.release();
        progView = null;
        setContentView(backView);
    }
    public boolean delProgFile() {
        stopProg();
        var files = new File(Util.programDir).listFiles();
        var ok = true;
        if(files != null) for(var file : files) if(! file.delete()) ok = false;
        state = 4;
        try {
            var out = new FileOutputStream(Util.programDir+"/program");
            out.write("{}".getBytes());
            out.flush();
            out.getFD().sync();
            out.close();
        } catch (Throwable ignored) {
        }
        return ok;
    }
    public void initProg() {
        try {
            var task = JSMap.fromClose(new BufferedInputStream(new FileInputStream(Util.programDir + "/program"))).jsmap("task");
            if(task==null) {
                state = 3;
                return;
            }
            var view = new Prog(task, this);
            if(view.getChildCount()==0) {
                state = 3;
                return;
            }
            if(progView!=null) progView.release();
            progView = view;
            setContentView(progView);
            state = 5;
            Util.println("Init Sync");
            syncProg((System.currentTimeMillis()+999)/1000*1000);
            if(canAdd) {
                choreographer.postFrameCallback(this);
                canAdd = false;
            }
        } catch (FileNotFoundException e) {
            state = 3;
            Util.printStackTrace(e);
        } catch (Throwable e) {
            state = 7;
            Util.makeText(this, Util.toStr(e)).show();
            Util.printStackTrace(e);
        }
    }

    Choreographer choreographer = Choreographer.getInstance();
    int curAva, curTimes = 1;
    long waitTo = Long.MAX_VALUE;
    boolean canAdd = true;

    @Override
    public void doFrame(long frameTimeNanos) {
        if(progView == null) {
            canAdd = true;
            return;
        }
        var milli = System.currentTimeMillis();
        if(progView.avas.isEmpty()) {
            if(milli >= waitTo) {
                waitTo = Long.MAX_VALUE;
                Util.println("wait sync");
                syncProg(milli);
            }
        } else {
            var lastPage = page(curAva);
            if(milli >= lastPage.endMilli) {
                lastPage.hide();
                if(waitTo>0) {  //waitTo==0 为点播，不换页
                    if(curTimes < lastPage.repeatTimes) curTimes++;
                    else {
                        curTimes = 1;
                        curAva++;
                        if(curAva >= progView.avas.size()) {
                            var isDiff = milli-lastPage.endMilli>=1000;
                            if(Util.buf.length()>1000000) Util.buf.replace(0, 100000, "");
                            Util.println("isDiff: "+isDiff+" endMs: "+lastPage.endMilli+" millis:"+milli);
                            syncProg(isDiff ? milli : lastPage.endMilli);
                            Util.println("after. curAva: "+curAva+" endMs: "+lastPage.endMilli);
                            choreographer.postFrameCallback(this);
                            canAdd = false;
                            return;
                        }
                    }
                }
                page(curAva).setMillis(lastPage.endMilli);
                Util.println("curAva: "+curAva+" endMs: "+lastPage.endMilli);
            } else {
                for(var layer : lastPage.layers) {
                    for(var src : layer.srcs) {
                        if(src.view.getVisibility()==VISIBLE) {
                            if(milli >= src.endMilli) src.hide();
                            else src.doEff();
                        } else if(milli < src.endMilli && milli >= src.startMilli) src.show();
                    }
                    if(milli >= layer.endMilli) {
                        layer.endMilli += layer.dur;
                        for(var src : layer.srcs) {
                            src.endMilli += layer.dur;
                            src.startMilli += layer.dur;
                            if(src.startTime > 0) src.hide();
                            else src.show();
                        }
                    }
                }
            }
        }
        choreographer.postFrameCallback(this);
        canAdd = false;
    }

    void syncProg(long milli) {
        curTimes = 1;
        progView.avas.clear();
        var dur = 0;
        for(int i=0; i<progView.pages.size(); i++) if(progView.pages.get(i).isSchedule(milli+dur)) {
            progView.avas.add(i);
            dur += progView.pages.get(i).tDur;
        }
        if(dur==0) {
            waitTo = milli + 1000;
            if(state!=2) {
                setContentView(backView);
                state = 2;
            }
        } else {
            var start = milli / dur * dur;
            curAva = 0;
            if(start < milli) {
                do {
                    start += page(curAva++).tDur;
                } while(curAva < progView.avas.size() && start<=milli);
                start -= page(--curAva).tDur;
                syncMs = milli;
                Util.println("Sync. milli: "+milli+" start: "+start+" diff: "+(milli - start));
            }
            page(curAva).setMillis(start);
            if(state != 6) {
                setContentView(progView);
                state = 6;
            }
        }
    }

    Prog.Page page(int i) {
        return progView.pages.get(progView.avas.get(i));
    }

    @OptIn(markerClass = UnstableApi.class) public void run() {
        try {
            var serverSocket = new ServerSocket(3333);
            while(true) {
                InputStream in = null;
                OutputStream out = null;
                try {
                    Util.println("Accepting ...");
                    final var socket = serverSocket.accept();
                    Util.println("Accepted");
                    in = socket.getInputStream();
                    out = socket.getOutputStream();
                    HashSet<String> hases = null;
                    while(true) {
                        var obj = JSMap.from(in);
                        var _type = obj.stnn("_type");
                        Util.println("_type: "+_type);
                        if("consult".equals(_type)) {
                            JSList<String> ids = obj.jslist("idList");
                            hases = new HashSet<>();
                            if(ids!=null) for(int i=0; i<ids.size(); i++) {
                                if(new File(Util.programDir + "/" + ids.get(i)).exists()) ids.remove(i--);
                                else hases.add(ids.get(i));
                            }
                            new JSMap("_type", _type, "idList", ids).write(out);
                        } else if("proStart".equals(_type)) {
                            Util.deleteFiles(obj.intg("proSize"), hases);
                        } else if("fileStart".equals(_type)) {
                            var size = obj.intg("size");
                            var name = obj.stnn("id");
                            Util.println("  size: " + size + "  name: " + name);
                            var fout = new FileOutputStream(Util.programDir + "/" + name);
                            IOs.write(fout, in, size);
                            fout.flush();
                            fout.getFD().sync();
                            fout.close();
                        } else if("imgFileStart".equals(_type)) {
                            var size = obj.intg("size");
                            var fout = new FileOutputStream(Util.backImgFile);
                            IOs.write(fout, in, size);
                            fout.flush();
                            fout.getFD().sync();
                            fout.close();
                            runOnUiThread(() -> {
                                backView.cosImg = BitmapFactory.decodeFile(Util.backImgFile);
                                backView.invalidate();
                            });
                        } else if("imgFileEnd".equals(_type)) {
                            new JSMap("success", true).write(out);
                        } else if("proEnd".equals(_type)) {
                            new JSMap("success", true).write(out);
                            runOnUiThread(this::initProg);
                        } else if("DelPrograms".equals(_type)) {
                            var latch = new CountDownLatch(1);
                            var ok = new AtomicBoolean(false);
                            runOnUiThread(() -> {
                                ok.set(delProgFile());
                                latch.countDown();
                            });
                            try {
                                latch.await();
                            } catch (InterruptedException ignored) {}
                            new JSMap("success", ok.get()).write(out);
                        } else if("DelBackImg".equals(_type)) {
                            MainActivity.ins.runOnUiThread(() -> {
                                MainActivity.ins.backView.cosImg = null;
                                MainActivity.ins.backView.invalidate();
                            });
                            new JSMap("success", new File(Util.backImgFile).delete()).write(out);
                        } else if("getPlayerState".equals(_type)) {
                            var descs = Util.getState(state);
                            new JSMap(
                                    "code", state,
                                    "des_en", descs[0],
                                    "des", descs[1]
                            ).write(out);
                        } else if("GetInfo".equals(_type)) {
                            var writer = new OutputStreamWriter(out);
                            var Fmt = new SimpleDateFormat("yy-MM-dd HH:mm:ss.SSS");
                            var dur = 0;
                            if(progView!=null) for(var page : progView.pages) dur += page.tDur;
                            writer.append("ProgSend: ").append(Fmt.format(new File(Util.programDir + "/program").lastModified())).append(" ProgDur: ").append(String.valueOf(dur));
                            if(progView!=null) writer.append(" Pages: ").append(String.valueOf(progView.avas.size())).append(" / ").append(String.valueOf(progView.pages.size()));
                            writer.append("\n");
                            writer.append("  Launch: ").append(Fmt.format(launchMilli)).append("\n");
                            writer.append("    Sync: ").append(Fmt.format(syncMs)).append("\n");
                            writer.append("Page End: ").append(progView==null || progView.avas.isEmpty() ? "0" : Fmt.format(page(curAva).endMilli)).append(" CurPage: ").append(String.valueOf(curAva)).append(" / ").append(progView==null || progView.avas.isEmpty() ? "0" : String.valueOf(progView.avas.get(curAva))).append("\n");
                            writer.append(" Current: ").append(Fmt.format(System.currentTimeMillis())).append("\n");
                            var statFs = new StatFs(Environment.getExternalStorageDirectory().getPath());
                            writer.append("   Disk: ").append(String.valueOf(statFs.getTotalBytes()/1000000)).append(" ").append(String.valueOf(statFs.getAvailableBytes()/1000000)).append(" ").append(String.valueOf(statFs.getFreeBytes()/1000000)).append(" (total avail free)\n");
                            var actManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
                            var memoryInfo = new ActivityManager.MemoryInfo();
                            actManager.getMemoryInfo(memoryInfo);
                            writer.append(" Memory: ").append(String.valueOf(memoryInfo.totalMem/1000000)).append(" ").append(String.valueOf(memoryInfo.availMem/1000000)).append(" ").append(String.valueOf(memoryInfo.threshold/1000000)).append(" ").append(String.valueOf(memoryInfo.lowMemory)).append(" (total avail threshold low)\n");
                            var runtime = Runtime.getRuntime();
                            writer.append("Runtime: ").append(String.valueOf(runtime.maxMemory()/1000000)).append(" ").append(String.valueOf(runtime.totalMemory()/1000000)).append(" ").append(NumFmts.cfix2().format(runtime.freeMemory()*0.000001)).append(" (max total free)\n");
                            writer.append("/proc/stat\n");
                            IOs.writeCloseIn(writer, new FileReader("/proc/stat"));
                            writer.append("\n");
                            var latch = new CountDownLatch(1);
                            runOnUiThread(() -> {
                                if(progView!=null && ! progView.avas.isEmpty()) {
                                    var page = page(curAva);
                                    for(var layer : page.layers) for(var src : layer.srcs) if(src.view.getVisibility()==VISIBLE) {
                                        try {
                                            if(src.view instanceof EleVideo) {
                                                var view = (EleVideo) src.view;
                                                if(view.ijkPlayer!=null) {
                                                    writer.append("VideoPlaying: ").append(String.valueOf(view.ijkPlayer.isPlaying())).append("\tCur/Dur: ").append(String.valueOf(view.ijkPlayer.getCurrentPosition())).append("/").append(String.valueOf(view.ijkPlayer.getDuration())).append("\n");
                                                    var mediaInfo = view.ijkPlayer.getMediaInfo();
                                                    var vStream = mediaInfo.mMeta.mVideoStream;
                                                    writer.append("     BitRate: ").append(String.valueOf(view.ijkPlayer.getBitRate()/1000)).append("k\tFPS: ").append(String.valueOf(vStream.mFpsNum/(float)vStream.mFpsDen)).append(" (").append(String.valueOf(vStream.mFpsNum)).append("/").append(String.valueOf(vStream.mFpsDen)).append(")\n");
                                                    var tracks = view.ijkPlayer.getTrackInfo();
                                                    for(var track : tracks) writer.append("  ").append(track.getInfoInline()).append("\n");
                                                    writer.append("      Format: ").append(mediaInfo.mMeta.mFormat).append("\n");
                                                    writer.append("VideoDecoder: ").append(mediaInfo.mVideoDecoder).append(" ").append(mediaInfo.mVideoDecoderImpl).append(" (").append(String.valueOf(view.ijkPlayer.getVideoDecoder())).append(")\n");
                                                    writer.append("AudioDecoder: ").append(mediaInfo.mAudioDecoder).append(" ").append(mediaInfo.mAudioDecoderImpl).append("\n");
                                                    //writer.append("PROFILE: ").append(mediaInfo.mMeta.getString(IjkMediaMeta.IJKM_KEY_CODEC_PROFILE)).append("\n");
                                                } else if(view.exoPlayer!=null) {
                                                    writer.append("VideoPlaying: ").append(String.valueOf(view.exoPlayer.isPlaying())).append(" ").append(String.valueOf(view.exoPlayer.getCurrentPosition())).append("/").append(String.valueOf(view.exoPlayer.getDuration())).append("\n");
                                                    writer.append("     BitRate: ").append(String.valueOf(view.bitRate/1000)).append("k\n");
                                                    writer.append("PlaybackStat: ").append(view.getState()).append("\n");
                                                    writer.append(" PlayerError: ").append(String.valueOf(view.exoPlayer.getPlayerError())).append("\n");
                                                    writer.append(" VideoFormat: ").append(String.valueOf(view.exoPlayer.getVideoFormat())).append("\n");
                                                    var cnt = view.exoPlayer.getRendererCount();
                                                    for(int rr=0; rr<cnt; rr++) writer.append(" Renderer: ").append(String.valueOf(view.exoPlayer.getRendererType(rr))).append(" ").append(String.valueOf(view.exoPlayer.getRenderer(rr))).append("\n");
                                                    writer.append("getPlaybackParameters: ").append(String.valueOf(view.exoPlayer.getPlaybackParameters())).append("\n");
                                                }
                                                writer.append("\n");
                                            } else if(src.view instanceof WebView) {
                                                var view = (WebView) src.view;
                                                writer.append("Title: ").append(view.getTitle()).append("\n");
                                                writer.append("  Url: ").append(view.getUrl()).append("\n");
                                                writer.append("OrUrl: ").append(view.getOriginalUrl()).append("\n");
                                                writer.append("Progress: ").append(String.valueOf(view.getProgress())).append("\n");
                                            }
                                        } catch (Exception ignored) {
                                        }
                                    }
                                }
                                latch.countDown();
                            });
                            try {
                                latch.await();
                            } catch (InterruptedException ignored) {}
                            writer.flush();
                        } else if("GetLog".equals(_type)) {
                            out.write(Util.buf.toString().getBytes());
                        } else if("ListProgFiles".equals(_type)) {
                            var files = new File(Util.programDir).listFiles();
                            if(files == null) return;
                            Arrays.sort(files, (f1, f2) -> (int) (f2.lastModified() - f1.lastModified()));
                            var writer = new OutputStreamWriter(out);
                            for(var file : files) {
                                var len = file.length();
                                writer.append(file.getName()).append(' ').append(String.valueOf(file.length())).append('\n');
                            }
                            writer.append('\n');
                            writer.flush();
                        } else if("GetFile".equals(_type)) {
                            var name = obj.str("name");
                            if(name!=null) IOs.writeCloseIn(out, new FileInputStream(Util.programDir+"/"+name));
                            else new JSMap("code", 1, "msg", "name is null").write(out);
                        }
                        out.flush();
                        Util.println("cmd end");
                    }
                } catch (Throwable e) {
                    var emsg = e.getMessage();
                    if(emsg!=null && ("Socket closed".equals(emsg) || emsg.endsWith("end-of-input"))) {
                        Util.println(emsg);
                        continue;
                    }
                    MainActivity.ins.runOnUiThread(() -> {
                        Util.makeText(MainActivity.this, Util.toStr(e)).show();
                    });
                    Util.printStackTrace(e);
                } finally {
                    O.close(in, out);
                    Util.println("conn end\n\n");
                }
            }
        } catch (Throwable e) {
            MainActivity.ins.runOnUiThread(() -> {
                Util.makeText(MainActivity.this, Util.toStr(e)).show();
            });
            Util.printStackTrace(e);
        }
    }
}