git commit -m "first commit"

This commit is contained in:
gallonyin@163.com
2022-05-25 23:23:33 +08:00
commit 565dae2b6c
66 changed files with 5525 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.yameida.worktool">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER" />
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
<application
android:name="org.yameida.worktool.MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<activity
android:name="org.yameida.worktool.activity.ListenActivity"
android:label="@string/app_name"
android:screenOrientation="portrait"
android:theme="@style/AppTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name="org.yameida.worktool.service.WeworkService"
android:enabled="true"
android:exported="true"
android:label="@string/app_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
</application>
</manifest>

View File

@@ -0,0 +1,16 @@
package org.yameida.worktool
import com.blankj.utilcode.util.SPUtils
import org.yameida.worktool.config.WebConfig
object Constant {
const val PACKAGE_NAMES = "com.tencent.wework"
val BASE_URL = WebConfig.HOST.replace("wss", "https").replace("ws", "http")
val URL_CHECK_UPDATE = "$BASE_URL/appUpdate/checkUpdate"
var key = "9876543210abcdef".toByteArray()
var iv = "0123456789abcdef".toByteArray()
val transformation = "AES/CBC/PKCS7Padding"
var encryptType = SPUtils.getInstance().getInt("encryptType", 0)
}

View File

@@ -0,0 +1,121 @@
package org.yameida.worktool
import org.yameida.worktool.model.WeworkMessageBean
import org.yameida.worktool.service.MyLooper
import org.yameida.worktool.service.WeworkController
import org.yameida.worktool.service.WeworkLoopImpl
import org.yameida.worktool.service.getRoot
import org.yameida.worktool.utils.AccessibilityUtil
/**
* 示例
*/
object Demo {
fun test(flag: Boolean) {
if (!flag) return
MyLooper.getInstance().removeCallbacksAndMessages(null)
//打印当前视图树
// AccessibilityUtil.printNodeClazzTree(getRoot())
//自动通过好友
// WeworkLoopImpl.getFriendRequest()
//自动通过好友(后台可配置开关)
// WeworkLoopImpl.mainLoop()
//创建群信息
// WeworkController.initGroup(WeworkMessageBean().apply {
// groupName = "新建外部群 " + UUID.randomUUID().toString().substring(0, 5)
// selectList = arrayListOf("冯燕", "尹甲仑")
// groupAnnouncement = "本群为雨花台区法院诉前调解官方微信群"
// })
//修改群信息
// WeworkController.intoGroupAndConfig(WeworkMessageBean().apply {
// groupName = "新建外部群 " + UUID.randomUUID().toString().substring(0, 5)
// selectList = arrayListOf("冯燕", "尹甲仑")
// groupAnnouncement = "本群为雨花台区法院诉前调解官方微信群"
// })
//获取群信息
// WeworkController.getGroupInfo(WeworkMessageBean().apply {
// selectList = arrayListOf("企微RPA机器人自测1")
// })
//在房间内发送消息
// WeworkController.sendMessage(WeworkMessageBean().apply {
// titleList = arrayListOf("下级群1", "上级群1")
// receivedContent = "aaa"
// })
//获取我的信息
// WeworkController.getMyInfo()
//推送任意小程序
// WeworkController.pushMicroprogram(WeworkMessageBean().apply {
// titleList = arrayListOf("尹甲仑")
// objectName = "小法名律"
// extraText = "123"
// })
//推送微盘图片
// WeworkController.pushMicroDiskImage(WeworkMessageBean().apply {
// titleList = arrayListOf("尹甲仑")
// objectName = "雨水.jpg"
// })
//推送微盘文件
// WeworkController.pushMicroDiskFile(WeworkMessageBean().apply {
// titleList = arrayListOf("尹甲仑")
// objectName = "雨水.jpg"
// })
//推送腾讯文档
// WeworkController.pushOffice(WeworkMessageBean().apply {
// titleList = arrayListOf("尹甲仑")
// objectName = "机器人中台"
// extraText = "附加留言"
// })
}
//通过好友请求后执行三中院脚本
fun test2(name: String) {
val groupName = "(北)诉前调解群05011"
val json = """
{
"socketType": 2,
"messageId": "",
"list": [
{
"type": 203,
"titleList": [
"$name"
],
"receivedContent": "您好,您好我是某某法院机器人助理,很高兴为您服务,请先填写个人信息,我们将为您联系案件相关法官。\nhttps://www.wjx.cn/vj/OjVAA02.aspx"
},
{
"type": 203,
"titleList": [
"$name"
],
"receivedContent": "您好,已为您查询到本案法官,开始建群中,请稍后..."
},
{
"type": 206,
"groupName": "$groupName",
"selectList": [
"$name",
"尹甲仑"
],
"groupAnnouncement": "本群为诉前调解官方微信群"
}
]
}
""".trimIndent()
MyLooper.onMessage(null, json)
}
}

View File

@@ -0,0 +1,28 @@
package org.yameida.worktool
import android.app.Application
import com.blankj.utilcode.util.SPUtils
import com.blankj.utilcode.util.Utils
import com.umeng.commonsdk.UMConfigure
import org.yameida.worktool.config.GlobalException
import update.UpdateAppUtils
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
//初始化工具类
Utils.init(this)
//初始化友盟统计
UMConfigure.preInit(this, "6284a3a3d024421570f97c3c", "main_channel")
//判断是否同意隐私协议uminit为1时为已经同意直接初始化umsdk
if (SPUtils.getInstance().getString("uminit", "1") == "1") {
UMConfigure.init(this, "6284a3a3d024421570f97c3c", "main_channel", UMConfigure.DEVICE_TYPE_PHONE, "")
}
//初始化自动更新
UpdateAppUtils.init(this)
//设置全局异常捕获重启
Thread.setDefaultUncaughtExceptionHandler(GlobalException.getInstance())
}
}

View File

