康佳应急广播 EmergRadioBZ(KBTA100-C)项目

GitLab 地址:git@git.konkawise.com:kongjunjie/EmergRadioBZ.git

测试信息

  1. 测试标准平台
    https://220.167.54.108:28443/loginView
    IP:220.167.54.108
    端口号:27443
  2. 登录账户信息
    账号:admin
    密码:123456

创建广播

适配器,资源调度器分别指什么?

适配器是服务器与终端的中间层,服务器与适配器进行通信,再由适配器与终端进行通信。

播放媒体、通道指什么?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GXm8ny07-1601286041157)(https://kcms.konkawise.com/upload/202008251032096695.png)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0261sF2J-1601286041175)(https://kcms.konkawise.com/upload/202008251032226082.png)]
播发媒体(线路、通道)属于输入通道,是输入内容的,告诉适配器通过什么通道 获取输入内容 ,包括但不限于(FM、LINE、USB、MIC)。
具体告诉适配器的逻辑业务由服务器完成,App只给客户提供一个创建广播的选项

广播的输入内容

适配器的内容或文件(线路、通道)
服务器文件(媒体库)
本地文件(文本、音频、录音)

输出通道、调频、IP具体指什么?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7nHFL3Mf-1601286041190)(https://kcms.konkawise.com/upload/202008251031553040.png)]
输出通道用于告诉适配器,当前广播通过什么方式传输给终端
输出通道指通过什么方式输出内容

适配器,资源调度器

  1. 172.29.22.201这个IP是谁的IP?
    内网的适配器IP
  2. 为什么填入IP,资源调度器选择否?此时可以选择输入/输出通道吗?
    输入/输出通道只在适配器上有
    如果选择资源调度则不能选择输入/输出通道

演练播发类型

系统演练、模拟演练的区别是什么?
系统演练,测试平台本身功能
模拟演练,测试终端的接收、回传等各项功能

广播内容

  1. 线路具体指什么?线路依据平台配置,那平台具体指什么?
    线路指适配器的输入通道,适配器与线路都由用户在平台手动创建。
    平台即是可以向适配器下发指令,再由适配器控制终端设备。
  2. 输出/输出通道指什么?
    指适配器输出内容的通道。

参数设置

资源编码设置的具体规范要求

最多两位数的阿拉伯数字
具体资源编码的组成参考《GXYJ-08-20181022-广西应急广播资源分类及编码规范》、《广西应急广播资源编码规范(含自约定设备)》
《广西应急广播资源编码规范(含自约定设备)》 的 含自约定设备 指的就是电视与手机默认资源编码
资源子类型识别码:
电视机 的资源子类型识别码为 57
手机APP 的资源子类型识别码为 61

周期、回传参数用来做什么?

回传周期指的是终端在多长时间回传一次自身状态
回传参数指的是终端通过什么方式回传,以及具体的回传地址。回传逻辑码和物理码到具体指定的终端,比如电脑、笔电、手机等。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HQfYUFka-1601286041208)(https://kcms.konkawise.com/upload/202008251030376942.png)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R9aW6TuD-1601286041221)(https://kcms.konkawise.com/upload/202008251030481208.png)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LpwXTwYW-1601286041235)(https://kcms.konkawise.com/upload/202008251031066279.png)]

启/禁用维持用来做什么?

服务端下发指令控制设备终端是否能够启用,即是对服务端下发指令响应与否。

项目架构设计

三个应急广播apk项目都只与服务器进行交互,不与适配器、终端交互。
标准App 只使用 API接口 与服务器进行交互
广西App 主要使用 API接口 与服务器进行交互,唯独实时广播功能是独立出来使用 指令 与服务器进行交互
终端适配App 主要使用 指令 与服务器进行交互,只使用了资源下载的 API接口文本图片 进行下载

注意:这里的指令交互即是依据IP协议,构造报文进行信息交互。具体的报文定义标准,传输指令规范可见《ip话筒通信协议----商讨版2.0》、《20181109-广西应急广播ip传输技术规范》。

媒体库、多文件上传、实时监听、历史回放

媒体库

业务逻辑
创建页面选择媒体库 -> 从服务器请求媒体库数据 -> 返回并显示到页面 -> 用户选择相应媒体,获取媒体ID -> 放入创建广播所需的参数集合中

多文件上传

业务逻辑
点击文本、本地、录音 -> 输入媒体/选择媒体 -> 点击创建广播时先上传文件获取 ebmid ,获取成功把 ebmid 加入到参数集合中再创建广播。

注意:通过ebmid唯一标识,可以获取RTMP地址,得到此地址后就可以播放音视频流文件。

接口文件

   /* BroadCreateDialog.java 643行 */
  private void uploadFileGetEbmid()

实时监听

业务逻辑
使用广播列表item中的 ebmid 取平台请求实时流地址 -> 平台返回后使用该地址播放音频
接口文件

  /* BroadAdapter.java 102行 */
  vh.tvRtmpListener.setOnClickListener(v -> {} 
  
  /* BroadAdapter.java 113行 */
  new BroadMediaPlayDialog(context, bean.getBroadName(), rtmpPath, true); 

历史回放

业务逻辑
广播详情中包含媒体id,使用媒体id去媒体接口获取媒体数据流
接口文件

/* BroadDetailsMediaAdapter.java 75~76行 */
new BroadMediaPlayDialog(context,bean.getResourceName(), bean.getId() + "", false);

/* BroadMediaPlayDialog.java 78行 */
String apiPath = "%s/BroadManageController/getResourceContent?id=%s";

/* MusicPlayUtil.java 33行 */
public void play(Context context, String url)

项目目录结构

cpp -> so库头文件(ffmpeg命令工具,mp3lame转换工具)
jni -> mp3lame so 生成文件,已用 so 文件代替
jniLibs -> 项目中用到的 so 文件,包括(ijkplayer、ffmpeg命令工具,mp3lame转换工具)

java文件
broadFragment -> 广播界面
dialog -> 广播创建、详情、筛选等页面及子页面
adapter -> 广播详情页面相关适配器
BroadAssociatedTasksAdapter -> 广播详情子任务适配器
BroadDetailsMediaAdapter -> 广播详情媒体资源适配器
BroadResAdapterAdapter -> 创建广播资源调度相关适配器
BroadResSchedulingAdapter -> 创建广播资源调度相关适配器
ChannelAdapter -> 创建广播输出通道选择适配器
ChannelShowAdapter -> 创建广播输出通道显示适配器
LocalContentAdapter -> 创建广播播发媒体显示适配器
MediaResAdapter -> 创建广播媒体显示适配器
MediaResourceAdapter -> 创建广播媒体显示适配器,已废弃
MonthAdapter -> 创建日常定时广播播发周期相关适配器
PlanContentAdapter -> 创建广播预案媒体列表显示适配器
BaseCreateDialog ->
BroadCreateAdapterDialog -> 适配器/资源调度
BroadCreateAreaDialog -> 逻辑区域选择
BroadCreateContentDialog -> 播放媒体,已废弃
BroadCreateContentLineChannelDialog -> 播放媒体添加线路/通道
BroadCreateContentMediaDialog -> 播放媒体添加媒体库
BroadCreateContentRecordDialog -> 播放媒体添加录音
BroadCreateContentTextDialog -> 播放媒体添加文本
BroadCreateDialog -> 广播创建
BroadCreateLevelDialog -> 重要等级/事件类型
BroadCreateModeDialog -> 广播播发方式
BroadCreateOutChannelDialog -> 输出通道
BroadCreateRealAreaDialog -> 实时广播区域选择
BroadCreateRecordDialog -> 播放媒体添加录音,已废弃
BroadCreateTimeDialog -> 开始/结束时间
BroadCreateTimePeriodDialog -> 日常广播播发周期
BroadCreateTimeTypeDialog -> 日常广播播发方式
BroadCreateTypeDialog -> 演练广播演练播发类型
BroadDetailsDialog -> 广播详情
BroadFiltrationDialog -> 广播筛选
BroadMediaPlayDialog -> 广播音频播放
BroadRealTimeDialog -> 创建实时广播
BroadShowTextDialog -> 广播文本资源查看
BroadStopWarnDialog -> 实时广播停止提醒页面
BroadAdapter ->
BroadCreateModel ->
BroadFragment ->
checkFragment -> 审核界面
CheckDetailsDialog -> 审核详情页面
CheckDetailsMediaListAdapter -> 审核详情页面媒体列表适配器
CheckFiltrationDialog -> 审核筛选页面
CheckFragment -> 审核页面
CheckListAdapter ->
CheckListFragment -> 审核列表页面
CheckPagerAdapter ->
customView -> 自定义View
deviceFragment -> 设备界面
paramConfig -> 参数下发相关页面
BaseConfigDialog ->
BaseDeviceDialog ->
DeviceDetailsDialog -> 设备详情页面
DeviceFiltrationDialog -> 设备筛选页面
DeviceFragment -> 设备页面
DeviceListAdapter ->
DeviceModel ->
DevicePagerAdapter ->
DeviceUpdateDialog -> 设备更新页面
entity -> 实体类
serverFragment -> 服务器界面
ServerFragment
WebClient -> WebSocket 通讯类
WebClientStompProtocol -> Stomp 框架的 WebSocket
utils -> 工具包
adaptive -> 屏幕适配
ffmpeg -> FFmpeg命令工具Java类
mp3Record -> MP3 录制工具
ssl -> 证书类
APIManager
AreaTreeBroadUtil -> 区域树封装工具
AreaTreeCheckBoxUtil -> 区域树封装工具
AreaTreeDeviceUtil -> 区域树封装工具
AreaTreeRadioBtnUtil -> 区域树封装工具
AudioCallBack -> 音频回调接口
AudioCallBackUtil -> 音频、录音回调接口
Config -> 常量类
ContentProviderUtil -> 音乐库歌曲信息加入工具类
DownloadUtils -> 下载工具类,音频、文本、APK
FilePathUtils -> 文件路径工具类
GetDeviceId -> 获取唯一标识工具类
LicenseConfigUtil -> 授权许可证工具类
MusicPlayUtil -> 音频播放工具类
NodeViewBinder -> 区域树节点构建类
NodeViewFactory -> 区域树视图创建类
PermissionUtils -> 权限请求封装类
RecordCallBack -> 录音回调接口
SpConfig -> SharedPreference 常量类
UserHeartbeatUtil -> 心跳类,已废弃
Utils -> 常用工具类集合
AuthorizationDialog -> 授权窗口
AuthorizationExplainDialog -> 授权说明窗口
BaseActivity ->
BaseDialog ->
BaseFragment ->
DialogCloseCache -> 清除缓存窗口
DialogLoading -> 网络请求加载窗口
RVLinearLayoutManager ->
UpdateDialog -> 版本更新窗口
HostActivity -> 主页Activity
IpPortFragment -> IP配置页面
LoginFragment -> 登陆页面
MainActivity -> 主Activity

框架应用&业务实现

EventBus框架的应用

项目中在AudioRecorder.java应用EventBus在组件之间通信

public AudioRecorder(File file, Mp3Recorder myMp3Recorder,boolean udpSend) {
        .	.	.
        sendNoPermission = new Runnable() {
            @Override
            public void run() {
                EventBus.getDefault().post(new AudioNoPermissionEvent());
                SP.setBoolean("mp3permission", false);
                Log.e(waitingTime / 1000 + "s等待时间已到,么有权限:");
            }
        };
    }

更多EventBus框架应用请见:
EventBus 的使用
EventBus 中文文档
Android开源框架源码鉴赏:EventBus

Stomp框架

项目中WebClientStompProtocol.java应用Stomp框架实现服务端和客户端之间订阅发布消息。

public void connectStomp() {

        mStompClient.withClientHeartbeat(1000).withServerHeartbeat(1000);
        resetSubscriptions();
        Disposable dispLifecycle = mStompClient.lifecycle()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(lifecycleEvent -> {
                    switch (lifecycleEvent.getType()) {
                        case OPENED:
                            Log.e(TAG, "Stomp connection opened");
                            break;
                        case ERROR:
                            Log.e(TAG, "Stomp connection error", lifecycleEvent.getException());
                            Log.e(TAG, "Stomp connection error");
                            break;
                        case CLOSED:
                            Log.e(TAG, "Stomp connection closed");
                            resetSubscriptions();
                            break;
                        case FAILED_SERVER_HEARTBEAT:
                            Log.e(TAG, "Stomp failed server heartbeat");
                            break;
                    }
                });
        mStompClient.connect();

        compositeDisposable.add(dispLifecycle);

        // Receive greetings
        Disposable dispTopic = mStompClient.topic(destinationPath)
                //Disposable dispTopic = mStompClient.topic("/topic")
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe((StompMessage topicMessage) -> {

                    Log.d(TAG, "Received " + topicMessage.getPayload());
                    //DataModel response1 = mGson.fromJson(topicMessage.getPayload(), DataModel.class);
                    //添加你的数据逻辑

                }, throwable -> {
                    Log.e(TAG, "连接错误", throwable);
                });

        compositeDisposable.add(dispTopic);

    }

框架更多应用请见
StompProtocolAndroid 使用方法

Navigation框架

以下主页、IP配置、登录页面使用Navigation框架进行导航。
HostActivity -> 主页Activity
IpPortFragment -> IP配置页面
LoginFragment -> 登陆页面
MainActivity -> 主Activity
NavHostFragment
activity_main.xml相当于NavHostFragment 相当于一个导航界面容器,用来换入和换出应用中各个 Destination 所代表的 Fragment。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.konka.emergradio.MainActivity">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/mobile_navigation" />

</FrameLayout>

MainActivity.java

public class MainActivity extends BaseActivity {
    @Override
    public boolean onSupportNavigateUp() {
	// 此方法调用是为了在此界面按返回键不会崩溃黑屏
        return findNavController(this, R.id.nav_host_fragment).navigateUp();
    }
		.	.	.

Navigation Graph
在activity_main.xml定义mobile_navigation.xml,这是Navigation Graph用于集中保存所有与导航相关的信息。在导航图中,一个屏幕代表一个 Destination(目的地)也就是导航指向的下一个视图。

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mobile_navigation"
    app:startDestination="@id/ipPortFragment">

    <fragment
        android:id="@+id/ipPortFragment"
        android:name="com.konka.emergradio.IpPortFragment"
        android:label="fragment_ip_port"
        tools:layout="@layout/fragment_ip_port">
        <action
            android:id="@+id/action_ipPortFragment_to_loginFragment"
            app:destination="@id/loginFragment"
            app:enterAnim="@anim/fragment_slide_right_in"
            app:exitAnim="@anim/fragment_slide_left_out"
            app:popEnterAnim="@anim/fragment_slide_left_in"
            app:popExitAnim="@anim/fragment_slide_right_out" />
    </fragment>

    <fragment
        android:id="@+id/loginFragment"
        android:name="com.konka.emergradio.LoginFragment"
        android:label="fragment_login"
        tools:layout="@layout/fragment_login">
        <action
            android:id="@+id/action_loginFragment_to_homeActivity"
            app:destination="@id/homeActivity" />
    </fragment>

    <activity
        android:id="@+id/homeActivity"
        android:name="com.konka.emergradio.HostActivity"
        android:label="activity_home"
        tools:layout="@layout/activity_host" />

</navigation>

NavController的navigate()方法实现界面跳转,示例代码如下

@OnClick({R.id.btn_connection})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.btn_connection:
               .	.	.
                APIManager.getInstance().setIpPort(true, ip, port);
                Navigation.findNavController(view).navigate(R.id.action_ipPortFragment_to_loginFragment);
                break;
        }
    }

更多Navigation框架信息请见:
Android 架构组件之 Navigation
Android架构组件-Navigation的使用(一)
Android架构组件-Navigation的使用(二)

ProjectApplication

具体实现代码

// 34a40b2fcf是注册时申请的APPID
Bugly.init(getApplicationContext(), "34a40b2fcf", false);

更多bugly信息请见:
腾讯bugly实现应用自动更新提示

版本文件更新

HostActivity.java注册版本文件点击响应版本升级事件,如果升级则实现升级进度

tvUpdate.setOnClickListener(v -> requestUpdateInfo());

private void requestUpdateInfo() {
        APIManager.getInstance().getVersionFile((isRequestSuccess, isSuccess, msg) -> {
            Log.d("HostActivity", " -=-=- initEvent: getVersionFile  isRequestSuccess:" +
                    isRequestSuccess + "  isSuccess:" + isSuccess + "  msg:" + msg);

            if (isRequestSuccess && isSuccess && msg.contains("true")) {
                isHaveUpdate = true;
                downloadNewApp(msg, progress -> runOnUiThread(() -> {
                    tvUpdateHint.setVisibility(View.VISIBLE);
                    tvUpdateHint.setText(((int) progress) + "%");
                    if (progress == 100) tvUpdateHint.setVisibility(View.GONE);
//                    Log.d("HostActivity", " -=-=- requestUpdateInfo: progress:" + progress);
                }));
            } else {
                isHaveUpdate = false;
                ToastUtils.showShort("目前已是最新版!");
            }

            tvUpdateHint.setVisibility(isHaveUpdate ? View.VISIBLE : View.GONE);

        });

    }
	
private void downloadNewApp(String msg, DownloadUtils.DownloadListener listener) {
        String downloadPath = APIManager.getInstance().downloadVersionFile();
        Log.d("HostActivity", " -=-=- requestUpdateInfo: " +
                "downloadVersionFile:" + downloadPath);

        String fileName = getFileName(msg);

        Log.d("HostActivity", " -=-=- downloadNewApp: fileName:" + fileName);

        if (fileName.endsWith(".apk")) {
            new DownloadUtils(this, downloadPath, 0, "", fileName, listener, ToastUtils::showShort);
        } else {
            Log.e("HostActivity", " -=-=- downloadNewApp: is not apk file:");
            ToastUtils.showShort("该文件不是apk格式");
        }

    }

    private String getFileName(String msg) {
        JsonObject jsonObject = new JsonParser().parse(msg).getAsJsonObject();
        String fileName = jsonObject.get("urlAddress").getAsString();
        return fileName;
    }

DownloadUtils.java负责下载apk,并安装apk

private void download() {
        if (dThread != null) return;
        dThread = new Thread(() -> {
            try {

                TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
                    public X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[0];
                    }

                    @Override
                    public void checkClientTrusted(X509Certificate[] certs, String authType) {
                    }

                    @Override
                    public void checkServerTrusted(X509Certificate[] certs, String authType) {
                    }
                }};

                SSLContext sc = SSLContext.getInstance("TLS");
                sc.init(null, trustAllCerts, new SecureRandom());

                HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
                HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true);

                if (fileSize == 0) {
                    HttpURLConnection getSizeCon = (HttpURLConnection) new URL(url).openConnection();
                    Log.d("DownloadUtils", " -=-=- download: getSizeCon response message:" + getSizeCon.getHeaderFields());
                    fileSize = getSizeCon.getContentLength();
                    getSizeCon.disconnect();
                }

                if (fileSize < 1) {
                    if (isShow) errorListener.listener("文件长度获取失败,下载失败!");
                    return;
                }

                HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
                con.setRequestProperty("Range", "bytes=" + 0 + "-" + fileSize);

                Log.d("DownloadUtils", " -=-=- run: fileSize:" + fileSize);
                Log.d("DownloadUtils", " -=-=- download: con response message:" + con.getHeaderFields());

                if (TextUtils.isEmpty(filePath)) {
                    filePath = mContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
                }

                downloadFile = new File(filePath, name);
                if (downloadFile.exists()) downloadFile.delete();
                downloadFile.createNewFile();

                Log.d("DownloadUtils", " -=-=- download: url:" + url);

                byteStream(downloadFile, fileSize, new BufferedInputStream(con.getInputStream()));

            } catch (IOException | NoSuchAlgorithmException | KeyManagementException e) {
//                    e.printStackTrace();
                Log.e("DownloadUtils", " -=-=- run: download error", e);
                if (isShow) errorListener.listener("下载失败!请使用浏览器下载");
                downloadFromChrome();
                dThread = null;
            }
        });
        dThread.start();
        timeThread = new Thread(() -> {
            float temp;
            while (total == 0 || (fileSize != total && isRun)) {
                if (downloadListener != null && isShow) {
                    temp = Utils.getProgress(fileSize, total);
                    downloadListener.listener(temp < 1 ? 0 : temp);
                }
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        timeThread.start();
    }

    private void byteStream(File file, int fileSize, BufferedInputStream bis) throws IOException {

        if (downloadListener != null && isShow) downloadListener.listener(0);

        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
        while ((len = bis.read(bytes)) != -1 && isRun) {
            total += len;

            bos.write(bytes, 0, len);

            Log.d("DownloadUtils", " -=-=- byteStream: len:" + len);
            Log.d("DownloadUtils", " -=-=- download: getProgress:" + Utils.getProgress(fileSize, total));

        }

        bos.flush();
        bos.close();
        bis.close();

        if (downloadListener != null && isShow)
            downloadListener.listener(Utils.getProgress(fileSize, total));
        dThread = null;

        Log.d("DownloadUtils", " -=-=- download: download complete" +
                "  file length:" + file.length() +
                "  file path:" + file.getAbsolutePath()
        );

        if (name.endsWith("apk")) installAPK(file.getAbsolutePath());
    }

    private void installAPK(String path) {
//        setPermission(pathstr);
        Intent intent = new Intent(Intent.ACTION_VIEW);
        // 由于没有在Activity环境下启动Activity,设置下面的标签
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        //Android 7.0以上要使用FileProvider
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            File file = new File(path);
            //参数1 上下文, 参数2 Provider主机地址 和配置文件中保持一致   参数3  共享的文件
            Uri apkUri = FileProvider.getUriForFile(mContext, "com.konka.emergradio.fileProvider", file);
//            mContext
            //添加这一句表示对目标应用临时授权该Uri所代表的文件
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
        } else {
            intent.setDataAndType(Uri.fromFile(new File(Environment.DIRECTORY_DOWNLOADS, name)), "application/vnd.android.package-archive");
        }
        mContext.startActivity(intent);
    }

    private void downloadFromChrome() {
        ToastUtils.showLong("下载失败!,请使用浏览器下载");
        if (!TextUtils.isEmpty(url) && url.contains("http")) {
            Intent intent = new Intent();
            intent.setAction("android.intent.action.VIEW");
            Uri content_url = Uri.parse(url);
            intent.setData(content_url);
            mContext.startActivity(intent);
        }
    }

数据更新实现

请求数据资源,转发给应用层做UI更新的实现类是APIManager

加密连接

init()中使用OkHttp做https的ssl信用连接认证

public void init() {

        okHttpClient = new OkHttpClient.Builder()
                .readTimeout(10, TimeUnit.SECONDS)
                .connectTimeout(10, TimeUnit.SECONDS)
                .sslSocketFactory(socketFactory, x509)
                .hostnameVerifier(createHostnameVerifier())
                .cookieJar(createCookieJar()).build();
				.	.	.
    }
	
public HostnameVerifier createHostnameVerifier() {
        return (hostname, session) -> true;
    }
	
private CookieJar createCookieJar() {
        return new CookieJar() {
            @Override
            public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
                cookieList.addAll(cookies);
                Log.d(TAG, " -=-=- saveFromResponse: url:" + url + "\ncookies:" + cookies + "\ncookieList:" + cookieList);
            }

            @Override
            public List<Cookie> loadForRequest(HttpUrl url) {
                try {
                    Log.d(TAG, " -=-=- loadForRequest: url:" + url + "  requestMap:" + requestMap + "  cookieList:" + cookieList);

                    if (url.isHttps() && !BASE_URL.contains(String.valueOf(url.port()))) {
                        Log.d("APIManager", " -=-=- saveFromResponse: isHttps:  host:" + url.host() + "   port:" + url.port());
                        setIpPort(true, url.host(), String.valueOf(url.port()));
                    }

                } catch (ConcurrentModificationException e) {
                    Log.e("APIManager", " -=-=- loadForRequest: e", e);
                }

                return cookieList;
            }
        };
    }

