七月份的第一周,该向大家汇报我们星闪通讯软件的情况啦。 原创

LinMeng林孟
发布于 2024-7-6 10:49
浏览
1收藏

我们的上篇文章:点我访问
(。・∀・)ノ゙嗨,又来到了新的一篇文章啦,先来一份喜报。我们的星闪通讯软件,截止2024.7.6日,已经在GitHub上获得了40颗星星啦。也就是今天,我们的软件版本成功来到了1.3版本。
七月份的第一周,该向大家汇报我们星闪通讯软件的情况啦。-鸿蒙开发者社区

第三个版本,我们的侧重点偏向了UI方面,当然也会跟前面的文章一样,在接下来的内容里我会继续讲述我们UI的修改。

我们的第一个功能更新:新聊天页面UI

首先就是我们的聊天页面,1.2版本时,我们的页面还是仅仅停留在文字的阶段,这对开发时期的软件还好,但绝不是长久之计。如图所示我们的1.2的剪贴板版本UI页面:
七月份的第一周,该向大家汇报我们星闪通讯软件的情况啦。-鸿蒙开发者社区

先跟大家讲述一下做一份UI聊天页面的所需吧,让各位都有一份这方面的概念:

  • 一个recyclerView,这个控件是Android上的,RecyclerView可以让我们轻松高效地显示大量数据,可提高性能和应用响应能力。
  • 一个ChatAdapter类,这个类负责初始化消息列表、根据消息的发送者类型分配不同视图类型、创建+绑定数据View
  • 一个ChatMessageQueueUpdater类,这个类负责消息滚动支持在RecyclerView控件上,用消息队列的做法
  • 俩个drawable绑定到Layout文件的气泡布局设计,这个是负责分配不同视图类型时绑定气泡布局
  • Edittext文本输入控件和按钮等等…

那么,先讲讲RecyclerView背后的ChatAdapter类吧(因为控件没啥好讲的,反而这个类都是干货)在这个函数方法里,多数都是Override重构函数,也就是说只要我们调用了自己的类,就会执行自己写好的想要的操作。

然后就是分辨接收消息和发送消息了,这下面的代码,可以支持我们直接在该类里做出判断:

    // 构造函数,初始化消息列表
    public ChatAdapter(Context context, List<ChatMessage> chatMessages) {
        this.context = context;
        this.chatMessages = chatMessages;
    }

    // 根据消息的发送者类型返回不同的视图类型
    @Override
    public int getItemViewType(int position) {
        ChatMessage message = chatMessages.get(position);
        if (message.isSent()) {
            return VIEW_TYPE_MESSAGE_RECEIVED;
        } else {
            return VIEW_TYPE_MESSAGE_SENT;
        }
    }

......

    // 发送消息的ViewHolder
    private class SentMessageHolder extends RecyclerView.ViewHolder {
        TextView messageText;

        SentMessageHolder(View itemView) {
            super(itemView);
            messageText = itemView.findViewById(R.id.text_message_body);

            // 设置自定义字体
            ChatFontUtils.applyCustomFont(context, messageText, 0);
        }

        void bind(ChatMessage message) {
            messageText.setText(message.getMessage());
        }
    }

    // 接收消息的ViewHolder
    private class ReceivedMessageHolder extends RecyclerView.ViewHolder {
        TextView messageText;

        ReceivedMessageHolder(View itemView) {
            super(itemView);
            messageText = itemView.findViewById(R.id.text_message_body);

            // 设置自定义字体
            ChatFontUtils.applyCustomFont(context, messageText);
        }

        void bind(ChatMessage message) {
            messageText.setText(message.getMessage());
        }
    }

至于消息滚动,我们在上一篇文章有写相关的内容了,这里就不废话了,想要了解最终代码的话我们Github也有注释,可以前往访问查阅:点我前往GitHub