@@ -0,0 +1,166 @@
package org.yameida.worktool.activity
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.provider.Settings
import android.text.TextUtils.SimpleStringSplitter
import android.widget.CompoundButton
import android.widget.Switch
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.blankj.utilcode.util.*
import com.umeng.analytics.MobclickAgent
import kotlinx.android.synthetic.main.activity_listen.*
import org.yameida.worktool.*
import org.yameida.worktool.service.WeworkService
import org.yameida.worktool.config.WebConfig
import org.yameida.worktool.utils.UpdateUtil
class ListenActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
title = "WorkTool"
setContentView(R.layout.activity_listen)
initView()
initAccessibility()
UpdateUtil.checkUpdate()
}
override fun onStart() {
super.onStart()
freshOpenServiceSwitch(
WeworkService::class.java,
sw_accessibility
)
}
private fun initView() {
et_channel.setText(SPUtils.getInstance().getString(WebConfig.LISTEN_CHANNEL_ID))
bt_save.setOnClickListener {
val channel = et_channel.text.toString().trim()
SPUtils.getInstance().put(WebConfig.LISTEN_CHANNEL_ID, channel)
ToastUtils.showLong("保存成功")
sendBroadcast(Intent(WebConfig.WEWORK_NOTIFY).apply {
putExtra("type", "modify_channel")
})
MobclickAgent.onProfileSignIn(channel)
}
Constant.encryptType = SPUtils.getInstance().getInt("encryptType", 0)
sw_encrypt.isChecked = Constant.encryptType == 1
sw_encrypt.setOnCheckedChangeListener(CompoundButton.OnCheckedChangeListener { buttonView, isChecked ->
LogUtils.i("sw_encrypt onCheckedChanged: $isChecked")
Constant.encryptType = if (isChecked) 1 else 0
SPUtils.getInstance().put("encryptType", Constant.encryptType)
})
tv_host.text = WebConfig.HOST
tv_version.text = AppUtils.getAppVersionName()
}
private fun initAccessibility() {
sw_accessibility.setOnCheckedChangeListener(CompoundButton.OnCheckedChangeListener { buttonView, isChecked ->
LogUtils.i("sw_accessibility onCheckedChanged: $isChecked")
if (isChecked) {
if (SPUtils.getInstance().getString(WebConfig.LISTEN_CHANNEL_ID).isNullOrBlank()) {
sw_accessibility.isChecked = false
ToastUtils.showLong("请先填写并保存链接号~")
} else if (!isAccessibilitySettingOn()) {
openAccessibility()
}
} else {
if (isAccessibilitySettingOn()) {
sw_accessibility.isChecked = true
}
}
})
}
private fun isAccessibilitySettingOn(): Boolean {
val context = Utils.getApp()
var enable = 0
val serviceName = context.packageName + "/" + WeworkService::class.java.canonicalName
LogUtils.i("isAccessibilitySettingOn: $serviceName")
try {
enable = Settings.Secure.getInt(
context.contentResolver,
Settings.Secure.ACCESSIBILITY_ENABLED,
0
)
} catch (e: Exception) {
e.printStackTrace()
}
if (enable == 1) {
val stringSplitter = SimpleStringSplitter(':')
val settingVal = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES
)
if (settingVal != null) {
stringSplitter.setString(settingVal)
while (stringSplitter.hasNext()) {
val accessibilityService = stringSplitter.next()
if (accessibilityService == serviceName) {
LogUtils.i("isAccessibilitySettingOn: true")
return true
}
}
}
}
LogUtils.i("isAccessibilitySettingOn: false")
return false
}
/**
* 打开辅助
*/
private fun openAccessibility() {
val clickListener =
DialogInterface.OnClickListener { dialog, which ->
freshOpenServiceSwitch(
WeworkService::class.java,
sw_accessibility
)
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
startActivity(intent)
}
val cancel = DialogInterface.OnCancelListener {
freshOpenServiceSwitch(
WeworkService::class.java,
sw_accessibility
)
}
val cancelListener = DialogInterface.OnClickListener { dialog, which ->
freshOpenServiceSwitch(
WeworkService::class.java,
sw_accessibility
)
}
val dialog: AlertDialog = AlertDialog.Builder(this)
.setMessage(R.string.tips)
.setOnCancelListener(cancel)
.setNegativeButton("取消", cancelListener)
.setPositiveButton("确定", clickListener)
.create()
dialog.show()
}
private fun freshOpenServiceSwitch(clazz: Class<*>, s: Switch) {
if (isAccessibilitySettingOn()) {
s.isChecked = true
when (s.id) {
R.id.sw_accessibility -> {
}
}
} else {
s.isChecked = false
when (s.id) {
R.id.sw_accessibility -> {
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
package org.yameida.worktool.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标识方法为请求接口
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface RequestMapping {
}

View File

@@ -0,0 +1,28 @@
package org.yameida.worktool.config;
import android.util.Log;
import com.blankj.utilcode.util.AppUtils;
public class GlobalException implements Thread.UncaughtExceptionHandler {
private final static GlobalException myCrashHandler = new GlobalException();
private GlobalException() {
}
public static synchronized GlobalException getInstance() {
return myCrashHandler;
}
@Override
public void uncaughtException(Thread arg0, Throwable arg1) {
Log.e("GlobalException", "-------------Caught Exception-------------");
arg1.printStackTrace();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
AppUtils.relaunchApp(true);
}
}

View File

@@ -0,0 +1,8 @@
package org.yameida.worktool.config;
public class WebConfig {
public static final String WEWORK_NOTIFY = "wework_notify";
public static final String HOST = "wss://worktool.asrtts.cn";
public static final String WEWORK_URL = HOST + "/webserver/wework/";
public static String LISTEN_CHANNEL_ID = "LISTEN_CHANNEL_ID";
}

View File

@@ -0,0 +1,162 @@
package org.yameida.worktool.model;
import java.io.Serializable;
import java.util.Date;
public class AppUpdate implements Serializable {
private Long id;
// @ApiModelProperty(value = "应用名称")
private String appName;
// @ApiModelProperty(value = "更新标题")
private String title;
// @ApiModelProperty(value = "更新日志")
private String updateLog;
// @ApiModelProperty(value = "备注")
private String remark;
// @ApiModelProperty(value = "更新版本号")
private String versionName;
// @ApiModelProperty(value = "内部版本号")
private Integer versionCode;
// @ApiModelProperty(value = "强制更新内部版本号")
private Integer minVersionCode;
// @ApiModelProperty(value = "apk链接")
private String downloadUrl;
// @ApiModelProperty(value = "创建时间")
private Date createTime;
// @ApiModelProperty(value = "安装包大小")
private String size;
// @ApiModelProperty(value = "可用")
private Boolean enable;
private static final long serialVersionUID = 1L;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getAppName() {
return appName;
}
public void setAppName(String appName) {
this.appName = appName;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getUpdateLog() {
return updateLog;
}
public void setUpdateLog(String updateLog) {
this.updateLog = updateLog;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public String getVersionName() {
return versionName;
}
public void setVersionName(String versionName) {
this.versionName = versionName;
}
public Integer getVersionCode() {
return versionCode;
}
public void setVersionCode(Integer versionCode) {
this.versionCode = versionCode;
}
public Integer getMinVersionCode() {
return minVersionCode;
}
public void setMinVersionCode(Integer minVersionCode) {
this.minVersionCode = minVersionCode;
}
public String getDownloadUrl() {
return downloadUrl;
}
public void setDownloadUrl(String downloadUrl) {
this.downloadUrl = downloadUrl;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public String getSize() {
return size;
}
public void setSize(String size) {
this.size = size;
}
public Boolean getEnable() {
return enable;
}
public void setEnable(Boolean enable) {
this.enable = enable;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", id=").append(id);
sb.append(", appName=").append(appName);
sb.append(", title=").append(title);
sb.append(", updateLog=").append(updateLog);
sb.append(", remark=").append(remark);
sb.append(", versionName=").append(versionName);
sb.append(", versionCode=").append(versionCode);
sb.append(", minVersionCode=").append(minVersionCode);
sb.append(", downloadUrl=").append(downloadUrl);
sb.append(", createTime=").append(createTime);
sb.append(", size=").append(size);
sb.append(", enable=").append(enable);
sb.append(", serialVersionUID=").append(serialVersionUID);
sb.append("]");
return sb.toString();
}
}

View File

@@ -0,0 +1,267 @@
package org.yameida.worktool.model;
import java.util.List;
import java.util.Objects;
public class WeworkMessageBean {
/**
* type
* <p>
*消息类型10
* 心跳 HEART_BEAT
* <p>
* 消息类型 100
* 上传消息列表 TYPE_RECEIVE_MESSAGE_LIST
* <p>
* 全局操作类型 200
* 停止所有任务并返回首页待命 STOP_AND_GO_HOME
* 回到首页等待接收新消息 LOOP_RECEIVE_NEW_MESSAGE
* 在房间内发送消息 SEND_MESSAGE
* 在房间内指定回复消息 REPLY_MESSAGE
* 在房间内转发消息 RELAY_MESSAGE
* 创建群 CREATE_GROUP
* 进入群聊并修改群配置 INTO_GROUP_AND_CONFIG
* 推送微盘图片 PUSH_MICRO_DISK_IMAGE
* 推送微盘文件 PUSH_MICRO_DISK_FILE
* 推送任意小程序 PUSH_MICROPROGRAM
* 推送腾讯文档 PUSH_OFFICE
* 通过当前所有好友请求 PASS_ALL_FRIEND_REQUEST
* 按手机号添加好友 ADD_FRIEND_BY_PHONE
* 展示群信息 SHOW_GROUP_INFO
* <p>
* 非操作类型 300
* 机器人普通日志记录 ROBOT_LOG
* 机器人异常日志记录 ROBOT_ERROR_LOG
* 机器人接口测试 ROBOT_CONTROLLER_TEST
* <p>
* 获取数据类型 500
* 获取群信息 GET_GROUP_INFO
* 获取好友信息 GET_FRIEND_INFO
* 获取我的信息 GET_MY_INFO
*/
public static final int HEART_BEAT = 11;
public static final int TYPE_RECEIVE_MESSAGE_LIST = 101;
public static final int STOP_AND_GO_HOME = 201;
public static final int LOOP_RECEIVE_NEW_MESSAGE = 202;
public static final int SEND_MESSAGE = 203;
public static final int REPLY_MESSAGE = 204;
public static final int RELAY_MESSAGE = 205;
public static final int INIT_GROUP = 206;
public static final int INTO_GROUP_AND_CONFIG = 207;
public static final int PUSH_MICRO_DISK_IMAGE = 208;
public static final int PUSH_MICRO_DISK_FILE = 209;
public static final int PUSH_MICROPROGRAM = 210;
public static final int PUSH_OFFICE = 211;
public static final int PASS_ALL_FRIEND_REQUEST = 212;
public static final int ADD_FRIEND_BY_PHONE = 213;
public static final int SHOW_GROUP_INFO = 214;
public static final int ROBOT_LOG = 301;
public static final int ROBOT_ERROR_LOG = 302;
public static final int ROBOT_CONTROLLER_TEST = 303;
public static final int GET_GROUP_INFO = 501;
public static final int GET_FRIEND_INFO = 502;
public static final int GET_MY_INFO = 503;
/**
* roomType
* <p>
* 外部群 ROOM_TYPE_EXTERNAL_GROUP
* 外部联系人 ROOM_TYPE_EXTERNAL_CONTACT
* 内部群 ROOM_TYPE_INTERNAL_GROUP
* 内部联系人 ROOM_TYPE_INTERNAL_CONTACT
*/
public static final int ROOM_TYPE = 0;
public static final int ROOM_TYPE_UNKNOWN = 0;
public static final int ROOM_TYPE_EXTERNAL_GROUP = 1;
public static final int ROOM_TYPE_EXTERNAL_CONTACT = 2;
public static final int ROOM_TYPE_INTERNAL_GROUP = 3;
public static final int ROOM_TYPE_INTERNAL_CONTACT = 4;
/**
* textType
* <p>
* 文本类型 TEXT_TYPE_PLAIN
* 表情类型 同文本类型
* 群公告类型 同文本类型
* 图片类型 TEXT_TYPE_IMAGE
* 语音类型 TEXT_TYPE_VOICE
* 名片类型 TEXT_TYPE_CARD
* 视频类型 TEXT_TYPE_VIDEO
* 定位类型 TEXT_TYPE_LOCATION
* 小程序类型 TEXT_TYPE_MICROPROGRAM
* 链接类型 TEXT_TYPE_LINK
* 群通知类型 同链接类型
* 文件类型 TEXT_TYPE_FILE
* 警告类型 TEXT_TYPE_WARNING
* 腾讯文档类型 TEXT_TYPE_OFFICE
* 群接龙类型 TEXT_TYPE_SOLITAIRE
* 合并聊天记录类型 TEXT_TYPE_CHAT_RECORD
* 群收集表类型 TEXT_TYPE_COLLECTION
* 接收带回复引用文本类型 TEXT_TYPE_REPLY
*/
public static final int TEXT_TYPE = 0;
public static final int TEXT_TYPE_UNKNOWN = 0;
public static final int TEXT_TYPE_PLAIN = 1;
public static final int TEXT_TYPE_IMAGE = 2;
public static final int TEXT_TYPE_VOICE = 3;
public static final int TEXT_TYPE_CARD = 4;
public static final int TEXT_TYPE_VIDEO = 5;
public static final int TEXT_TYPE_LOCATION = 6;
public static final int TEXT_TYPE_MICROPROGRAM = 7;
public static final int TEXT_TYPE_LINK = 8;
public static final int TEXT_TYPE_FILE = 9;
public static final int TEXT_TYPE_WARNING = 10;
public static final int TEXT_TYPE_OFFICE = 11;
public static final int TEXT_TYPE_SOLITAIRE = 12;
public static final int TEXT_TYPE_CHAT_RECORD = 13;
public static final int TEXT_TYPE_COLLECTION = 14;
public static final int TEXT_TYPE_REPLY = 15;
//标题 通常是群名或联系人
public List<String> titleList;
//上传聊天列表
public List<SubMessageBean> messageList;
//上传日志内容
public String log;
//外部群1 外部联系人2 内部群3 内部联系人4
public Integer roomType;
//接收人名称
public String receivedName;
//内容移除了@me
public String receivedContent;
//原始内容text
public String originalContent;
//多选(转发等)
public List<String> nameList;
//转发附加留言
public String extraText;
//接收消息类型
public int textType;
//群名
public String groupName;
//群主名称
public String groupOwner;
//成员名单
public List<String> selectList;
//成员数
public Integer groupNumber;
//群公告
public String groupAnnouncement;
//新群名
public String newGroupName;
//新群公告
public String newGroupAnnouncement;
//踢人列表
public List<String> removeList;
//拉人是否附带历史记录
public boolean showMessageHistory = false;
//我的信息
public MyInfo myInfo;
//对象名称(图片、文件、小程序等)
public String objectName;
public WeworkMessageBean() {}
public WeworkMessageBean(String receivedName, String receivedContent, int type, Integer roomType, List<String> titleList, List<SubMessageBean> messageList, String log) {
this.type = type;
this.roomType = roomType;
this.titleList = titleList;
this.messageList = messageList;
this.log = log;
this.receivedContent = receivedContent;
this.receivedName = receivedName;
}
public int type = 0;
//消息列表的每条消息
public static class SubMessageBean {
//0其他人 1机器人自己 2unknown(如系统消息)
public int sender = 0;
//消息类型判断 仅针对sender=0
public int textType;
public List<ItemMessageBean> itemMessageList;
public List<String> nameList;
public SubMessageBean(int sender, int textType, List<ItemMessageBean> itemMessageList, List<String> nameList) {
this.sender = sender;
this.textType = textType;
this.itemMessageList = itemMessageList;
this.nameList = nameList;
}
}
//消息列表每条消息的text推断
public static class ItemMessageBean {
//0消息主体上方信息 如日期等 系统消息(拉人/撤回/外部群等居中的提示语)
//2消息内容
public int feature = 0;
public String text;
public ItemMessageBean(int feature, String text) {
this.feature = feature;
this.text = text;
}
}
//我的信息
public static class MyInfo {
//{姓名=企微RPA机器人, 工作签名=添加工作签名…, 手机=17326101105, 别名=企微RPA机器人, 对外信息显示=企微RPA机器人擎盾数据, 职务=企微RPA机器人, 所在企业=TEST 擎盾数据, 性别=男}
public String name;
public String alias;
public String gender;
public String showName;
public String workSign;
public String corporation;
public String phone;
public String job;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WeworkMessageBean that = (WeworkMessageBean) o;
return textType == that.textType && showMessageHistory == that.showMessageHistory && type == that.type && Objects.equals(titleList, that.titleList) && Objects.equals(messageList, that.messageList) && Objects.equals(log, that.log) && Objects.equals(roomType, that.roomType) && Objects.equals(receivedName, that.receivedName) && Objects.equals(receivedContent, that.receivedContent) && Objects.equals(originalContent, that.originalContent) && Objects.equals(nameList, that.nameList) && Objects.equals(extraText, that.extraText) && Objects.equals(groupName, that.groupName) && Objects.equals(groupOwner, that.groupOwner) && Objects.equals(selectList, that.selectList) && Objects.equals(groupNumber, that.groupNumber) && Objects.equals(groupAnnouncement, that.groupAnnouncement) && Objects.equals(newGroupName, that.newGroupName) && Objects.equals(newGroupAnnouncement, that.newGroupAnnouncement) && Objects.equals(removeList, that.removeList) && Objects.equals(myInfo, that.myInfo) && Objects.equals(objectName, that.objectName);
}
@Override
public int hashCode() {
return Objects.hash(titleList, messageList, log, roomType, receivedName, receivedContent, originalContent, nameList, extraText, textType, groupName, groupOwner, selectList, groupNumber, groupAnnouncement, newGroupName, newGroupAnnouncement, removeList, showMessageHistory, myInfo, objectName, type);
}
@Override
public String toString() {
return "WeworkMessageBean{" +
"titleList=" + titleList +
", messageList=" + messageList +
", log='" + log + '\'' +
", roomType=" + roomType +
", receivedName='" + receivedName + '\'' +
", receivedContent='" + receivedContent + '\'' +
", originalContent='" + originalContent + '\'' +
", nameList=" + nameList +
", extraText='" + extraText + '\'' +
", textType=" + textType +
", groupName='" + groupName + '\'' +
", groupOwner='" + groupOwner + '\'' +
", selectList=" + selectList +
", groupNumber=" + groupNumber +
", groupAnnouncement='" + groupAnnouncement + '\'' +
", newGroupName='" + newGroupName + '\'' +
", newGroupAnnouncement='" + newGroupAnnouncement + '\'' +
", removeList=" + removeList +
", showMessageHistory=" + showMessageHistory +
", myInfo=" + myInfo +
", objectName='" + objectName + '\'' +
", type=" + type +
'}';
}
}

View File

@@ -0,0 +1,70 @@
package org.yameida.worktool.model
import com.blankj.utilcode.util.EncryptUtils
import com.blankj.utilcode.util.GsonUtils
import com.blankj.utilcode.util.TimeUtils
import org.yameida.worktool.Constant
import java.util.*
import kotlin.collections.ArrayList
class WeworkMessageListBean {
companion object {
const val SOCKET_TYPE_HEARTBEAT = 0
const val SOCKET_TYPE_MESSAGE_CONFIRM = 1
const val SOCKET_TYPE_MESSAGE_LIST = 2
}
/**
* type
* TYPE_HEARTBEAT 心跳检测
* TYPE_MESSAGE_CONFIRM 消息确认
* TYPE_MESSAGE_LIST 消息列表
*/
var socketType = SOCKET_TYPE_HEARTBEAT
//消息id
var messageId = TimeUtils.date2String(Date()).replace(" ", "#") + "#" + UUID.randomUUID()
//消息列表
var list: ArrayList<WeworkMessageBean> = arrayListOf()
//加密消息列表
var encryptedList: String = ""
//消息加密 0不加密 1AES
var encryptType = Constant.encryptType
constructor(weworkMessageBean: WeworkMessageBean, type: Int) {
if (encryptType == 0) {
list.add(weworkMessageBean)
} else if (encryptType == 1) {
encryptedList = EncryptUtils.encryptAES2HexString(
GsonUtils.toJson(arrayListOf(weworkMessageBean)).toByteArray(),
Constant.key,
Constant.transformation,
Constant.iv
)
}
this.socketType = type
}
constructor(messageId: String, type: Int) {
this.messageId = messageId
this.socketType = type
}
constructor(weworkMessageBeanList: List<WeworkMessageBean>, type: Int) {
if (encryptType == 0) {
list.addAll(weworkMessageBeanList)
} else if (encryptType == 1) {
encryptedList = EncryptUtils.encryptAES2HexString(
GsonUtils.toJson(weworkMessageBeanList).toByteArray(),
Constant.key,
Constant.transformation,
Constant.iv
)
}
this.socketType = type
}
}

View File

@@ -0,0 +1,11 @@
package org.yameida.worktool.model.network;
import org.yameida.worktool.model.AppUpdate;
public class CheckUpdateResult {
public Integer code;
public String message;
public AppUpdate data;
}

View File

@@ -0,0 +1,156 @@
package org.yameida.worktool.service
import android.view.accessibility.AccessibilityNodeInfo
import com.blankj.utilcode.util.GsonUtils
import com.blankj.utilcode.util.LogUtils
import org.yameida.worktool.Constant
import org.yameida.worktool.model.WeworkMessageBean
import org.yameida.worktool.model.WeworkMessageListBean
import org.yameida.worktool.utils.AccessibilityUtil
import org.yameida.worktool.utils.Views
import java.lang.Exception
/**
* 进入首页-消息页
*/
fun goHome() {
goHomeTab("消息")
}
/**
* 进入首页tab
* 1.检查是否有底部tab
* 2.回退到首页
* @param title 消息/文档/通讯录/工作台/我
* 可能因为管理员排版首页Tab而导致找不到匹配title
*/
fun goHomeTab(title: String): Boolean {
var atHome = false
var find = false
while (!atHome) {
val list = getRoot().findAccessibilityNodeInfosByText("消息")
for (item in list) {
if (item.parent.parent.parent.childCount == 5) {
atHome = true
val tempList = getRoot().findAccessibilityNodeInfosByText(title)
for (tempItem in tempList) {
if (tempItem.parent.parent.parent.childCount == 5) {
AccessibilityUtil.performClick(tempItem)
sleep(300)
find = true
}
}
}
}
if (!atHome) {
backPress()
sleep(1500)
}
}
LogUtils.v("进入首页-${title}")
return find
}
/**
* 当前是否在首页
*/
fun isAtHome(): Boolean {
val list = getRoot().findAccessibilityNodeInfosByText("消息")
return list.count { it.parent.parent.parent.childCount == 5 } > 0
}
/**
* 获取企业微信窗口
*/
fun getRoot(): AccessibilityNodeInfo {
return getRoot(false)
}
/**
* 获取前台窗口
* @param ignoreCheck false 必须等待前台为企业微信 true 直接返回当前前台窗口
*/
fun getRoot(ignoreCheck: Boolean): AccessibilityNodeInfo {
while (true) {
val tempRoot = WeworkController.weworkService.rootInActiveWindow
val root = WeworkController.weworkService.rootInActiveWindow
if (tempRoot != root) {
LogUtils.e("tempRoot != root")
}
if (root != null) {
if (root.packageName == Constant.PACKAGE_NAMES) {
return root
} else {
LogUtils.e("当前不在企业微信: ${root.packageName}")
error("当前不在企业微信: ${root.packageName}")
if (ignoreCheck) {
return root
}
}
}
sleep(1000)
}
}
/**
* 后退
*/
fun backPress() {
val textView = AccessibilityUtil.findOneByClazz(getRoot(), Views.TextView)
if (textView != null && textView.text.isNullOrBlank()) {
LogUtils.d("找到回退按钮")
AccessibilityUtil.performClick(textView)
} else {
val ivButton = AccessibilityUtil.findOneByClazz(getRoot(), Views.ImageView)
if (ivButton != null && ivButton.isClickable && AccessibilityUtil.findFrontNode(ivButton) == null) {
LogUtils.d("未找到回退按钮 点击第一个IV按钮")
AccessibilityUtil.performClick(ivButton)
} else {
LogUtils.d("未找到回退按钮 点击第一个BT按钮")
val button = AccessibilityUtil.findOneByClazz(getRoot(), Views.Button)
if (button != null && button.childCount > 0) {
AccessibilityUtil.performClick(button.getChild(0))
} else {
AccessibilityUtil.performClick(button)
}
}
}
sleep(1000)
}
/**
* 上传运行日志 简单封装 info log
*/
fun log(message: Any?, type: Int = WeworkMessageBean.ROBOT_LOG) {
WeworkController.weworkService.webSocketManager.send(
WeworkMessageListBean(
WeworkMessageBean(
null, null,
type,
null,
null,
null,
if (message is String) message else GsonUtils.toJson(message)
),
WeworkMessageListBean.SOCKET_TYPE_MESSAGE_LIST
), true
)
}
/**
* 上传运行日志 简单封装 error log
*/
fun error(message: Any?) {
log(message, WeworkMessageBean.ROBOT_ERROR_LOG)
}
/**
* 简单封装 sleep
*/
fun sleep(time: Long) {
try {
Thread.sleep(time)
} catch (e: Exception) {
e.printStackTrace()
}
}

View File

@@ -0,0 +1,155 @@
package org.yameida.worktool.service
import android.os.Handler
import android.os.Looper
import android.os.Message
import com.blankj.utilcode.util.EncryptUtils
import com.blankj.utilcode.util.GsonUtils
import com.blankj.utilcode.util.LogUtils
import com.google.gson.reflect.TypeToken
import okhttp3.WebSocket
import org.yameida.worktool.Constant
import org.yameida.worktool.model.WeworkMessageBean
import org.yameida.worktool.model.WeworkMessageListBean
import java.nio.charset.StandardCharsets
import java.util.LinkedHashSet
import kotlin.concurrent.thread
object MyLooper {
private var threadHandler: Handler? = null
val looper = thread {
LogUtils.e("myLooper starting...")
Looper.prepare()
val myLooper = Looper.myLooper()
if (myLooper != null) {
threadHandler = object : Handler(myLooper) {
override fun handleMessage(msg: Message) {
LogUtils.d("handle message: " + Thread.currentThread().name, msg)
try {
dealWithMessage(msg.obj as WeworkMessageBean)
} catch (e: Exception) {
LogUtils.e(e)
}
}
}
} else {
LogUtils.e("myLooper is null!")
}
Looper.loop()
}
fun init() {}
fun getInstance(): Handler {
while (true) {
threadHandler?.let { return it }
LogUtils.e("threadHandler is not ready...")
}
}
fun onMessage(webSocket: WebSocket?, text: String) {
val messageList =
GsonUtils.fromJson<WeworkMessageListBean>(text, WeworkMessageListBean::class.java)
if (messageList.socketType == WeworkMessageListBean.SOCKET_TYPE_HEARTBEAT) {
return
}
if (messageList.socketType == WeworkMessageListBean.SOCKET_TYPE_MESSAGE_CONFIRM) {
return
}
if (messageList.socketType == WeworkMessageListBean.SOCKET_TYPE_MESSAGE_LIST) {
val confirm = WeworkController.weworkService.webSocketManager.confirm(messageList.messageId)
if (!confirm) return
if (messageList.encryptType == 1) {
val decryptHexStringAES = EncryptUtils.decryptHexStringAES(
messageList.encryptedList,
Constant.key,
Constant.transformation,
Constant.iv
)
messageList.list =
GsonUtils.fromJson(
String(decryptHexStringAES, StandardCharsets.UTF_8),
object : TypeToken<ArrayList<WeworkMessageBean>>() {}.type
)
}
//去重处理 丢弃之前的重复指令 丢弃之前的获取新消息指令
for (message in LinkedHashSet(messageList.list)) {
getInstance().removeMessages(WeworkMessageBean.LOOP_RECEIVE_NEW_MESSAGE)
if (message.type == WeworkMessageBean.LOOP_RECEIVE_NEW_MESSAGE) {
if (!WeworkController.mainLoopRunning) {
getInstance().sendMessage(Message.obtain().apply {
what = WeworkMessageBean.LOOP_RECEIVE_NEW_MESSAGE
obj = message
})
}
} else {
WeworkController.mainLoopRunning = false
getInstance().removeMessages(message.type * message.hashCode())
getInstance().sendMessage(Message.obtain().apply {
what = message.type * message.hashCode()
obj = message
})
}
}
}
}
private fun dealWithMessage(message: WeworkMessageBean) {
when (message.type) {
WeworkMessageBean.STOP_AND_GO_HOME -> {
WeworkController.stopAndGoHome()
}
WeworkMessageBean.LOOP_RECEIVE_NEW_MESSAGE -> {
WeworkController.loopReceiveNewMessage()
}
WeworkMessageBean.SEND_MESSAGE -> {
WeworkController.sendMessage(message)
}
WeworkMessageBean.REPLY_MESSAGE -> {
WeworkController.replyMessage(message)
}
WeworkMessageBean.RELAY_MESSAGE -> {
WeworkController.relayMessage(message)
}
WeworkMessageBean.INIT_GROUP -> {
WeworkController.initGroup(message)
}
WeworkMessageBean.INTO_GROUP_AND_CONFIG -> {
WeworkController.intoGroupAndConfig(message)
}
WeworkMessageBean.PUSH_MICRO_DISK_IMAGE -> {
WeworkController.pushMicroDiskImage(message)
}
WeworkMessageBean.PUSH_MICRO_DISK_FILE -> {
WeworkController.pushMicroDiskFile(message)
}
WeworkMessageBean.PUSH_MICROPROGRAM -> {
WeworkController.pushMicroprogram(message)
}
WeworkMessageBean.PUSH_OFFICE -> {
WeworkController.pushOffice(message)
}
WeworkMessageBean.PASS_ALL_FRIEND_REQUEST -> {
}
WeworkMessageBean.ADD_FRIEND_BY_PHONE -> {
}
WeworkMessageBean.SHOW_GROUP_INFO -> {
WeworkController.showGroupInfo(message)
}
WeworkMessageBean.GET_GROUP_INFO -> {
WeworkController.getGroupInfo(message)
}
WeworkMessageBean.GET_FRIEND_INFO -> {
WeworkController.getFriendInfo(message)
}
WeworkMessageBean.GET_MY_INFO -> {
WeworkController.getMyInfo()
}
WeworkMessageBean.ROBOT_CONTROLLER_TEST -> {
WeworkController.test(message)
}
}
}
}

View File

@@ -0,0 +1,269 @@
package org.yameida.worktool.service
import com.blankj.utilcode.util.*
import org.yameida.worktool.Demo
import org.yameida.worktool.annotation.RequestMapping
import org.yameida.worktool.model.WeworkMessageBean
/**
* 企业微信客服端反转
* 被服务端远程调用的服务Controller类
*/
object WeworkController {
lateinit var weworkService: WeworkService
var mainLoopRunning = false
/**
* 停止所有任务并返回首页待命
* @see WeworkMessageBean.STOP_AND_GO_HOME
*/
@RequestMapping
fun stopAndGoHome() {
LogUtils.d("stopAndGoHome()")
mainLoopRunning = false
goHome()
}
/**
* 回到首页等待接收新消息
* @see WeworkMessageBean.LOOP_RECEIVE_NEW_MESSAGE
*/
@RequestMapping
fun loopReceiveNewMessage() {
LogUtils.d("loopReceiveNewMessage()")
WeworkLoopImpl.mainLoop()
}
/**
* 在房间内发送消息
* @see WeworkMessageBean.SEND_MESSAGE
* @param message#titleList 房间名称
* @param message#receivedContent 回复内容
* @see WeworkMessageBean.TEXT_TYPE
*/
@RequestMapping
fun sendMessage(message: WeworkMessageBean): Boolean {
LogUtils.d("sendMessage(): ${message.titleList} ${message.receivedContent}")
return WeworkOperationImpl.sendMessage(message.titleList, message.receivedContent)
}
/**
* 在房间内指定回复消息
* @see WeworkMessageBean.REPLY_MESSAGE
* @param message#titleList 房间名称
* @param message#receivedName 原始消息的发送者姓名
* @param message#originalContent 原始消息的内容
* @param message#textType 原始消息的消息类型
* @param message#receivedContent 回复内容
* @see WeworkMessageBean.TEXT_TYPE
*/
@RequestMapping
fun replyMessage(message: WeworkMessageBean): Boolean {
LogUtils.d("replyMessage(): ${message.receivedContent}")
return WeworkOperationImpl.replyMessage(
message.titleList,
message.receivedName,
message.originalContent,
message.textType,
message.receivedContent
)
}
/**
* 在房间内转发消息
* @see WeworkMessageBean.RELAY_MESSAGE
* @param message#titleList 房间名称
* @param message#receivedName 原始消息的发送者姓名
* @param message#originalContent 原始消息的内容
* @param message#textType 原始消息的消息类型
* @param message#nameList 待转发姓名列表
* @param message#extraText 附加留言 选填
* @see WeworkMessageBean.TEXT_TYPE
*/
@RequestMapping
fun relayMessage(message: WeworkMessageBean): Boolean {
LogUtils.d("relayMessage(): ${message.titleList} ${message.receivedName} ${message.originalContent} ${message.textType} ${message.nameList} ${message.extraText}")
return WeworkOperationImpl.relayMessage(
message.titleList,
message.receivedName,
message.originalContent,
message.textType,
message.nameList,
message.extraText
)
}
/**
* 初始化群设置
* @see WeworkMessageBean.INIT_GROUP
* @param message#groupName 修改群名称
* @param message#selectList 添加群成员名称列表 选填
* @param message#groupAnnouncement 修改群公告 选填
*/
@RequestMapping
fun initGroup(message: WeworkMessageBean): Boolean {
LogUtils.d("initGroup(): ${message.groupName} ${message.selectList} ${message.groupAnnouncement}")
return WeworkOperationImpl.initGroup(
message.groupName,
message.selectList,
message.groupAnnouncement
)
}
/**
* 机器人接口测试
* @see WeworkMessageBean.ROBOT_CONTROLLER_TEST
*/
@RequestMapping
fun test(message: WeworkMessageBean? = null) {
LogUtils.d(message)
Demo.test(true)
}
/**
* 进入群聊并修改群配置
* 群名称、群公告、拉人、踢人
* @see WeworkMessageBean.INTO_GROUP_AND_CONFIG
* @param message#groupName 待修改的群
* @param message#newGroupName 修改群名 选填
* @param message#newGroupAnnouncement 修改群公告 选填
* @param message#selectList 添加群成员名称列表/拉人 选填
* @param message#showMessageHistory 拉人是否附带历史记录 选填
* @param message#removeList 移除群成员名称列表/踢人 选填
*/
@RequestMapping
fun intoGroupAndConfig(message: WeworkMessageBean): Boolean {
LogUtils.d("intoGroupAndConfig(): ${message.groupName} ${message.newGroupName} ${message.newGroupAnnouncement} ${message.selectList} ${message.showMessageHistory} ${message.removeList}")
return WeworkOperationImpl.intoGroupAndConfig(
message.groupName,
message.newGroupName,
message.newGroupAnnouncement,
message.selectList,
message.showMessageHistory,
message.removeList
)
}
/**
* 推送微盘图片
* @see WeworkMessageBean.PUSH_MICRO_DISK_IMAGE
* @param message#titleList 待发送姓名列表
* @param message#objectName 图片名称
* @param message#extraText 附加留言 可选
*/
@RequestMapping
fun pushMicroDiskImage(message: WeworkMessageBean): Boolean {
LogUtils.d("pushMicroDiskImage(): ${message.titleList} ${message.objectName} ${message.extraText}")
return WeworkOperationImpl.pushMicroDiskImage(
message.titleList,
message.objectName,
message.extraText
)
}
/**
* 推送微盘文件
* @see WeworkMessageBean.PUSH_MICRO_DISK_FILE
* @param message#titleList 待发送姓名列表
* @param message#objectName 文件名称
* @param message#extraText 附加留言 可选
*/
@RequestMapping
fun pushMicroDiskFile(message: WeworkMessageBean): Boolean {
LogUtils.d("pushMicroDiskFile(): ${message.titleList} ${message.objectName} ${message.extraText}")
return WeworkOperationImpl.pushMicroDiskFile(
message.titleList,
message.objectName,
message.extraText
)
}
/**
* 推送任意小程序
* @see WeworkMessageBean.PUSH_MICROPROGRAM
* @param message#titleList 待发送姓名列表
* @param message#objectName 小程序名称
* @param message#extraText 附加留言 可选
*/
@RequestMapping
fun pushMicroprogram(message: WeworkMessageBean): Boolean {
LogUtils.d("pushMicroprogram(): ${message.titleList} ${message.objectName} ${message.extraText}")
return WeworkOperationImpl.pushMicroprogram(
message.titleList,
message.objectName,
message.extraText
)
}
/**
* 推送腾讯文档
* @see WeworkMessageBean.PUSH_OFFICE
* TODO 自己的文档分享时可选择权限级别
* @param message#titleList 待发送姓名列表
* @param message#objectName 腾讯文档名称
* @param message#extraText 附加留言 可选
*/
@RequestMapping
fun pushOffice(message: WeworkMessageBean): Boolean {
LogUtils.d("pushOffice(): ${message.titleList} ${message.objectName} ${message.extraText}")
return WeworkOperationImpl.pushOffice(
message.titleList,
message.objectName,
message.extraText
)
}
/**
* 展示群信息
* @see WeworkMessageBean.SHOW_GROUP_INFO
* @param message#titleList 待查询群名
* @param message#receivedName 原始消息的发送者姓名
* @param message#originalContent 原始消息的内容
* @param message#textType 原始消息的消息类型
*/
@RequestMapping
fun showGroupInfo(message: WeworkMessageBean): Boolean {
LogUtils.d("showGroupInfo(): ${message.titleList} ${message.receivedName} ${message.originalContent} ${message.textType}")
return WeworkOperationImpl.showGroupInfo(
message.titleList,
message.receivedName,
message.originalContent,
message.textType
)
}
/**
* 获取群信息
* @see WeworkMessageBean.GET_GROUP_INFO
* @param message#selectList 群名列表 为空时去群管理页查询并返回群聊页
*/
@RequestMapping
fun getGroupInfo(message: WeworkMessageBean): Boolean {
LogUtils.d("getGroupInfo(): ${message.selectList}")
return WeworkGetImpl.getGroupInfo(message.selectList)
}
/**
* 获取好友信息
* @see WeworkMessageBean.GET_FRIEND_INFO
* TODO
* @param message#selectList 好友名列表
*/
@RequestMapping
fun getFriendInfo(message: WeworkMessageBean): Boolean {
LogUtils.d("getFriendInfo(): ${message.selectList}")
return WeworkGetImpl.getFriendInfo(message.selectList)
}
/**
* 获取我的信息
* @see WeworkMessageBean.GET_MY_INFO
*/
@RequestMapping
fun getMyInfo(): Boolean {
LogUtils.d("getMyInfo():")
return WeworkGetImpl.getMyInfo()
}
}

View File

@@ -0,0 +1,149 @@
package org.yameida.worktool.service
import com.blankj.utilcode.util.LogUtils
import org.yameida.worktool.model.WeworkMessageBean
import org.yameida.worktool.utils.AccessibilityUtil
import org.yameida.worktool.utils.Views
import org.yameida.worktool.utils.WeworkRoomUtil
/**
* 获取数据类型 500 实现类
*/
object WeworkGetImpl {
/**
* 获取群信息
* @param selectList 群名列表 为空时去群管理页查询并返回群聊页
*/
fun getGroupInfo(selectList: List<String>): Boolean {
if (selectList.isNullOrEmpty()) {
WeworkRoomUtil.intoGroupManager()
val groupInfo = getGroupInfoDetail()
WeworkController.weworkService.webSocketManager.send(groupInfo)
backPress()
} else {
for (groupName in selectList) {
if (WeworkRoomUtil.intoRoom(groupName) && WeworkRoomUtil.intoGroupManager()) {
val groupInfo = getGroupInfoDetail()
WeworkController.weworkService.webSocketManager.send(groupInfo)
}
}
}
return true
}
/**
* 获取好友信息
* @param selectList 好友名列表
*/
fun getFriendInfo(selectList: List<String>): Boolean {
return true
}
/**
* 获取我的信息
*/
fun getMyInfo(): Boolean {
if (!goHomeTab("")) {
LogUtils.d("未找到我的信息")
goHomeTab("消息")
val firstTv = AccessibilityUtil.findAllByClazz(getRoot(), Views.TextView)
.firstOrNull { it.text == null }
AccessibilityUtil.performClick(firstTv)
sleep(1000)
val newFirstTv = AccessibilityUtil.findOneByClazz(getRoot(), Views.TextView)
val nickname = newFirstTv?.text?.toString()
AccessibilityUtil.performClick(firstTv)
if (nickname != null) {
LogUtils.d("我的昵称: $nickname")
val weworkMessageBean = WeworkMessageBean()
weworkMessageBean.type = WeworkMessageBean.GET_MY_INFO
weworkMessageBean.myInfo = WeworkMessageBean.MyInfo().apply { name = nickname }
WeworkController.weworkService.webSocketManager.send(weworkMessageBean)
return true
} else {
LogUtils.d("未找到我的昵称")
return false
}
}
AccessibilityUtil.performClick(AccessibilityUtil.findOneByClazz(getRoot(), Views.ImageView))
sleep(1500)
val relativeLayoutList = AccessibilityUtil.findAllByClazz(getRoot(), Views.RelativeLayout)
val myInfo = WeworkMessageBean.MyInfo()
for (relativeLayout in relativeLayoutList.filter { it.childCount >= 2 }) {
val textViewList = AccessibilityUtil.findAllByClazz(relativeLayout, Views.TextView)
if (textViewList.size >= 2) {
val firstText = textViewList[0].text?.toString()
if (firstText == "姓名" && myInfo.name == null) {
myInfo.name = textViewList[1].text?.toString() ?: ""
}
if (firstText == "别名" && myInfo.alias == null) {
myInfo.alias = textViewList[1].text?.toString() ?: ""
}
if (firstText == "性别" && myInfo.gender == null) {
myInfo.gender = textViewList[1].text?.toString() ?: ""
}
if (firstText == "对外信息显示" && myInfo.showName == null) {
myInfo.showName = textViewList[1].text?.toString() ?: ""
}
if (firstText == "工作签名" && myInfo.workSign == null) {
myInfo.workSign = textViewList[1].text?.toString() ?: ""
}
if (firstText == "所在企业" && myInfo.corporation == null) {
myInfo.corporation = textViewList[1].text?.toString() ?: ""
}
if (firstText == "手机" && myInfo.phone == null) {
myInfo.phone = textViewList[1].text?.toString() ?: ""
}
if (firstText == "职务" && myInfo.job == null) {
myInfo.job = textViewList[1].text?.toString() ?: ""
}
}
}
LogUtils.d("我的信息", myInfo)
val weworkMessageBean = WeworkMessageBean()
weworkMessageBean.type = WeworkMessageBean.GET_MY_INFO
weworkMessageBean.myInfo = myInfo
WeworkController.weworkService.webSocketManager.send(weworkMessageBean)
return true
}
/**
* 获取群名、群主、群成员数、群公告
*/
fun getGroupInfoDetail(): WeworkMessageBean {
val weworkMessageBean = WeworkMessageBean()
weworkMessageBean.type = WeworkMessageBean.GET_GROUP_INFO
val tvManagerFlag =
AccessibilityUtil.findOneByText(getRoot(), "由企业微信用户创建,可邀请微信用户进群", "该群由企业微信用户创建")
val button = AccessibilityUtil.findFrontNode(tvManagerFlag)
val tvGroupName = AccessibilityUtil.findOneByClazz(button, Views.TextView)
if (tvGroupName != null && tvGroupName.text != null) {
LogUtils.d("群名: " + tvGroupName.text)
weworkMessageBean.groupName = tvGroupName.text.toString()
}
val gridView = AccessibilityUtil.findOneByClazz(getRoot(), Views.GridView)
if (gridView != null && gridView.childCount >= 2) {
val tvOwnerName = AccessibilityUtil.findOneByClazz(gridView.getChild(0), Views.TextView)
if (tvOwnerName != null && tvOwnerName.text != null) {
LogUtils.d("群主: " + tvOwnerName.text)
weworkMessageBean.groupOwner = tvOwnerName.text.toString()
}
}
val tvCountFlag = AccessibilityUtil.findOneByText(getRoot(), "查看全部群成员")
val tvCount = AccessibilityUtil.findBackNode(tvCountFlag)
if (tvCount != null && tvCount.text != null) {
LogUtils.d("群成员: " + tvCount.text)
val count = tvCount.text.toString().replace("", "")
weworkMessageBean.groupNumber = count.toIntOrNull()
}
val tvAnnouncementFlag = AccessibilityUtil.findOneByText(getRoot(), "群公告")
val tvAnnouncement = AccessibilityUtil.findBackNode(tvAnnouncementFlag)
if (tvAnnouncement != null && tvAnnouncement.text != null) {
LogUtils.d("群公告: " + tvAnnouncement.text)
weworkMessageBean.groupAnnouncement = tvAnnouncement.text.toString()
}
backPress()
return weworkMessageBean
}
}

View File

@@ -0,0 +1,348 @@
package org.yameida.worktool.service
import android.view.accessibility.AccessibilityNodeInfo
import androidx.core.text.isDigitsOnly
import com.blankj.utilcode.util.LogUtils
import org.yameida.worktool.Demo
import org.yameida.worktool.model.WeworkMessageBean
import org.yameida.worktool.service.WeworkController.mainLoopRunning
import org.yameida.worktool.utils.*
import java.lang.Exception
import java.lang.StringBuilder
/**
* 获取数据类型 201 202 主循环
*/
object WeworkLoopImpl {
var logIndex = 0
// @Synchronized
// fun mainLoop() {
// mainLoopRunning = true
// if (!threadStart) {
// threadStart = true
// thread {
// while (true) {
// try {
// if (mainLoopRunning) {
// goHomeTab("消息")
// //todo 下上滚动
// if (mainLoopRunning && getChatroomList()) {
// if (mainLoopRunning)
// getChatMessageList()
// mainLoopRunning = false
// }
// if (mainLoopRunning) {
// getFriendRequest()
// }
// }
// sleep(1000)
// } catch (e: Exception) {
// LogUtils.e(e)
// error(e.printStackTrace())
// mainLoopRunning = false
// }
// }
// }
// }
// }
fun mainLoop() {
mainLoopRunning = true
try {
while (mainLoopRunning) {
goHomeTab("消息")
//todo 下上滚动
if (getChatroomList() && getChatMessageList()) {
mainLoopRunning = false
break
}
if (getFriendRequest()) {
mainLoopRunning = false
break
}
sleep(1000)
}
} catch (e: Exception) {
mainLoopRunning = false
error("ERROR mainLoop: " + e.message)
}
}
/**
* 读取通讯录好友请求
*/
fun getFriendRequest(): Boolean {
val list = getRoot().findAccessibilityNodeInfosByText("通讯录")
for (item in list) {
if (item.parent.parent.parent.childCount == 5) {
if (item.parent.childCount > 1) {
LogUtils.d("通讯录有红点")
AccessibilityUtil.performClick(item)
sleep(500)
val addButton = AccessibilityUtil.findOneByText(getRoot(), "添加客户")
val backNode = AccessibilityUtil.findBackNode(addButton)
LogUtils.d(backNode?.className)
if (backNode?.className == Views.TextView) {
LogUtils.d("有待添加客户")
AccessibilityUtil.performClick(backNode)
sleep(2000)
AccessibilityUtil.findTextAndClick(getRoot(), "新的客户")
sleep(500)
var retry = 5
while (retry-- > 0) {
if (!AccessibilityUtil.findTextAndClick(getRoot(), "查看"))
break
sleep(2000)
val nameList = passFriendRequest()
if (nameList.isEmpty())
break
//TODO nameList 通过的好友加入演示脚本
Demo.test2(nameList[0])
}
return true
} else {
LogUtils.d("未发现待添加客户")
}
} else {
LogUtils.v("通讯录无红点")
}
}
}
return false
}
/**
* 查看好友请求并通过
*/
private fun passFriendRequest(): List<String> {
val nameList = arrayListOf<String>()
val imageView = AccessibilityUtil.findOneByClazz(getRoot(), Views.ImageView)
if (imageView != null) {
val textViewList = AccessibilityUtil.findAllByClazz(imageView.parent, Views.TextView)
val filter = textViewList.filter { it.text != null && it.text.toString() != "微信" }
if (filter.isNotEmpty()) {
val tvNick = filter[0]
LogUtils.d("好友请求: " + tvNick.text)
AccessibilityUtil.findTextAndClick(getRoot(), "通过验证")
sleep(1000)
AccessibilityUtil.findTextAndClick(getRoot(), "完成")
sleep(5000)
if (AccessibilityUtil.findTextAndClick(getRoot(), "确定")) {
sleep(500)
LogUtils.d("添加好友失败")
} else {
val weworkMessageBean = WeworkMessageBean()
weworkMessageBean.type = WeworkMessageBean.GET_FRIEND_INFO
weworkMessageBean.nameList = arrayListOf(tvNick.text.toString())
WeworkController.weworkService.webSocketManager.send(weworkMessageBean)
nameList.add(tvNick.text.toString())
}
//回到上一页
var retry = 5
while (retry-- > 0 && !isAtHome()) {
val textView = AccessibilityUtil.findOneByText(getRoot(), "新的客户")
if (textView == null) {
backPress()
}
}
}
}
return nameList
}
/**
* 读取聊天列表
*/
private fun getChatroomList(): Boolean {
if (logIndex++ % 15 == 0) {
LogUtils.i("读取首页聊天列表")
log("读取首页聊天列表")
}
val listview = AccessibilityUtil.findOneByClazz(getRoot(), Views.ListView)
if (listview != null) {
if (listview.childCount >= 2) {
if (checkUnreadChatRoom(listview)) {
//进入聊天页
return true
}
} else {
LogUtils.e("读取聊天列表失败")
error("读取聊天列表失败")
}
} else {
LogUtils.e("读取聊天列表失败")
error("读取聊天列表失败")
}
return false
}
/**
* 检查首页-聊天列表是否有未读红点并点击进入
* 获取红点
*/
private fun checkUnreadChatRoom(list: AccessibilityNodeInfo): Boolean {
val spotNodeList = arrayListOf<AccessibilityNodeInfo>()
for (i in 0 until list.childCount) {
val item = list.getChild(i)
if (item != null && Views.RelativeLayout.equals(item.className)) {
if (item.childCount >= 2) {
val spotNode = item.getChild(1)
if (spotNode != null
&& Views.TextView.equals(spotNode.className)
&& spotNode.text != null
&& spotNode.text.toString().replace("+", "").isDigitsOnly()
) {
spotNodeList.add(spotNode)
}
}
}
}
if (spotNodeList.size > 0) {
LogUtils.i("发现未读消息: " + spotNodeList.size + "")
log("发现未读消息: " + spotNodeList.size + "")
for (spotNode in spotNodeList) {
if (AccessibilityUtil.performClick(spotNode)) {
//进入聊天页 下一步 getChatMessageList
break
}
}
sleep(1000)
return true
} else {
return false
}
}
/**
* 聊天页
* 1.获取群名
* 2.获取消息列表
*/
private fun getChatMessageList(): Boolean {
AccessibilityUtil.performScrollDown(getRoot(), 0)
val roomType = WeworkRoomUtil.getRoomType(getRoot())
var titleList = WeworkRoomUtil.getRoomTitle(getRoot())
if (titleList.contains("对方正在输入…")) {
titleList = WeworkRoomUtil.getFriendName()
}
if (titleList.size > 0) {
val title = titleList.joinToString()
LogUtils.i("聊天: $title")
log("聊天: $title")
val list = AccessibilityUtil.findOneByClazz(getRoot(), Views.ListView)
if (list != null) {
LogUtils.d("消息条数: " + list.childCount)
val messageList = arrayListOf<WeworkMessageBean.SubMessageBean>()
for (i in 0 until list.childCount) {
val item = list.getChild(i)
if (item != null && item.childCount > 0) {
messageList.add(parseChatMessageItem(item, roomType))
}
}
WeworkController.weworkService.webSocketManager.send(
WeworkMessageBean(
null, null,
WeworkMessageBean.TYPE_RECEIVE_MESSAGE_LIST,
roomType,
titleList,
messageList,
null
)
)
return true
} else {
LogUtils.e("未找到聊天消息列表")
error("未找到聊天消息列表")
}
}
return false
}
/**
* 解析消息列表里的一条消息
*/
private fun parseChatMessageItem(
node: AccessibilityNodeInfo,
roomType: Int
): WeworkMessageBean.SubMessageBean {
val message: WeworkMessageBean.SubMessageBean
val nameList = arrayListOf<String>()
val itemMessageList = arrayListOf<WeworkMessageBean.ItemMessageBean>()
LogUtils.d("开始解析一条消息...")
//消息头(在消息主体上方 如时间信息)
val linearLayoutItem = AccessibilityUtil.findOneByClazz(node, Views.LinearLayout, 1)
if (linearLayoutItem != null) {
val sb = StringBuilder("消息头: ")
val tvList = AccessibilityUtil.findAllByClazz(linearLayoutItem, Views.TextView)
for (item in tvList.filter { it.text != null && !it.text.isNullOrBlank() }) {
val text = item.text.toString()
val itemMessage = WeworkMessageBean.ItemMessageBean(0, text)
sb.append(text).append("\t")
itemMessageList.add(itemMessage)
}
LogUtils.d(sb.toString())
}
//消息主体
val relativeLayoutItem = AccessibilityUtil.findOneByClazz(node, Views.RelativeLayout, 1)
if (relativeLayoutItem != null && relativeLayoutItem.childCount >= 2) {
if (Views.ImageView.equals(relativeLayoutItem.getChild(0).className)) {
LogUtils.v("头像在左边 本条消息发送者为其他联系人")
nameList.addAll(WeworkTextUtil.getNameList(node))
var textType = WeworkMessageBean.TEXT_TYPE_UNKNOWN
val relativeLayoutContent =
AccessibilityUtil.findOneByClazz(relativeLayoutItem, Views.RelativeLayout, 2)
if (relativeLayoutContent != null) {
// AccessibilityUtil.printNodeClazzTree(relativeLayoutContent)
textType = WeworkTextUtil.getTextType(relativeLayoutContent)
LogUtils.v("textType: $textType")
val tvList =
AccessibilityUtil.findAllByClazz(relativeLayoutContent, Views.TextView)
for (item in tvList.filter { it.text != null && !it.text.isNullOrBlank() }) {
val text = item.text.toString()
LogUtils.d(text)
val itemMessage = WeworkMessageBean.ItemMessageBean(2, text)
itemMessageList.add(itemMessage)
}
}
message = WeworkMessageBean.SubMessageBean(0, textType, itemMessageList, nameList)
} else if (Views.ImageView.equals(relativeLayoutItem.getChild(1).className)) {
LogUtils.v("头像在右边 本条消息发送者为自己")
val tvList = AccessibilityUtil.findAllByClazz(relativeLayoutItem, Views.TextView)
for (item in tvList.filter { it.text != null && !it.text.isNullOrBlank() }) {
val text = item.text.toString()
LogUtils.d(text)
val itemMessage = WeworkMessageBean.ItemMessageBean(2, text)
itemMessageList.add(itemMessage)
}
message = WeworkMessageBean.SubMessageBean(1, 0, itemMessageList, nameList)
} else {
// 没有头像的消息(撤销消息、其他可能的系统消息)
val tvList = AccessibilityUtil.findAllByClazz(node, Views.TextView)
for (item in tvList.filter { it.text != null && !it.text.isNullOrBlank() }) {
val text = item.text.toString()
LogUtils.d(text)
val itemMessage = WeworkMessageBean.ItemMessageBean(1, text)
itemMessageList.add(itemMessage)
}
message = WeworkMessageBean.SubMessageBean(2, 0, itemMessageList, nameList)
LogUtils.e("消息解析异常 未知异常")
}
} else {
// 没有头像的消息(撤销消息、其他可能的系统消息)
val sb = StringBuilder("未发现头像 本条消息发送者未知")
val tvList = AccessibilityUtil.findAllByClazz(node, Views.TextView)
for (item in tvList.filter { it.text != null && !it.text.isNullOrBlank() }) {
val text = item.text.toString()
sb.append(text).append("/t")
val itemMessage = WeworkMessageBean.ItemMessageBean(0, text)
itemMessageList.add(itemMessage)
}
LogUtils.d(sb.toString())
message = WeworkMessageBean.SubMessageBean(2, 0, itemMessageList, nameList)
}
return message
}
}

View File

@@ -0,0 +1,707 @@
package org.yameida.worktool.service
import android.view.accessibility.AccessibilityNodeInfo
import com.blankj.utilcode.util.LogUtils
import org.yameida.worktool.model.WeworkMessageBean
import org.yameida.worktool.utils.AccessibilityUtil
import org.yameida.worktool.utils.Views
import org.yameida.worktool.utils.WeworkRoomUtil
import org.yameida.worktool.utils.WeworkTextUtil
/**
* 全局操作类型 200 实现类
*/
object WeworkOperationImpl {
/**
* 在房间内发送消息
* @param titleList 房间名称
* @param receivedContent 回复内容
* @see WeworkMessageBean.TEXT_TYPE
*/
fun sendMessage(titleList: List<String>, receivedContent: String): Boolean {
for (title in titleList) {
if (WeworkRoomUtil.intoRoom(title)) {
sendChatMessage(receivedContent)
LogUtils.d("$title: 发送成功")
} else {
LogUtils.d("$title: 发送失败")
error("$title: 发送失败 $receivedContent")
}
}
return true
}
/**
* 在房间内指定回复消息
* @param titleList 房间名称
* @param receivedName 原始消息的发送者姓名
* @param originalContent 原始消息的内容
* @param textType 原始消息的消息类型
* @param receivedContent 回复内容
* @see WeworkMessageBean.TEXT_TYPE
*/
fun replyMessage(
titleList: List<String>,
receivedName: String,
originalContent: String,
textType: Int,
receivedContent: String
): Boolean {
for (title in titleList) {
if (WeworkRoomUtil.intoRoom(title)) {
if (WeworkTextUtil.longClickMessageItem(
AccessibilityUtil.findOneByClazz(getRoot(), Views.ListView),
textType,
receivedName,
originalContent,
"回复"
)
) {
LogUtils.d("开始回复")
sleep(1000)
sendChatMessage(receivedContent, "[自动回复]")
LogUtils.d("$title: 回复成功")
return true
} else {
LogUtils.d("$title: 回复失败")
error("$title: 回复失败 $receivedContent")
}
} else {
LogUtils.d("$title: 回复失败")
error("$title: 回复失败 $receivedContent")
}
}
return false
}
/**
* 在房间内转发消息
* @param titleList 房间名称
* @param receivedName 原始消息的发送者姓名
* @param originalContent 原始消息的内容
* @param textType 原始消息的消息类型
* @param nameList 待转发姓名列表
* @param extraText 附加留言 选填
* @see WeworkMessageBean.TEXT_TYPE
*/
fun relayMessage(
titleList: List<String>,
receivedName: String,
originalContent: String,
textType: Int,
nameList: List<String>,
extraText: String? = null
): Boolean {
for (title in titleList) {
if (WeworkRoomUtil.intoRoom(title)) {
if (WeworkTextUtil.longClickMessageItem(
AccessibilityUtil.findOneByClazz(getRoot(), Views.ListView),
textType,
receivedName,
originalContent,
"转发"
)
) {
LogUtils.d("开始转发")
sleep(1000)
relaySelectTarget(nameList, extraText)
}
LogUtils.d("$title: 转发成功")
} else {
LogUtils.d("$title: 转发失败")
error("$title: 转发失败 $originalContent")
}
}
return true
}
/**
* 初始化群设置
* 1.修改群名
* 2.添加群成员 默认为空
* 3.修改群公告 默认为空
* 4.成员改群名 默认禁止
* 5.私自邀请 默认开启
* 6.设置群管理员 延迟开发
* 7.设置入群欢迎语 默认为空
* 8.拉入机器人 暂不开发
* 9.防骚扰 默认警告模式
* 10.使用群配置模板 延迟开发
* 11.消息免打扰 默认禁止
* 12.保存到通讯录 默认开启
* 注:群配置模板 1.群名称 2.禁群改名(使用) 3.设置管理员 4.入群欢迎语(使用) 5.自动回复 6.防骚扰规则(使用)
* 必须人工给机器人预先设置防骚扰规则 规则名"机器人"
* 必须人工给机器人预先设置群配置模板 模板名"机器人"
* @param groupName 修改群名称
* @param selectList 添加群成员名称列表 选填
* @param groupAnnouncement 修改群公告 选填
*/
fun initGroup(
groupName: String,
selectList: List<String>?,
groupAnnouncement: String?
): Boolean {
if (!createGroup() || !groupRename(groupName) || !groupAddMember(selectList)
|| !groupChangeAnnouncement(groupAnnouncement)
) return false
backPress()
return true
}
/**
* 进入群聊并修改群配置
* 群名称、群公告、拉人、踢人
* @param groupName 待修改的群
* @param newGroupName 修改群名 选填
* @param newGroupAnnouncement 修改群公告 选填
* @param selectList 添加群成员名称列表/拉人 选填
* @param showMessageHistory 拉人是否附带历史记录 选填
* @param removeList 移除群成员名称列表/踢人 选填
*/
fun intoGroupAndConfig(
groupName: String,
newGroupName: String?,
newGroupAnnouncement: String?,
selectList: List<String>?,
showMessageHistory: Boolean = false,
removeList: List<String>?
): Boolean {
if (WeworkRoomUtil.intoRoom(groupName)) {
if (newGroupName != null) {
groupRename(newGroupName)
}
if (selectList != null) {
groupAddMember(selectList, showMessageHistory)
}
if (removeList != null) {
groupRemoveMember(removeList)
}
if (newGroupAnnouncement != null) {
groupChangeAnnouncement(newGroupAnnouncement)
}
backPress()
return true
}
return false
}
/**
* 推送微盘图片
* @param titleList 待发送姓名列表
* @param objectName 图片名称
* @param extraText 附加留言 可选
*/
fun pushMicroDiskImage(
titleList: List<String>,
objectName: String,
extraText: String? = null
): Boolean {
goHomeTab("工作台")
val node = AccessibilityUtil.scrollAndFindByText(getRoot(), "微盘")
if (node != null) {
AccessibilityUtil.performClick(node)
sleep(2000)
val buttonList = AccessibilityUtil.findAllByClazz(getRoot(), Views.Button)
if (buttonList.size >= 4) {
AccessibilityUtil.performClick(buttonList[2])
sleep(1000)
AccessibilityUtil.findTextInput(getRoot(), objectName)
sleep(2000)
val editText = AccessibilityUtil.findOneByClazz(getRoot(), Views.EditText)
val backNode = AccessibilityUtil.findBackNode(editText)
val imageViewList = AccessibilityUtil.findAllByClazz(backNode, Views.ImageView)
if (imageViewList.size >= 2) {
AccessibilityUtil.performClick(imageViewList[1])
sleep(2000)
val shareFileButton = AccessibilityUtil.findOneByDesc(getRoot(), "以原文件分享")
AccessibilityUtil.performClick(shareFileButton)
sleep(2000)
val shareToWorkButton = AccessibilityUtil.findOneByText(getRoot(true), "发送给同事")
AccessibilityUtil.performClick(shareToWorkButton)
sleep(2000)
relaySelectTarget(titleList, extraText)
sleep(2000)
val stayButton = AccessibilityUtil.findOneByText(getRoot(), "留在企业微信")
AccessibilityUtil.performClick(stayButton)
return true
} else {
LogUtils.e("微盘未搜索到相关文件: $objectName")
}
} else {
LogUtils.e("未找到微盘内搜索")
}
} else {
LogUtils.e("未找到微盘")
}
return false
}
/**
* 推送微盘文件
* @param titleList 待发送姓名列表
* @param objectName 文件名称
* @param extraText 附加留言 可选
*/
fun pushMicroDiskFile(
titleList: List<String>,
objectName: String,
extraText: String? = null
): Boolean {
goHomeTab("工作台")
val node = AccessibilityUtil.scrollAndFindByText(getRoot(), "微盘")
if (node != null) {
AccessibilityUtil.performClick(node)
sleep(2000)
val buttonList = AccessibilityUtil.findAllByClazz(getRoot(), Views.Button)
if (buttonList.size >= 4) {
AccessibilityUtil.performClick(buttonList[2])
sleep(1000)
AccessibilityUtil.findTextInput(getRoot(), objectName)
sleep(2000)
val editText = AccessibilityUtil.findOneByClazz(getRoot(), Views.EditText)
val backNode = AccessibilityUtil.findBackNode(editText)
val imageViewList = AccessibilityUtil.findAllByClazz(backNode, Views.ImageView)
if (imageViewList.size >= 2) {
AccessibilityUtil.performClick(imageViewList[1])
sleep(2000)
val shareFileButton = AccessibilityUtil.findOneByDesc(getRoot(), "转发")
AccessibilityUtil.performClick(shareFileButton)
sleep(2000)
relaySelectTarget(titleList, extraText)
sleep(2000)
return true
} else {
LogUtils.e("微盘未搜索到相关文件: $objectName")
}
} else {
LogUtils.e("未找到微盘内搜索")
}
} else {
LogUtils.e("未找到微盘")
}
return false
}
/**
* 推送任意小程序
* @param titleList 待发送姓名列表
* @param objectName 小程序名称
* @param extraText 附加留言 可选
*/
fun pushMicroprogram(
titleList: List<String>,
objectName: String,
extraText: String? = null
): Boolean {
goHomeTab("工作台")
val node = AccessibilityUtil.scrollAndFindByText(getRoot(), "用过的小程序")
if (node != null) {
AccessibilityUtil.performClick(node)
sleep(2000)
val textViewList = AccessibilityUtil.findAllByClazz(getRoot(), Views.TextView)
if (textViewList.size > 3) {
AccessibilityUtil.performClick(textViewList[2])
sleep(1000)
AccessibilityUtil.findTextInput(getRoot(), objectName)
sleep(2000)
AccessibilityUtil.findListOneAndClick(getRoot(), 1)
sleep(2000)
//todo 转发小程序
return true
} else {
LogUtils.e("未找到小程序内搜索")
}
} else {
LogUtils.e("未找到小程序")
}
return false
}
/**
* 推送腾讯文档
* @param titleList 待发送姓名列表
* @param objectName 小程序名称
* @param extraText 附加留言 可选
*/
fun pushOffice(
titleList: List<String>,
objectName: String,
extraText: String? = null
): Boolean {
goHomeTab("文档")
val allButton = AccessibilityUtil.findOneByText(getRoot(), "全部")
if (allButton == null) {
LogUtils.e("未找到全部按钮")
return false
}
AccessibilityUtil.performClick(allButton)
sleep(1000)
val myFileButton = AccessibilityUtil.findOneByText(getRoot(), "共享空间")
if (myFileButton == null) {
LogUtils.e("未找到共享空间按钮")
return false
}
AccessibilityUtil.performClick(myFileButton)
sleep(2000)
val buttonList = AccessibilityUtil.findAllByClazz(getRoot(), Views.Button)
if (buttonList.size >= 4) {
AccessibilityUtil.performClick(buttonList[3])
sleep(1000)
AccessibilityUtil.findTextInput(getRoot(), objectName)
sleep(2000)
val editText = AccessibilityUtil.findOneByClazz(getRoot(), Views.EditText)
val backNode = AccessibilityUtil.findBackNode(editText)
val imageViewList = AccessibilityUtil.findAllByClazz(backNode, Views.ImageView)
if (imageViewList.size >= 2) {
AccessibilityUtil.performClick(imageViewList[1])
sleep(2000)
val shareFileButton = AccessibilityUtil.findOneByDesc(getRoot(), "转发")
AccessibilityUtil.performClick(shareFileButton)
sleep(2000)
relaySelectTarget(titleList, extraText)
sleep(2000)
return true
} else {
LogUtils.e("文档未搜索到相关文件: $objectName")
}
} else {
LogUtils.e("未找到文档搜索按钮")
}
return false
}
/**
* 展示群信息
* @see WeworkMessageBean.SHOW_GROUP_INFO
* @param titleList 待查询群名
* @param receivedName 原始消息的发送者姓名
* @param originalContent 原始消息的内容
* @param textType 原始消息的消息类型
*/
fun showGroupInfo(
titleList: MutableList<String>,
receivedName: String,
originalContent: String,
textType: Int
): Boolean {
for (groupName in titleList) {
if (WeworkRoomUtil.intoRoom(groupName) && WeworkRoomUtil.intoGroupManager()) {
val groupInfo = WeworkGetImpl.getGroupInfoDetail()
groupInfo.titleList = arrayListOf(groupName)
groupInfo.type = WeworkMessageBean.SHOW_GROUP_INFO
groupInfo.receivedName = receivedName
groupInfo.originalContent = originalContent
groupInfo.textType = textType
WeworkController.weworkService.webSocketManager.send(groupInfo)
}
}
return true
}
/**
* 转发消息到目标列表
* 支持场景:长按消息转发、微盘图片转发
* selectList 昵称或群名列表
* extraText 转发是否附加一条文本
*/
private fun relaySelectTarget(selectList: List<String>, extraText: String? = null): Boolean {
val list = AccessibilityUtil.findOneByClazz(getRoot(), Views.ListView)
if (list != null) {
val frontNode = AccessibilityUtil.findFrontNode(list)
val textViewList = AccessibilityUtil.findAllByClazz(frontNode, Views.TextView)
if (textViewList.size >= 2) {
val searchButton: AccessibilityNodeInfo = textViewList[textViewList.size - 2]
val multiButton: AccessibilityNodeInfo = textViewList[textViewList.size - 1]
AccessibilityUtil.performClick(multiButton)
sleep(1000)
AccessibilityUtil.performClick(searchButton)
sleep(1000)
for (select in selectList) {
AccessibilityUtil.findTextInput(getRoot(), select)
sleep(2000)
val selectListView = AccessibilityUtil.findOneByClazz(getRoot(), Views.ListView)
val imageView =
AccessibilityUtil.findOneByClazz(selectListView, Views.ImageView)
if (imageView != null) {
AccessibilityUtil.performClick(imageView)
}
sleep(1000)
}
val confirmButton =
AccessibilityUtil.findOneByText(getRoot(), "确定(${selectList.size})")
if (confirmButton != null) {
AccessibilityUtil.performClick(confirmButton)
sleep(1000)
if (!extraText.isNullOrBlank()) {
LogUtils.d("extraText: $extraText")
AccessibilityUtil.findTextInput(getRoot(), extraText)
sleep(1000)
}
val sendButtonList = getRoot().findAccessibilityNodeInfosByText("发送")
for (sendButton in sendButtonList.filter { it.text != null }) {
if (sendButton.text == "发送" || sendButton.text == "发送(${selectList.size})") {
AccessibilityUtil.performClick(sendButton)
return true
}
}
LogUtils.e("未发现发送按钮: ")
return false
} else {
LogUtils.e("未发现确认按钮: ")
return false
}
} else {
LogUtils.e("未发现搜索和多选按钮: ")
return false
}
}
LogUtils.e("未知错误: ")
return false
}
/**
* 创建一个外部群
*/
private fun createGroup(): Boolean {
goHomeTab("工作台")
val textViewGroup = AccessibilityUtil.scrollAndFindByText(getRoot(), "客户群")
if (AccessibilityUtil.performClick(textViewGroup)) {
LogUtils.d("进入客户群应用")
sleep(2000)
val textView = AccessibilityUtil.findOneByText(getRoot(), "创建一个客户群")
AccessibilityUtil.performClick(textView)
sleep(3000)
return true
} else {
LogUtils.d("未找到客户群应用")
return false
}
}
/**
* 修改群名称
*/
private fun groupRename(groupName: String): Boolean {
if (WeworkRoomUtil.intoGroupManager()) {
val textView =
AccessibilityUtil.findOneByText(getRoot(), "由企业微信用户创建,可邀请微信用户进群", "该群由企业微信用户创建")
val button = AccessibilityUtil.findFrontNode(textView)
if (button != null) {
AccessibilityUtil.performClick(button)
sleep(1000)
AccessibilityUtil.findTextInput(getRoot(), groupName)
val confirmButton = AccessibilityUtil.findOneByText(getRoot(), "确定")
AccessibilityUtil.performClick(confirmButton)
sleep(2000)
return true
} else {
LogUtils.e("未找到填写群名按钮")
return false
}
}
return false
}
/**
* 添加群成员/拉人
* 默认不附带历史记录
*/
private fun groupAddMember(
selectList: List<String>? = null,
showMessageHistory: Boolean = false
): Boolean {
if (selectList.isNullOrEmpty()) return true
if (WeworkRoomUtil.intoGroupManager()) {
val gridView = AccessibilityUtil.findOneByClazz(getRoot(), Views.GridView)
if (gridView != null && gridView.childCount >= 2) {
if (gridView.childCount == 2) {
AccessibilityUtil.performClick(gridView.getChild(gridView.childCount - 1))
} else {
AccessibilityUtil.performClick(gridView.getChild(gridView.childCount - 2))
}
sleep(1000)
} else {
LogUtils.e("未找到添加成员按钮")
return false
}
val list = AccessibilityUtil.findOneByClazz(getRoot(), Views.ListView)
if (list != null) {
val frontNode = AccessibilityUtil.findFrontNode(list)
val textViewList = AccessibilityUtil.findAllByClazz(frontNode, Views.TextView)
if (textViewList.size >= 2) {
val multiButton = textViewList.lastOrNull()
AccessibilityUtil.performClick(multiButton)
sleep(1000)
for (select in selectList) {
AccessibilityUtil.findTextInput(getRoot(), select)
sleep(2000)
val selectListView =
AccessibilityUtil.findOneByClazz(getRoot(), Views.ListView)
val imageView =
AccessibilityUtil.findOneByClazz(selectListView, Views.ImageView)
if (imageView != null) {
AccessibilityUtil.performClick(imageView)
}
sleep(1000)
}
if (showMessageHistory) {
val button = AccessibilityUtil.findOneByText(getRoot(), "附带聊天记录")
if (button != null) AccessibilityUtil.performClick(button)
}
val confirmButton =
AccessibilityUtil.findOneByText(getRoot(), "确定(${selectList.size})")
if (confirmButton != null) {
AccessibilityUtil.performClick(confirmButton)
sleep(1000)
} else {
LogUtils.e("未发现确认按钮: ")
return false
}
} else {
LogUtils.e("未找到搜索按钮")
return false
}
} else {
LogUtils.e("未找到成员列表")
return false
}
}
return true
}
/**
* 移除群成员/踢人
*/
private fun groupRemoveMember(removeList: List<String>): Boolean {
if (WeworkRoomUtil.intoGroupManager()) {
val gridView = AccessibilityUtil.findOneByClazz(getRoot(), Views.GridView)
if (gridView != null && gridView.childCount >= 2) {
if (gridView.childCount == 2) {
return true
} else {
AccessibilityUtil.performClick(gridView.getChild(gridView.childCount - 1))
}
sleep(1000)
} else {
LogUtils.e("未找到删除成员按钮")
return false
}
val list = AccessibilityUtil.findOneByClazz(getRoot(), Views.ListView)
if (list != null) {
val frontNode = AccessibilityUtil.findFrontNode(list)
val textViewList = AccessibilityUtil.findAllByClazz(frontNode, Views.TextView)
if (textViewList.size >= 2) {
val multiButton = textViewList.lastOrNull()
AccessibilityUtil.performClick(multiButton)
sleep(1000)
for (select in removeList) {
AccessibilityUtil.findTextInput(getRoot(), select)
sleep(2000)
val selectListView =
AccessibilityUtil.findOneByClazz(getRoot(), Views.ListView)
val imageView =
AccessibilityUtil.findOneByClazz(selectListView, Views.ImageView)
if (imageView != null) {
AccessibilityUtil.performClick(imageView)
}
sleep(1000)
}
val confirmButton =
AccessibilityUtil.findOneByText(getRoot(), "移出(${removeList.size})")
if (confirmButton != null) {
AccessibilityUtil.performClick(confirmButton)
sleep(1000)
} else {
LogUtils.e("未发现移出按钮: ")
return false
}
} else {
LogUtils.e("未找到搜索按钮")
return false
}
} else {
LogUtils.e("未找到成员列表")
return false
}
}
return true
}
/**
* 修改群公告
* 注:首次为发布 后续为编辑
*/
private fun groupChangeAnnouncement(groupAnnouncement: String? = null): Boolean {
if (groupAnnouncement == null) return true
if (WeworkRoomUtil.intoGroupManager()) {
val textView = AccessibilityUtil.findOneByText(getRoot(), "群公告")
if (textView != null) {
AccessibilityUtil.performClick(textView)
sleep(1000)
val editButton = AccessibilityUtil.findOneByText(getRoot(), "编辑")
if (editButton != null) {
LogUtils.d("群公告编辑中: $groupAnnouncement")
AccessibilityUtil.performClick(editButton)
sleep(1000)
}
if (AccessibilityUtil.findTextInput(getRoot(), groupAnnouncement)) {
LogUtils.d("群公告发布中: $groupAnnouncement")
sleep(500)
val button = AccessibilityUtil.findOneByText(getRoot(), "发布")
AccessibilityUtil.performClick(button)
sleep(1000)
val publishButtonList = getRoot().findAccessibilityNodeInfosByText("发布")
if (publishButtonList.size >= 2) {
AccessibilityUtil.performClick(publishButtonList[1])
}
sleep(3000)
} else {
LogUtils.e("无法进行群公告发布和编辑: ")
return false
}
} else {
LogUtils.e("未找到群公告按钮")
return false
}
}
return true
}
/**
* 发送消息
*/
private fun sendChatMessage(text: String, prefix: String = "") {
val editText = AccessibilityUtil.findOneByClazz(getRoot(), Views.EditText)
if (editText != null) {
AccessibilityUtil.editTextInput(editText, prefix + text)
//输入完文字等待出现发送按钮
sleep(500)
} else {
LogUtils.e("未找到输入框")
error("未找到输入框")
}
var index = 0
while (index++ < 5) {
val buttonList = getRoot().findAccessibilityNodeInfosByText("发送")
var sendButton: AccessibilityNodeInfo? = null
for (button in buttonList) {
if (button.className == Views.Button) {
sendButton = button
}
}
if (sendButton != null) {
LogUtils.i("发送消息: \n$text")
log("发送消息: \n$text")
AccessibilityUtil.performClick(sendButton)
sleep(500)
break
} else {
LogUtils.e("未找到发送按钮")
error("未找到发送按钮")
}
sleep(500)
}
}
}

View File

@@ -0,0 +1,122 @@
package org.yameida.worktool.service
import android.accessibilityservice.AccessibilityService
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import androidx.annotation.RequiresApi
import com.blankj.utilcode.util.*
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import org.yameida.worktool.Constant
import org.yameida.worktool.Demo
import org.yameida.worktool.config.WebConfig
import org.yameida.worktool.utils.*
import java.lang.Exception
/**
* 企业微信辅助服务
* rootInActiveWindow获取的是当前交互界面窗口的根view 需要验证包名
* event.source则不需要验证包名获取窗口并可获得事件详情
*/
class WeworkService : AccessibilityService() {
lateinit var webSocketManager: WebSocketManager
override fun onServiceConnected() {
LogUtils.i("初始化成功")
//隐藏软键盘模式
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
softKeyboardController.showMode = SHOW_MODE_HIDDEN
}
WeworkController.weworkService = this
//初始化长连接
initWebSocket()
//初始化消息处理器
MyLooper.init()
//开发者可以在这里添加测试代码 启动时调用一次
Demo.test(AppUtils.isAppDebug())
//监听是否修改链接号并重新长连接
registerReceiver(object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.getStringExtra("type") == "modify_channel") {
LogUtils.e("更新channel")
webSocketManager.close(1000, "modify_channel")
initWebSocket()
}
}
}, IntentFilter(WebConfig.WEWORK_NOTIFY))
}
private fun initWebSocket() {
val url =
WebConfig.WEWORK_URL + SPUtils.getInstance().getString(WebConfig.LISTEN_CHANNEL_ID)
val listener = EchoWebSocketListener()
LogUtils.d("initWebSocket: $url")
webSocketManager = WebSocketManager(url, listener)
}
/**
* TYPE_WINDOW_CONTENT_CHANGED 内容变化
* TYPE_VIEW_SCROLLED 列表滚动
* @param event
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
override fun onAccessibilityEvent(event: AccessibilityEvent) {
}
override fun onInterrupt() {
LogUtils.i("onInterrupt")
}
override fun onDestroy() {
super.onDestroy()
LogUtils.i("onDestroy")
//隐藏软键盘模式
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
softKeyboardController.showMode = SHOW_MODE_AUTO
}
webSocketManager.close(1000, "service Destroy")
}
class EchoWebSocketListener() : WebSocketListener() {
private lateinit var socket: WebSocket
override fun onOpen(webSocket: WebSocket, response: Response) {
socket = webSocket
Log.e("ChatMaskActivityListener", "链接建立")
}
override fun onMessage(webSocket: WebSocket, text: String) {
LogUtils.i("onMessage: $text")
try {
MyLooper.onMessage(webSocket, text)
} catch (e: Exception) {
LogUtils.e(e)
error(e.message)
}
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
super.onClosed(webSocket, code, reason)
//服务器关闭后
Log.e("ChatMaskActivityListener", "链接关闭 $reason")
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
super.onClosing(webSocket, code, reason)
socket.close(code, reason)
Log.e("ChatMaskActivityListener", "服务端关闭连接 $code: $reason")
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
//服务器中断
Log.e("ChatMaskActivityListener", "链接错误: " + t.toString() + response.toString())
}
}
}

View File

@@ -0,0 +1,480 @@
package org.yameida.worktool.utils
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.GestureDescription
import android.annotation.TargetApi
import android.app.Notification
import android.app.PendingIntent
import android.graphics.Path
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
/**
* 1.查询类
* findOneByClazz 按类名寻找节点和子节点内的一个匹配项
* findAllByClazz 按类名寻找节点和子节点内的所有匹配项
* findFrontNode 查找节点的前兄弟节点
* findBackNode 查找节点的后兄弟节点
* findCanScrollNode 返回可滚动元素集合
* findOneByDesc 按描述寻找节点和子节点内的一个匹配项
* findOneByText 按文本(关键词)寻找节点和子节点内的一个匹配项
* findAllByText 按文本(关键词)寻找节点和子节点内的所有匹配项
*
* 2.全局操作
* globalGoBack 回退
* globalGoHome 回桌面
*
* 3.窗口操作
* printNodeClazzTree 深度搜索打印节点及其子节点
* performScrollUp 对某个节点向上滚动 未生效
* performScrollDown 对某个节点向下滚动 未生效
* performClick 对某个节点进行点击
* performXYClick 输入x, y坐标模拟点击事件
* editTextInput 编辑EditView(非粘贴 推荐)
* findTextAndClick 寻找第一个文本匹配(关键词)并点击
* findTextInput 寻找第一个EditView编辑框并输入文本
* findListOneAndClick 寻找第一个列表并点击指定条目(默认点击第一个条目)
* scrollAndFindByText 滚动并按文本寻找第一个控件
* performClickWithSon 对某个节点或子节点进行点击
* performLongClick 对某个节点或父节点进行长按
* performLongClickWithSon 对某个节点或子节点进行长按
*
*/
object AccessibilityUtil {
private const val tag = "AccessibilityUtil"
//编辑EditView(非粘贴 推荐)
fun editTextInput(nodeInfo: AccessibilityNodeInfo?, text: String): Boolean {
val nodeInfo: AccessibilityNodeInfo = nodeInfo ?: return false
val arguments = Bundle()
arguments.putCharSequence(
AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
text
)
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
return true
}
//寻找第一个文本匹配(关键词)并点击
fun findTextAndClick(nodeInfo: AccessibilityNodeInfo?, text: String): Boolean {
val textView = findOneByText(nodeInfo, text) ?: return false
return performClick(textView)
}
//寻找第一个EditView编辑框并输入文本
fun findTextInput(nodeInfo: AccessibilityNodeInfo?, text: String): Boolean {
val nodeInfo: AccessibilityNodeInfo = nodeInfo ?: return false
val editText = findOneByClazz(nodeInfo, "android.widget.EditText") ?: return false
val arguments = Bundle()
arguments.putCharSequence(
AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
text
)
editText.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
return true
}
//寻找第一个列表并点击指定条目(默认点击第一个条目)
fun findListOneAndClick(nodeInfo: AccessibilityNodeInfo, index: Int = 0): Boolean {
val rv = findOneByClazz(nodeInfo, "androidx.recyclerview.widget.RecyclerView")
val lv = findOneByClazz(nodeInfo, "android.widget.ListView")
if (rv == null && lv == null) return false
if (rv != null && rv.childCount > index) {
performClick(rv.getChild(index))
} else if (lv != null && lv.childCount > index) {
performClick(lv.getChild(index))
}
return true
}
//滚动并按文本寻找第一个控件
fun scrollAndFindByText(
nodeInfo: AccessibilityNodeInfo,
text: String,
maxRetry: Int = 3
): AccessibilityNodeInfo? {
var index = 0
while (index++ < maxRetry) {
performScrollUp(nodeInfo, 0)
Thread.sleep(300)
val node = findOneByText(nodeInfo, text)
if (node != null) {
return node
}
}
while (index++ < maxRetry * 2) {
performScrollDown(nodeInfo, 0)
Thread.sleep(300)
val node = findOneByText(nodeInfo, text)
if (node != null) {
return node
}
}
return null
}
//输入x, y坐标模拟点击事件
@TargetApi(Build.VERSION_CODES.N)
fun performXYClick(service: AccessibilityService, x: Float, y: Float) {
val path = Path()
path.moveTo(x, y)
val builder = GestureDescription.Builder()
builder.addStroke(GestureDescription.StrokeDescription(path, 0, 1))
val gestureDescription = builder.build()
service.dispatchGesture(
gestureDescription,
object : AccessibilityService.GestureResultCallback() {
override fun onCompleted(gestureDescription: GestureDescription) {
super.onCompleted(gestureDescription)
//Log.i(Constant.TAG, "onCompleted: completed");
}
override fun onCancelled(gestureDescription: GestureDescription) {
super.onCancelled(gestureDescription)
//Log.i(Constant.TAG, "onCancelled: cancelled");
}
},
null
)
}
/**
* 对某个节点或父节点进行点击
*/
fun performClick(nodeInfo: AccessibilityNodeInfo?): Boolean {
var nodeInfo: AccessibilityNodeInfo? = nodeInfo ?: return false
while (nodeInfo != null) {
if (nodeInfo.isClickable) {
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK)
return true
}
nodeInfo = nodeInfo.parent
}
return false
}
/**
* 对某个节点或子节点进行点击
*/
fun performClickWithSon(nodeInfo: AccessibilityNodeInfo?): Boolean {
var nodeInfo: AccessibilityNodeInfo? = nodeInfo ?: return false
while (nodeInfo != null) {
if (nodeInfo.isClickable) {
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK)
return true
}
if (nodeInfo.childCount > 0) {
for (i in 0 until nodeInfo.childCount) {
if (performClickWithSon(nodeInfo.getChild(i))) {
return true
}
}
} else {
nodeInfo = null
}
}
return false
}
/**
* 对某个节点或父节点进行长按
*/
fun performLongClick(nodeInfo: AccessibilityNodeInfo?): Boolean {
var nodeInfo: AccessibilityNodeInfo? = nodeInfo ?: return false
while (nodeInfo != null) {
if (nodeInfo.isLongClickable) {
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)
return true
}
nodeInfo = nodeInfo.parent
}
return false
}
/**
* 对某个节点或子节点进行长按
*/
fun performLongClickWithSon(nodeInfo: AccessibilityNodeInfo?): Boolean {
var nodeInfo: AccessibilityNodeInfo? = nodeInfo ?: return false
while (nodeInfo != null) {
if (nodeInfo.isLongClickable) {
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)
return true
}
if (nodeInfo.childCount > 0) {
for (i in 0 until nodeInfo.childCount) {
if (performLongClickWithSon(nodeInfo.getChild(i))) {
return true
}
}
} else {
nodeInfo = null
}
}
return false
}
//对某个节点向上滚动
fun performScrollUp(nodeInfo: AccessibilityNodeInfo?): Boolean {
var nodeInfo: AccessibilityNodeInfo? = nodeInfo ?: return false
while (nodeInfo != null) {
if (nodeInfo.isScrollable) {
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD)
return true
}
nodeInfo = nodeInfo.parent
}
return false
}
//对某个节点或父节点向下滚动
fun performScrollDown(node: AccessibilityNodeInfo?): Boolean {
var node: AccessibilityNodeInfo? = node ?: return false
while (node != null) {
if (node.isScrollable) {
node.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
return true
}
node = node.parent
}
return false
}
//对第几个节点向上滚动
fun performScrollUp(node: AccessibilityNodeInfo?, index: Int): Boolean {
if (node == null) return false
val canScrollNodeList = findCanScrollNode(node)
if (canScrollNodeList.size > index) {
canScrollNodeList[index].performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD)
return true
}
return false
}
//对第几个节点向下滚动
fun performScrollDown(node: AccessibilityNodeInfo?, index: Int): Boolean {
if (node == null) return false
val canScrollNodeList = findCanScrollNode(node)
if (canScrollNodeList.size > index) {
canScrollNodeList[index].performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
return true
}
return false
}
//返回可滚动元素集合
fun findCanScrollNode(
node: AccessibilityNodeInfo?,
list: ArrayList<AccessibilityNodeInfo> = ArrayList()
): ArrayList<AccessibilityNodeInfo> {
if (node == null) return list
if (node.isScrollable) list.add(node)
for (i in 0 until node.childCount) {
findCanScrollNode(node.getChild(i), list)
}
return list
}
//通知栏事件进入应用
fun gotoApp(event: AccessibilityEvent) {
val data = event.parcelableData
if (data != null && data is Notification) {
val intent = data.contentIntent
try {
intent.send()
} catch (e: PendingIntent.CanceledException) {
e.printStackTrace()
}
}
}
//回退
fun globalGoBack(service: AccessibilityService) {
service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)
}
//回首页
fun globalGoHome(service: AccessibilityService) {
service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME)
}
//按描述寻找节点和子节点内的一个匹配项
fun findOneByDesc(
node: AccessibilityNodeInfo?,
desc: String,
desc2: String? = null
): AccessibilityNodeInfo? {
if (node == null) return null
val description = node.contentDescription?.toString()
if (description == desc || (desc2 != null && description == desc2)) {
return node
}
for (i in 0 until node.childCount) {
val result = findOneByDesc(node.getChild(i), desc, desc2)
if (result != null) return result
}
return null
}
//按文本(关键词)寻找节点和子节点内的一个匹配项
fun findOneByText(
node: AccessibilityNodeInfo?,
text: String,
text2: String? = null
): AccessibilityNodeInfo? {
if (node == null) return null
val textViewList = node.findAccessibilityNodeInfosByText(text)
if (textViewList != null && textViewList.size > 0) {
for (textView in textViewList) {
if (textView.text == text) {
return textView
}
}
return textViewList[0]
} else if (text2 != null)
return findOneByText(node, text2)
return null
}
//按文本(关键词)寻找节点和子节点内的所有匹配项
fun findAllByText(
node: AccessibilityNodeInfo?,
text: String,
text2: String? = null
): List<AccessibilityNodeInfo> {
if (node == null) return arrayListOf()
val textViewList = node.findAccessibilityNodeInfosByText(text)
if (textViewList != null && textViewList.size > 0)
return textViewList
else if (text2 != null)
return findAllByText(node, text2)
return arrayListOf()
}
/**
* 按类名寻找节点和子节点内的一个匹配项
* node 节点
* clazz 类名
* limitDepth 深度 限制深度搜索深度必须匹配提供值且类名相同才返回 不填默认不限制
*/
fun findOneByClazz(
node: AccessibilityNodeInfo?,
clazz: String,
limitDepth: Int? = null,
depth: Int = 0
): AccessibilityNodeInfo? {
if (node == null) return null
// Log.d(tag, "node.className: " + node.className)
if (node.className == clazz) {
if (limitDepth == null || limitDepth == depth)
return node
}
for (i in 0 until node.childCount) {
val result = findOneByClazz(node.getChild(i), clazz, limitDepth, depth + 1)
if (result != null) return result
}
return null
}
/**
* 按类名寻找节点和子节点内的所有匹配项
* node 节点
* clazz 类名
* limitDepth 深度 限制深度搜索深度必须匹配提供值且类名相同才返回 不填默认不限制
*/
fun findAllByClazz(
node: AccessibilityNodeInfo?,
clazz: String,
list: ArrayList<AccessibilityNodeInfo> = ArrayList()
): ArrayList<AccessibilityNodeInfo> {
if (node == null) return list
// Log.d(tag, "node.className: " + node.className)
if (node.className == clazz) list.add(node)
for (i in 0 until node.childCount) {
findAllByClazz(node.getChild(i), clazz, list)
}
return list
}
/**
* 查找节点的前兄弟节点
* node 节点
*/
fun findFrontNode(node: AccessibilityNodeInfo?): AccessibilityNodeInfo? {
if (node == null) return null
// Log.d(tag, "node.className: " + node.className)
var parent: AccessibilityNodeInfo? = node.parent
var son: AccessibilityNodeInfo? = node
while (parent != null) {
var index = -1
for (i in 0 until parent.childCount) {
if (parent.getChild(i) == son) {
index = i
break
}
}
if (index > 0) {
return parent.getChild(index - 1)
}
son = parent
parent = parent.parent
}
return null
}
/**
* 查找节点的后兄弟节点
* node 节点
*/
fun findBackNode(node: AccessibilityNodeInfo?): AccessibilityNodeInfo? {
if (node == null) return null
// Log.d(tag, "node.className: " + node.className)
var parent: AccessibilityNodeInfo? = node.parent
var son: AccessibilityNodeInfo? = node
while (parent != null) {
var index = -1
for (i in 0 until parent.childCount) {
if (parent.getChild(i) == son) {
index = i
break
}
}
if (index < parent.childCount - 1) {
return parent.getChild(index + 1)
}
son = parent
parent = parent.parent
}
return null
}
/**
* 深度搜索打印节点及其子节点
* node 节点
*/
fun printNodeClazzTree(
node: AccessibilityNodeInfo?,
printText: Boolean = true,
depth: Int = 0
) {
if (node == null) return
var s = ""
for (i in 0 until depth) {
s += "---"
}
Log.d(tag, "$s depth: $depth className: " + node.className)
if (printText && node.text != null) {
Log.d(tag, "$s depth: $depth text: " + node.text)
}
if (printText && node.contentDescription != null) {
Log.d(tag, "$s depth: $depth desc: " + node.contentDescription)
}
for (i in 0 until node.childCount) {
printNodeClazzTree(node.getChild(i), printText, depth + 1)
}
}
}