创建网络请求接口

在创建的OkHttpClient定义createCookieJar(),在createCookieJar()中loadForRequest()中使用retrofit创建实例、网络请求接口APIManager.APIInterface.class

public APIManager setIpPort(boolean isHttps, String ip, String port) {
        IP_PORT = ip + ":" + port;
        String prefix;

        if (isHttps) prefix = "https://";
        else prefix = "http://";

        BASE_URL = prefix + IP_PORT/* + "/" + PROJECT_ROOT_NAME + "/"*/;
        Log.d("APIManager", " -=-=- setIpPort: BaseUrl:" + BASE_URL);

        retrofit = new Retrofit.Builder().baseUrl(BASE_URL).client(okHttpClient).build();
        apiInterface = retrofit.create(APIManager.APIInterface.class);
        return this;
    }

封装业务接口

类似public Call XXX(String …str, RequestIsSuccess requestIsSuccess)格式,将具体业务接口进行封装;

// 获取播发媒体线路
    public Call<ResponseBody> getAudioSource(RequestIsSuccess requestIsSuccess) {
        if (!checkIpPort()) return null;
        return request(requestIsSuccess, SoundConsoleController, getAudioSource, 1, 0, 1000);
    }

在以上暴露业务接口中调用Call request(RequestIsSuccess requestIsSuccess, String path, String apiPath, Object… objects)使用APIManager.APIInterface构造不同请求体;