接下来让我讲讲布局的写法吧,其实本来我们的布局是气泡样式。但是在我最近研究了微信这款APP后,我决定借鉴一波微信的原版聊天气泡,如下是我微信的气泡写法:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <layer-list>
            <item android:left="8dp">
                <shape>
                    <corners android:radius="4dp" />
                    <solid android:color="@color/blue_biaozhun_logowai" />
                </shape>
            </item>
        </layer-list>
    </item>
    <item
        android:gravity="left|top"
        android:top="15dp">
        <rotate
            android:fromDegrees="45"
            android:pivotX="50%"
            android:pivotY="135%">
            <shape android:shape="rectangle">
                <size
                    android:width="8dp"
                    android:height="8dp" />
                <solid android:color="@color/blue_biaozhun_logowai" />
            </shape>
        </rotate>
    </item>
</layer-list>

效果呢?如图所示:
七月份的第一周,该向大家汇报我们星闪通讯软件的情况啦。-鸿蒙开发者社区

这中间,其实还有我们自己的小插曲,其实我们1.3版本内测时我们有在GitHub公开过我们的版本。但由于太多的BUG,和各种奇奇怪怪的小问题,于是就在后面我们把UI功能给回撤了。

现在的UI版本,我们还多了一款字体功能,既然写了新UI,用些免费商用版本的字体,提升聊天气泡的美感也是一件好事!代码如下:


    // 加载并应用自定义字体
    public static void applyCustomFont(Context context, TextView textView, int fontType) {
        String fontPath = getFontPath(fontType);
        try {
            Typeface customFont = Typeface.createFromAsset(context.getAssets(), fontPath);
            textView.setTypeface(customFont);
            Log.i(TAG, "Font asset found: " + fontPath);
        } catch (RuntimeException e) {
            Log.e(TAG, "Font asset not found: " + fontPath, e);
        }
    }

    public static void applyCustomFont(Context context, TextView textView) {
        int fontType = new Random().nextInt(3) + 1; // 生成1到3之间的随机数
        String fontPath = getFontPath(fontType);
        try {
            Typeface customFont = Typeface.createFromAsset(context.getAssets(), fontPath);
            textView.setTypeface(customFont);
            Log.i(TAG, "Font asset found: " + fontPath);
        } catch (RuntimeException e) {
            Log.e(TAG, "Font asset not found: " + fontPath, e);
        }
    }

    // 根据传入的整数值返回对应的字体路径
    private static String getFontPath(int fontType) {
        switch (fontType) {
            case 1:
                return "fonts/dingtalk_jinbuti.ttf";
            case 2:
                return "fonts/alimama_dongfangdakai_regular.ttf";
            case 3:
                return "fonts/smileysans_oblique.ttf";
            default:
                return "fonts/source_han_sans_sc_regular.otf"; // 默认字体
        }
    }

我们的第二个功能更新,也算是我们之前功能的进阶版了:剪贴板不再仅验证码了。

现在我们把剪贴板的功能做到了如今的ChatProcessorForExtract类里,代码片段如下:

// 提取四位和六位数字,但排除年份相关的四位数字
            Pattern pattern = Pattern.compile("\\b(?!19\\d{2}|20\\d{2})\\d{4}\\b|\\b\\d{6}\\b");
            Matcher matcher = pattern.matcher(string);
            while (matcher.find()) {
                Log.v(TAG, "找到疑似验证码,提取中");
                String foundNumber = matcher.group();
                extractedNumbers.append(foundNumber).append("\n");
            }

            // 提取链接
            Pattern urlPattern = Pattern.compile(
                    "(https?://(?:www\\.|(?!www))[^\\s\\.]+\\.[^\\s]{2,}|www\\.[^\\s]+\\.[^\\s]{2,})",
                    Pattern.CASE_INSENSITIVE);
            Matcher urlMatcher = urlPattern.matcher(string);
            while (urlMatcher.find()) {
                Log.v(TAG, "找到互联网链接,提取中");
                String foundUrl = urlMatcher.group();
                extractedUrls.append(foundUrl).append("\n");
            }

            // 提取电子邮件地址
            Pattern emailPattern = Pattern.compile(
                    "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}",
                    Pattern.CASE_INSENSITIVE);
            Matcher emailMatcher = emailPattern.matcher(string);
            while (emailMatcher.find()) {
                String foundEmail = emailMatcher.group();
                extractedEmails.append(foundEmail).append("\n");
            }

            // 提取符合条件的中国大陆电话号码(11位数字,第一位是1,第二位是3、5、7、8、9)
            Pattern phonePattern = Pattern.compile("\\b1[35789]\\d{9}\\b");
            Matcher phoneMatcher = phonePattern.matcher(string);
            while (phoneMatcher.find()) {
                String foundPhone = phoneMatcher.group();
                extractedPhones.append(foundPhone).append("\n");
            }
            // 将提取到的数字和链接分别复制到剪贴板
            ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
            if (extractedNumbers.length() > 0) {
                ClipData clip = ClipData.newPlainText("extractedNumbers", extractedNumbers.toString().trim());
                clipboard.setPrimaryClip(clip);
                SnackBarToastForDebug(context,"提取到疑似验证码,已复制到剪贴板!", "推荐去粘贴", 0, Snackbar.LENGTH_INDEFINITE);

                // 取消之前的清空任务并重新设置定时任务
                HhandlerClipBoard.removeCallbacksAndMessages(null);
                HhandlerClipBoard.postDelayed(new ClipboardRunnable(context), ClipboardRunnable.DELAY_NORMAL); // 验证码可以60秒以上,到达时间后清空剪贴板
            }

            // 将提取到的链接复制到剪贴板
            if (extractedUrls.length() > 0) {
                ClipData clipUrls = ClipData.newPlainText("extractedUrls", extractedUrls.toString().trim());
                clipboard.setPrimaryClip(clipUrls);
                SnackBarToastForDebug(context,"提取到链接,已复制到剪贴板!", "推荐去浏览器", 0, Snackbar.LENGTH_LONG);

                // 取消之前的清空任务并重新设置定时任务
                HhandlerClipBoard.removeCallbacksAndMessages(null);
                HhandlerClipBoard.postDelayed(new ClipboardRunnable(context), ClipboardRunnable.DELAY_LONG); // 链接可以90秒后清空剪贴板
            }

            // 将提取到的电子邮件地址复制到剪贴板
            if (extractedEmails.length() > 0) {
                ClipData clipEmails = ClipData.newPlainText("extractedEmails", extractedEmails.toString().trim());
                clipboard.setPrimaryClip(clipEmails);
                SnackBarToastForDebug(context,"提取到电子邮件地址,已复制到剪贴板!", "推荐去发邮件", 0, Snackbar.LENGTH_LONG);

                // 取消之前的清空任务并重新设置定时任务
                HhandlerClipBoard.removeCallbacksAndMessages(null);
                HhandlerClipBoard.postDelayed(new ClipboardRunnable(context), ClipboardRunnable.DELAY_SHORT); // 电子邮件可以30秒、60秒后清空剪贴板
            }

            // 将提取到的电话号码复制到剪贴板并调用电话应用程序
            if (extractedPhones.length() > 0) {
                String phoneNumber = extractedPhones.toString().trim();
                ClipData clipPhones = ClipData.newPlainText("extractedPhones", phoneNumber);
                clipboard.setPrimaryClip(clipPhones);

                // 取消之前的清空任务并重新设置定时任务
                HhandlerClipBoard.removeCallbacksAndMessages(null);
                HhandlerClipBoard.postDelayed(new ClipboardRunnable(context), ClipboardRunnable.DELAY_VERY_SHORT); // 电话号码有点特殊,15秒后清空剪贴板,或者注释掉不用清空

                // 调用电话应用程序拨打号码
                Intent intent = new Intent(Intent.ACTION_DIAL);
                intent.setData(Uri.parse("tel:" + phoneNumber));
                context.startActivity(intent);

                // 在主线程上显示Toast提示
                ((Activity) context).runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(context, "已添加电话到安卓软件上,号码为:" + phoneNumber, Toast.LENGTH_LONG).show();
                    }
                });
            }

现在剪贴板功能,支持了诸如验证码、网页链接、电子邮件、电话号码(仅国内电话号码)。这些功能绝大多数情况下可以满足大家的使用了,而且我们还对如上的信息,做隐私保护,设定好了定时器,达到规定时间后删除提取复制的剪贴板内容。代码如下:

// 取消之前的清空任务并重新设置定时任务
                HhandlerClipBoard.removeCallbacksAndMessages(null);
                HhandlerClipBoard.postDelayed(new ClipboardRunnable(context), ClipboardRunnable.DELAY_VERY_SHORT); // 电话号码有点特殊,15秒后清空剪贴板,或者注释掉不用清空