View File

@@ -0,0 +1,51 @@
package org.yameida.worktool.utils
import okhttp3.*
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
import javax.net.ssl.*
object OkHttpUtil {
val okHttpClient = getUnsafeOkHttpClient()
private fun getUnsafeOkHttpClient(): OkHttpClient {
try {
// Create a trust manager that does not validate certificate chains
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
override fun getAcceptedIssuers(): Array<X509Certificate> {
return arrayOf()
}
@Throws(CertificateException::class)
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
}
@Throws(CertificateException::class)
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
}
})
// Install the all-trusting trust manager
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, trustAllCerts, java.security.SecureRandom())
// Create an ssl socket factory with our all-trusting manager
val sslSocketFactory = sslContext.socketFactory
val builder = OkHttpClient.Builder()
builder.sslSocketFactory(sslSocketFactory)
builder.hostnameVerifier(object : HostnameVerifier {
override fun verify(hostname: String, session: SSLSession): Boolean {
return true
}
})
builder.readTimeout(20, TimeUnit.SECONDS)
builder.writeTimeout(20, TimeUnit.SECONDS)
return builder.build()
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}

View File

@@ -0,0 +1,66 @@
package org.yameida.worktool.utils
import com.blankj.utilcode.util.AppUtils
import com.blankj.utilcode.util.GsonUtils
import com.blankj.utilcode.util.LogUtils
import com.blankj.utilcode.util.ToastUtils
import com.lzy.okgo.OkGo
import com.lzy.okgo.callback.StringCallback
import com.lzy.okgo.model.Response
import model.UpdateConfig
import org.yameida.worktool.Constant
import org.yameida.worktool.R
import org.yameida.worktool.model.network.CheckUpdateResult
import update.UpdateAppUtils
object UpdateUtil {
fun checkUpdate() {
// val remoteVersionCode = 10
// val remoteVersionName = "1.0.1"
// val forceUpdate = false
// val updateLog = "修复Bug\n优化用户体验"
// val downloadUrl = "https://down.qq.com/qqweb/QQ_1/android_apk/Android_8.5.0.5025_537066738.apk"
// val fileMD5 = "560017dc94e8f9b65f4ca997c7feb326"
OkGo.get<String>(Constant.URL_CHECK_UPDATE)
.execute(object : StringCallback() {
override fun onSuccess(response: Response<String>) {
val commonResult =
GsonUtils.fromJson(
response.body(),
CheckUpdateResult::class.java
)
if (commonResult.code != 200) {
return onError(response)
}
LogUtils.i(commonResult.data)
commonResult.data?.apply {
if (AppUtils.getAppVersionCode() < this.versionCode) {
UpdateAppUtils
.getInstance()
.apkUrl(this.downloadUrl)
.updateTitle(this.title)
.updateContent(this.updateLog.replace("\\n", "\n"))
.updateConfig(
UpdateConfig(
force = AppUtils.getAppVersionCode() < this.minVersionCode,
serverVersionName = this.versionName,
serverVersionCode = this.versionCode
)
)
.update()
} else {
ToastUtils.showShort(R.string.update_no_update)
}
return
}
ToastUtils.showLong(R.string.update_failed)
}
override fun onError(response: Response<String>) {
ToastUtils.showLong(R.string.update_failed)
LogUtils.e("检查更新失败")
}
})
}
}