private Call<ResponseBody> request(RequestIsSuccess requestIsSuccess, String path, String apiPath, Object... objects) {
        requestMap.clear();
        int hashCode = requestIsSuccess.hashCode();
        requestInterfMap.put(hashCode, requestIsSuccess);
        switch (path + apiPath) {
            case AndroAppController + logout:
            case MediaResourceManageController + loadMediaRecord:
                requestMap = get3Param(objects);
                requestMap.put(Config.datasParam, objects[3]);
                break;
			.	.	.
        }
        return retrofitEnqueue(hashCode, path, apiPath, apiInterface.request(path, apiPath, requestMap));
    }

在Call retrofitEnqueue(int hashCode, String path, String apiPath, Call responseBodyCall)定义请求(成功/失败)回调方法,在回调方法中使用handler发送请求结果和请求数据。

private Call<ResponseBody> retrofitEnqueue(int hashCode, String path, String apiPath, Call<ResponseBody> responseBodyCall) {

        responseBodyCall.enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                try {
                    if (response.code() == 200) {
                        String body = response.body().string();
                        Log.d("APIManager", " -=-=- onResponse: api:" + apiPath + "  body:" + body);

                        if (path.equals(license) || apiPath.equals(nextEbmid) || apiPath.equals(getTextFile)
                                || apiPath.equals(buildBroadtaskLiveMonitorUrl)) {
                            handler.obtainMessage(REQUEST_SUCCESS_TRUE, hashCode, 0, body).sendToTarget();
                            return;
                        }

                        if (checkIsReLogin(body)) return;
                        if (!checkIsSuccess(body, hashCode)) return;
                        handler.obtainMessage(REQUEST_SUCCESS_TRUE, hashCode, 0, body).sendToTarget();
                    } else {
                        handler.obtainMessage(REQUEST_FAILED, hashCode, 0, "Response Code = " + response.code()).sendToTarget();
                    }
                } catch (Exception e) {
                    Log.e(TAG, " -=-=- onResponse: ", e);
                    handler.obtainMessage(REQUEST_FAILED, hashCode, 0, "APIManager Error -> " + e.getMessage()).sendToTarget();
                }finally {
                    requestMap.clear();
                }
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                Log.e(TAG, " -=-=- onFailure: ", t);
                handler.obtainMessage(REQUEST_FAILED, hashCode, 0, t.getMessage()).sendToTarget();
                requestMap.clear();
            }
        });
        return responseBodyCall;
    }