private static class ClipboardRunnable implements Runnable {
        // 定义静态全局变量来管理延迟时间
        public static final long DELAY_VERY_SHORT = 15000; // 15秒
        public static final long DELAY_SHORT = 30000; // 30秒
        public static final long DELAY_NORMAL = 60000; // 60秒
        public static final long DELAY_LONG = 90000;  // 90秒
        public static final long DELAY_VERY_LONG = 180000;  // 180秒
        public static final long DELAY_VERY_VERY_LONG = 600000; // 600秒,根据自己想要的时间来做决定,这里保留复现接口
        public static final long DELAY_SAFETY_TIMER = 1800000; // 1800秒,1/2小时,剪贴板功能为安全起见,请务必对剪贴板信息保存勿超过30分钟

        private Context context;

        //构造方法,接收Context对象,使用应用上下文避免遇到内存泄露问题
        ClipboardRunnable(Context context) {
            this.context = context.getApplicationContext(); // 使用应用上下文避免内存泄漏
        }

        @Override
        public void run() {
            //获取剪贴板杠两千,创建一个空的剪贴板对象,将空的剪贴板设置为有内容的剪贴板对象里去,记录日志。(清空过程)
            ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
            ClipData emptyClip = ClipData.newPlainText("", "");
            clipboard.setPrimaryClip(emptyClip);
            Log.v(TAG, "剪贴板已清空");
        }
    }

来到我们第三个功能了,针对开发者的:星闪串口日志全部获取,串口对方MAC地址打印在UI上。

为我们后续的串口Log功能做铺垫,所以这个功能我目前仅仅做了一个提取MAC地址。星闪硬件侧端仓库代码,对星闪MAC地址分配的有些问题,我们提取并不好做,代码如下:

            // 提取符合条件的星闪设备Mac地址(Log)
            //开头是两位十六进制,接下来是三组 :**,然后是两位十六进制,最后是两到五位十六进制
            //Pattern macPattern = Pattern.compile("addr:([0-9a-fA-F]{2}(:\\*{2}){3}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2,5})");
            //前四组是俩位十六进制或星号,第五组是俩位十六进制,最后一组是俩到伍位十六进制
            Pattern macPattern = Pattern.compile("addr:((?:[0-9a-fA-F]{2}|\\*{2})(?::(?:[0-9a-fA-F]{2}|\\*{2})){3}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2,5})");
            Matcher macMatcher = macPattern.matcher(string);
            while (macMatcher.find()) {
                String foundMac = macMatcher.group();
                extractedLogForNearLinkAddr.append(foundMac).append("\n");
            }

相关开发者的串口Log功能,我们后续还会继续做补充和针对优化,敬请期待这项功能,能让大家和开发者们满意。

最后的大功能,也就是现在很多功能设置,可以在UI上自由决定了。

目前我们的功能不多,仅仅开放设置如下功能:

  • 聊天保存(SQLite)
  • 历史设备记录(SQLite)
  • 聊天文本进入剪贴板

功能虽少,但给未来的代码的基础,我们省不了一点,今天写的该复杂就复杂,代码如下:

//星闪网络相关设置初始化,目前多数还不允许UI设置,敬请期待
        CompoundButton.OnCheckedChangeListener SettingsChangeListener = new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
                if (compoundButton.isEnabled()) {
                    if (compoundButton.getId() == id.cbSettingsForShowLog) {
                        SnackBarToastForDebug(context,"敬请期待!","如有不适,那没办法,做的慢怪我咯o(*^@^*)o",0,Snackbar.LENGTH_SHORT);
                    } else if (compoundButton.getId() == id.cbSettingsForSaveSQL) {
                        if (isChecked) {
                                ChatUtils.setSqlitemanager(true);
                            SnackBarToastForDebug(context,"您已开始保存您的聊天记录啦!","目前为" + ChatUtils.isSqlitemanager(),0,Snackbar.LENGTH_SHORT);
                        } else {
                            if (ChatUIAlertDialog.show(compoundButton.getContext(), "聊天保存(SQLite)", "您确定要停止保存聊天数据吗?停止保存您的聊天,将会在接下来聊天时无法保存内容,可能会造成聊天记录丢失。", compoundButton))
                                ChatUtils.setSqlitemanager(false);
                            SnackBarToastForDebug(context,"已为您取消保存聊天记录!","目前为" + ChatUtils.isSqlitemanager(),0,Snackbar.LENGTH_SHORT);
                        }
                    } else if (compoundButton.getId() == id.cbSettingsForDelSQL) {
                        SnackBarToastForDebug(context,"敬请期待!","如有不适,那没办法,做的慢怪我咯o(*^@^*)o",0,Snackbar.LENGTH_SHORT);
                    } else if (compoundButton.getId() == id.cbSettingsForHistory) {
                        if (isChecked) {
                            ChatUtils.setSqliteHistory(true);
                            SnackBarToastForDebug(context,"您已开始展示您的聊天记录啦!","目前为" + ChatUtils.isSqliteHistory(),0,Snackbar.LENGTH_SHORT);
                        } else {
                            if (ChatUIAlertDialog.show(compoundButton.getContext(), "历史设备记录(SQLite)", "您确定要停止展示聊天数据在UI上吗?", compoundButton))
                                ChatUtils.setSqliteHistory(false);
                            SnackBarToastForDebug(context,"已为您取消保存聊天记录!","目前为" + ChatUtils.isSqliteHistory(),0,Snackbar.LENGTH_SHORT);
                        }
                    } else if (compoundButton.getId() == id.cbSettingsForClearSCR) {
                        SnackBarToastForDebug(context,"敬请期待!","如有不适,那没办法,做的慢怪我咯o(*^@^*)o",0,Snackbar.LENGTH_SHORT);
                    } else if (compoundButton.getId() == id.cbSettingsForEncryption) {
                        SnackBarToastForDebug(context,"敬请期待!","如有不适,那没办法,做的慢怪我咯o(*^@^*)o",0,Snackbar.LENGTH_SHORT);
                    } else if (compoundButton.getId() == id.cbSettingsForClip) {
                        if (isChecked) {
                            ChatUtils.setClipMessages(true);
                            SnackBarToastForDebug(context,"您已开启剪贴板功能!","目前为" + ChatUtils.isClipMessages(),0,Snackbar.LENGTH_SHORT);
                        } else {
                            if (ChatUIAlertDialog.show(compoundButton.getContext(), "聊天文本进入剪贴板", "您确定要停止剪贴板吗?剪贴板功能可以帮您自动按规则捕获内容,可以很大程度上帮助到您手动任务耗时的情况,取消则需要您自行处理屏幕上的UI信息。", compoundButton))
                                ChatUtils.setClipMessages(false);
                            SnackBarToastForDebug(context,"已为您取消剪贴板功能!","目前为" + ChatUtils.isClipMessages(),0,Snackbar.LENGTH_SHORT);
                        }
                    } else if (compoundButton.getId() == id.cbSettingsForPush) {
                        SnackBarToastForDebug(context,"敬请期待!","如有不适,那没办法,做的慢怪我咯o(*^@^*)o",0,Snackbar.LENGTH_SHORT);
                    } else if (compoundButton.getId() == id.cbSettingsForBackground) {
                        SnackBarToastForDebug(context,"敬请期待!","如有不适,那没办法,做的慢怪我咯o(*^@^*)o",0,Snackbar.LENGTH_SHORT);
                    } else if (compoundButton.getId() == id.cbSettingsForBackup) {
                        SnackBarToastForDebug(context,"敬请期待!","如有不适,那没办法,做的慢怪我咯o(*^@^*)o",0,Snackbar.LENGTH_SHORT);
                    } else if (compoundButton.getId() == id.cbSettingsForNFC) {
                        SnackBarToastForDebug(context,"敬请期待!","如有不适,那没办法,做的慢怪我咯o(*^@^*)o",0,Snackbar.LENGTH_SHORT);
                    }
                }
            }
        };
        SettingsForShowLog = findViewById(id.cbSettingsForShowLog);
        SettingsForShowLog.setEnabled(false);
        SettingsForShowLog.setOnCheckedChangeListener(SettingsChangeListener);
        SettingsForSaveSQL = findViewById(id.cbSettingsForSaveSQL);
        SettingsForSaveSQL.setEnabled(true);
        SettingsForSaveSQL.setChecked(true);
        SettingsForSaveSQL.setOnCheckedChangeListener(SettingsChangeListener);
        SettingsForDelSQL = findViewById(id.cbSettingsForDelSQL);
        SettingsForDelSQL.setEnabled(false);
        SettingsForDelSQL.setOnCheckedChangeListener(SettingsChangeListener);
        SettingsForHistory = findViewById(id.cbSettingsForHistory);
        SettingsForHistory.setEnabled(true);
        SettingsForHistory.setChecked(false);
        SettingsForHistory.setOnCheckedChangeListener(SettingsChangeListener);
        SettingsForClearSCR = findViewById(id.cbSettingsForClearSCR);
        SettingsForClearSCR.setEnabled(false);
        SettingsForClearSCR.setOnCheckedChangeListener(SettingsChangeListener);
        SettingsForEncryption = findViewById(id.cbSettingsForEncryption);
        SettingsForEncryption.setEnabled(false);
        SettingsForEncryption.setOnCheckedChangeListener(SettingsChangeListener);
        SettingsForClip = findViewById(id.cbSettingsForClip);
        SettingsForClip.setEnabled(true);
        SettingsForClip.setChecked(true);
        SettingsForClip.setOnCheckedChangeListener(SettingsChangeListener);
        SettingsForPush = findViewById(id.cbSettingsForPush);
        SettingsForPush.setEnabled(false);
        SettingsForPush.setOnCheckedChangeListener(SettingsChangeListener);
        SettingsForBackground = findViewById(id.cbSettingsForBackground);
        SettingsForBackground.setEnabled(false);
        SettingsForBackground.setOnCheckedChangeListener(SettingsChangeListener);
        SettingsForBackup = findViewById(id.cbSettingsForBackup);
        SettingsForBackup.setEnabled(false);
        SettingsForBackup.setOnCheckedChangeListener(SettingsChangeListener);
        SettingsForNFC = findViewById(id.cbSettingsForNFC);
        SettingsForNFC.setEnabled(false);
        SettingsForNFC.setOnCheckedChangeListener(SettingsChangeListener);

