/**
 * 回放页面
 */
package com.videogo.ui.playback;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Application;
import android.app.DatePickerDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.graphics.drawable.BitmapDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;

import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.DatePicker;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.RelativeLayout;
import android.widget.RelativeLayout.LayoutParams;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;

import com.videogo.constant.Constant;
import com.videogo.constant.IntentConsts;
import com.videogo.exception.BaseException;
import com.videogo.global.GlobalKit;
import com.videogo.openapi.EZConstants;
import com.videogo.ui.common.EZStreamConfigLoader;
import com.videogo.ui.playback.download.DownLoadTaskRecordAbstract;
import com.videogo.ui.playback.download.DownloadTaskRecordOfCloud;
import com.videogo.ui.playback.download.DownloadTaskRecordOfDevice;
import com.videogo.errorlayer.ErrorInfo;
import com.videogo.exception.ErrorCode;
import com.videogo.openapi.EZConstants.EZFecPlaceType;
import com.videogo.openapi.EZConstants.EZFecCorrectType;
import com.videogo.openapi.EZConstants.EZPlaybackConstants;
import com.videogo.openapi.EZOpenSDKListener;
import com.videogo.openapi.EZPlayer;
import com.videogo.openapi.EzvizAPI;
import com.videogo.openapi.bean.EZCameraInfo;
import com.videogo.openapi.bean.EZCloudRecordFile;
import com.videogo.openapi.bean.EZDeviceInfo;
import com.videogo.openapi.bean.EZDeviceRecordFile;
import com.videogo.openapi.bean.resp.CloudPartInfoFile;
import com.videogo.remoteplayback.RecordCoverFetcherManager;
import com.videogo.ui.playback.bean.ClickedListItem;
import com.videogo.ui.playback.querylist.QueryCloudRecordFilesAsyncTask;
import com.videogo.ui.playback.querylist.QueryDeviceRecordFilesAsyncTask;
import com.videogo.ui.playback.querylist.QueryPlayBackListTaskCallback;
import com.videogo.ui.playback.querylist.QuerySDKCloudRecordFilesAsyncTask;
import com.videogo.ui.playback.querylist.RecordRecyclerAdapter;
import com.videogo.stream.EZCloudStreamDownload;
import com.videogo.stream.EZDeviceStreamDownload;
import com.videogo.ui.common.EZBusinessTool;
import com.videogo.ui.common.ScreenOrientationHelper;
import com.videogo.ui.realplay.fec.FecViewLayoutHelper;
import com.videogo.util.DataManager;
import com.videogo.util.DateTimeUtil;
import ezviz.ezopensdkcommon.common.EZPermissionUtil;
import com.videogo.util.EZUtils;
import com.videogo.util.VerifyCodeInput;
import com.videogo.util.LocalInfo;
import com.videogo.util.LogUtil;
import com.videogo.util.Utils;
import com.videogo.widget.CheckTextButton;
import com.videogo.widget.TitleBar;
import com.videogo.widget.bottommenu.BottomMenuFragment;

import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.videogo.widget.loading.LoadingTextView;
import com.videogo.widget.toprightmenu.EZMenuItem;
import com.videogo.widget.toprightmenu.EZTopRightMenu;

import java.io.File;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import ezviz.ezopensdk.R;

import com.videogo.global.DemoConfig;

import ezviz.ezopensdkcommon.common.RootActivity;

import static com.videogo.EzvizApplication.getOpenSDK;