View File

@@ -0,0 +1,15 @@
package org.yameida.worktool.utils;
public class Views {
public static String View = "android.view.View";
public static String ListView = "android.widget.ListView";
public static String TextView = "android.widget.TextView";
public static String Button = "android.widget.Button";
public static String EditText = "android.widget.EditText";
public static String ViewGroup = "android.view.ViewGroup";
public static String RecyclerView = "androidx.recyclerview.widget.RecyclerView";
public static String ImageView = "android.widget.ImageView";
public static String GridView = "android.widget.GridView";
public static String RelativeLayout = "android.widget.RelativeLayout";
public static String LinearLayout = "android.widget.LinearLayout";
}

View File

@@ -0,0 +1,154 @@
package org.yameida.worktool.utils;
import android.util.Log;
import com.blankj.utilcode.util.GsonUtils;
import com.blankj.utilcode.util.LogUtils;
import org.yameida.worktool.model.WeworkMessageBean;
import org.yameida.worktool.model.WeworkMessageListBean;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
public class WebSocketManager {
public static final String HEARTBEAT = "{" +
"\"type\": " + WeworkMessageBean.HEART_BEAT +
",\"hearBeat\": \"心跳检测\"" +
"}";
private static final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
public static Map<String, WebSocketManager> webSocketManager = new ConcurrentHashMap<>();
private static final int reconnectTimes = 7;
private static final int reconnectInt = 5000; //毫秒
private static final long heartBeatRate = 15; //秒
private Map<String, Long> messageIdMap = new ConcurrentHashMap<>();
private ScheduledFuture task;
private WebSocket socket;
private String url;
private WebSocketListener listener;
private boolean connecting = false;
public WebSocketManager(String url, WebSocketListener listener) {
Log.e(url, "新建链接");
this.url = url;
this.listener = listener;
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(url).build();
this.socket = client.newWebSocket(request, listener);
webSocketManager.put(url, this);
task = heartCheckStart();
}
public void send(WeworkMessageBean msg) {
send(new WeworkMessageListBean(msg, WeworkMessageListBean.SOCKET_TYPE_MESSAGE_LIST));
}
/**
* 确认消息
* @param messageId 保存在map中30秒后清除
* @return true继续消费事件 false重复事件不需消息
*/
public synchronized boolean confirm(String messageId) {
if (messageId == null || messageId.isEmpty()) return true;
send(new WeworkMessageListBean(messageId, WeworkMessageListBean.SOCKET_TYPE_MESSAGE_CONFIRM));
if (messageIdMap.containsKey(messageId)) return false;
long currentTimeMillis = System.currentTimeMillis();
messageIdMap.put(messageId, currentTimeMillis + 30 * 1000);
for (Map.Entry<String, Long> entry : messageIdMap.entrySet()) {
String key = entry.getKey();
Long value = entry.getValue();
if (currentTimeMillis > value) {
messageIdMap.remove(key);
}
}
return true;
}
public void send(WeworkMessageListBean msg) {
send(msg, false);
}
public void send(WeworkMessageListBean msg, boolean log) {
String json = GsonUtils.toJson(msg);
boolean suc = socket.send(json);
if (log)
LogUtils.v(url, json, (suc ? "通讯消息发送成功!" : "通讯消息发送失败!"));
else
LogUtils.e(url, json, (suc ? "通讯消息发送成功!" : "通讯消息发送失败!"));
}
public void send(String msg) {
boolean suc = socket.send(msg);
LogUtils.e(url, msg, (suc ? "通讯消息发送成功!" : "通讯消息发送失败!"));
}
public void close(int code, String reason) {
task.cancel(true);
Log.e("url", "task 取消");
this.socket.close(code, reason);
Log.e(url, "链接关闭");
}
public static void closeManager() {
Log.e("SocketManager", "关闭Manager:");
for (Map.Entry<String, WebSocketManager> e : webSocketManager.entrySet()) {
e.getValue().close(1000, "closeAll");
}
webSocketManager.clear();
scheduledExecutorService.shutdown();
}
public void reConnect() {
connecting = true;
Log.e(url, "重连");
boolean isConnect = false;
while (!isConnect) {
try {
isConnect = connect();
Thread.sleep(reconnectInt);
} catch (Exception e) {
e.printStackTrace();
}
}
connecting = false;
}
private boolean connect() {
WebSocket s = new OkHttpClient().newWebSocket(new Request.Builder().url(url).build(), listener);
if (s.send(WebSocketManager.HEARTBEAT)) {
this.socket = s;
return true;
}
return false;
}
private ScheduledFuture heartCheckStart() {
Runnable r = () -> {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
Log.e(url, "心跳检测" + df.format(new Date()));// new Date()为获取当前系统时间
if (!connecting && (socket == null || !socket.send(HEARTBEAT))) {
reConnect();
}
};
//每heartBeatRate秒发一次心跳包
return scheduledExecutorService.scheduleAtFixedRate(r, heartBeatRate, heartBeatRate, TimeUnit.SECONDS);
}
public static WebSocketManager getWebSocketManager(String id) {
return webSocketManager.get(id);
}
}