首先,我直接在Init()初始化里,完成好每个Checkbox们的勾选监听方法,这样只需要每个Checkbox调用该方法就可以了,给那些无法设置和没做出来的先写上setEnabled(false),能用的上的写好setEnabled(true)再完善方法里的判断和AlertDialog对话框提示。效果如下:

七月份的第一周,该向大家汇报我们星闪通讯软件的情况啦。-鸿蒙开发者社区

这个对话框我们还设置好了防止外部点击消失的做法,需要确认取消好了才能取消掉哦。

剩下的小功能和其余更新,诸如:

  1. 给星闪LOGO做个“闪烁效果”在右上角按钮
  2. 调整发送机制目前做到单行输入+输入法或键盘Enter即发送操作
  3. 完善串口日志全部获取
  4. 修复星闪网络随机无法接收聊天信息的Bug
  5. 修复消息滚动随机的Bug
  6. 修复剪贴板问题
  7. 优化代码结构,利于维护

其余的更新我就不多介绍了~

距离我们先前俩篇文章的发布,已经过去10天多了。我们过去向大家承诺的功能,都已经做出来了,接下来的功能,做得出来测试完了,我们会继续写文章给大家讲解,敬请期待下一篇文章和接下来的1.4更新。

本项目还是那样的承诺给大家,我们参加了海思社区的星闪开发者体验官活动,我们只要入选该活动,对软件的开发我们将继续保持下去。希望51CTO的大家能多多支持一些我们,您们的支持就是我坚持开发最大的动力。

Github仓库链接
Gitee仓库链接
硬件侧端链接

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
NLChat_v1.3.80.2024.0706.apk 39.61M 12次下载
已于2024-7-6 10:49:10修改
2
收藏 1
回复
举报
1条回复
按时间正序
/
按时间倒序
Soon_L
Soon_L

可以跟鸿蒙手机如meta 60系类通讯吗?

回复
2024-8-5 14:20:09
回复
    相关推荐