@SuppressLint({"DefaultLocale", "HandlerLeak", "NewApi", "NonConstantResourceId"})
public class EZPlayBackListActivity extends RootActivity implements QueryPlayBackListTaskCallback,
        RecordRecyclerAdapter.OnRecordItemClickListener, TextureView.SurfaceTextureListener,
        VerifyCodeInput.VerifyCodeInputListener {

    // region --- 全局变量|global variable ---

    private static final String TAG = EZPlayBackListActivity.class.getSimpleName();// TAG

    private LocalInfo localInfo = null;// 本地信息
    private EZPlayer mPlaybackPlayer = null;
    private Date queryDate = null;// 查询时间

    // 云存储RecyclerView & 适配器
    private RecyclerView mRecyclerViewForCloud;
    private RecordRecyclerAdapter mCloudRecordsAdapter;
    // 云录制RecyclerView & 适配器
    private RecyclerView mRecyclerViewForSDKCloud;
    private RecordRecyclerAdapter mSDKCloudRecordsAdapter;
    // SD卡录像RecyclerView & 适配器
    private RecyclerView mRecyclerViewForLocal;
    private RecordRecyclerAdapter mDeviceRecordsAdapter;

    // 列表查询task(云存储)
    private QueryCloudRecordFilesAsyncTask queryCloudRecordFilesAsyncTask;
    // 列表查询task(云录制)
    private QuerySDKCloudRecordFilesAsyncTask querySDKCloudRecordFilesAsyncTask;
    // 列表查询task(本地)
    private QueryDeviceRecordFilesAsyncTask queryDeviceRecordFilesAsyncTask;

    private TitleBar mTitleBar;// 标题
    private TitleBar mLandscapeTitleBar = null;// 横屏标题
    private Button backBtn;
    private Button rightButton;
    private ViewGroup remoteListPage = null;// 整个回放页面Layout
    // 播放区域
    private RelativeLayout remotePlayBackArea;
    private TextureView mTextureView = null;
    private TextureView mSecondTextureView = null;
    private TextView streamTypeTv;// 取流方式
    // 播放加载进程UI
    private LinearLayout loadingPbLayout;
    // 播放区域错误信息
    private ViewGroup errorTipsVg;// 错误信息显示父视图
    private ImageButton errorReplay;// 错误重播按钮
    private TextView errorInfoTV;// 错误信息
    // 矫正模式分屏
    private RelativeLayout playBackPtzRL;
    private SurfaceView mPlayBackSv1;
    private SurfaceView mPlayBackSv2;
    private SurfaceView mPlayBackSv3;
    private SurfaceView mPlayBackSv4;
    private SurfaceView mPlayBackSv5;
    private SurfaceView mPlayBackSv6;

    // 播放进度条区域 + 播放工具栏区域
    private LinearLayout controlArea = null;
    // 播放进度条区域
    private LinearLayout progressArea = null;// 播放进度条区域
    private TextView playTimeTV = null;// 播放进度条左侧，录像已播放时长
    private TextView duringTimeTV = null;// 播放进度条右侧，录像总时长
    private SeekBar progressSeekbar = null;// 播放进度条
    // 播放工具栏区域
    private ViewGroup mControlBarRL;// 工具栏父视图
    private ImageButton pauseBtn = null;// 暂停/播放按钮
    private ImageButton soundBtn = null;// 声音开关按钮
    private LinearLayout downloadBtn = null;// 下载按钮
    private Button playbackRateBtn = null;// 倍数设置按钮
    private CheckTextButton mFullscreenButton;// 全屏按钮

    // 云存储广告图片
    private ImageView mCloudVideoActiveImg;
    // 录像列表区域，没有录像时需要展示的组件
    private LoadingTextView recordLoadingBar;// 录像列表加载进度圈
    private LinearLayout mNovideoImgCloud;// 没有云存储数据
    private LinearLayout mNovideoImgSDKCloud;// 没有云录制数据
    private LinearLayout mNoVideoImgLocal;// 没有SD卡数据

    private FrameLayout mTabContentMainFrame;// 所有录像列表父区域
    private RelativeLayout mContentTabCloudRl;// 云存储录像列表显示区域
    private RelativeLayout mContentTabSDKCloudRl;// 云录制录像列表显示区域
    private RelativeLayout mContentTabDeviceRl;// SD卡录像列表显示区域
    // 云存储、SDK云录制、SD卡录像 Tab按钮
    private CheckTextButton mCheckBtnCloud;
    private CheckTextButton mCheckBtnSDKCloud;
    private CheckTextButton mCheckBtnDevice;
    private int mRecordType;// 录像类型
    // 右上角录像类型选择按钮
    private EZTopRightMenu mTopRightMenu;
    private List<EZMenuItem> menuItems;
    private EZConstants.EZVideoRecordType recordType = EZConstants.EZVideoRecordType.EZ_VIDEO_RECORD_TYPE_ALL;
    // 录像item点击、下载相关全局变量
    private ClickedListItem currentClickItemFile;// 当前被点击的item
    private EZDeviceRecordFile mDeviceRecordInfo = null;// item点击时记录录像的时间用，下载的时候需要时间
    private EZCloudRecordFile mCloudRecordInfo = null;// item点击时记录录像的时间用，下载的时候需要时间
    public static ArrayList<DownLoadTaskRecordAbstract> mDownloadTaskRecordListAbstract = new ArrayList<>();// 下载任务列表
    private String downloadFilePath;// 录像下载路径
    private long recordInfoDuration;// 下载的录像时长
    // 鱼眼矫正模式相关
    private PopupWindow mFecPopupWindow;// 鱼眼矫正模式pop
    private Button[] fecCorrectTypeButtons;// 鱼眼设备矫正模式按钮数组
    private EZFecPlaceType fecPlaceType;// 鱼眼安装模式
    private EZFecCorrectType fecCorrectType;// 鱼眼矫正模式
    private FecViewLayoutHelper fecViewLayoutHelper;// 鱼眼辅助类
    // 状态
    private Rect mRemotePlayBackRect = null;
    private float mRealRatio = Constant.LIVE_VIEW_RATIO;// 播放分辨率
    private int status = RemoteListContant.STATUS_INIT;// 播放状态
    private boolean notPause = true;// 是否暂停播放，默认为没有暂停
    private boolean isDateSelected = false;// 是否为选择日期事件
    private int mOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;// 屏幕方向
    // 其他
    private ScreenOrientationHelper mScreenOrientationHelper;
    private Timer mUpdateTimer = null;// 播放定时器，每秒刷新
    private TimerTask mUpdateTimerTask = null;// 定时器执行的任务
    // 以下为设备列表页面通过Bundle传递的数据
    private EZDeviceInfo mDeviceInfo = null;
    private EZCameraInfo mCameraInfo = null;
    // 以上为设备列表页面通过Bundle传递的数据
    private boolean isMultiChannelDevice;// 是否是双目设备，如果接入的设备没有双目设备，可以忽略相关逻辑

    // endregion

    // region --- 页面启动方法|Page startup method ---

    public static void launch(Context context, EZDeviceInfo deviceInfo, EZCameraInfo cameraInfo) {
        Intent intent = new Intent(context, EZPlayBackListActivity.class);
        intent.putExtra(RemoteListContant.QUERY_DATE_INTENT_KEY, DateTimeUtil.getNow());
        intent.putExtra(IntentConsts.EXTRA_CAMERA_INFO, cameraInfo);
        intent.putExtra(IntentConsts.EXTRA_DEVICE_INFO, deviceInfo);
        context.startActivity(intent);
    }

    public static void launch(Context context, String deviceSerial, int cameraNo) {
        EZCameraInfo cameraInfo = new EZCameraInfo();
        cameraInfo.setDeviceSerial(deviceSerial);
        cameraInfo.setCameraNo(cameraNo);
        launch(context, new EZDeviceInfo(), cameraInfo);
    }

    // endregion

    // region --- 生命周期|Life Cycle ---

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.ez_playback_list_page);
        // 保持屏幕常亮 Keep the screen on
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        initData();// 数据初始化
        initView();// UI初始化
        initEZPlayer();// 播放器初始化
        initListener();// 设置监听
        startQueryCloudRecordFiles();
        // 国内支持SD卡录像封面获取，海外不支持
        if (!EzvizAPI.getInstance().isUsingGlobalSDK()) {
            // 与设备建立链接，获取SD卡录像封面（页面退出的时候必须断开链接，释放资源，见onDestroy方法）
            RecordCoverFetcherManager.getInstance().initFetcher(this, mCameraInfo.getDeviceSerial(),
                    mCameraInfo.getCameraNo(), recordCoverFetcherInitCallBack);
        }
        /// 鱼眼设备专用设置，如果没有鱼眼设备，不需要如下代码
        initFecView();
        /// 鱼眼设备专用设置，如果没有鱼眼设备，不需要如上代码
    }

    @Override
    protected void onResume() {
        super.onResume();
        LogUtil.d(TAG, "onResume()");
        EZPermissionUtil.onResume(this);
        new Handler().postDelayed(() -> {
            InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
            imm.hideSoftInputFromWindow(mTextureView.getWindowToken(), 0);
        }, 200);
        // 判断是否处理暂停状态
        if (notPause || status == RemoteListContant.STATUS_DECRYPT) {
            onActivityResume();
            isDateSelected = false;
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        mScreenOrientationHelper.postOnStart();
    }

    @Override
    protected void onStop() {
        super.onStop();
        mScreenOrientationHelper.postOnStop();
        LogUtil.d(TAG, "onStop():" + notPause + " status:" + status);
        if (notPause) {
            closePlayBack();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        closePlayBack();
        if (mPlaybackPlayer != null) {
            getOpenSDK().releasePlayer(mPlaybackPlayer);
        }
        if (!EzvizAPI.getInstance().isUsingGlobalSDK()) {
            RecordCoverFetcherManager.getInstance().stopFetcher();// 断开与设备的链接
        }
        stopQueryTask();
        removeHandler(playBackHandler);
        stopAllDownloadTasks();
    }

    @Override
    public void onBackPressed() {
        if (this.getResources().getConfiguration().orientation != Configuration.ORIENTATION_PORTRAIT) {
            mScreenOrientationHelper.portrait();
            return;
        }
        onExitCurrentPageClick();
        finish();
    }

    @Override
    public void finish() {
        if (mCloudRecordsAdapter != null) {
            mCloudRecordsAdapter.setData(null);
        }
        super.finish();
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        mOrientation = newConfig.orientation;
        onOrientationChanged();
        super.onConfigurationChanged(newConfig);
    }

    // endregion

    // region --- 播放器消息回调|Player message callback ---

    private Handler playBackHandler = new Handler() {

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            LogUtil.i(TAG, "handleMessage:" + msg.what);
            switch (msg.what) {
                // 以下消息为SDK发送的消息，用于取流相关消息回调
                case EZPlaybackConstants.MSG_REMOTE_PLAYBACK_PLAY_PREPARED:// 播放准备
                    // 如果开始回放时需立即开启倍数，须在此消息回调中调用setPlaybackRate，不要在MSG_REMOTEPLAYBACK_PLAY_START消息回调中设置。
                    // 因为setPlaybackRate设置后也会有一个MSG_REMOTEPLAYBACK_PLAY_START消息通知，在MSG_REMOTEPLAYBACK_PLAY_START消息回调中设置会陷入循环
//                    mPlaybackPlayer.setPlaybackRate(EZConstants.EZPlaybackRate.EZ_PLAYBACK_RATE_4);

                    // 如果打开了纯音频播放配置，需要在此处打开声音
                    if (GlobalKit.getInstance().enablePureAudio) {
                        updateSoundState(true);
                    }
                    break;
                case EZPlaybackConstants.MSG_REMOTEPLAYBACK_PLAY_START:// 录像回放开始|seek成功
                    handlePlaybackStart(msg);
                    break;
                case EZPlaybackConstants.MSG_REMOTEPLAYBACK_PLAY_FINISH:// 录像回放完成，回放完成后回调
                    handlePlaybackFinish();
                    break;
                case EZPlaybackConstants.MSG_REMOTE_PLAYBACK_RATE_LOWER:// 回放倍速降低消息
                    showToast("回放倍速降速通知");
                    // TODO 4倍速以上则直接降速到4倍速，4倍速及其以下则直接降速到1倍速
                    break;
                case EZPlaybackConstants.MSG_REMOTEPLAYBACK_PLAY_FAIL:// 录像回放失败
                    ErrorInfo errorInfo = (ErrorInfo) msg.obj;
                    handlePlaybackFail(errorInfo);
                    break;
                // 以上消息为SDK发送的消息，用于取流相关消息回调
                // 以下消息为该页面发送的消息，用于UI更新
                case RemoteListContant.MSG_REMOTELIST_UI_UPDATE:// 回放进度 UI刷新
                    playBoxToolRefresh();
                    break;
                // 以上消息为该页面发送的消息，用于UI更新
                default:
                    break;
            }
        }

    };

    /**
     * 播放失败处理
     *
     * @param errorInfo
     */
    private void handlePlaybackFail(ErrorInfo errorInfo) {
        LogUtil.d(TAG, "handlePlaybackFail. Playback failed. error info is " + errorInfo.toString());
        status = RemoteListContant.STATUS_STOP;
        mPlaybackPlayer.stopPlayback();
        switch (errorInfo.errorCode) {
            // 收到这两个错误码，弹出对话框，让用户输入密码后，重新取流
            case ErrorCode.ERROR_INNER_VERIFYCODE_NEED:// 需要验证码
            case ErrorCode.ERROR_INNER_VERIFYCODE_ERROR: {// 验证码错误
                showPlayEventTip("");
                VerifyCodeInput.VerifyCodeInputDialog(this, this).show();
            }
            break;
            default: {// 显示错误码信息
                String errorInfoText = getErrorTip(R.string.remoteplayback_fail, errorInfo.errorCode);
                showPlayEventTip(errorInfoText);
                showToast(errorInfo.toString());
            }
            break;
        }
    }

    /**
     * 录像回放开始，更新UI
     */
    private void handlePlaybackStart(Message msg) {
        if (msg.arg1 != 0) {
            mRealRatio = (float) msg.arg2 / msg.arg1;// 计算画面比例
        }
        status = RemoteListContant.STATUS_PLAYING;// 记录播放状态：播放中
        loadingPbLayout.setVisibility(View.GONE);// 加载进度隐藏
        progressArea.setVisibility(View.VISIBLE);
        mControlBarRL.setVisibility(View.VISIBLE);
        errorTipsVg.setVisibility(View.GONE);
        errorReplay.setVisibility(View.GONE);
        setRemoteListSvLayout();
        mScreenOrientationHelper.enableSensorOrientation();// 横竖屏感应
        // 播放器声音开启或关闭
        if (mPlaybackPlayer != null) {
            updateSoundState(localInfo.isSoundOpen());
        }
        startUpdateTimer();
        showStreamFetchType();
        /// 鱼眼设备专用设置，如果没有鱼眼设备，不需要如下代码
        if (fecViewLayoutHelper != null) {
            fecViewLayoutHelper.openFecCorrect(fecCorrectType, fecPlaceType);
        }
        /// 鱼眼设备专用设置，如果没有鱼眼设备，不需要如上代码
    }

    /**
     * 执行播放完毕的操作：如停止player、更新界面
     * execute some operations when play has finished. such as stop player, update UI
     */
    private void handlePlaybackFinish() {
        LogUtil.d(TAG, "handlePlaybackFinish");
        stopUpdateTimer();
        mPlaybackPlayer.stopPlayback();
        status = RemoteListContant.STATUS_STOP;
        progressArea.setVisibility(View.INVISIBLE);// 播放完毕隐藏进度条（当前片段播放时长、进度条、总时长）
        mControlBarRL.setVisibility(View.INVISIBLE);// 播放完毕隐藏工具栏视图（播放/暂停、声音开关、下载、倍数、全屏）
        showPlayEventTip(getString(R.string.tip_playback_again));// 展示再次播放功能按钮
        /// 鱼眼设备专用设置，如果没有鱼眼设备，不需要如下代码
        if (fecViewLayoutHelper != null) {
            fecViewLayoutHelper.resetFecType();
            closeFecViewModePopupWindow();
        }
        /// 鱼眼设备专用设置，如果没有鱼眼设备，不需要如上代码
    }

    /**
     * 回放进度 UI每秒刷新
     * Playback progress UI refreshes every second
     */
    private void playBoxToolRefresh() {
        // 已播放时长 & 播放进度更新
        if (mPlaybackPlayer != null && status == RemoteListContant.STATUS_PLAYING) {
            Calendar osdTime = mPlaybackPlayer.getOSDTime();
            if (osdTime != null) {
                long osd = osdTime.getTimeInMillis();
                long begin = currentClickItemFile.getBeginTime();
                long end = currentClickItemFile.getEndTime();

                // 根据 OSD 时间刷新录像选中状态：如果当前时间超出了当前片段范围，查找对应片段并更新
                updateSelectedRecordByOSD(osd);
                // 重新获取当前片段的时间范围（可能已被 updateSelectedRecordByOSD 更新）
                begin = currentClickItemFile.getBeginTime();
                end = currentClickItemFile.getEndTime();

                double x = ((osd - begin) * RemoteListContant.PROGRESS_MAX_VALUE) / (double) (end - begin);
                int progress = (int) x;
                progressSeekbar.setProgress(progress);

                LogUtil.i(TAG,
                        "handlePlayProgress, begin time:" + DateTimeUtil.formatTimeToYMDHMSString(begin) +
                                " endtime:" + DateTimeUtil.formatTimeToYMDHMSString(end) +
                                " osdTime:" + DateTimeUtil.formatTimeToYMDHMSString(osd) +
                                " progress:" + progress + " maxProgress:" + RemoteListContant.PROGRESS_MAX_VALUE);
                if (osd >= begin && osd <= end) {
                    int beginTimeClock = (int) ((osd - begin) / 1000);
                    playTimeTV.setText(RemoteListUtil.convToUIDuration(beginTimeClock));
                }
            }
        }
    }

    /**
     * 根据 OSD 时间戳更新录像列表的选中状态
     * 当连续回放跨越片段时，自动切换到对应的录像片段
     */
    private void updateSelectedRecordByOSD(long osdMillis) {
        RecordRecyclerAdapter adapter = getCurrentAdapter();
        if (adapter == null) return;
        List<CloudPartInfoFile> dataList = adapter.getData();
        if (dataList == null || dataList.isEmpty()) return;

        long currentBegin = currentClickItemFile.getBeginTime();
        long currentEnd = currentClickItemFile.getEndTime();
        // 如果 OSD 时间仍在当前片段范围内，不需要切换
        if (osdMillis >= currentBegin && osdMillis <= currentEnd) return;

        // 查找 OSD 时间所属的录像片段
        int index = EZBusinessTool.findRecordIndexByTime(dataList, osdMillis);
        if (index >= 0) {
            CloudPartInfoFile file = dataList.get(index);
            long fileStart = file.getStartMillis();
            long fileEnd = file.getEndMillis();
            currentClickItemFile = new ClickedListItem(
                    file.getPosition(), mRecordType, fileStart, fileEnd, index);
            currentClickItemFile.setFileSize(file.getFileSize());
            setPlaybackProgressUI(fileStart, fileEnd);
            adapter.setSelectedPosition(index);
            // 同步更新下载用的录像信息对象，避免连续播放切片后下载的仍是上一个片段
            updateRecordInfoByCloudPartFile(file);
        }
    }

    /**
     * 根据当前播放的录像片段，更新下载用的录像信息对象（mDeviceRecordInfo / mCloudRecordInfo）
     * 确保下载按钮始终下载当前正在播放的片段
     */
    private void updateRecordInfoByCloudPartFile(CloudPartInfoFile cloudFile) {
        if (mRecordType == RemoteListContant.TYPE_LOCAL) {
            mDeviceRecordInfo = new EZDeviceRecordFile();
            EZBusinessTool.convertCloudPartInfoFile2EZDeviceRecordFile(mDeviceRecordInfo, cloudFile);
            mCloudRecordInfo = null;
        } else if (mRecordType == RemoteListContant.TYPE_CLOUD || mRecordType == RemoteListContant.TYPE_SDKCLOUD) {
            mCloudRecordInfo = new EZCloudRecordFile();
            EZBusinessTool.convertCloudPartInfoFile2EZCloudRecordFile(mCloudRecordInfo, cloudFile);
            mDeviceRecordInfo = null;
        }
    }

    /**
     * 获取当前录像类型对应的适配器
     */
    private RecordRecyclerAdapter getCurrentAdapter() {
        if (mRecordType == RemoteListContant.TYPE_CLOUD) return mCloudRecordsAdapter;
        if (mRecordType == RemoteListContant.TYPE_SDKCLOUD) return mSDKCloudRecordsAdapter;
        if (mRecordType == RemoteListContant.TYPE_LOCAL) return mDeviceRecordsAdapter;
        return null;
    }

    // endregion

    // region --- Click点击事件|Click event ---

    /**
     * 播放|暂停Click
     */
    private OnClickListener mPlayButtonClickListener = v -> {
        if (mPlaybackPlayer == null) {
            showToast(getString(R.string.please_operate_after_select_any_record));
            return;
        }
        if (notPause) {
            // 暂停播放
            notPause = false;
            pauseBtn.setBackgroundResource(R.drawable.remote_list_play_btn_selector);
            if (status == RemoteListContant.STATUS_PLAYING) {
                status = RemoteListContant.STATUS_PAUSE;
                if (mPlaybackPlayer != null) {
                    // 加保护，规避CAS库小概率出现的10S死锁导致的ANR问题
                    getTaskManager().submit(() -> mPlaybackPlayer.pausePlayback());
                }
            }
        } else {
            notPause = true;
            pauseBtn.setBackgroundResource(R.drawable.ez_remote_list_pause_btn_selector);
            if (status == RemoteListContant.STATUS_PAUSE) {
                if (mPlaybackPlayer != null) {
                    // 加保护，规避CAS库小概率出现的10S死锁导致的ANR问题
                    getTaskManager().submit(() -> mPlaybackPlayer.resumePlayback());
                }
                mScreenOrientationHelper.enableSensorOrientation();
                status = RemoteListContant.STATUS_PLAYING;
            }
        }
    };

    /**
     * 开启/关闭声音Click
     */
    private OnClickListener mSoundButtonClickListener = v-> {
        if (mPlaybackPlayer == null) {
            return;
        }
        // 切换声音状态
        boolean newSoundState = !localInfo.isSoundOpen();
        localInfo.setSoundOpen(newSoundState);
        updateSoundState(newSoundState);
    };

    /**
     * 录像下载Click
     */
    private OnClickListener mDownloadButtonClickListener = v -> {
        if (mCloudRecordInfo != null) {
            recordInfoDuration = RemoteListUtil.converToSecondDuration(mCloudRecordInfo.getStartTime(),
                    mCloudRecordInfo.getStopTime());
            startDownloadCloudVideo(mCloudRecordInfo);
        } else {
            recordInfoDuration = RemoteListUtil.converToSecondDuration(mDeviceRecordInfo.getStartTime(),
                    mDeviceRecordInfo.getStopTime());
            startDownloadDeviceVideo(mDeviceRecordInfo);
        }
    };

    /**
     * 倍数设置按钮Click，弹出倍数设置选项框
     */
    public void onPlaybackRateClick(View view) {
        if (mCheckBtnDevice.isChecked()) {
            showSDCardRateBottomMenu(view);
        } else {
            showCloudRateBottomMenu(view);
        }
    }

    /**
     * 显示云录像倍速选择底部弹框
     */
    private void showCloudRateBottomMenu(View view) {
        List<EZConstants.EZPlaybackRate> rates = EZBusinessTool.getCloudPlaybackRates();
        List<String> rateStrings = EZBusinessTool.getCloudPlaybackRateStrings();
        Collections.reverse(rates);
        Collections.reverse(rateStrings);

        String currentRateText = ((Button) view).getText().toString();
        new BottomMenuFragment(this)
                .setHeadTitle(getResources().getString(R.string.playback_rate_select))
                .addMenuItems(rateStrings)
                .setSelectContent(currentRateText)
                .setOnItemClickListener(position -> {
                    EZConstants.EZPlaybackRate targetRate = rates.get(position);
                    String targetRateStr = rateStrings.get(position);
                    setPlaybackRate(targetRate, targetRateStr);
                    ((Button) view).setText(targetRateStr);
                })
                .show();
    }

    /**
     * 显示SD卡录像倍速选择底部弹框
     */
    private void showSDCardRateBottomMenu(View view) {
        int streamFetchType = mPlaybackPlayer.getStreamFetchType();
        List<EZConstants.EZPlaybackRate> rates = EZBusinessTool.getSDCardPlaybackRates(mDeviceInfo, mCameraInfo, streamFetchType);
        List<String> rateStrings = EZBusinessTool.getSDCardPlaybackRateStrings(mDeviceInfo, mCameraInfo, streamFetchType);
        if (rates == null || rates.isEmpty()) {
            showToast(R.string.device_playbackrate_not_support);
            return;
        }
        Collections.reverse(rates);
        Collections.reverse(rateStrings);

        String currentRateText = ((Button) view).getText().toString();
        new BottomMenuFragment(this)
                .setHeadTitle(getResources().getString(R.string.playback_rate_select))
                .addMenuItems(rateStrings)
                .setSelectContent(currentRateText)
                .setOnItemClickListener(position -> {
                    EZConstants.EZPlaybackRate targetRate = rates.get(position);
                    String targetRateStr = rateStrings.get(position);
                    setPlaybackRate(targetRate, targetRateStr);
                    ((Button) view).setText(targetRateStr);
                })
                .show();
    }

    private void setPlaybackRate(EZConstants.EZPlaybackRate targetRateEnum, String targetRateWithX) {
        // 切换到指定倍速，倍数设置为耗时任务，需要在子线程中执行
        new Thread(() -> {
            if (!mPlaybackPlayer.setPlaybackRate(targetRateEnum)) {
                showToast("failed to change to " + targetRateWithX);
            }
        }).start();
    }

    /**
     * 云存储Tab 点击事件监听
     */
    private final OnClickListener mCloudCheckListener = v -> {
        mRecordType = RemoteListContant.TYPE_CLOUD;
        if (!mCheckBtnCloud.isChecked()) {
            // 切换Tab时停止当前回放
            stopCurrentPlayback();
            resetRecordCheckBtns();
            mCheckBtnCloud.setChecked(true);
            mContentTabCloudRl.setVisibility(View.VISIBLE);
            downloadBtn.setVisibility(View.VISIBLE);
            rightButton.setBackgroundDrawable(getResources().getDrawable(R.drawable.common_title_extension_selector));
            rightButton.setVisibility(EzvizAPI.getInstance().isUsingGlobalSDK() ? View.VISIBLE : View.GONE);
            startQueryCloudRecordFiles();
        }
    };

    /**
     * SDK云录制Tab 点击事件监听
     */
    private final OnClickListener mSDKCloudCheckListener = v -> {
        mRecordType = RemoteListContant.TYPE_SDKCLOUD;
        if (!mCheckBtnSDKCloud.isChecked()) {
            // 切换Tab时停止当前回放
            stopCurrentPlayback();
            resetRecordCheckBtns();
            mCheckBtnSDKCloud.setChecked(true);
            mContentTabSDKCloudRl.setVisibility(View.VISIBLE);
            downloadBtn.setVisibility(View.GONE);
            rightButton.setBackgroundDrawable(getResources().getDrawable(R.drawable.common_title_extension_selector));
            rightButton.setVisibility(View.GONE);
            startQuerySDKCloudRecordFiles();
        }
    };

    /**
     * SD卡录像Tab 点击事件监听
     */
    private final OnClickListener mDeviceCheckListener = v -> {
        mRecordType = RemoteListContant.TYPE_LOCAL;
        if (!mCheckBtnDevice.isChecked()) {
            // 切换Tab时停止当前回放
            stopCurrentPlayback();
            resetRecordCheckBtns();
            mCheckBtnDevice.setChecked(true);
            mContentTabDeviceRl.setVisibility(View.VISIBLE);
            downloadBtn.setVisibility(EZBusinessTool.isSupportSDRecordDownload(mDeviceInfo, mCameraInfo) ? View.VISIBLE : View.GONE);
            rightButton.setBackgroundDrawable(getResources().getDrawable(R.drawable.common_title_recordtype_selector));
            rightButton.setVisibility(View.VISIBLE);
            startQueryDeviceRecordFiles();
        }
    };

    /**
     * 重播当前录像片段Click
     */
    private final OnClickListener mReplayButtonClickListener = v -> {
        newPlayInit();
        setPlaybackProgressUI(currentClickItemFile.getBeginTime(), currentClickItemFile.getEndTime());
        startPlaybackByRecordCategory();
    };

    /**
     * 云存储图片Click
     */
    public OnClickListener mCloudAdImageViewClickListener = v -> {
        String errorInfo = "fail to call openCloudPage!";
        if (mCameraInfo == null) {
            showToast(errorInfo);
            return;
        }
        try {
            getOpenSDK().openCloudPage(mCameraInfo.getDeviceSerial(), mCameraInfo.getCameraNo());
        } catch (BaseException e) {
            showToast(errorInfo);
            e.printStackTrace();
        }
    };

    /**
     * 左上角返回Click
     */
    private void onExitCurrentPageClick() {
        notPause = true;
        stopQueryTask();
        closePlayBack();
    }

    /**
     * 右上角录像类型按钮Click
     */
    private final OnClickListener mTopRightMenuClickListener = v -> {
        if (mCheckBtnCloud.isChecked()) {
            if (EzvizAPI.getInstance().isUsingGlobalSDK()) {
                EZAbroadCloudServiceExAct.launch(EZPlayBackListActivity.this, mCameraInfo.getDeviceSerial(),
                        mCameraInfo.getCameraNo());
            }
        } else {
            if (menuItems == null) {
                menuItems = new ArrayList<>();
                menuItems.add(new EZMenuItem(R.mipmap.videogo_icon, "全部"));
                menuItems.add(new EZMenuItem(R.mipmap.videogo_icon, "定时"));
                menuItems.add(new EZMenuItem(R.mipmap.videogo_icon, "事件"));
            }
            if (mTopRightMenu == null) {
                mTopRightMenu = new EZTopRightMenu(EZPlayBackListActivity.this);
                mTopRightMenu.setWidth(450)         // 默认宽度wrap_content
                        .showIcon(false)            // 显示菜单图标，默认为true
                        .dimBackground(true)        // 背景变暗，默认为true
                        .needAnimationStyle(true)   // 显示动画，默认为true
                        .setAnimationStyle(R.style.TRM_ANIM_STYLE) // 默认为R.style.TRM_ANIM_STYLE
                        .addMenuList(menuItems).setOnMenuItemClickListener(new EZTopRightMenu.OnMenuItemClickListener() {
                            @Override
                            public void onMenuItemClick(int position) {
                                if (position == 1) {
                                    recordType = EZConstants.EZVideoRecordType.EZ_VIDEO_RECORD_TYPE_CMR;
                                } else if (position == 2) {
                                    recordType = EZConstants.EZVideoRecordType.EZ_VIDEO_RECORD_TYPE_Event;
                                } else {
                                    recordType = EZConstants.EZVideoRecordType.EZ_VIDEO_RECORD_TYPE_ALL;
                                }
                                startQueryDeviceRecordFiles();
                            }
                        });
            }
            mTopRightMenu.showAsDropDown(rightButton, -300, 10);
        }
    };

    /**
     * 鱼眼矫正模式的点击事件
     */
    private final OnClickListener mOnFecWndClickListener = v -> {
        // 没有在播放，拦截
        if (status != RemoteListContant.STATUS_PLAYING) {
            return;
        }
        int id = v.getId();
        if (id == R.id.fec_place_wall || id == R.id.fec_place_floor || id == R.id.fec_place_ceiling) {
            // 壁装/底装/顶装
            fecPlaceType = EZFecPlaceType.values()[Integer.parseInt(String.valueOf(v.getTag()))];
            setFecCorrectTypeBtnsEnable(fecPlaceType);
        } else if (id == R.id.fec_correct_fish || id == R.id.fec_correct_4ptz || id == R.id.fec_correct_5ptz ||
                id == R.id.fec_correct_full5ptz || id == R.id.fec_correct_lat || id == R.id.fec_correct_arc_hor ||
                id == R.id.fec_correct_arc_ver || id == R.id.fec_correct_wide_angle || id == R.id.fec_correct_180 ||
                id == R.id.fec_correct_360 || id == R.id.fec_correct_cyc) {
            // 默认鱼眼/4分屏/5分屏/全景5分屏/维度拉伸/ARC/ARCV/广角/180°全景/360°全景/柱状
            fecCorrectType = FecViewLayoutHelper.getFecCorrectTypeFromTag(Integer.parseInt(String.valueOf(v.getTag())));
            fecViewLayoutHelper.openFecCorrect(fecCorrectType, fecPlaceType);
        } else if (id == R.id.fec_close_btn) {
            closeFecViewModePopupWindow();
        }
    };

    // endregion

    // region --- 顶部日历点击事件|Top Calendar Click Event ---

    // 切换到日历界面
    private OnClickListener goToCalendarClickListener = v -> {
        if (EZBusinessTool.getMinDate() != null && new Date().before(EZBusinessTool.getMinDate())) {
            showToast(R.string.calendar_setting_error);
            return;
        }
        showDatePicker();
    };

    private void showDatePicker() {
        Calendar nowCalendar = Calendar.getInstance();
        nowCalendar.setTime(queryDate);
        DatePickerDialog dpd = new DatePickerDialog(this, null, nowCalendar.get(Calendar.YEAR),
                nowCalendar.get(Calendar.MONTH), nowCalendar.get(Calendar.DAY_OF_MONTH));

        dpd.setCancelable(true);
        dpd.setTitle(R.string.select_date);
        dpd.setCanceledOnTouchOutside(true);
        dpd.setButton(DialogInterface.BUTTON_POSITIVE, getString(R.string.certain), (dg, which) -> {
            DatePicker dp = null;
            Field[] fields = dg.getClass().getDeclaredFields();
            for (Field field : fields) {
                field.setAccessible(true);
                if (field.getName().equals("mDatePicker")) {
                    try {
                        dp = (DatePicker) field.get(dg);
                    } catch (IllegalArgumentException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
            if (dp != null) {
                dp.clearFocus();
                Calendar selectCalendar = Calendar.getInstance();
                selectCalendar.set(Calendar.YEAR, dp.getYear());
                selectCalendar.set(Calendar.MONTH, dp.getMonth());
                selectCalendar.set(Calendar.DAY_OF_MONTH, dp.getDayOfMonth());
                isDateSelected = true;
                queryDate = selectCalendar.getTime();
                onDateChanged();
            }
        });
        dpd.setButton(DialogInterface.BUTTON_NEGATIVE, getString(R.string.cancel), (dialog, which) -> {
            LogUtil.d("Picker", "Cancel!");
            if (!isFinishing()) {
                dialog.dismiss();
            }

        });
        dpd.show();
    }

    private void onDateChanged() {
        if (queryDate != null) {
            mTitleBar.setTitle(RemoteListUtil.converToMonthAndDay(queryDate));
        }
        switch (mRecordType) {
            case RemoteListContant.TYPE_CLOUD:
                mSDKCloudRecordsAdapter = null;
                mDeviceRecordsAdapter = null;
                startQueryCloudRecordFiles();
                break;
            case RemoteListContant.TYPE_SDKCLOUD:
                mCloudRecordsAdapter = null;
                mDeviceRecordsAdapter = null;
                startQuerySDKCloudRecordFiles();
                break;
            case RemoteListContant.TYPE_LOCAL:
                mCloudRecordsAdapter = null;
                mSDKCloudRecordsAdapter = null;
                startQueryDeviceRecordFiles();
                break;
            default:
                // do nothing
                break;
        }
    }

    // endregion

    // region --- 回放进度条拖动事件|Replay progress bar drag event ---

    private final OnSeekBarChangeListener mSeekBarChangeListener = new OnSeekBarChangeListener() {
        /**
         * 拖动条停止拖动的时候调用
         */
        @Override
        public void onStopTrackingTouch(SeekBar arg0) {
            int progress = arg0.getProgress();
            if (progress == RemoteListContant.PROGRESS_MAX_VALUE) {// 拖动到最后
                handlePlaybackFinish();
                return;
            }
            if (currentClickItemFile != null) {
                // 计算拖动后的时间
                long beginTime = currentClickItemFile.getBeginTime();
                long endTime = currentClickItemFile.getEndTime();
                long avg = (endTime - beginTime) / RemoteListContant.PROGRESS_MAX_VALUE;
                long trackTime = beginTime + (progress * avg);
                LogUtil.i(TAG,
                        "onSeekBarStopTracking, begin time:" + DateTimeUtil.formatTimeToYMDHMSString(beginTime) +
                                " endtime:" + DateTimeUtil.formatTimeToYMDHMSString(endTime) +
                                " avg:" + avg +
                                " MAX:" + RemoteListContant.PROGRESS_MAX_VALUE +
                                " trackTime:" + DateTimeUtil.formatTimeToYMDHMSString(trackTime));
                stopUpdateTimer();
                if (mPlaybackPlayer != null) {
                    Calendar seekTime = Calendar.getInstance();
                    seekTime.setTime(new Date(trackTime));
                    mPlaybackPlayer.seekPlayback(seekTime);
                }
                seekInit();
            }
        }

        /**
         * 拖动条开始拖动的时候调用
         */
        @Override
        public void onStartTrackingTouch(SeekBar arg0) {
        }

        /**
         * 拖动条进度改变的时候调用
         */
        @Override
        public void onProgressChanged(SeekBar arg0, int arg1, boolean arg2) {
            if (currentClickItemFile != null) {
                long time = currentClickItemFile.getEndTime() - currentClickItemFile.getBeginTime();
                int diffSeconds = (int) (time * arg1 / 1000) / 1000;
                String convToUIDuration = RemoteListUtil.convToUIDuration(diffSeconds);
                playTimeTV.setText(convToUIDuration);
            }
        }
    };

    // endregion

    // region --- 录像下载|video download ---

    /**
     * 云存储录像下载 | download video from ezviz cloud
     *
     * @param cloudFile video file of ezviz cloud
     */
    private void startDownloadCloudVideo(final EZCloudRecordFile cloudFile) {
        if (cloudFile == null) {
            return;
        }
        getTaskManager().submit(() -> {
            // 设置下载路径并创建文件
            String strFileNameWithPath = DemoConfig.getRecordsFolder() + "/cloud_" + System.currentTimeMillis() + ".mp4";
            final File file = new File(strFileNameWithPath);
            if (!file.getParentFile().exists()) {
                file.getParentFile().mkdirs();
            }
            // 创建下载器
            final EZCloudStreamDownload ezCloudStreamDownloader = new EZCloudStreamDownload(strFileNameWithPath, cloudFile);
            // 云存储录像支持下载进度回调，SD卡录像下载不支持
            ezCloudStreamDownloader.setStreamDownloadCallback(new EZStreamDownloadCallback(cloudFile));
            // 设置下载验证码（录像未加密的话可以不设置）
            ezCloudStreamDownloader.setSecretKey(DataManager.getInstance().getDeviceSerialVerifyCode(mCameraInfo.getDeviceSerial()));
            // 如果是双目设备，需要设置
            if (isMultiChannelDevice) {
                ezCloudStreamDownloader.setMultiChannelDevice(true);
            }
            // 开始下载
            ezCloudStreamDownloader.start();
            // 记录本次下载任务，onDestory时需要暂停下载任务
            mDownloadTaskRecordListAbstract.add(new DownloadTaskRecordOfCloud(ezCloudStreamDownloader));

            showToast(R.string.download_start);
        });
    }

    /**
     * 设备SD卡录像下载 | download video from ezviz device
     *
     * @param deviceFile video file of device SdCard
     */
    private void startDownloadDeviceVideo(final EZDeviceRecordFile deviceFile) {
        if (deviceFile == null) {
            return;
        }
        getTaskManager().submit(() -> {
            // 设置下载路径并创建文件
            String strRecordFilePath = DemoConfig.getRecordsFolder() + "/device_" + System.currentTimeMillis() + ".mp4";
            File file = new File(strRecordFilePath);
            if (!file.getParentFile().exists()) {
                file.getParentFile().mkdirs();
            }
            // 创建下载器
            final EZDeviceStreamDownload ezDeviceStreamDownloader = new EZDeviceStreamDownload(strRecordFilePath,
                    mCameraInfo.getDeviceSerial(), mCameraInfo.getCameraNo(), deviceFile);
            // 设置下载回调监听
            ezDeviceStreamDownloader.setStreamDownloadCallback(new EZStreamDownloadCallback());
            // 设置下载验证码
            ezDeviceStreamDownloader.setSecretKey(DataManager.getInstance().getDeviceSerialVerifyCode(mCameraInfo.getDeviceSerial()));
            // 如果是双目设备，需要设置
            if (isMultiChannelDevice) {
                ezDeviceStreamDownloader.setMultiChannelDevice(true);
            }
            // 加载SD卡下载特殊配置，一般开发者可不用配置，按需设置
            EZStreamConfigLoader.loadDownloadDeviceVideoSpecialConfigToStreamDownload(ezDeviceStreamDownloader);
            // 开始下载
            ezDeviceStreamDownloader.start();
            // 记录本次下载任务，onDestory时需要暂停下载任务
            mDownloadTaskRecordListAbstract.add(new DownloadTaskRecordOfDevice(ezDeviceStreamDownloader));

            showToast(R.string.download_start);
        });
    }

    /**
     * 下载回调监听
     */
    private class EZStreamDownloadCallback extends EZOpenSDKListener.EZStreamDownloadCallbackEx {

        private EZCloudRecordFile cloudFile;

        public EZStreamDownloadCallback() {
        }

        public EZStreamDownloadCallback(EZCloudRecordFile cloudFile) {
            this.cloudFile = cloudFile;
        }

        @Override
        public void onDownloadingSize(long downloadSize) {// 只有云存储录像支持下载进度回调，SD卡录像下载不支持
            if (cloudFile != null) {
                LogUtil.d(TAG, "percent--->" + downloadSize * 100 / cloudFile.getFileSize());
            }
        }

        @Override
        public void onSuccess(final String filepath) {
            downloadFilePath = filepath;
            String successMsg = "saved video to " + filepath;
            LogUtil.d(TAG, successMsg);
            showToast(successMsg);
            // 申请动态权限将下载文件存储到相册
            requestStoragePermissionAndSave();
        }

        @Override
        public void onError(final EZOpenSDKListener.EZStreamDownloadError code) {
            String failMsg = "failed: ";
            switch (code) {
                case ERROR_EZSTREAM_DOWNLOAD_MAX_CONNECTIONS:
                    failMsg += "device reached max connections!";
                    break;
                case ERROR_EZSTREAM_DOWNLOAD_VERIFYCODE:
                    failMsg += "The video has been encrypted, please enter the password!";
                    break;
                case ERROR_EZSTREAM_DOWNLOAD_START:
                    failMsg += "download parameter error or download startup failure, please check the log";
                    /**
                     * 下载启动失败可能有两个原因：
                     * 1、下载参数设置错误，请检查下载器参数设置是否错误
                     * 2、媒体类错误码，日志中搜索 handleDownloadError: 找到具体的错误码，再查阅错误码文档
                     *
                     * There may be two reasons for download startup failure:
                     * 1.Download parameter settings are incorrect. Please check if the downloader parameter settings
                     * are incorrect
                     * 2.Media related error codes, search for handleDownloadError:  in the log: find the specific
                     * error code,
                     *   and then refer to the error code document
                     */
                    break;
                default:
                    failMsg += code;
                    break;
            }
            showToast(failMsg);
        }

        @Override
        public void onErrorCode(int code) {
            showToast(R.string.download_fail, code);
        }

    }

    // endregion

    // region --- 验证码校验|CAPTCHA Verification ---

    @Override
    public void onInputVerifyCode(final String verifyCode) {
        LogUtil.d(TAG, "verify code is " + verifyCode);
        DataManager.getInstance().setDeviceSerialVerifyCode(mCameraInfo.getDeviceSerial(), verifyCode);
        if (mPlaybackPlayer != null) {
            mPlaybackPlayer.setPlayVerifyCode(verifyCode);
            resetPlayUIVisible();
            startPlaybackByRecordCategory();
        }
    }

    // endregion

    // region --- 录像查询|Video inquiry ---

    private void stopQueryTask() {
        if (queryCloudRecordFilesAsyncTask != null) {
            queryCloudRecordFilesAsyncTask.cancel(true);
            queryCloudRecordFilesAsyncTask.setAbort(true);
            queryCloudRecordFilesAsyncTask = null;
        }
        if (querySDKCloudRecordFilesAsyncTask != null) {
            querySDKCloudRecordFilesAsyncTask.cancel(true);
            querySDKCloudRecordFilesAsyncTask.setAbort(true);
            querySDKCloudRecordFilesAsyncTask = null;
        }
        if (queryDeviceRecordFilesAsyncTask != null) {
            queryDeviceRecordFilesAsyncTask.cancel(true);
            queryDeviceRecordFilesAsyncTask.setAbort(true);
            queryDeviceRecordFilesAsyncTask = null;
        }
    }

    private void startQueryCloudRecordFiles() {
        if (queryDate != null) {
            mTitleBar.setTitle(RemoteListUtil.converToMonthAndDay(queryDate));
        }
        mRecyclerViewForCloud.setVisibility(View.GONE);
        stopQueryTask();
        mCloudRecordsAdapter = null;
        queryCloudRecordFilesAsyncTask = new QueryCloudRecordFilesAsyncTask(mCameraInfo.getDeviceSerial(),
                mCameraInfo.getCameraNo(), EZPlayBackListActivity.this);
        recordLoadingBar.setVisibility(View.VISIBLE);
        showTab(R.id.record_loadingTextView);
        queryCloudRecordFilesAsyncTask.setSearchDate(queryDate);
        queryCloudRecordFilesAsyncTask.execute();
    }

    private void startQuerySDKCloudRecordFiles() {
        mRecyclerViewForSDKCloud.setVisibility(View.GONE);
        stopQueryTask();
        mSDKCloudRecordsAdapter = null;
        querySDKCloudRecordFilesAsyncTask = new QuerySDKCloudRecordFilesAsyncTask(mCameraInfo.getDeviceSerial(),
                mCameraInfo.getCameraNo(), EZPlayBackListActivity.this);
        recordLoadingBar.setVisibility(View.VISIBLE);
        showTab(R.id.record_loadingTextView);
        querySDKCloudRecordFilesAsyncTask.setSearchDate(queryDate);
        querySDKCloudRecordFilesAsyncTask.execute();
    }

    private void startQueryDeviceRecordFiles() {
        int cloudTotal = 100000;
        mRecyclerViewForLocal.setVisibility(View.GONE);
        stopQueryTask();
        queryDeviceRecordFilesAsyncTask = new QueryDeviceRecordFilesAsyncTask(mCameraInfo.getDeviceSerial(),
                mCameraInfo.getCameraNo(), recordType, EZPlayBackListActivity.this);
        recordLoadingBar.setVisibility(View.VISIBLE);
        showTab(R.id.record_loadingTextView);
        queryDeviceRecordFilesAsyncTask.setQueryDate(queryDate);
        queryDeviceRecordFilesAsyncTask.execute(String.valueOf(cloudTotal));
    }

    @Override
    public void queryHasNoData(int type) {
        if (type == RemoteListContant.TYPE_CLOUD) {
            showTab(R.id.novideo_img_cloud);
        } else if (type == RemoteListContant.TYPE_SDKCLOUD) {
            showTab(R.id.novideo_img_sdkcloud);
        } else if (type == RemoteListContant.TYPE_LOCAL) {
            showTab(R.id.novideo_img_device);
        }
    }

    @Override
    public void querySuccessFromCloud(List<CloudPartInfoFile> cloudPartInfoFile) {
        findViewById(R.id.display_layout).setVisibility(View.VISIBLE);
        recordLoadingBar.setVisibility(View.GONE);
        mRecyclerViewForCloud.setVisibility(View.VISIBLE);
        showTab(R.id.content_tab_cloud_root);

        mCloudRecordsAdapter = new RecordRecyclerAdapter(this);
        mCloudRecordsAdapter.setRecordType(RemoteListContant.TYPE_CLOUD);
        mCloudRecordsAdapter.setDeviceSerial(mCameraInfo.getDeviceSerial());
        mCloudRecordsAdapter.setOnRecordItemClickListener(this);
        mCloudRecordsAdapter.setData(cloudPartInfoFile);
        mRecyclerViewForCloud.setAdapter(mCloudRecordsAdapter);

        doPlayback(cloudPartInfoFile, RemoteListContant.TYPE_CLOUD);
    }

    @Override
    public void querySuccessFromSDKCloud(List<CloudPartInfoFile> cloudPartInfoFile) {
        recordLoadingBar.setVisibility(View.GONE);
        mRecyclerViewForSDKCloud.setVisibility(View.VISIBLE);
        showTab(R.id.content_tab_sdkcloud_root);

        mSDKCloudRecordsAdapter = new RecordRecyclerAdapter(this);
        mSDKCloudRecordsAdapter.setRecordType(RemoteListContant.TYPE_SDKCLOUD);
        mSDKCloudRecordsAdapter.setDeviceSerial(mCameraInfo.getDeviceSerial());
        mSDKCloudRecordsAdapter.setOnRecordItemClickListener(this);
        mSDKCloudRecordsAdapter.setData(cloudPartInfoFile);
        mRecyclerViewForSDKCloud.setAdapter(mSDKCloudRecordsAdapter);

        doPlayback(cloudPartInfoFile, RemoteListContant.TYPE_SDKCLOUD);
    }

    @Override
    public void querySuccessFromDevice(List<CloudPartInfoFile> cloudPartInfoFile) {
        recordLoadingBar.setVisibility(View.GONE);
        showTab(R.id.content_tab_device_root);
        mRecyclerViewForLocal.setVisibility(View.VISIBLE);

        if (mDeviceRecordsAdapter != null) {
            // 追加数据并刷新
            mDeviceRecordsAdapter.setData(cloudPartInfoFile);
            int scrollTo = cloudPartInfoFile.size() - 1;
            if (scrollTo > 0) {
                mRecyclerViewForLocal.scrollToPosition(scrollTo);
            }
        } else {
            mDeviceRecordsAdapter = new RecordRecyclerAdapter(this);
            mDeviceRecordsAdapter.setRecordType(RemoteListContant.TYPE_LOCAL);
            mDeviceRecordsAdapter.setDeviceSerial(mCameraInfo.getDeviceSerial());
            mDeviceRecordsAdapter.setOnRecordItemClickListener(this);
            mDeviceRecordsAdapter.setData(cloudPartInfoFile);
            mRecyclerViewForLocal.setAdapter(mDeviceRecordsAdapter);
        }
        doPlayback(cloudPartInfoFile, RemoteListContant.TYPE_LOCAL);
        if (!EzvizAPI.getInstance().isUsingGlobalSDK() && EZBusinessTool.isSupportSdCover(mDeviceInfo, mCameraInfo)) {// 国内 & 支持SD卡录像封面
            // 去获取SD卡视频封面
            List<EZDeviceRecordFile> recordFiles = new ArrayList<>();
            for (int i = 0; i < cloudPartInfoFile.size(); i++) {
                CloudPartInfoFile file = cloudPartInfoFile.get(i);
                EZDeviceRecordFile recordFile = new EZDeviceRecordFile();
                recordFile.setBegin(file.getBegin());
                recordFile.setEnd(file.getEnd());
                recordFile.setSeq(i);// 设置索引，封面回调的时候知道对应哪一个录像
                recordFiles.add(recordFile);
            }
            RecordCoverFetcherManager.getInstance().requestRecordCover(recordFiles,
                    new RecordCoverFetcherManager.RecordCoverFetcherCallBack() {

                        @Override
                        public void onGetCoverSuccess(int seq, byte[] bytes) {
                            // 以下情况做拦截，否则会将SD卡录像封面显示在云存储录像上或者数组越界崩溃
                            if (mCheckBtnCloud.isChecked() || seq > cloudPartInfoFile.size() - 1) {
                                return;
                            }
                            // 此处将bytes转为bitmap。开发者也可自行将bytes转为文件，进行缓存管理。
                            Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
                            // 将获取到的图片赋值给列表中的对象，并局部刷新RecyclerView
                            CloudPartInfoFile cloudFile = cloudPartInfoFile.get(seq);
                            cloudFile.setBitmap(bitmap);
                            // RecyclerView支持局部刷新，直接刷新对应位置的item
                            runOnUiThread(() -> {
                                if (mDeviceRecordsAdapter != null) {
                                    mDeviceRecordsAdapter.notifyItemChanged(seq);
                                }
                            });
                        }

                        @Override
                        public void onGetCoverFailed(int errorCode) {
                            LogUtil.e(TAG, "onGetCoverFailed");
                        }
                    });
        }
    }

    private void doPlayback(List<CloudPartInfoFile> cloudPartInfoFile, int playbackType) {
        if (cloudPartInfoFile != null && !cloudPartInfoFile.isEmpty()) {
            CloudPartInfoFile file = cloudPartInfoFile.get(0);
            ClickedListItem clickedListItem = new ClickedListItem(0, playbackType, file.getStartMillis(),
                    file.getEndMillis(), 0);
            onRecordItemClick(cloudPartInfoFile.get(0), clickedListItem);
        }
    }

    // endregion

    // region --- SD卡录像封面监听|SD card recording cover monitoring ---

    private RecordCoverFetcherManager.RecordCoverFetcherInitCallBack recordCoverFetcherInitCallBack =
            new RecordCoverFetcherManager.RecordCoverFetcherInitCallBack() {
                @Override
                public void onFetcherInitSuccess() {

                }

                @Override
                public void onFetcherInitFailed() {

                }
            };

    // endregion

    // region --- OnHikItemClickListener 录像片段点击|OnHikItemClickListener video clip click ---

    @Override
    public void onRecordItemClick(CloudPartInfoFile cloudFile, ClickedListItem playClickItem) {
        // 判断取流链路已经存在，如果是则用 seekPlayback 跳转到选中片段
        boolean isStreamConnected = (status == RemoteListContant.STATUS_PLAYING || status == RemoteListContant.STATUS_PAUSE);
        if (isStreamConnected) {
            currentClickItemFile = playClickItem;
            setPlaybackProgressUI(playClickItem.getBeginTime(), playClickItem.getEndTime());
            // 更新列表选中状态
            updateAdapterSelection(playClickItem.getPosition());
            // 同步更新下载用的录像信息对象，避免seek后下载的仍是上一个片段
            updateRecordInfoByCloudPartFile(cloudFile);
            // seek 到选中片段的开始时间
            Calendar seekTime = Calendar.getInstance();
            seekTime.setTimeInMillis(playClickItem.getBeginTime());
            stopUpdateTimer();
            mPlaybackPlayer.seekPlayback(seekTime);
            seekInit();
            return;
        }

        stopCurrentPlayback();
        setPlaybackProgressUI(playClickItem.getBeginTime(), playClickItem.getEndTime());

        currentClickItemFile = playClickItem;
        mDeviceRecordInfo = null;
        mCloudRecordInfo = null;
        if (mRecordType == RemoteListContant.TYPE_LOCAL) {
            mDeviceRecordInfo = new EZDeviceRecordFile();
            EZBusinessTool.convertCloudPartInfoFile2EZDeviceRecordFile(mDeviceRecordInfo, cloudFile);
            // 将结束时间设置为列表中第一个录像（最近时间）的结束时间，实现连续回放
            Calendar latestStopTime = EZBusinessTool.getLatestStopTimeFromList(mDeviceRecordsAdapter.getData());
            if (latestStopTime != null) {
                mDeviceRecordInfo.setStopTime(latestStopTime);
            }
            if (mDeviceRecordsAdapter != null) {
                mDeviceRecordsAdapter.setSelectedPosition(playClickItem.getPosition());
            }
            mRecyclerViewForLocal.scrollToPosition(playClickItem.getPosition());
        } else if (mRecordType == RemoteListContant.TYPE_CLOUD) {
            mCloudRecordInfo = new EZCloudRecordFile();
            EZBusinessTool.convertCloudPartInfoFile2EZCloudRecordFile(mCloudRecordInfo, cloudFile);
            // 深拷贝后将结束时间设置为列表中第一个录像（最近时间）的结束时间，实现连续回放
            Calendar latestStopTime = EZBusinessTool.getLatestStopTimeFromList(mCloudRecordsAdapter.getData());
            if (latestStopTime != null) {
                mCloudRecordInfo.setStopTime(latestStopTime);
            }
            if (mCloudRecordsAdapter != null) {
                mCloudRecordsAdapter.setSelectedPosition(playClickItem.getPosition());
            }
            mRecyclerViewForCloud.scrollToPosition(playClickItem.getPosition());
        } else if (mRecordType == RemoteListContant.TYPE_SDKCLOUD) {
            mCloudRecordInfo = new EZCloudRecordFile();
            EZBusinessTool.convertCloudPartInfoFile2EZCloudRecordFile(mCloudRecordInfo, cloudFile);
            // 深拷贝后将结束时间设置为列表中第一个录像（最近时间）的结束时间，实现连续回放
            Calendar latestStopTime2 = EZBusinessTool.getLatestStopTimeFromList(mSDKCloudRecordsAdapter.getData());
            if (latestStopTime2 != null) {
                mCloudRecordInfo.setStopTime(latestStopTime2);
            }
            if (mSDKCloudRecordsAdapter != null) {
                mSDKCloudRecordsAdapter.setSelectedPosition(playClickItem.getPosition());
            }
            mRecyclerViewForSDKCloud.scrollToPosition(playClickItem.getPosition());
        }
        startPlaybackByRecordCategory();
    }

    /**
     * 更新适配器的选中位置
     */
    private void updateAdapterSelection(int position) {
        if (mRecordType == RemoteListContant.TYPE_CLOUD && mCloudRecordsAdapter != null) {
            mCloudRecordsAdapter.setSelectedPosition(position);
            mRecyclerViewForCloud.scrollToPosition(position);
        } else if (mRecordType == RemoteListContant.TYPE_SDKCLOUD && mSDKCloudRecordsAdapter != null) {
            mSDKCloudRecordsAdapter.setSelectedPosition(position);
            mRecyclerViewForSDKCloud.scrollToPosition(position);
        } else if (mRecordType == RemoteListContant.TYPE_LOCAL && mDeviceRecordsAdapter != null) {
            mDeviceRecordsAdapter.setSelectedPosition(position);
            mRecyclerViewForLocal.scrollToPosition(position);
        }
    }

    // endregion

    // region --- SurfaceTexture生命周期|SurfaceTexture Lifecycle ---

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        if (mPlaybackPlayer != null) {
            if (mTextureView.getSurfaceTexture() == surface) {
                mPlaybackPlayer.setSurfaceEx(surface);
            } else if (mSecondTextureView.getSurfaceTexture() == surface) {
                mPlaybackPlayer.setSurfaceEx(surface, 1);
            }
        }
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        if (mPlaybackPlayer != null) {
            if (mTextureView.getSurfaceTexture() == surface) {
                mPlaybackPlayer.setSurfaceEx(null);
            } else if (mSecondTextureView.getSurfaceTexture() == surface) {
                mPlaybackPlayer.setSurfaceEx(null, 1);
            }
        }
        return false;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {

    }

    // endregion

    // region --- 自定义方法|custom method ---

    private void initEZPlayer() {
        // 1.创建播放器
        mPlaybackPlayer = getOpenSDK().createPlayer(mCameraInfo.getDeviceSerial(), mCameraInfo.getCameraNo());
        // 2.配置播放器：设置回调，设置验证码，设置播放视图等配置
        mPlaybackPlayer.setHandler(playBackHandler);
        mPlaybackPlayer.setSurfaceEx(mTextureView.getSurfaceTexture());
        if (isMultiChannelDevice) {// 双目设备
            mPlaybackPlayer.setSurfaceEx(mSecondTextureView.getSurfaceTexture(), 1);
        }
        if (mDeviceInfo.getIsEncrypt() == 1) {
            mPlaybackPlayer.setPlayVerifyCode(DataManager.getInstance().getDeviceSerialVerifyCode(mCameraInfo.getDeviceSerial()));
        }
    }

    /**
     * 根据录像类别开始回放
     * 调用EZPlayer.startPlayback接口开始回放（重播或验证码输入后调用）
     * call EZPlayer.startPlayback to start playback
     */
    private void startPlaybackByRecordCategory() {
        if (mPlaybackPlayer == null) {
            return;
        }
        if (mDeviceRecordInfo != null) {
            // 加载SD卡回放特殊配置，一般开发者可不用配置，按需设置
            EZStreamConfigLoader.loadPlaybackSpecialConfigToPlayer(mPlaybackPlayer);
            mPlaybackPlayer.startPlayback(mDeviceRecordInfo);
        } else if (mCloudRecordInfo != null) {
            mPlaybackPlayer.startPlayback(mCloudRecordInfo);
        }
    }

    private void closePlayBack() {
        if (status == RemoteListContant.STATUS_EXIT_PAGE) {
            return;
        }
        LogUtil.d(TAG, "停止运行.........");
        mPlaybackPlayer.stopPlayback();
        stopUpdateTimer();
        status = RemoteListContant.STATUS_EXIT_PAGE;
    }

    /**
     * 统一更新声音状态（按钮UI + 播放器声音开关）
     *
     * @param soundOpen true-打开声音，false-关闭声音
     */
    private void updateSoundState(boolean soundOpen) {
        if (soundOpen) {
            soundBtn.setBackgroundResource(R.drawable.remote_list_soundon_btn_selector);
            if (mPlaybackPlayer != null) {
                mPlaybackPlayer.openSound();
            }
        } else {
            soundBtn.setBackgroundResource(R.drawable.remote_list_soundoff_btn_selector);
            if (mPlaybackPlayer != null) {
                mPlaybackPlayer.closeSound();
            }
        }
    }

    /**
     * 根据安装模式和能力集设置哪些矫正模式可用
     */
    private void setFecCorrectTypeBtnsEnable(EZFecPlaceType fecPlaceType) {
        int supportValue = FecViewLayoutHelper.getSupportInt(fecPlaceType, mDeviceInfo);
        FecViewLayoutHelper.setFecCorrectButtonsState(fecCorrectTypeButtons, supportValue);
    }

    // 取消所有下载任务
    private void stopAllDownloadTasks() {
        for (DownLoadTaskRecordAbstract downloadRecord : mDownloadTaskRecordListAbstract) {
            downloadRecord.stopDownloader();
        }
    }

    // 开启定时器，回放进度 UI 每秒刷新
    private void startUpdateTimer() {
        stopUpdateTimer();
        mUpdateTimer = new Timer();
        mUpdateTimerTask = new TimerTask() {
            @Override
            public void run() {
                sendMessage(RemoteListContant.MSG_REMOTELIST_UI_UPDATE, 0, 0);
            }
        };
        mUpdateTimer.schedule(mUpdateTimerTask, 0, 1000);
    }

    // 停止定时器
    private void stopUpdateTimer() {
        // 停止录像计时
        if (mUpdateTimer != null) {
            mUpdateTimer.cancel();
            mUpdateTimer = null;
        }

        if (mUpdateTimerTask != null) {
            mUpdateTimerTask.cancel();
            mUpdateTimerTask = null;
        }
    }

    private void sendMessage(int message, int arg1, int arg2) {
        if (playBackHandler != null) {
            Message msg = playBackHandler.obtainMessage();
            msg.what = message;
            msg.arg1 = arg1;
            msg.arg2 = arg2;
            playBackHandler.sendMessage(msg);
        }
    }

    // 页面可见
    private void onActivityResume() {
        if (!isDateSelected && currentClickItemFile != null) {
            if (status == RemoteListContant.STATUS_EXIT_PAGE || status == RemoteListContant.STATUS_DECRYPT) {
                newPlayInit();
                setPlaybackProgressUI(currentClickItemFile.getBeginTime(), currentClickItemFile.getEndTime());
                startPlaybackByRecordCategory();
            }
        }
    }

    /**
     * 数据初始化
     */
    private void initData() {
        localInfo = LocalInfo.getInstance();
        Bundle bundle = getIntent().getExtras();
        if (bundle != null) {
            queryDate = (Date) bundle.getSerializable(RemoteListContant.QUERY_DATE_INTENT_KEY);
            mCameraInfo = getIntent().getParcelableExtra(IntentConsts.EXTRA_CAMERA_INFO);
            mDeviceInfo = getIntent().getParcelableExtra(IntentConsts.EXTRA_DEVICE_INFO);
            isMultiChannelDevice = EZBusinessTool.isSupportMultiChannel(mDeviceInfo, mCameraInfo);
            if (isMultiChannelDevice) {// 双目设备调整画面比例
                mRealRatio = Constant.LIVE_MULTI_VIEW_RATIO;
            }
        }
        Application application = getApplication();
        DisplayMetrics metric = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(metric);
        localInfo.setScreenWidthHeight(metric.widthPixels, metric.heightPixels);
        localInfo.setNavigationBarHeight((int) Math.ceil(25 * getResources().getDisplayMetrics().density));
        if (mCameraInfo == null) {
            LogUtil.d(TAG, "cameraInfo is null");
            finish();
        }
    }

    // endregion

    // region --- UI ---

    public void initView() {
        mContentTabCloudRl = (RelativeLayout) findViewById(R.id.content_tab_cloud_root);
        mContentTabSDKCloudRl = (RelativeLayout) findViewById(R.id.content_tab_sdkcloud_root);
        mContentTabDeviceRl = (RelativeLayout) findViewById(R.id.content_tab_device_root);
        mCloudVideoActiveImg = (ImageView) findViewById(R.id.img_active_cloud_video);
        mCheckBtnCloud = (CheckTextButton) findViewById(R.id.pb_search_tab_btn_cloud);
        mCheckBtnSDKCloud = (CheckTextButton) findViewById(R.id.pb_search_tab_btn_sdkcloud);
        mCheckBtnDevice = (CheckTextButton) findViewById(R.id.pb_search_tab_btn_device);
        mTabContentMainFrame = (FrameLayout) findViewById(R.id.ez_tab_content_frame);

        mCheckBtnCloud.setToggleEnable(false);
        mCheckBtnSDKCloud.setToggleEnable(false);
        mCheckBtnDevice.setToggleEnable(false);
        mCheckBtnCloud.setChecked(true);

        mRecyclerViewForCloud = (RecyclerView) findViewById(R.id.recyclerView_cloud);
        mRecyclerViewForCloud.setLayoutManager(new GridLayoutManager(this, 3));
        mRecyclerViewForCloud.addItemDecoration(new RecordRecyclerAdapter.GridSpacingItemDecoration(3, Utils.dip2px(this, 5)));
        mRecyclerViewForSDKCloud = (RecyclerView) findViewById(R.id.recyclerView_sdkcloud);
        mRecyclerViewForSDKCloud.setLayoutManager(new GridLayoutManager(this, 3));
        mRecyclerViewForSDKCloud.addItemDecoration(new RecordRecyclerAdapter.GridSpacingItemDecoration(3, Utils.dip2px(this, 5)));
        mRecyclerViewForLocal = (RecyclerView) findViewById(R.id.recyclerView_device);
        mRecyclerViewForLocal.setLayoutManager(new GridLayoutManager(this, 3));
        mRecyclerViewForLocal.addItemDecoration(new RecordRecyclerAdapter.GridSpacingItemDecoration(3, Utils.dip2px(this, 5)));
        remoteListPage = (ViewGroup) findViewById(R.id.remote_list_page);
        mTitleBar = (TitleBar) findViewById(R.id.title);
        /* 测量状态栏高度 **/
        ViewTreeObserver viewTreeObserver = remoteListPage.getViewTreeObserver();
        viewTreeObserver.addOnGlobalLayoutListener(() -> {
            if (mRemotePlayBackRect == null) {
                // 获取状况栏高度
                mRemotePlayBackRect = new Rect();
                getWindow().getDecorView().getWindowVisibleDisplayFrame(mRemotePlayBackRect);
            }
        });
        mNovideoImgCloud = (LinearLayout) findViewById(R.id.novideo_img_cloud);
        mNovideoImgSDKCloud = (LinearLayout) findViewById(R.id.novideo_img_sdkcloud);
        mNoVideoImgLocal = (LinearLayout) findViewById(R.id.novideo_img_device);
        recordLoadingBar = (LoadingTextView) findViewById(R.id.record_loadingTextView);
        recordLoadingBar.setText(R.string.loading_text_default);
        remotePlayBackArea = (RelativeLayout) findViewById(R.id.remote_playback_area);
        duringTimeTV = (TextView) findViewById(R.id.during_time_tv);
        playBackPtzRL = findViewById(R.id.play_ptz_rl);
        mFullscreenButton = (CheckTextButton) findViewById(R.id.fullscreen_button);
        mScreenOrientationHelper = new ScreenOrientationHelper(this, mFullscreenButton);
        mTextureView = findViewById(R.id.remote_playback_wnd_sv);
        mTextureView.setSurfaceTextureListener(this);
        // 双目设备
        if (isMultiChannelDevice) {
            mSecondTextureView = findViewById(R.id.remote_playback_wnd_second_sv);
            mSecondTextureView.setSurfaceTextureListener(this);
            mSecondTextureView.setVisibility(View.VISIBLE);
            mFullscreenButton.setVisibility(View.INVISIBLE);
        }
        setRemoteListSvLayout();

        progressSeekbar = (SeekBar) findViewById(R.id.progress_seekbar);
        playTimeTV = (TextView) findViewById(R.id.play_time_tv);
        if (GlobalKit.isJCTest) {
            playTimeTV.setVisibility(View.VISIBLE);
        }
        controlArea = (LinearLayout) findViewById(R.id.control_area);
        progressArea = (LinearLayout) findViewById(R.id.progress_area);
        downloadBtn = (LinearLayout) findViewById(R.id.remote_playback_download_btn);
        playbackRateBtn = (Button) findViewById(R.id.btn_change_playback_rate);
        measure(downloadBtn);
        measure(controlArea);
        loadingPbLayout = (LinearLayout) findViewById(R.id.loading_pb_ly);

        errorInfoTV = (TextView) findViewById(R.id.error_info_tv);
        errorTipsVg = (ViewGroup) findViewById(R.id.vg_error_tips);
        errorReplay = (ImageButton) findViewById(R.id.error_replay_btn);
        pauseBtn = (ImageButton) findViewById(R.id.remote_playback_pause_btn);
        soundBtn = (ImageButton) findViewById(R.id.remote_playback_sound_btn);
        progressSeekbar.setMax(RemoteListContant.PROGRESS_MAX_VALUE);
        streamTypeTv = (TextView) findViewById(R.id.tv_stream_type);

        notPause = true;
        mControlBarRL = (ViewGroup) findViewById(R.id.control_bar);

        mLandscapeTitleBar = (TitleBar) findViewById(R.id.pb_title_bar_landscape);
        mLandscapeTitleBar.setStyle(Color.rgb(0xff, 0xff, 0xff), getResources().getDrawable(R.color.dark_bg_70p),
                null/*getResources().getDrawable(R.drawable.message_back_selector)*/);
        if (mCameraInfo != null) {
            mLandscapeTitleBar.setTitle(mCameraInfo.getCameraName());
        }
        mLandscapeTitleBar.addBackButton(v -> onBackPressed());
    }

    /**
     * 鱼眼设备专用设置
     */
    private void initFecView() {
        if (FecViewLayoutHelper.isFecDevice(mDeviceInfo)) {
            // 鱼眼设备显示"查看模式" & 调整画面比例为1:1
            mFullscreenButton.setVisibility(View.GONE);// 鱼眼设备不支持全屏，隐藏全屏按钮
            mCloudVideoActiveImg.setVisibility(View.GONE);// 页面太挤，云存储开通图片不显示
            ImageButton viewTypeBtn = (ImageButton) findViewById(R.id.remote_playback_viewtype_btn);
            viewTypeBtn.setVisibility(View.VISIBLE);
            viewTypeBtn.setOnClickListener(v -> openFecViewModePopupWindow(controlArea));

            fecPlaceType = EZFecPlaceType.EZ_FEC_PLACE_CEILING;// demo中默认顶装
            fecCorrectType = EZFecCorrectType.EZ_FEC_CORRECT_FISH;// demo中默认鱼眼（原始码流）
            mRealRatio = 1;

            mPlayBackSv1 = (SurfaceView) findViewById(R.id.playback_sv1);
            mPlayBackSv2 = (SurfaceView) findViewById(R.id.playback_sv2);
            mPlayBackSv3 = (SurfaceView) findViewById(R.id.playback_sv3);
            mPlayBackSv4 = (SurfaceView) findViewById(R.id.playback_sv4);
            mPlayBackSv5 = (SurfaceView) findViewById(R.id.playback_sv5);
            mPlayBackSv6 = (SurfaceView) findViewById(R.id.playback_sv6);
            ViewGroup playWindowVg = (ViewGroup) findViewById(R.id.vg_play_window);

            // fecViewLayoutHelper.fecPopupWindow 需要延后设置，全局搜索查看
            fecViewLayoutHelper = new FecViewLayoutHelper(this);
            fecViewLayoutHelper.playerView = mTextureView;
            fecViewLayoutHelper.playWindowVg = playWindowVg;
            fecViewLayoutHelper.player = mPlaybackPlayer;
            fecViewLayoutHelper.playPtzRL = playBackPtzRL;
            fecViewLayoutHelper.setSurfaceViews(new SurfaceView[]{mPlayBackSv1, mPlayBackSv2, mPlayBackSv3,
                    mPlayBackSv4, mPlayBackSv5, mPlayBackSv6});
            playWindowVg.post(() -> fecViewLayoutHelper.setPlayViewAspectRadioWith1V1());
        }
    }

    private void initListener() {
        backBtn = mTitleBar.addBackButton(v -> {
            onExitCurrentPageClick();
            finish();
        });
        // 右上角录像类型按钮事件
        rightButton = mTitleBar.addRightButton(R.drawable.common_title_extension_selector, mTopRightMenuClickListener);
        rightButton.setVisibility(EzvizAPI.getInstance().isUsingGlobalSDK() ? View.VISIBLE : View.GONE);
        // 标题栏日历时间选择事件
        mTitleBar.addTitleButton(R.drawable.remote_cal_selector, goToCalendarClickListener);
        mTitleBar.setOnTitleClickListener(goToCalendarClickListener);
        mCloudVideoActiveImg.setOnClickListener(mCloudAdImageViewClickListener);// 云存储图片事件
        downloadBtn.setOnClickListener(mDownloadButtonClickListener);// 下载按钮事件
        playbackRateBtn.setOnClickListener(v -> onPlaybackRateClick(v));// 倍数回放按钮事件
        errorReplay.setOnClickListener(mReplayButtonClickListener);// 播放成后显示的重播按钮事件
        pauseBtn.setOnClickListener(mPlayButtonClickListener);// 暂停播放按钮事件
        soundBtn.setOnClickListener(mSoundButtonClickListener);// 声音按钮事件
        progressSeekbar.setOnSeekBarChangeListener(mSeekBarChangeListener);// 回放进度条拖动事件
        // 录像类型切换事件
        mCheckBtnCloud.setOnClickListener(mCloudCheckListener);
        mCheckBtnSDKCloud.setOnClickListener(mSDKCloudCheckListener);
        mCheckBtnDevice.setOnClickListener(mDeviceCheckListener);
    }

    /**
     * 停止当前回放并重置播放UI
     * stop current playback and reset play UI
     */
    private void stopCurrentPlayback() {
        stopUpdateTimer();
        mPlaybackPlayer.stopPlayback();
        status = RemoteListContant.STATUS_STOP;
        newPlayInit();
    }

    /**
     * 开始播放前，UI重置
     */
    private void newPlayInit() {
        resetPlayUIVisible();// 初始化播放界面组件的可见性
        resetPauseBtnUI();// 暂停按钮初始化
        progressSeekbar.setProgress(0);
    }

    /**
     * 暂停按钮 UI和状态值重置
     */
    private void resetPauseBtnUI() {
        notPause = true;
        pauseBtn.setBackgroundResource(R.drawable.ez_remote_list_pause_btn_selector);
    }

    /**
     * 初始化播放界面组件的可见性
     */
    private void resetPlayUIVisible() {
        loadingPbLayout.setVisibility(View.VISIBLE);
        errorTipsVg.setVisibility(View.GONE);
        errorReplay.setVisibility(View.GONE);
        playbackRateBtn.setText("1x");
    }

    /**
     * 回放进度条拖动后，UI重置
     */
    private void seekInit() {
        resetSeekPlayUI();
        resetPauseBtnUI();
    }

    /**
     * 回放进度条拖动后，设置相关组件的可见性
     */
    private void resetSeekPlayUI() {
        loadingPbLayout.setVisibility(View.VISIBLE);
        errorTipsVg.setVisibility(View.GONE);
        errorReplay.setVisibility(View.GONE);
    }

    /**
     * 录像播放进度UI初始化（已播放时长、总时长、SeekBar进度）
     */
    private void setPlaybackProgressUI(long beginTime, long endTime) {
        int diffSeconds = (int) (endTime - beginTime) / 1000;
        String convToUIDuration = RemoteListUtil.convToUIDuration(diffSeconds);
        duringTimeTV.setText(convToUIDuration);
        playTimeTV.setText(RemoteListContant.VIDEO_DUAR_BEGIN_INIT);
        progressSeekbar.setProgress(0);
    }

    /**
     * 设置取流方式
     */
    private void showStreamFetchType() {
        int type = mPlaybackPlayer.getStreamFetchType();
        LogUtil.i(TAG, "getStreamFetchType: " + type);
        String streamTypeMsg = getApplicationContext().getString(R.string.stream_type) + EZBusinessTool.getStreamType(type);
        streamTypeTv.setText(streamTypeMsg);
        streamTypeTv.setVisibility(View.VISIBLE);
    }

    /**
     * 录像类型选中状态重置
     */
    private void resetRecordCheckBtns() {
        mCheckBtnCloud.setChecked(false);
        mCheckBtnSDKCloud.setChecked(false);
        mCheckBtnDevice.setChecked(false);
        mContentTabCloudRl.setVisibility(View.GONE);
        mContentTabSDKCloudRl.setVisibility(View.GONE);
        mContentTabDeviceRl.setVisibility(View.GONE);
    }

    /**
     * 根据id对视图进行显示/隐藏
     *
     * @param id
     */
    private void showTab(int id) {
        if (id == R.id.novideo_img_cloud) {
            mNovideoImgCloud.setVisibility(View.VISIBLE);
            recordLoadingBar.setVisibility(View.GONE);
            mTabContentMainFrame.setVisibility(View.VISIBLE);
        } else if (id == R.id.novideo_img_sdkcloud) {
            mNovideoImgSDKCloud.setVisibility(View.VISIBLE);
            recordLoadingBar.setVisibility(View.GONE);
            mTabContentMainFrame.setVisibility(View.VISIBLE);
        } else if (id == R.id.novideo_img_device) {
            mNoVideoImgLocal.setVisibility(View.VISIBLE);
            mRecyclerViewForLocal.setVisibility(View.GONE);
            recordLoadingBar.setVisibility(View.GONE);
            mTabContentMainFrame.setVisibility(View.VISIBLE);
        } else if (id == R.id.record_loadingTextView) {
            mNovideoImgCloud.setVisibility(View.GONE);
            recordLoadingBar.setVisibility(View.VISIBLE);
            mTabContentMainFrame.setVisibility(View.GONE);
        } else if (id == R.id.content_tab_cloud_root) {
            mNovideoImgCloud.setVisibility(View.GONE);
            recordLoadingBar.setVisibility(View.GONE);
            mTabContentMainFrame.setVisibility(View.VISIBLE);
        } else if (id == R.id.content_tab_sdkcloud_root) {
            mNovideoImgSDKCloud.setVisibility(View.GONE);
            recordLoadingBar.setVisibility(View.GONE);
            mTabContentMainFrame.setVisibility(View.VISIBLE);
        } else if (id == R.id.content_tab_device_root) {
            mNoVideoImgLocal.setVisibility(View.GONE);
            recordLoadingBar.setVisibility(View.GONE);
            mTabContentMainFrame.setVisibility(View.VISIBLE);
        }
    }

    /**
     * 展示播放过程中的重要事件提示，如播放出错、播放完成
     * show important tip during playing. just like error event, finish event
     */
    private void showPlayEventTip(final String tip) {
        runOnUiThread(() -> {
            loadingPbLayout.setVisibility(View.GONE);
            errorTipsVg.setVisibility(View.VISIBLE);// 错误提示布局显示
            errorReplay.setVisibility(View.VISIBLE);// 重播按钮显示
            errorInfoTV.setText(tip);// 错误提示信息
        });
    }

    /**
     * 根据分辨率调整SurfaceView的大小
     */
    private void setRemoteListSvLayout() {
        final int screenWidth = localInfo.getScreenWidth();
        final int screenHeight = (mOrientation == Configuration.ORIENTATION_PORTRAIT) ?
                (localInfo.getScreenHeight() - localInfo.getNavigationBarHeight()) : localInfo.getScreenHeight();
        final RelativeLayout.LayoutParams realPlaySvlp = Utils.getPlayViewLp(mRealRatio, mOrientation,
                localInfo.getScreenWidth(), (int) (localInfo.getScreenWidth() * Constant.LIVE_VIEW_RATIO),
                screenWidth, screenHeight);

        RelativeLayout.LayoutParams svLp = new RelativeLayout.LayoutParams(realPlaySvlp.width, realPlaySvlp.height);
        svLp.addRule(RelativeLayout.CENTER_HORIZONTAL);

        ViewGroup playWindowVg = (ViewGroup) findViewById(R.id.vg_play_window);
        playWindowVg.setLayoutParams(svLp);
    }

    private void onOrientationChanged() {
        setRemoteListSvLayout();
        if (mOrientation == Configuration.ORIENTATION_PORTRAIT) {
            // 显示状态栏
            fullScreen(false);
            if (status != RemoteListContant.STATUS_PLAYING) {
                // 不允许选择屏幕
                mScreenOrientationHelper.disableSensorOrientation();
            }
            // 竖屏处理
            remoteListPage.setBackgroundColor(getResources().getColor(R.color.white));
            mTitleBar.setVisibility(View.VISIBLE);
            mRecyclerViewForCloud.setVisibility(View.VISIBLE);
            mControlBarRL.setVisibility(View.VISIBLE);
            mLandscapeTitleBar.setVisibility(View.GONE);
        } else {
            // 横屏处理
            // 隐藏状态栏
            fullScreen(true);
            remoteListPage.setBackgroundColor(getResources().getColor(R.color.black_bg));
            mTitleBar.setVisibility(View.GONE);
            mRecyclerViewForCloud.setVisibility(View.GONE);
            mControlBarRL.setVisibility(View.GONE);
            mLandscapeTitleBar.setVisibility(View.VISIBLE);
        }
    }

    private void fullScreen(boolean enable) {
        if (enable) {
            WindowManager.LayoutParams lp = getWindow().getAttributes();
            lp.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
            getWindow().setAttributes(lp);
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
        } else {
            WindowManager.LayoutParams attr = getWindow().getAttributes();
            attr.flags &= (~WindowManager.LayoutParams.FLAG_FULLSCREEN);
            getWindow().setAttributes(attr);
            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
        }
    }

    // endregion

    // region --- 鱼眼矫正模式弹出框打开 & 关闭|Fish eye correction mode pop-up box on&off ---

    /**
     * 打开鱼眼矫正模式操作弹出框
     *
     * @param parent
     */
    private void openFecViewModePopupWindow(View parent) {
        closeFecViewModePopupWindow();

        LayoutInflater layoutInflater = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        ViewGroup layoutView = (ViewGroup) layoutInflater.inflate(R.layout.realplay_fec_wnd, null, true);
        layoutView.findViewById(R.id.fec_close_btn).setOnClickListener(mOnFecWndClickListener);

        Button placeWallBtn = layoutView.findViewById(R.id.fec_place_wall);
        Button placeFloorBtn = layoutView.findViewById(R.id.fec_place_floor);
        Button placeCeilingBtn = layoutView.findViewById(R.id.fec_place_ceiling);
        Button correctFishBtn = layoutView.findViewById(R.id.fec_correct_fish);
        Button correct4PtzBtn = layoutView.findViewById(R.id.fec_correct_4ptz);
        Button correct5PtzBtn = layoutView.findViewById(R.id.fec_correct_5ptz);
        Button correctFull5PtzBtn = layoutView.findViewById(R.id.fec_correct_full5ptz);
        Button correctLatBtn = layoutView.findViewById(R.id.fec_correct_lat);
        Button correctARCHorBtn = layoutView.findViewById(R.id.fec_correct_arc_hor);
        Button correctARCVerBtn = layoutView.findViewById(R.id.fec_correct_arc_ver);
        Button correctWideAngleBtn = layoutView.findViewById(R.id.fec_correct_wide_angle);
        Button correct180Btn = layoutView.findViewById(R.id.fec_correct_180);
        Button correct360Btn = layoutView.findViewById(R.id.fec_correct_360);
        Button correctCycBtn = layoutView.findViewById(R.id.fec_correct_cyc);
        fecCorrectTypeButtons = new Button[]{correct4PtzBtn, correct5PtzBtn, correctFull5PtzBtn, correctLatBtn,
                correctARCHorBtn, correctARCVerBtn, correctWideAngleBtn, correct180Btn, correct360Btn, correctCycBtn};

        placeWallBtn.setOnClickListener(mOnFecWndClickListener);
        placeFloorBtn.setOnClickListener(mOnFecWndClickListener);
        placeCeilingBtn.setOnClickListener(mOnFecWndClickListener);
        correctFishBtn.setOnClickListener(mOnFecWndClickListener);
        correct4PtzBtn.setOnClickListener(mOnFecWndClickListener);
        correct5PtzBtn.setOnClickListener(mOnFecWndClickListener);
        correctFull5PtzBtn.setOnClickListener(mOnFecWndClickListener);
        correctLatBtn.setOnClickListener(mOnFecWndClickListener);
        correctARCHorBtn.setOnClickListener(mOnFecWndClickListener);
        correctARCVerBtn.setOnClickListener(mOnFecWndClickListener);
        correctWideAngleBtn.setOnClickListener(mOnFecWndClickListener);
        correct180Btn.setOnClickListener(mOnFecWndClickListener);
        correct360Btn.setOnClickListener(mOnFecWndClickListener);
        correctCycBtn.setOnClickListener(mOnFecWndClickListener);
        // 设置按钮的可见和可用状态
        int wallTypeValue = FecViewLayoutHelper.getSupportInt(EZFecPlaceType.EZ_FEC_PLACE_WALL, mDeviceInfo);
        int floorTypeValue = FecViewLayoutHelper.getSupportInt(EZFecPlaceType.EZ_FEC_PLACE_FLOOR, mDeviceInfo);
        int ceilingTypeValue = FecViewLayoutHelper.getSupportInt(EZFecPlaceType.EZ_FEC_PLACE_CEILING, mDeviceInfo);
        placeWallBtn.setVisibility(wallTypeValue > 0 ? View.VISIBLE : View.GONE);
        placeFloorBtn.setVisibility(floorTypeValue > 0 ? View.VISIBLE : View.GONE);
        placeCeilingBtn.setVisibility(ceilingTypeValue > 0 ? View.VISIBLE : View.GONE);
        setFecCorrectTypeBtnsEnable(fecPlaceType);

        int height = localInfo.getScreenHeight() - mTitleBar.getHeight() - remotePlayBackArea.getHeight() - controlArea.getHeight()
                - (mRemotePlayBackRect != null ? mRemotePlayBackRect.top : localInfo.getNavigationBarHeight());
        mFecPopupWindow = new PopupWindow(layoutView, LayoutParams.MATCH_PARENT, height, true);
        mFecPopupWindow.setBackgroundDrawable(new BitmapDrawable());
        mFecPopupWindow.setAnimationStyle(R.style.popwindowUpAnim);
        mFecPopupWindow.setFocusable(false);
        mFecPopupWindow.setOutsideTouchable(false);
        mFecPopupWindow.showAsDropDown(parent);
        mFecPopupWindow.update();

        fecViewLayoutHelper.fecPopupWindow = mFecPopupWindow;
    }

    /**
     * 关闭鱼眼查看模式操作弹出框
     */
    private void closeFecViewModePopupWindow() {
        if (mFecPopupWindow != null) {
            dismissPopWindow(mFecPopupWindow);
            mFecPopupWindow = null;
            fecViewLayoutHelper.fecPopupWindow = null;// fecViewLayoutHelper中的窗口对象置空，必须
        }
    }

    // endregion

    // region --- 动态权限申请|Dynamic permission application ---

    /**
     * 申请存储权限并保存录像到相册
     * Android 10+ 使用 MediaStore 不需要存储权限，直接保存
     * Android 9 及以下需要 WRITE_EXTERNAL_STORAGE 权限
     */
    private void requestStoragePermissionAndSave() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            // Android 10+ 通过 MediaStore 保存不需要存储权限
            saveVideo2Album();
        } else {
            // Android 9 及以下需要写权限
            EZPermissionUtil.requestPermissions(this,
                    new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                    1000,
                    () -> saveVideo2Album()
            );
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        EZPermissionUtil.handleResult(requestCode, permissions, grantResults);
    }

    private void saveVideo2Album() {
        // 将录像存储到相册
        File file = new File(downloadFilePath);
        EZUtils.saveVideo2Album(EZPlayBackListActivity.this, file, recordInfoDuration);
        if (isMultiChannelDevice) {
            int index = downloadFilePath.lastIndexOf(".");
            String downloadFilePath2 = downloadFilePath.substring(0, index) + "_1" + downloadFilePath.substring(index);
            File file2 = new File(downloadFilePath2);
            EZUtils.saveVideo2Album(EZPlayBackListActivity.this, file2, recordInfoDuration);
        }
        showToast(getResources().getString(R.string.already_saved_to_volume));
        // TODO downloadFilePath的录像可以自行删除，避免占用手机内存，可以在onDestroy的时候。不能立即调用file.delete();因为文件存储到相册是异步耗时操作。
    }

    // endregion

}