View File

@@ -0,0 +1,250 @@
package org.yameida.worktool.utils
import android.view.accessibility.AccessibilityNodeInfo
import org.yameida.worktool.utils.AccessibilityUtil.findOneByClazz
import org.yameida.worktool.utils.AccessibilityUtil.findFrontNode
import org.yameida.worktool.model.WeworkMessageBean
import com.blankj.utilcode.util.LogUtils
import org.yameida.worktool.service.backPress
import org.yameida.worktool.service.getRoot
import org.yameida.worktool.service.goHome
import org.yameida.worktool.service.sleep
import org.yameida.worktool.utils.AccessibilityUtil.findAllByClazz
/**
* 房间特征分析工具类
*/
object WeworkRoomUtil {
/**
* 房间类型 ROOM_TYPE
* @see WeworkMessageBean.ROOM_TYPE
*/
fun getRoomType(root: AccessibilityNodeInfo): Int {
when {
isExternalSingleChat(root) -> {
LogUtils.d("ROOM_TYPE: ROOM_TYPE_EXTERNAL_CONTACT")
return WeworkMessageBean.ROOM_TYPE_EXTERNAL_CONTACT
}
isGroupChat(root) -> {
return if (isExternalGroup(root)) {
LogUtils.d("ROOM_TYPE: ROOM_TYPE_EXTERNAL_GROUP")
WeworkMessageBean.ROOM_TYPE_EXTERNAL_GROUP
} else {
LogUtils.d("ROOM_TYPE: ROOM_TYPE_INTERNAL_GROUP")
WeworkMessageBean.ROOM_TYPE_INTERNAL_GROUP
}
}
isSingleChat(root) -> {
LogUtils.d("ROOM_TYPE: ROOM_TYPE_INTERNAL_CONTACT")
return WeworkMessageBean.ROOM_TYPE_INTERNAL_CONTACT
}
else -> {
LogUtils.d("ROOM_TYPE: ROOM_TYPE_UNKNOWN")
return WeworkMessageBean.ROOM_TYPE_UNKNOWN
}
}
}
/**
* 房间类型
* @see WeworkMessageBean.ROOM_TYPE_UNKNOWN
* @see WeworkMessageBean.ROOM_TYPE_EXTERNAL_GROUP
* @see WeworkMessageBean.ROOM_TYPE_EXTERNAL_CONTACT
* @see WeworkMessageBean.ROOM_TYPE_INTERNAL_GROUP
* @see WeworkMessageBean.ROOM_TYPE_INTERNAL_CONTACT
*/
fun getRoomTitle(root: AccessibilityNodeInfo): ArrayList<String> {
val titleList = arrayListOf<String>()
val list = findOneByClazz(root, Views.ListView)
if (list != null) {
val frontNode = findFrontNode(list.parent.parent)
val textViewList = findAllByClazz(frontNode, Views.TextView)
for (textView in textViewList) {
if (!textView.text.isNullOrBlank()) {
titleList.add(textView.text.toString().replace("\\(\\d+\\)$".toRegex(), ""))
}
}
}
LogUtils.d("getRoomTitle: ", titleList)
return titleList
}
/**
* 进入房间(单聊或群聊)
*/
fun intoRoom(title: String): Boolean {
LogUtils.d("intoRoom(): $title")
val titleList = getRoomTitle(getRoot())
val roomType = getRoomType(getRoot())
if (roomType != WeworkMessageBean.ROOM_TYPE_UNKNOWN
&& titleList.count { it.replace("", "") == title.replace("", "") } > 0) {
LogUtils.d("当前正在房间")
return true
}
goHome()
val list = findOneByClazz(getRoot(), Views.ListView)
if (list != null) {
val frontNode = findFrontNode(list)
val textViewList = findAllByClazz(frontNode, Views.TextView)
if (textViewList.size >= 2) {
val searchButton: AccessibilityNodeInfo = textViewList[textViewList.size - 2]
val multiButton: AccessibilityNodeInfo = textViewList[textViewList.size - 1]
AccessibilityUtil.performClick(searchButton)
sleep(1000)
AccessibilityUtil.findTextInput(getRoot(), title.replace("", ""))
sleep(1000)
var selectListView: AccessibilityNodeInfo? = null
while (selectListView == null) {
LogUtils.d("未找到搜索结果列表")
selectListView = findOneByClazz(getRoot(), Views.ListView)
sleep(500)
}
val imageView = findOneByClazz(selectListView, Views.ImageView)
if (imageView != null) {
AccessibilityUtil.performClick(imageView)
}
sleep(2000)
return true
} else {
LogUtils.e("未找到搜索按钮")
}
}
LogUtils.e("未找到聊天列表")
return false
}
/**
* 进入群管理页
* @return true 成功进入群管理页
*/
fun intoGroupManager(): Boolean {
if (AccessibilityUtil.findOneByText(
getRoot(),
"由企业微信用户创建,可邀请微信用户进群",
"该群由企业微信用户创建"
) != null) {
return true
}
val list = findOneByClazz(getRoot(), Views.ListView)
if (list != null) {
val frontNode = AccessibilityUtil.findFrontNode(list.parent.parent)
val textViewList = findAllByClazz(frontNode, Views.TextView)
if (textViewList.size >= 2) {
val multiButton = textViewList.lastOrNull()
AccessibilityUtil.performClick(multiButton)
sleep(2000)
return true
} else {
LogUtils.e("未找到群管理按钮")
}
}
return false
}
/**
* 进入好友详情页
* @return true 成功进入好友详情页
*/
fun intoFriendDetail(): Boolean {
if (AccessibilityUtil.findOneByText(getRoot(), "设置聊天背景") != null) {
return true
}
val list = findOneByClazz(getRoot(), Views.ListView)
if (list != null) {
val frontNode = AccessibilityUtil.findFrontNode(list.parent.parent)
val textViewList = findAllByClazz(frontNode, Views.TextView)
if (textViewList.size >= 2) {
val multiButton = textViewList.lastOrNull()
AccessibilityUtil.performClick(multiButton)
sleep(2000)
return true
} else {
LogUtils.e("未找到好友详情按钮")
}
}
return false
}
/**
* 获取当前聊天人姓名并返回房间
* 解决title为对方正在输入中问题
* @return name 单聊对方姓名
*/
fun getFriendName(): ArrayList<String> {
val titleList = arrayListOf<String>()
if (intoFriendDetail()) {
val gridView = findOneByClazz(getRoot(), Views.GridView)
if (gridView != null && gridView.childCount >= 2) {
val tvList = findAllByClazz(gridView.getChild(0), Views.TextView)
for (textView in tvList) {
if (textView.text != null) {
titleList.add(textView.text.toString())
backPress()
}
}
}
}
return titleList
}
/**
* 是否是群聊
* 群右上角有两个按钮(快速会议按钮、更多按钮)
*/
private fun isGroupChat(root: AccessibilityNodeInfo): Boolean {
val list = findOneByClazz(root, Views.ListView)
if (list != null) {
val frontNode = findFrontNode(list.parent.parent)
val textViewList = findAllByClazz(frontNode, Views.TextView)
if (textViewList.size >= 2) {
val buttonList = findAllByClazz(textViewList.last().parent.parent, Views.TextView)
return buttonList.size == 2
} else {
LogUtils.d("未找到群管理按钮")
}
} else {
LogUtils.d("未找到消息列表")
}
return false
}
/**
* 是否是外部群
* listview前兄弟控件 && text包含外部群
*/
private fun isExternalGroup(root: AccessibilityNodeInfo): Boolean {
val listView = findOneByClazz(root, Views.ListView, null, 0)
if (listView != null) {
val frontNode = findFrontNode(listView)
if (frontNode != null) {
val nodeList = frontNode.findAccessibilityNodeInfosByText("外部群")
return nodeList.size > 0
}
}
return false
}
/**
* 是否是单聊
* 有列表和输入框
*/
private fun isSingleChat(root: AccessibilityNodeInfo): Boolean {
val list = findOneByClazz(root, Views.ListView)
val editText = findOneByClazz(root, Views.EditText)
if (list != null && editText != null) {
return true
}
return false
}
/**
* 是否是外部单聊
* 姓名下面有@xx
*/
private fun isExternalSingleChat(root: AccessibilityNodeInfo): Boolean {
val roomTitle = getRoomTitle(root)
return roomTitle.size > 1 && roomTitle.count { it.matches("^[@].*?".toRegex()) } > 0
}
}