Handler回传请求结果

在handler依据服务器请求结果(成功/失败),通过requestIsSuccess.isSuccess返回不同信息。

private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            RequestIsSuccess requestIsSuccess = requestInterfMap.get(msg.arg1);
            Log.d("APIManager", " -=-=- handleMessage:  what:" + msg.what +
                    "  requestIsSuccess:" + requestIsSuccess/* + "  isResume:" + isResume*/);
            if (msg.what != REQUEST_RELOGIN && requestIsSuccess == null) return;
            switch (msg.what) {
                case REQUEST_SUCCESS_TRUE:
                    requestIsSuccess.isSuccess(true, true, msg.obj + "");
                    break;
                case REQUEST_SUCCESS_FALSE:
                    requestIsSuccess.isSuccess(true, false, msg.obj + "");
                    break;
                case REQUEST_FAILED:
                    requestIsSuccess.isSuccess(false, false, msg.obj + "");
                    break;
                case REQUEST_RELOGIN:
                    requestReLogin("登陆信息过期!请重新登陆!");
                    break;
            }
        }
    };

应用层显示

在fragment/dialog界面调用APIManager封装接口,在requestIsSuccess.isSuccess回调方法中拿到数据做UI更新。

APIManager.getInstance().updataDevice(new Gson().toJson(requestMap), (isRequestSuccess, isSuccess, msg) -> {
                if (isRequestSuccess && isSuccess) {
                    cancelOn();
                    showToast("更新成功! ");
                    callback.callback();
                } else showToast("更新失败! " + msg);
            });
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值