View File

@@ -0,0 +1,339 @@
package org.yameida.worktool.utils
import android.view.accessibility.AccessibilityNodeInfo
import com.blankj.utilcode.util.LogUtils
import org.yameida.worktool.model.WeworkMessageBean
import org.yameida.worktool.service.getRoot
import org.yameida.worktool.service.sleep
import org.yameida.worktool.utils.AccessibilityUtil.findOneByClazz
import org.yameida.worktool.utils.AccessibilityUtil.findAllByClazz
import java.util.*
/**
* 消息特征分析工具类
*/
object WeworkTextUtil {
/*
文字 1tv 0iv (文字)
depth: 0 className: android.widget.RelativeLayout
--- depth: 1 className: android.widget.RelativeLayout
------ depth: 2 className: android.widget.LinearLayout
--------- depth: 3 className: android.widget.LinearLayout
------------ depth: 4 className: android.widget.FrameLayout
--------------- depth: 5 className: android.widget.TextView
图片 0tv 1iv (图片)
depth: 0 className: android.widget.RelativeLayout
--- depth: 1 className: android.widget.RelativeLayout
------ depth: 2 className: android.widget.ImageView
视频 2tv 2iv (视频大小、视频时长、缩略图、播放按钮)
depth: 0 className: android.widget.RelativeLayout
--- depth: 1 className: android.widget.RelativeLayout
------ depth: 2 className: android.widget.RelativeLayout
--------- depth: 3 className: android.widget.RelativeLayout
------------ depth: 4 className: android.widget.ImageView
------------ depth: 4 className: android.widget.ImageView
------------ depth: 4 className: android.view.View
------------ depth: 4 className: android.widget.TextView
------------ depth: 4 className: android.widget.TextView
腾讯文档 2tv 2iv (标题、创建者、图标、缩略图) (需要和视频区分 视频子节点数>3)
depth: 0 className: android.widget.RelativeLayout
--- depth: 1 className: android.widget.LinearLayout
------ depth: 2 className: android.widget.LinearLayout
--------- depth: 3 className: android.widget.RelativeLayout
------------ depth: 4 className: android.widget.RelativeLayout
--------------- depth: 5 className: android.widget.LinearLayout
------------------ depth: 6 className: android.widget.LinearLayout
--------------------- depth: 7 className: android.widget.TextView
--------------------- depth: 7 className: android.widget.TextView
------------------ depth: 6 className: android.widget.ImageView
--------------- depth: 5 className: android.widget.ImageView
链接 3tv 1iv (标题、副标题、下方来源、图标)
发送纯文本但含链接被识别为网页 (纯文本、网页title、网址域名、图标)
depth: 0 className: android.widget.RelativeLayout
--- depth: 1 className: android.widget.LinearLayout
------ depth: 2 className: android.widget.RelativeLayout
--------- depth: 3 className: android.widget.RelativeLayout
------------ depth: 4 className: android.widget.TextView
------------ depth: 4 className: android.widget.RelativeLayout
--------------- depth: 5 className: android.widget.LinearLayout
------------------ depth: 6 className: android.widget.TextView
--------------- depth: 5 className: android.widget.ImageView
------ depth: 2 className: android.widget.RelativeLayout
--------- depth: 3 className: android.view.View
--------- depth: 3 className: android.widget.TextView
文件 3tv 1iv (文件名、文件大小、下方来源、图标) (需要和链接区分 文件大小特征匹配)
depth: 0 className: android.widget.RelativeLayout
--- depth: 1 className: android.widget.LinearLayout
------ depth: 2 className: android.widget.RelativeLayout
--------- depth: 3 className: android.widget.RelativeLayout
------------ depth: 4 className: android.widget.RelativeLayout
--------------- depth: 5 className: android.widget.TextView
--------------- depth: 5 className: android.widget.TextView
------------ depth: 4 className: android.widget.ImageView
------ depth: 2 className: android.widget.RelativeLayout
--------- depth: 3 className: android.view.View
--------- depth: 3 className: android.widget.TextView
小程序 3tv 2iv (标题、副标题、下方来源、小程序icon、图标)
depth: 0 className: android.widget.RelativeLayout
--- depth: 1 className: android.widget.LinearLayout
------ depth: 2 className: android.widget.LinearLayout
--------- depth: 3 className: android.widget.LinearLayout
------------ depth: 4 className: android.widget.ImageView
------------ depth: 4 className: android.widget.TextView
--------- depth: 3 className: android.widget.TextView
--------- depth: 3 className: android.widget.ImageView
------ depth: 2 className: android.widget.RelativeLayout
--------- depth: 3 className: android.view.View
--------- depth: 3 className: android.widget.TextView
合并聊天记录 2tv 0iv (标题、摘要)
depth: 0 className: android.widget.RelativeLayout
--- depth: 1 className: android.widget.LinearLayout
------ depth: 2 className: android.widget.RelativeLayout
--------- depth: 3 className: android.widget.RelativeLayout
------------ depth: 4 className: android.widget.TextView
------------ depth: 4 className: android.widget.TextView
收集表 6tv 0iv (标题、副标题、行1、行2、行3、下方来源)
depth: 0 className: android.widget.RelativeLayout
--- depth: 1 className: android.widget.LinearLayout
------ depth: 2 className: android.widget.TextView
------ depth: 2 className: android.widget.TextView
------ depth: 2 className: android.widget.LinearLayout
--------- depth: 3 className: android.widget.RelativeLayout
------------ depth: 4 className: android.widget.TextView
--------- depth: 3 className: android.widget.RelativeLayout
------------ depth: 4 className: android.widget.TextView
--------- depth: 3 className: android.widget.RelativeLayout
------------ depth: 4 className: android.widget.TextView
------ depth: 2 className: android.widget.RelativeLayout
--------- depth: 3 className: android.view.View
--------- depth: 3 className: android.widget.TextView
接龙 2tv 1iv (内容、接龙标识、跳转按钮)
depth: 0 className: android.widget.RelativeLayout
--- depth: 1 className: android.widget.RelativeLayout
------ depth: 2 className: android.widget.LinearLayout
--------- depth: 3 className: android.widget.LinearLayout
------------ depth: 4 className: android.widget.TextView
--------- depth: 3 className: android.widget.LinearLayout
------------ depth: 4 className: android.widget.TextView
------------ depth: 4 className: android.widget.ImageView
语音 4tv 2iv (空、语音时长、转写文字、转写状态、语音图片、转写图标)
depth: 0 className: android.widget.RelativeLayout
--- depth: 1 className: android.widget.LinearLayout
------ depth: 2 className: android.widget.RelativeLayout
--------- depth: 3 className: android.widget.RelativeLayout
------------ depth: 4 className: android.widget.TextView
------------ depth: 4 className: android.widget.RelativeLayout
--------------- depth: 5 className: android.widget.RelativeLayout
------------------ depth: 6 className: android.widget.ImageView
--------------- depth: 5 className: android.widget.ImageView
------------ depth: 4 className: android.widget.TextView
------ depth: 2 className: android.widget.RelativeLayout
--------- depth: 3 className: android.widget.TextView
--------- depth: 3 className: android.widget.TextView
名片 5tv 1iv (机构名、姓名、别名、职务、下方来源、头像)
depth: 0 className: android.widget.RelativeLayout
--- depth: 1 className: android.widget.RelativeLayout
------ depth: 2 className: android.widget.LinearLayout
--------- depth: 3 className: android.widget.TextView
--------- depth: 3 className: android.widget.LinearLayout
------------ depth: 4 className: android.widget.TextView
--------- depth: 3 className: android.widget.TextView
--------- depth: 3 className: android.widget.TextView
------ depth: 2 className: android.widget.ImageView
------ depth: 2 className: android.widget.RelativeLayout
--------- depth: 3 className: android.view.View
--------- depth: 3 className: android.widget.TextView
位置 1tv 2iv (地址、位置、定位柄)
depth: 0 className: android.widget.RelativeLayout
--- depth: 1 className: android.widget.RelativeLayout
------ depth: 2 className: android.widget.RelativeLayout
--------- depth: 3 className: android.widget.FrameLayout
------------ depth: 4 className: android.widget.ImageView
--------- depth: 3 className: android.widget.TextView
--------- depth: 3 className: android.widget.ImageView
带回复引用文本 3tv 0iv (引用发言人、引用发言内容、本次消息内容)
depth: 0 className: android.widget.RelativeLayout
--- depth: 1 className: android.widget.RelativeLayout
------ depth: 2 className: android.widget.LinearLayout
--------- depth: 3 className: android.widget.RelativeLayout
------------ depth: 4 className: android.widget.LinearLayout
--------------- depth: 5 className: android.widget.RelativeLayout
------------------ depth: 6 className: android.view.View
------------------ depth: 6 className: android.widget.RelativeLayout
--------------------- depth: 7 className: android.widget.TextView
--------------------- depth: 7 text: 企微RPA机器人
--------------------- depth: 7 className: android.widget.LinearLayout
------------------------ depth: 8 className: android.widget.RelativeLayout
--------------------------- depth: 9 className: android.widget.RelativeLayout
------------------------------ depth: 10 className: android.widget.TextView
------------------------------ depth: 10 text: 新公告
--------------- depth: 5 className: android.widget.TextView
--------------- depth: 5 text: 111
------------------------------总结------------------------------
图片 0tv 1iv (图片)
视频 2tv 2iv (视频大小、视频时长、缩略图、播放按钮)
链接 3tv 1iv (标题、副标题、下方来源、图标)
文件 3tv 1iv (文件名、文件大小、下方来源、图标) (需要和链接区分)
小程序 3tv 2iv (标题、副标题、下方来源、小程序icon、图标)
合并聊天记录 2tv 0iv (标题、摘要)
收集表 6tv 0iv (标题、副标题、行1、行2、行3、下方来源)
接龙 2tv 1iv (内容、接龙标识、跳转按钮)
语音 4tv 2iv (空、语音时长、转写文字、转写状态、语音图片、转写图标)
名片 5tv 1iv (机构名、姓名、别名、职务、下方来源、头像)
位置 1tv 2iv (地址、位置、定位柄)
带回复引用文本 1tv 2iv (引用发言人、引用发言内容、本次消息内容)
*/
/**
* 企微消息类型 TEXT_TYPE
* @see WeworkMessageBean.TEXT_TYPE
*/
fun getTextType(node: AccessibilityNodeInfo, isGroup: Boolean = true): Int {
val tvList = findAllByClazz(node, Views.TextView)
val tvCount = tvList.size
val ivCount = findAllByClazz(node, Views.ImageView).size
return when {
tvCount == 1 && ivCount == 0 -> WeworkMessageBean.TEXT_TYPE_PLAIN
tvCount == 0 && ivCount == 1 -> WeworkMessageBean.TEXT_TYPE_IMAGE
tvCount == 2 && ivCount == 2 -> {
if (tvList[0].parent.childCount > 3) {
WeworkMessageBean.TEXT_TYPE_VIDEO
} else {
WeworkMessageBean.TEXT_TYPE_OFFICE
}
}
tvCount == 3 && ivCount == 1 -> {
if (isFileSize(tvList[1].text?.toString())) {
WeworkMessageBean.TEXT_TYPE_FILE
} else {
WeworkMessageBean.TEXT_TYPE_LINK
}
}
tvCount == 3 && ivCount == 1 -> WeworkMessageBean.TEXT_TYPE_FILE
tvCount == 3 && ivCount == 2 -> WeworkMessageBean.TEXT_TYPE_MICROPROGRAM
tvCount == 2 && ivCount == 0 -> WeworkMessageBean.TEXT_TYPE_CHAT_RECORD
tvCount == 6 && ivCount == 0 -> WeworkMessageBean.TEXT_TYPE_COLLECTION
tvCount == 2 && ivCount == 1 -> WeworkMessageBean.TEXT_TYPE_SOLITAIRE
tvCount == 4 && ivCount == 2 -> WeworkMessageBean.TEXT_TYPE_VOICE
tvCount == 5 && ivCount == 1 -> WeworkMessageBean.TEXT_TYPE_CARD
tvCount == 1 && ivCount == 2 -> WeworkMessageBean.TEXT_TYPE_LOCATION
tvCount == 3 && ivCount == 0 -> WeworkMessageBean.TEXT_TYPE_REPLY
else -> WeworkMessageBean.TEXT_TYPE_UNKNOWN
}
}
/**
* 是否为消息上方时间
*/
fun isDate(date: String): Boolean {
return date.matches(".*?([上下]午)[\\s ]+?[0-9]+:[0-9]+".toRegex())
}
/**
* 是否为文件上方时间
*/
fun isFileSize(size: String?): Boolean {
return size?.matches("[0-9\\.]+[BKMG]".toRegex()) ?: false
}
/**
* 群聊 提取发言人昵称
* 适用于左侧发言者
* @param item 消息item节点
*/
fun getNameList(item: AccessibilityNodeInfo): List<String> {
val nameList = ArrayList<String>()
val node = findOneByClazz(item, Views.ViewGroup)
if (node != null) {
val textViewList = findAllByClazz(node, Views.TextView)
for (textView in textViewList) {
if (textView.text != null) {
nameList.add(textView.text.toString())
}
}
}
return nameList
}
/**
* 群聊 长按消息条目
* 复制、转发、回复、收藏、置顶、多选、日程、待办、翻译、删除
* @param node 消息列表节点
* @param replyTextType 带回复消息类型
* @param replyNick 待回复人姓名
* @param replyContent 待回复内容
* @param key 复制、转发、回复、收藏、多选
* @return true 进行了长按 否则 false
*/
fun longClickMessageItem(
node: AccessibilityNodeInfo?,
replyTextType: Int,
replyNick: String,
replyContent: String,
key: String
): Boolean {
if (node == null) return false
for (i in 0 until node.childCount) {
val item = node.getChild(i)
val nameList = getNameList(item)
for (name in nameList.reversed()) {
if (name == replyNick) {
val backNode = getMessageListNode(item)
if (backNode != null) {
val textNode = AccessibilityUtil.findOneByText(backNode, replyContent)
if (textNode != null) {
LogUtils.d("nameList: $nameList\nreplyContent: $replyContent")
return longClickMessageItem(item, key)
}
}
}
}
}
return false
}
private fun longClickMessageItem(item: AccessibilityNodeInfo, key: String): Boolean {
val backNode = getMessageListNode(item)
AccessibilityUtil.performLongClickWithSon(backNode)
sleep(1000)
val optionRvList = findAllByClazz(getRoot(), Views.RecyclerView)
for (optionRv in optionRvList) {
val optionTvList = findAllByClazz(optionRv, Views.TextView)
for (optionTv in optionTvList) {
if (optionTv.text == key) {
AccessibilityUtil.performClick(optionTv)
return true
}
}
}
return false
}
/**
* 群聊 提取消息主体框节点(昵称下面的气泡框)
* 适用于左侧发言者
* @param item 消息item节点
*/
private fun getMessageListNode(item: AccessibilityNodeInfo): AccessibilityNodeInfo? {
val node = findOneByClazz(item, Views.ViewGroup)
if (node != null) {
return AccessibilityUtil.findBackNode(node)
}
return null
}
}

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape>
<solid android:color="@color/color_dashen_passed" />
<corners android:radius="5dp" />
</shape>
</item>
<item android:state_enabled="false">
<shape>
<solid android:color="@android:color/transparent" />
<corners android:radius="5dp" />
<stroke android:width="1dp" android:color="@color/c8c8c8" />
</shape>
</item>
<item>
<shape>
<solid android:color="@color/color_dashen" />
<corners android:radius="5dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,292 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/setting_start_padding"
android:paddingTop="@dimen/setting_vertical_padding"
android:paddingEnd="@dimen/setting_end_start_padding"
android:paddingBottom="@dimen/setting_vertical_padding"
android:text="使用流程"
android:textColor="@color/float_time_color"
android:textSize="@dimen/setting_end_font_size"
android:textStyle="bold" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/setting_start_padding"
android:paddingTop="@dimen/setting_vertical_padding"
android:paddingEnd="@dimen/setting_end_padding"
android:paddingBottom="@dimen/setting_vertical_padding">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/setting_start_padding"
android:layout_marginEnd="@dimen/setting_end_padding"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="1.申请链接号后在下方填写并点击保存"
android:textColor="@color/color_333333"
android:textSize="@dimen/setting_start_font_size" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/setting_vertical_padding"
android:text="2.开启主功能在无障碍栏目里找到WorkTool并开启"
android:textColor="@color/color_333333"
android:textSize="@dimen/setting_start_font_size" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/setting_vertical_padding"
android:text="3.让本程序放在后台运行,点击进入企业微信保持手机不动屏幕常亮即可"
android:textColor="@color/color_333333"
android:textSize="@dimen/setting_start_font_size" />
</LinearLayout>
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/setting_start_padding"
android:paddingTop="@dimen/setting_vertical_padding"
android:paddingEnd="@dimen/setting_end_start_padding"
android:paddingBottom="@dimen/setting_vertical_padding"
android:text="环境信息"
android:textColor="@color/float_time_color"
android:textSize="@dimen/setting_end_font_size"
android:textStyle="bold" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/setting_start_padding"
android:paddingTop="@dimen/setting_vertical_padding"
android:paddingEnd="@dimen/setting_end_padding"
android:paddingBottom="@dimen/setting_vertical_padding">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/setting_start_padding"
android:layout_marginEnd="@dimen/setting_end_padding"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="当前运行host"
android:textColor="@color/color_333333"
android:textSize="@dimen/setting_start_font_size" />
<TextView
android:id="@+id/tv_host"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="127.0.0.1"
android:textColor="@color/color_999999"
android:textSize="@dimen/setting_end_font_size" />
</LinearLayout>
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/setting_start_padding"
android:paddingTop="@dimen/setting_vertical_padding"
android:paddingEnd="@dimen/setting_end_padding"
android:paddingBottom="@dimen/setting_vertical_padding">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/setting_start_padding"
android:layout_marginEnd="@dimen/setting_end_padding"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="当前运行版本"
android:textColor="@color/color_333333"
android:textSize="@dimen/setting_start_font_size" />
<TextView
android:id="@+id/tv_version"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="1.0"
android:textColor="@color/color_999999"
android:textSize="@dimen/setting_end_font_size" />
</LinearLayout>
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/setting_start_padding"
android:paddingTop="@dimen/setting_vertical_padding"
android:paddingEnd="@dimen/setting_end_start_padding"
android:paddingBottom="@dimen/setting_vertical_padding"
android:text="控制台"
android:textColor="@color/float_time_color"
android:textSize="@dimen/setting_end_font_size"
android:textStyle="bold" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/setting_start_padding"
android:paddingTop="@dimen/setting_vertical_padding"
android:paddingEnd="@dimen/setting_end_start_padding"
android:paddingBottom="@dimen/setting_vertical_padding">
<Switch
android:id="@+id/sw_accessibility"
android:layout_width="@dimen/setting_end_font_width"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/setting_end_start_padding" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/setting_start_padding"
android:layout_toStartOf="@id/sw_accessibility"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="开启主功能"
android:textColor="@color/color_333333"
android:textSize="@dimen/setting_start_font_size" />
</LinearLayout>
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/setting_start_padding"
android:paddingTop="@dimen/setting_vertical_padding"
android:paddingEnd="@dimen/setting_end_start_padding"
android:paddingBottom="@dimen/setting_vertical_padding">
<Switch
android:id="@+id/sw_encrypt"
android:layout_width="@dimen/setting_end_font_width"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/setting_end_start_padding" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/setting_start_padding"
android:layout_toStartOf="@id/sw_encrypt"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="开启通讯加密"
android:textColor="@color/color_333333"
android:textSize="@dimen/setting_start_font_size" />
</LinearLayout>
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/setting_start_padding"
android:paddingTop="@dimen/setting_vertical_padding"
android:paddingEnd="@dimen/setting_end_padding"
android:paddingBottom="@dimen/setting_vertical_padding">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/setting_start_padding"
android:layout_marginEnd="@dimen/setting_end_padding"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="链接号"
android:textColor="@color/color_333333"
android:textSize="@dimen/setting_start_font_size" />
<EditText
android:id="@+id/et_channel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入申请的链接号"
android:textColor="@color/color_999999"
android:textSize="@dimen/setting_end_font_size" />
</LinearLayout>
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="20dp"
android:orientation="vertical">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="50dp"
android:paddingEnd="50dp"
android:layout_gravity="center_horizontal"
android:background="@drawable/comment_red_btn"
android:id="@+id/bt_save"
android:text="保存" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</RelativeLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="background">@color/color_f5f5f5</color>
<color name="list_divider_line">@color/color_e5e5e5</color>
<color name="white">#fff</color>
<color name="color_323232">#323232</color>
<color name="color_333333">#333333</color>
<color name="color_666666">#666666</color>
<color name="color_969696">#969696</color>
<color name="color_999999">#999999</color>
<color name="color_b2000000">#b2000000</color>
<color name="playmate_tabWidget_text_color">#969696</color>
<color name="navigation_bar_bg">@color/color_f9f9f9</color>
<color name="navigation_bar_line">@color/color_cccccc</color>
<color name="greed_color">#ff5215</color>
<color name="black_text_color">#000000</color>
<color name="color_dashen">#ff5215</color>
<color name="color_dashen_passed">#e54b12</color>
<color name="white_80_percent">#ccffffff</color>
<color name="white_33_percent">#33ffffff</color>
<color name="black_33_percent">#33000000</color>
<color name="white_66_percent">#66ffffff</color>
<color name="black_66_percent">#66000000</color>
<color name="item_select_color">#ff5215</color>
<color name="select_game_search_bg_color">#e6e6e6</color>
<color name="float_time_color">#f58220</color>
<color name="float_time_text_color">#bd6758</color>
<color name="color_e5e5e5">#e5e5e5</color>
<color name="color_f5f5f5">#f5f5f5</color>
<color name="color_f9f9f9">#f9f9f9</color>
<color name="color_cccccc">#cccccc</color>
<color name="c8c8c8">#c8c8c8</color>
<color name="transparent">#00000000</color>
<color name="bar_gray">#37474F</color>
<color name="while_bg">#F7F7F7</color>
<color name="ip_color_primary_dark">#303135</color>
<color name="ip_color_primary_dark_alpha">#66000000</color>
<color name="ic_back_press">#323336</color>
<color name="balloonperformer_translucent">#a0000000</color>
<color name="tab_color">#848282</color>
<color name="tab_color_s">#0dd21d</color>
<color name="send_bt_color">#0cb319</color>
<color name="line_color">#beb9b9</color>
<color name="decoration_color">#EDEDED</color>
<color name="action_bar_color">#0d0c0c</color>
<color name="tiem_bg_color">#f4cac7c7</color>
<color name="dot_color">#ff0000</color>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#6200EE</color>
<color name="colorPrimaryDark">#000000</color>
<color name="colorAccent">#03DAC5</color>
</resources>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="size_text">15sp</dimen>
<dimen name="setting_gray_size">12sp</dimen>
<dimen name="setting_item_height">50dp</dimen>
<dimen name="setting_start_padding">22dp</dimen>
<dimen name="setting_end_start_padding">25dp</dimen>
<dimen name="setting_end_padding">10dp</dimen>
<dimen name="setting_vertical_padding">7dp</dimen>
<dimen name="setting_start_font_width">150dp</dimen>
<dimen name="setting_end_font_width">60dp</dimen>
<dimen name="setting_start_image_width">20dp</dimen>
<dimen name="size_button">18sp</dimen>
<dimen name="btn_top_bottom_padding">10dp</dimen>
<dimen name="btn_height">50dp</dimen>
<dimen name="play_btn_width_height">38dp</dimen>
<dimen name="home_tab_height">70dp</dimen>
<dimen name="float_size">60dp</dimen>
<dimen name="float_margin_start">50dp</dimen>
<dimen name="float_stand_size">36dp</dimen>
<dimen name="float_logo_size">39dp</dimen>
<dimen name="float_logo_margin">43dp</dimen>
<dimen name="float_margin">7dp</dimen>
<dimen name="float_start_image_width">30dp</dimen>
<dimen name="guide_notification_icon_size">16dp</dimen>
<dimen name="guide_notification_width_size">260dp</dimen>
<dimen name="setting_start_font_size">13sp</dimen>
<dimen name="setting_end_font_size">13sp</dimen>
</resources>

View File

@@ -0,0 +1,22 @@
<resources>
<string name="app_name">WorkTool</string>
<string name="accessibility_desc">WorkTool</string>
<string name="tips">如果需要使用或停止该应用,应用需打开 无障碍服务 进行权限设置</string>
<!-- 升级对话框 -->
<string name="update_title" translatable="false">发现新版本</string>
<string name="update_content" translatable="false">更新内容</string>
<string name="update_no" translatable="false">下次再说</string>
<string name="update_yes" translatable="false">立即更新</string>
<string name="update_permission_hint" translatable="false">必须先要授予权限才能正常下载更新哦</string>
<string name="update_status_start" translatable="false">正在下载</string>
<string name="update_status_running" translatable="false">下载中 %d%%</string>
<string name="update_status_successful" translatable="false">下载完成,点击安装</string>
<string name="update_status_failed" translatable="false">下载失败,点击重试</string>
<string name="update_failed" translatable="false">自动更新失败</string>
<string name="update_no_update" translatable="false">当前已是最新版本</string>
<string name="update_notification_channel_id" translatable="false">update</string>
<string name="update_notification_channel_name" translatable="false">升级通知</string>
</resources>

View File

@@ -0,0 +1,15 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
</resources>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/accessibility_desc"
android:packageNames="com.tencent.wework"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagIncludeNotImportantViews"
android:accessibilityFeedbackType="feedbackAllMask"
android:notificationTimeout="100"
android:canRetrieveWindowContent="true"
android:canPerformGestures="true"/>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>