uniapp 实现通话上传

703 阅读8分钟

记录一下前段时间一个项目解决方案,需求是销售人员在app内或者pc拨打客户电话,拨打完成后上传通话录音。类似于Ec。

实现思路

判断通话自动录音权限是否开启,让用户手动打开通话自动录音功能,然后获取相应的通话录音上传。(自动打开录音开关权限是不对第三方应用开放的,只能手动打开)

用户拨打电话时,定时检测通话记录,当通话记录多出后,说明电话已挂断。开始通过录音文件时间,电话名称等匹配,找到需要的录音文件进行上传。

代码实现

这其中有部分业务代码,主要重点关注 call.js, 这部分代码是实现的核心逻辑。

App.vue

入口页面

<script>
	import {
		permission
	} from '@/utils/router.js'
	import {
		checkSystemUpdate
	} from "@/utils/updateVersion"
	// #ifdef APP-PLUS
	import {
		checkSystemAuth,
		onCheckRecord
	} from '@/utils/call';
	import {
		onConnectPc,
		onWsSendMsgRunModel
	} from '@/utils/PcConnectApp'
	// #endif

	export default {
		globalData: {
			show: false,
			isDev: process.env.NODE_ENV !== 'production',
		},
		onLaunch: function() {
			permission()
			console.log('系统信息', uni.getSystemInfoSync());
			// #ifdef APP-PLUS
			checkSystemAuth()

			onConnectPc()
			// #endif
		},
		onShow: function() {
			console.log('App onShow');
			if (process.env.NODE_ENV === 'production') {
				checkSystemUpdate()
			}
			onCheckRecord()
			this.globalData.show = true
			onWsSendMsgRunModel(0)
		},
		onHide: function() {
			console.log('App onHide');
			this.globalData.show = false
			onWsSendMsgRunModel(1)
		}
	}
</script>

<style lang="scss">
	/* 注意要写在第一行,同时给style标签加入lang="scss"属性 */
	@import "@/uni_modules/uview-ui/index.scss";
	/* 公共样式 */
	@import "@/style/common.scss";
</style>

app权限控制

urils/router.js 此文件主要做权限控制,每次切换页面拉取最新用户数据,获取最新的按钮权限。

// 路由监听
import store from '@/store'
export const permission = function() {
	function watchRouter() {
		store.dispatch('getUserInfo')
	}
	uni.addInterceptor('navigateTo', { //监听跳转
		success(e) {
			watchRouter();
		}
	})
	uni.addInterceptor('redirectTo', { //监听关闭本页面跳转
		success(e) {
			watchRouter();
		}
	})
	uni.addInterceptor('switchTab', { //监听tabBar跳转
		success(e) {
			watchRouter();
		}
	})
	uni.addInterceptor('navigateBack', { //监听返回
		success(e) {
			watchRouter();
		}
	})
}

检测APP版本

urils/updateVersion.js

此文件主要是用来检测APP版本

/**
 * 更新APP版本
 */
import {
	baseUrl,
	header
} from '@/config/config.js'
import {
	closeApp,
	downApp
} from '@/utils/appPlus.js'

function showModal() {
	uni.showModal({
		title: '提示',
		content: '请下载最新版本APP',
		showCancel: false,
		confirmColor: '#1D85EB',
		success(res) {
			if (res.confirm) {
				downApp()
				closeApp()
			} else {
				downApp()
				closeApp()
			}
		}
	})
}

/**
 * 比较版本号
 */
export function compareVersions(version1, version2) {
	const v1 = version1.split(".");
	const v2 = version2.split(".");
	const len = Math.max(v1.length, v2.length);

	for (let i = 0; i < len; i++) {
		const num1 = parseInt(v1[i] || 0);
		const num2 = parseInt(v2[i] || 0);

		if (num1 > num2) {
			return 1;
		} else if (num1 < num2) {
			return -1;
		}
	}

	return 0;
}

/**
 * 是否更新
 */
export const getSysAndroidUpdate = async function() {
	console.log('getSysAndroidUpdate');
	return new Promise((resolve, reject) => {
		try {
			const res = uni.request({
				url: `${baseUrl/***`,
				method: 'POST',
				data: {
					'configKey': '**'
				},
				header,
				success(res) {
					console.log('sys.android.update', res);
					if (res.statusCode != 200) {
						resolve(true)
					} else {
						const d = res.data
						if (d.msg == "true") {
							resolve(true)
						} else {
							resolve(false)
						}
					}
				},
				fail(error) {
					console.log('fail', error);
					resolve(true)
				}
			})
		} catch (e) {
			console.log(e);
			resolve(true)
		}
	})
}

/**
 * 是否更新版本
 */
export const getSysAndroidVersion = function() {
	console.log('getSysAndroidVersion');
	return new Promise((resolve, reject) => {
		try {
			function showModalVersion() {
				uni.showModal({
					title: '提示',
					content: '您的APP版本过旧,请下载更新最新版本的APP',
					confirmColor: '#1D85EB',
					success(res) {
						if (res.confirm) {
							downApp()
						}
					}
				})
			}
			const res = uni.request({
				url: `${baseUrl}/**`,
				method: 'POST',
				data: {
					'configKey': '**'
				},
				header,
				success(res) {
					console.log('最新系统版本', res);
					const d = res.data
					const version = d.msg
					let storageVersion = plus.runtime.version
					console.log('本地版本', storageVersion);
					const isUpdate = compareVersions(version, storageVersion)
					if (isUpdate) {
						// 如果本地存储版本与最新版本不一致则执行下载最新app的逻辑
						resolve(true)
						showModalVersion()
					} else {
						resolve(false)
					}
				},
				fail(error) {
					console.log('fail', error);
					resolve(false)
				}
			})
		} catch (e) {
			console.log(e);
			rresolve(false)
		}
	})
}

/**
 * 检查是否更新系统版本
 */
export const checkSystemUpdate = async function() {
	const sysAndroidUpdate = await getSysAndroidUpdate()
	console.log('sysAndroidUpdate', sysAndroidUpdate);
	// 如果后台系统已设置为需要更新,那么不再检查系统版本,直接进行更新
	if (sysAndroidUpdate) {
		showModal()
		return
	}
	getSysAndroidVersion()
}

pc连接app 双向通信

此文件用来pc和app进行通信,保证通话状态统一。

需要引入 device-detector-js - npm (npmjs.com) 获取每个设备型号。

import {
	makePhone,
} from "@/utils/call.js";
import {
	getVendorCn
} from '@/utils/deviceVendors.js'
import store from "@/store/index.js"
import DeviceDetector from "device-detector-js";
import {
	webSocketBaseUrl
} from "@/config/config.js"
// #ifdef APP-PLUS
const systemInfo = uni.getSystemInfoSync()
const deviceBrand = getVendorCn()
console.log(deviceBrand);
const deviceDetector = new DeviceDetector();
const device = deviceDetector.parse(systemInfo.ua)
const model = device.device.model
const modelS = systemInfo.deviceModel
const phoneModel =
	`${deviceBrand}-${model || modelS}`
export let phoneMac = systemInfo.deviceId
let socketState = ''
// #endif

/**
 * 自动拨打电话
 */
export function onMakePhoneFunc(d) {
	if (d.operate == 1) {
		const phoneNumber = d.callOn
		store.commit('setCurrentTelInfo', {
			phoneNumber,
			customerId: d.customerId,
			callOnCompany: d.callOnCompany,
			callOnName: d.callOnName,
			identityType: d.identityType
		})
		// 执行自动拨打电话
		makePhone(phoneNumber)
	}
}


/**
 * PC打电话功能,连接到PC端
 */
export async function onConnectPc() {
	// #ifdef APP-PLUS
	const userid = uni.getStorageSync('userId')
	if (!userid) return
	try {
		createWebSocket()
		const timeout = 1000 * 30
		const heartCheck = {
			sendTimeoutObj: null,
			serverTimeoutObj: null,
			// 重置心跳发送
			reset: function() {
				clearTimeout(this.sendTimeoutObj)
				clearTimeout(this.serverTimeoutObj)
			},
			// 发送心跳
			start: function() {
				// 定时发送心跳
				this.sendTimeoutObj = setTimeout(() => {
					// console.log('发送心跳: heart')
					uni.sendSocketMessage({
						data: 'heart'
					})
					// 正常来说,当发送完心跳包后,服务端会响应即在onmessage中做出响应,并清除此心跳包发送新的心跳包,
					// 如果没有做出响应的,则达到超时时间主动关闭websocket,开始重连
					this.serverTimeoutObj = setTimeout(() => {
						console.log('主动关闭Socket')
						uni.closeSocket()
						createWebSocket()
						this.reset()
					}, timeout)
				}, timeout)
			}
		}

		// 创建websocket
		function createWebSocket() {
			const url = `${webSocketBaseUrl}`
			console.log('websocket 创建连接', webSocketBaseUrl);
			uni.connectSocket({
				url
			});
			init()
		}

		// 与WebScket发送第一次消息,建立通道
		function onSendWsChannel() {
			const message = JSON.stringify({
				userid,
				source: 'app',
				phoneModel,
				phoneMac,
				type: 1
			})
			console.log('webSocket第一次发送消息给服务端', `${message}`)
			uni.sendSocketMessage({
				data: `${message}`
			})
		}

		// 初始化websocket
		function init() {
			// websocket打开时
			console.log('初始化websocket');
			uni.onSocketOpen(function() {
				console.log('WebSocket 连接打开')
				socketState = 'open'
				onSendWsChannel()
				heartCheck.reset()
				heartCheck.start()
			});

			uni.onSocketClose(function(response) {
				console.log('WebSocket 连接关闭', response)
				reconnect()
			})
			// 接收消息
			uni.onSocketMessage(function(response) {
				console.log('WebSocket 接收到服务器消息:', response)
				heartCheck.reset()
				heartCheck.start()
				if (response.data == 'health') {
					// console.log('心跳健康');
				} else {
					const d = JSON.parse(response.data)
					const app = getApp()
					if (app.globalData.show) {
						onMakePhoneFunc(d)
					}
				}
			})

			uni.onSocketError(function(e) {
				console.log('WebSocket连接打开失败,请检查!', e);
			});
		}

		let isConnected = false
		let reconnectTimeout = null
		// 重连
		function reconnect() {
			if (!uni.getStorageSync('userId')) {
				return
			}
			console.log('准备重连');
			// 当前正在操作连接的时候就不进行连接,防止出现重复连接的情况
			if (isConnected) return
			isConnected = true
			reconnectTimeout && clearTimeout(reconnectTimeout)
			reconnectTimeout = setTimeout(() => {
				heartCheck.reset()
				isConnected = false
				console.log('开始重连');
				createWebSocket()
			}, 1000)
		}
	} catch (e) {
		//TODO handle the exception
		console.log(e);
	}
	// #endif
}


/**
 * 在手机上打电话,同步给PC端,以展示通话状态
 * @param {Object} 
 */
export function onSendSocketMessage(operate) {
	const userid = uni.getStorageSync('userId')
	const callInfo = store.state.currentTelInfo
	const message = JSON.stringify({
		userid,
		operate,
		source: 'app',
		callOnName: callInfo.callOnName,
		identityType: callInfo.identityType,
		customerId: callInfo.customerId,
		callOn: callInfo.phoneNumber,
		callOnCompany: callInfo.callOnCompany,
		phoneModel,
		phoneMac,
	})

	console.log('socketState', socketState);
	console.log('WebSocket发送消息给PC', message)

	let s = setInterval(() => {
		if (socketState == 'open') {
			clearInterval(s)
			uni.sendSocketMessage({
				data: `${message}`
			});
		}
	}, 1000)
}

/**
 * app切到后台/前台 同步给服务端
 *  runModel 操作类型  0  前台  1后台
 */
export function onWsSendMsgRunModel(runModel) {
	const userid = uni.getStorageSync('userId')
	const message = JSON.stringify({
		userid,
		runModel,
		source: 'app',
		phoneMac,
	})
	let s = setInterval(() => {
		if (socketState == 'open') {
			clearInterval(s)
			uni.sendSocketMessage({
				data: `${message}`
			});
		}
	}, 2000)
}

录音文件路径获取

urils/deviceVendors.js

// #ifdef APP-PLUS
export const vendor = plus.device.vendor.toLocaleUpperCase()
export const platform = uni.getSystemInfoSync().platform
export const osVersion = Number(uni.getSystemInfoSync().osVersion)
export const romName = uni.getSystemInfoSync().romName

export const recordPath = {
	'XIAOMI': '/storage/emulated/0/MIUI/sound_recorder/call_rec/',
	'HUAWEI': '/storage/emulated/0/Sounds/CallRecord',
	'MEIZU': '/storage/emulated/0/Recorder',
	'OPPO': '/storage/emulated/0/Music/Recordings/Call Recordings',
	'VIVO': '/storage/emulated/0/Record/Call',
	'SAMSUNG': '/storage/emulated/0/Sounds',
	'HONOR': "/storage/emulated/0/Sounds/CallRecord"
}

/**
 * 获取录音文件路径
 */
export function getRecordPath() {
	return recordPath[vendor]
}

export const vendorCn = {
	'XIAOMI': '小米',
	'HUAWEI': '华为',
	'MEIZU': '魅族',
	'OPPO': 'OPPO',
	'VIVO': 'vivo',
	'SAMSUNG': '三星',
	'HONOR': "荣耀"
}

export function getVendorCn() {
	return vendorCn[vendor]
}
// #endif

app专有方法

urils/appPlus.js

 * 针对于APP的一些工具方法封装
 */

import {
	vendor,
	platform
} from "@/utils/deviceVendors.js";


/**
 * 关闭app
 */
export const closeApp = function() {
	switch (platform) {
		case 'android':
			plus.runtime.quit();
			break;
		case 'ios':
			plus.ios.import('UIApplication').sharedApplication().performSelector('exit');
			break;
		case 'windows': {
			location.reload()
			break;
		}
	}
}


/**
 * 下载app
 */
export const downApp = function() {
	// #ifdef APP-PLUS
	plus.runtime.openURL(
		'***')
	// #endif
	// #ifdef H5
	location.href = '***'
	// #endif
}

/**
 * 检查通话自动录音是否开启
 */
export function checkRecord() {
	let settingKey = ''
	let recordKey = ''
	if (vendor == 'XIAOMI') {
		settingKey = 'android.provider.Settings$System'
		recordKey = 'button_auto_record_call'
	} else if (vendor == 'HUAWEI' || vendor == 'HONOR') {
		settingKey = 'android.provider.Settings$Secure'
		recordKey = 'enable_record_auto_key'
	} else if (vendor == 'OPPO') {
		settingKey = 'android.provider.Settings$Global'
		recordKey = 'oplus_customize_all_call_audio_record'
	} else if (vendor == 'VIVO') {
		settingKey = 'android.provider.Settings$Global'
		recordKey = 'call_record_state_global'
	}

	const isOpenSetting = plus.android.invoke(settingKey, 'getInt', plus.android.runtimeMainActivity()
		.getContentResolver(), recordKey)
	return isOpenSetting == 1
}

/**
 * 打开通话录音设置
 */
export function onOpenRecordSetting() {
	var Intent = plus.android.importClass("android.content.Intent");
	var ComponentName = plus.android.importClass('android.content.ComponentName')
	var intent = new Intent();
	if (vendor == 'XIAOMI') {
		intent.setComponent(new ComponentName("com.android.phone",
			"com.android.phone.settings.CallRecordSetting"));
	}
	if (vendor == 'HUAWEI' || vendor == 'HONOR') {
		intent.setComponent(new ComponentName("com.android.phone",
			"com.android.phone.MSimCallFeaturesSetting"));
	}
	if (vendor == 'VIVO') {
		intent.setComponent(new ComponentName("com.android.phone",
			"com.android.incallui.record.CallRecordSetting"));
	}
	if (vendor == 'OPPO') {
		intent.setComponent(new ComponentName("com.android.phone",
			"com.android.phone.OplusCallFeaturesSetting"));
	}
	intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
	intent.setAction("android.intent.action.VIEW");
	var main = plus.android.runtimeMainActivity();
	main.startActivity(intent);
}

/**
 * 打电话
 * @param {String | Number} phoneNumber 电话号码
 */
export const onMakePhone = function(phoneNumber) {
	// #ifdef APP-PLUS
	// 导入Activity、Intent类
	var Intent = plus.android.importClass("android.content.Intent");
	var Uri = plus.android.importClass("android.net.Uri");
	// 获取主Activity对象的实例  
	var main = plus.android.runtimeMainActivity();
	// 创建Intent  
	var uri = Uri.parse(`tel:${phoneNumber}`); // 这里可修改电话号码  
	var call = new Intent("android.intent.action.CALL", uri);
	// 调用startActivity方法拨打电话  
	main.startActivity(call);
	// #endif
}

/**
 * 唤醒APP
 */
export function onLaunchApp() {
	// #ifdef APP-PLUS
	var android_main = plus.android.runtimeMainActivity();
	var Intent = plus.android.importClass('android.content.Intent');
	var intent = new Intent(android_main.getIntent());
	intent.setClassName(android_main, 'io.dcloud.PandoraEntryActivity');
	intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
	android_main.startActivity(intent);
	// #endif
}

export function getFindContact(phoneNumber) {
	plus.contacts.getAddressBook(plus.contacts.ADDRESSBOOK_PHONE, function(addressbook) {
		console.log('获取通讯录对象成功')
		console.log(addressbook)
		addressbook.find(["displayName", "phoneNumbers"], function(contacts) {
			console.log("获取联系人成功")
			console.log(contacts)
			let findItem = contacts.find(item => {
				return item.phoneNumbers.some(i => {
					const value = uni.$u.trim(i.value, 'all')
					return value == phoneNumber
				})
			})
			return findItem
		})
	})
}

/**
 * @param {Array[String]} 收件人信息
 * @param {String} body 发送消息内容
 */
export function onSendMessage(to, body) {
	// #ifdef APP-PLUS
	var msg = plus.messaging.createMessage(plus.messaging.TYPE_SMS);
	msg.to = to;
	msg.body = body;
	plus.messaging.sendMessage(msg, function() {
		console.log("Send success!");
	}, function() {
		console.log("Send failed!");
	});
	// #endif
}

通话录音检测上传

urils/call.js

需要引入 保活 前台运行 - DCloud 插件市场 插件,保证APP后台运行。

/**
 * 有关通话录音相关的方法
 */
import {
	baseUrl,
	header
} from '@/config/config.js'
import {
	onSendSocketMessage,
	phoneMac
} from '@/utils/PcConnectApp.js';
import store from "@/store/index.js"
import {
	platform,
	getRecordPath,
	vendor
} from '@/utils/deviceVendors.js';
import {
	checkRecord,
	onOpenRecordSetting,
	onMakePhone
} from '@/utils/appPlus.js';

// #ifdef APP-PLUS
const recordAudioPath = getRecordPath()
const hgService = uni.requireNativePlugin("HG-Background");
const mainActivity = plus.android.runtimeMainActivity()
const packageName = mainActivity.getPackageName()
const Settings = plus.android.importClass('android.provider.Settings')
const Uri = plus.android.importClass('android.net.Uri')
const Intent = plus.android.importClass('android.content.Intent')
// #endif


/**
 * 检测app省电策略
 */
export function checkPower() {
	const intent = new Intent();
	intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
	intent.setData(Uri.parse("package:" + packageName));
	plus.android.importClass("android.os.PowerManager");
	var pm = mainActivity.getSystemService(mainActivity.POWER_SERVICE);
	var isIgnoringBatteryOptimizations = pm.isIgnoringBatteryOptimizations(mainActivity.getPackageName());
	if (isIgnoringBatteryOptimizations) {
		console.log("已忽略电池优化");
	} else {
		let content = '请允许应用后台运行,否则将会造成通话异常'
		if (vendor == 'HUAWEI' || vendor == 'HONOR') {
			content = '请点击允许忽略电话优化,否则将会造成通话异常'
		} else if (vendor == 'XIAOMI') {
			content = '请将省电策略修改为无限制,否则将会造成通话异常'
		}
		console.log("未忽略电池优化");
		uni.showModal({
			title: '温馨提示',
			content,
			showCancel: false,
			confirmColor: '#1D85EB',
			success: function(res) {
				if (res.confirm) {
					mainActivity.startActivity(intent);
				}
			}
		})
	}
}

/**
 * HG插件保活策略
 */
export function onHgPluginKeep() {
	hgService.config({
		title: "宇成客app",
		content: "前台服务运行中",
		mode: 1, //0省电模式 1流氓模式
	});
	hgService.showSafeSetting(); //支持小米,华为,锤子,opp,vivo,三星,乐视,魅族
	// if (vendor !== 'XIAOMI') {
	// 	var result = hgService.checkIfLimited();
	// 	if (result.isLimit) {
	// 		hgService.requestIgnoreLimit();
	// 	}
	// }
	hgService.startService();
	var globalEvent = uni.requireNativePlugin('globalEvent');
	let counts = 0
	globalEvent.addEventListener('doJob', function() {
		counts += 1;
		console.log("保活任务执行次数:" + counts);
	});
}


/**
 * app保活策略,主要保证app进程不被杀掉
 */
export function onKeepAlive() {
	if (platform === 'android') {
		checkPower()
		onHgPluginKeep()
		// let s = 0
		// setInterval(() => {
		// 	console.log('保活:' + s++);
		// }, 60000)
	}
}

/**
 * 检测手机品牌,是否有适配录音文件路径
 */
export function onCheckBrand() {
	const isFindBrand = getRecordPath()
	if (!isFindBrand) {
		uni.showModal({
			title: '提示',
			content: '未检测到您的手机品牌,请联系管理员适配!',
			showCancel: false,
			confirmColor: '#1D85EB'
		})
	}
}

/**
 * 检查手机系统权限
 */
export function checkSystemAuth() {
	onCheckBrand()
	onKeepAlive()
	plus.android.requestPermissions(
		['android.permission.WRITE_EXTERNAL_STORAGE',
			'android.permission.READ_EXTERNAL_STORAGE',
			'android.permission.READ_PHONE_STATE',
			'android.permission.CALL_PHONE',
			'android.permission.READ_CONTACTS',
			'android.permission.READ_CALL_LOG',
			'android.permission.PROCESS_OUTGOING_CALLS',
			'android.permission.WAKE_LOCK',
			'android.permission.SYSTEM_ALERT_WINDOW',
			'android.permission.FOREGROUND_SERVICE',
			'android.permission.BOOT_COMPLETED',
			'android.permission.RECEIVE_BOOT_COMPLETED',
			'android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS'
		],
		function(resultObj) {
			let result = 0;
			for (const i = 0; i < resultObj.granted.length; i++) {
				const grantedPermission = resultObj.granted[i];
				console.log('已获取的权限:' + grantedPermission);
				result = 1
			}
			for (const i = 0; i < resultObj.deniedPresent.length; i++) {
				const deniedPresentPermission = resultObj.deniedPresent[i];
				console.log('拒绝本次申请的权限:' + deniedPresentPermission);
				result = 0
			}
			for (const i = 0; i < resultObj.deniedAlways.length; i++) {
				const deniedAlwaysPermission = resultObj.deniedAlways[i];
				console.log('永久拒绝申请的权限:' + deniedAlwaysPermission);
				result = -1
			}
			if (result !== 1) {
				uni.showModal({
					title: '提示',
					content: '请打开手机系统应用权限!',
					showCancel: false,
					confirmColor: '#1D85EB'
				})
			}
		},
		function(error) {
			console.log('申请权限错误:' + error.code + " = " + error.message);
		}
	);
}

/**
 * 添加操作记录
 */
export async function FollowUpLogSave() {
	try {
		const {
			customerId
		} = store.state.currentTelInfo
		console.log('添加操作记录传参', customerId);
		const res = await uni.$u.http.post('/***', {
			followUpType: 2, // 2 表示联系记录
			customerId,
			followModule: 1, // 1  (表示录音文件)  2(表示未接通)
			mac: phoneMac
		})
		console.log('添加操作记录返回结果', res);
		if (res.code == 200) {
			const followUpId = res.data.followUpId
			uni.setStorageSync('followUpId', followUpId)
		}
	} catch (e) {
		//TODO handle the exception
		console.log(e);
	}
}

/**
 * 更新操作记录
 */
export async function FollowUpLogModify(followContent) {
	try {
		return new Promise(async (resolve) => {
			const followUpId = uni.getStorageSync('followUpId')
			console.log('更新操作记录传参', {
				followUpId,
				followContent
			});
			const res = await uni.$u.http.put('***', {
				followUpId, // 日志ID
				followContent // 录音文件地址或JSON串
			})
			console.log('更新操作记录返回结果', res);
			if (res.code == 200) {
				removeStorageCallInfo()
				resolve()
			}
		})

	} catch (e) {
		//TODO handle the exception
		console.log(e);
		reject()
	}
}


/**
 * 检查是否通话完成
 */
function recordCheck() {
	try {
		if (platform == 'android') {
			const CallLog = plus.android.importClass('android.provider.CallLog');
			const Main = plus.android.runtimeMainActivity(); // 此处相当于 context
			const Resolver = Main.getContentResolver(); // 获取ContentResolver实例
			plus.android.importClass(Resolver);
			const makePhoneTime = uni.getStorageSync('makePhoneTime')
			console.log('拨出电话的时间戳', makePhoneTime);
			const cursor = Resolver.query(CallLog.Calls.CONTENT_URI, null, CallLog.Calls.DATE + '>=' + makePhoneTime, null,
				CallLog.Calls.DATE + " DESC");
			plus.android.importClass(cursor);
			const countOld = cursor.getCount()
			// 定时检测,检测到比拨出电话前通话记录条数多出,则证明电话已挂断
			const sid = setInterval(() => {
				const cursor = Resolver.query(CallLog.Calls.CONTENT_URI, null, CallLog.Calls.DATE + '>=' + makePhoneTime,
					null,
					CallLog.Calls.DATE + " DESC");
				plus.android.importClass(cursor);
				const countNew = cursor.getCount()
				console.log('拨出前的通话记录条数', countOld);
				console.log('拨出后的通话记录条数', countNew);
				if (countNew > countOld) {
					uni.setStorageSync('callEndTime', Date.now())
					clearInterval(sid)
					onSendSocketMessage(0)
					readFile()
				}
				cursor.close();
			}, 1000 * 10)

			cursor.close();
		}
	} catch (err) {
		console.log('callWatch', err)
	}
}


/**
 * 拨打电话
 */
export async function makePhone(phoneNumber) {
	// #ifdef APP-PLUS
	const isCheckRecord = checkRecord()
	// 未打开通话自动录音不允许打电话
	if (!isCheckRecord) {
		uni.showModal({
			title: '提示',
			content: '请打开通话自动录音功能',
			showCancel: false,
			confirmColor: '#1D85EB',
			success(res) {
				if (res.confirm) {
					onOpenRecordSetting()
				}
			}
		})
		return
	}
	// 拨出电话的时间戳
	const makePhoneTime = Date.now()
	uni.setStorageSync('makePhoneTime', makePhoneTime)

	await FollowUpLogSave()
	onMakePhone(phoneNumber)
	callWatch()
	// #endif
}

/**
 * 通话状态监听
 */
export async function callWatch() {
	onSendSocketMessage(2)
	recordCheck()
}


/**
 * 检查是否有未上传的录音
 */
export async function onCheckRecord() {
	// 暂时无法处理,无法获取到接听时间,每个品牌的CALL_DATE不一样,MIUI为拨出时间,HONOR为接听时间
}


/**
 * 获取录音文件路径
 */
function getRecordFilePath() {
	return new Promise((resolve) => {
		// console.log('获取录音文件路径', recordAudioPath);
		if (!recordAudioPath) {
			return
		}
		var file = plus.android.newObject("java.io.File", recordAudioPath);
		if (!file) {
			uni.showModal({
				title: '提示',
				content: '未获取到录音路径,请联系管理员适配设备机型',
				showCancel: false,
				confirmColor: '#1D85EB'
			})
			resolve(false)
			return
		}
		resolve(file)
	})
}


/**
 * 读取录音文件夹,找出需要上传的录音文件
 */
export async function readFile() {
	const filePath = await getRecordFilePath()
	// console.log('filePath', filePath);
	if (!filePath) {
		return
	}

	const files = plus.android.invoke(filePath, "listFiles");
	const name = plus.android.invoke(filePath, "getName");
	// console.log('录音文件夹名字', name);
	// console.log('files', files);
	if (!files) {
		return
	}
	const len = files.length;
	for (let i = 0; i < len; i++) {
		const file = files[i];
		// 过滤隐藏文件  
		if (!plus.android.invoke(file, "isHidden")) {
			// 过滤文件夹
			if (plus.android.invoke(file, "isDirectory")) {} else {
				filterFileUpLoad(file)
			}
		}
	}
}

/**
 * 适配录音文件名称
 */
function adapterCallFile(fileName, phoneNumber) {
	return new Promise((resolve) => {
		let name = uni.$u.trim(fileName, 'all')
		if (vendor == 'OPPO') {
			// 由于OPPO在拨打联通,移动此类公用电话号码以及手机内已存在的联系人,录音文件名为中文名,不含电话号码
			// 需要对此进行特殊处理
			plus.contacts.getAddressBook(plus.contacts.ADDRESSBOOK_PHONE, function(addressbook) {
				addressbook.find(["displayName", "phoneNumbers"], function(contacts) {
					// 如果当前拨打的电话能在手机联系人中找到,那么在录音文件名上加上电话号码。用来适配正则校验
					let findItem = contacts.find((item) => {
						return item.phoneNumbers.some((i) => {
							return i.value === phoneNumber;
						});
					});
					console.log('OPPO 查找到的联系人', findItem);
					if (findItem) {
						resolve(`${phoneNumber}_${name}`)
					} else {
						resolve(name)
					}
				})
			})
		} else {
			resolve(name)
		}
	})
}

/**
 * 筛选出真正需要的录音文件
 * @param {Object} file 录音文件
 */
async function filterFileUpLoad(file) {
	const fileName = plus.android.invoke(file, "getName");
	const lastModified = plus.android.invoke(file, "lastModified");
	const fileSuffix = fileName.split('.')[1]
	const mediaType = ['mp3', 'm4a', 'flac', 'ogg', 'ape', 'amr', 'wma', 'wav', 'mp4', 'aac']
	// 过滤文件格式
	if (mediaType.includes(fileSuffix)) {
		const callEndTime = uni.getStorageSync('callEndTime')
		const errorTime = -20 * 1000 // 误差时间
		const minusTime = 10 * 1000 // 由于定时器10s一循环,需要考虑定时器消耗的时间,最多消耗掉10s
		// console.log('callEndTime', callEndTime);
		if (vendor == 'HUAWEI') {
			// HUAWEI系统的lastModified 为创建时间
			const audio = uni.createInnerAudioContext()
			console.log('录音文件路径', `${recordAudioPath}/${fileName}`);
			audio.src = `${recordAudioPath}/${fileName}`
			audio.onCanplay(async () => {
				const audioTime = audio.duration
				// 录音完成时间=创建时间+录音持续时间
				const lastModifiedHuaWei = lastModified + (audioTime * 1000)
				const diffTime = callEndTime - lastModifiedHuaWei;
				// 如果在拨出时间后,并且在拨出时间后的10秒内,可以认为是我们需要的录音文件
				// console.log('HUAWEI creationTime', lastModified);
				// console.log('HUAWEI diffTime', diffTime);
				// console.log('HUAWEI fileName', fileName);
				if (diffTime < minusTime && diffTime > errorTime) {
					// 由于要读取录音时长,此事件为异步。所以要等待录音读取完毕后再执行上传
					filterNameFile()
				}
			})
		} else {
			// 其它系统
			const diffTime = callEndTime - lastModified
			// console.log('diffTime', diffTime);
			// console.log('fileName', fileName);
			if (diffTime < minusTime && diffTime > errorTime) {
				filterNameFile()
			}
		}
	}

	async function filterNameFile() {
		// 员工当前拨打的客户电话号码
		const currentMakePhoneCall = store.state.currentTelInfo.phoneNumber
		const makeCall = uni.$u.trim(currentMakePhoneCall, 'all')
		console.log('员工当前拨打的电话', makeCall);
		const fileNameAdapter = await adapterCallFile(fileName, makeCall)
		console.log('录音文件名', fileNameAdapter);
		const callRegex = new RegExp(`${makeCall}(?!=\d)`)
		const fileCallRegexResult = fileNameAdapter.match(callRegex)
		console.log('电话号码匹配的正则', fileCallRegexResult);
		if (fileCallRegexResult) {
			// 通话-已接通
			// 如果录音文件电话号码与拨打电话号码一致,可以确定是需要上传的电话录音文件
			const fileNameCall = fileCallRegexResult[0]
			console.log('录音文件电话号码', fileNameCall);
			if (fileNameCall == makeCall) {
				// 准备进行上传文件
				const fileName = fileNameAdapter.split('.')[0]
				const fileSuffix = fileNameAdapter.split('.')[1]
				const pathTo =
					`/storage/emulated/0/music/${fileName}_${Date.now()}.${fileSuffix}`
				const backupFile = plus.android.newObject("java.io.File",
					pathTo);
				const getCanonicalPath = plus.android.invoke(backupFile,
					"createNewFile");
				let input
				let output
				try {
					console.log('pathTo', pathTo);
					input = plus.android.newObject("java.io.FileInputStream", file);
					output = plus.android.newObject("java.io.FileOutputStream",
						backupFile);
					const channel1 = plus.android.invoke(input, "getChannel");
					const channel2 = plus.android.invoke(output, "getChannel");
					const size = plus.android.invoke(channel1, "size");
					plus.android.invoke(channel1, "transferTo", 0, size, channel2);
					const uploadFileSrc = await uploadFile(pathTo)
					if (uploadFileSrc) {
						addFile(file, uploadFileSrc, backupFile)
					}
				} catch (e) {
					console.log('readFile', e);
				} finally {
					plus.android.invoke(input, "close")
					plus.android.invoke(output, "close")
				}
			} else {
				// 通话-未接通
				removeStorageCallInfo()
			}
		} else {
			removeStorageCallInfo()
		}
	}
}

/**
 * 清空已处理(成功或者失败)的通话信息
 */
function removeStorageCallInfo() {
	// 清空拨出电话时间戳,操作日志ID
	uni.removeStorageSync('makePhoneTime')
	uni.removeStorageSync('followUpId')
	uni.removeStorageSync('callEndTime')
	// 清空当前拨打客户的信息
	store.commit('setCurrentTelInfo', {})
	console.log('清空拨出电话时间戳,操作日志ID');
}

/**
 * 上传录音文件
 * @param {String} filePath 录音文件路径
 */
function uploadFile(filePath) {
	return new Promise((resolve, reject) => {
		function uploadFail() {
			uni.hideLoading()
			reject()
			uni.showModal({
				content: '录音文件上传失败,请联系管理员处理',
				title: '提示',
				showCancel: false,
				confirmColor: '#1D85EB'
			})
		}
		uni.showLoading({
			mask: true,
			title: '录音上传中..'
		})
		try {
			setTimeout(() => {
				const userId = store.state.userInfo?.user?.userId
				const task = plus.uploader.createUpload(`${baseUrl}/***`, {
						method: "POST",
						priority: 100,
						retry: 2,
						retryInterval: 10,
						timeout: 0
					},
					function(t, status) {
						// 上传完成
						if (status == 200) {
							const data = JSON.parse(t.responseText)
							console.log('data', data);
							if (data.code == 200) {
								console.log('录音文件上传成功');
								resolve(data.data)
							} else {
								uploadFail()
							}
						} else {
							uploadFail()
							reject(false)
						}
					}
				);
				task.addFile(filePath, {
					key: "file"
				});
				task.addData("userId", userId);
				task.setRequestHeader('Request-From', header['Request-From'])
				task.setRequestHeader('token', uni.getStorageSync('token'))
				task.addEventListener("statechanged", (upload, status) => {
					switch (upload.state) {
						case 1: // 上传任务开始请求
							break
						case 2: // 上传任务请求已经建立
							break
						case 3: // 上传任务提交数据,监听 statechanged 事件时可多次触发此状态。(重点)
							// uploadedSize表示当前已经上传了的数据大小,totalSize表示文件总大小,单位是字节b
							// console.log('上传进度', parseInt(100 * upload.uploadedSize / upload
							// 	.totalSize))
							break
						case 4: // 上传任务已完成, 无论成功或失败都会执行到 4 这里
							if (status === 200) {
								// 上传成功
							} else {
								// 上传失败
								uploadFail()
								reject(false)
							}
					}
				});
				task.start();
			}, 0)

		} catch (e) {
			//TODO handle the exception
			console.log('uploadFile Error', e);
			uploadFail()
			reject(false)
		}
	})


}

/**
 * 添加录音文件
 * @param {Object} file 原始录音文件
 * @param {Object} fileSrc 上传后的录音文件地址
 * @param {Object} backupFile 移动到公共目录下的录音文件
 */
export async function addFile(file, fileSrc, backupFile) {
	return new Promise((resolve, reject) => {
		function addFail() {
			uni.hideLoading()
			uni.showModal({
				content: '录音文件添加失败,请联系管理员处理',
				title: '提示',
				showCancel: false,
				confirmColor: '#1D85EB'
			})
			reject()
		}
		try {
			const {
				phoneNumber: callOn,
				callOnName,
				identityType,
				customerId,
				callOnCompany
			} = store.state.currentTelInfo
			const userInfo = store.state.userInfo
			const audio = uni.createInnerAudioContext()
			audio.src = fileSrc
			console.log('audio.src', audio.src);
			audio.onCanplay(async () => {
				const audioTime = parseInt(audio.duration)
				// 添加录音
				const params = {
					userId: userInfo?.user?.userId,
					userName: userInfo?.user?.userName,
					deptId: userInfo?.user?.deptId,
					deptName: userInfo?.user?.dept?.deptName,
					parentDeptId: userInfo?.user?.dept?.parentId,
					parentDeptName: userInfo?.user?.dept?.parentName,
					file: fileSrc,
					audioTime,
					callOnName,
					identityType,
					customerId,
					callOn,
					callOnCompany
				}

				console.log('添加录音传的参数', params);
				const res = await uni.$u.http.post('***', params)
				console.log('添加录音返回的结果', res);
				if (res.code == 200) {
					FollowUpLogModify(JSON.stringify(res.data))
					// 上传成功删除录音文件
					console.log('上传成功删除录音文件');
					plus.android.invoke(file, 'delete')
					plus.android.invoke(backupFile, 'delete')
					uni.hideLoading()
					uni.showToast({
						title: '录音上传成功'
					})
					resolve()
				} else {
					addFail()
					reject()
				}
			})
		} catch (e) {
			console.log('addFile Error', e);
			addFail()
			reject()
		}
	})
}

uniapp配置

image.png

image.png

权限配置表

<uses-feature android:name="android.hardware.camera"/>

<uses-feature android:name="android.hardware.camera.autofocus"/>

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>

<uses-permission android:name="android.permission.CALL_PHONE"/>

<uses-permission android:name="android.permission.CAMERA"/>

<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>

<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>

<uses-permission android:name="android.permission.FLASHLIGHT"/>

<uses-permission android:name="android.permission.GET_ACCOUNTS"/>

<uses-permission android:name="android.permission.INTERNET"/>

<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>

<uses-permission android:name="android.permission.READ_CALL_LOG"/>

<uses-permission android:name="android.permission.READ_CONTACTS"/>

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

<uses-permission android:name="android.permission.READ_LOGS"/>

<uses-permission android:name="android.permission.READ_PHONE_STATE"/>

<uses-permission android:name="android.permission.READ_SMS"/>

<uses-permission android:name="android.permission.SEND_SMS"/>

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

<uses-permission android:name="android.permission.VIBRATE"/>

<uses-permission android:name="android.permission.WAKE_LOCK"/>

<uses-permission android:name="android.permission.WRITE_CONTACTS"/>

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

<uses-permission android:name="android.permission.WRITE_SETTINGS"/>

<uses-permission android:name="android.permission.WRITE_SMS"/>

云端插件,具体链接 保活 前台运行 - DCloud 插件市场

image.png

这里要选择28, 否则华为鸿蒙系统会出问题。

image.png

一些其它方案

通话状态监听2

还有一种监听通话状态的方案,就是可以直接读取用户的通话状态。但是在小米手机上需要额外设置一下,允许读取通话状态。这个也无法检测该权限去给用户提示,所以最后被放弃了。

/**
 * 通话状态监听
 */
export function callWatch() {
	try {
		var isAlreadyRead = false // 读取录音文件状态, false未读取
		if (uni.getSystemInfoSync().platform == 'android') { //Android
			var main = plus.android.runtimeMainActivity();
			var Context = plus.android.importClass("android.content.Context");
			plus.android.importClass("android.telephony.TelephonyManager");
			var telephonyManager = plus.android.runtimeMainActivity().getSystemService(Context
				.TELEPHONY_SERVICE);
			var phonetype = telephonyManager
				.getCallState(); // 通话状态: 0:空闲状态 1:拨打状态 2:结束通话
			var receiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', {
				onReceive: function(context, intent) {
					// console.log('context', context);
					plus.android.importClass(intent);
					var telephonyManager = plus.android.importClass(
						"android.telephony.TelephonyManager");
					var telephonyManager = plus.android.runtimeMainActivity().getSystemService(
						Context
						.TELEPHONY_SERVICE);
					var phonetype = telephonyManager
						.getCallState(); // 通话状态: 0:空闲状态 1:拨打状态 2:结束通话
					// 结束通话后,并且通话状态变更为空闲状态,开始进行录音文件的上传
					console.log('通话状态', phonetype, isAlreadyRead);
					if (phonetype == 2) {
						isAlreadyRead = true
						onSendSocketMessage(2)
					} else if (phonetype == 0 && isAlreadyRead) {
						isAlreadyRead = false
						console.log('开始读取录音文件');
						onSendSocketMessage(0)
						readFile()
					}
				}
			});
			var IntentFilter = plus.android.importClass('android.content.IntentFilter');
			var filter = new IntentFilter();
			filter.addAction(telephonyManager.ACTION_PHONE_STATE_CHANGED);
			main.registerReceiver(receiver, filter);

		} else if (uni.getSystemInfoSync().platform == 'ios') { //ios
			// const callstatus = false
			// const CTCall = plus.ios.importClass('CTCall');
			// const CTCallCenter = plus.ios.importClass('CTCallCenter');
			// const center = new CTCallCenter();
			// center.init()
			// center.setCallEventHandler(function() {
			// 	callstatus = !callstatus
			// })
		}
	} catch (err) {
		console.log('callWatch', err)
	}
}

背景音乐保活

还有一种保活的方案,就是播放背景音乐,这个会影响用户手机的使用。可以酌情考虑。

/**
 * 背景音乐保活策略
 */
export const backgroundAudio = {
	innerAudioContext: null,
	onPlay() {
		const innerAudioContext = this.innerAudioContext = uni.createInnerAudioContext();
		innerAudioContext.autoplay = true;
		innerAudioContext.volume = 1
		innerAudioContext.loop = true
		innerAudioContext.src =
			'https://bjetxgzv.cdn.bspapp.com/VKCEYUGU-hello-uniapp/2cc220e0-c27a-11ea-9dfb-6da8e309e0d8.mp3';
		innerAudioContext.onPlay(() => {
			console.log('背景音乐保活:开始播放');
		});
		innerAudioContext.onError((res) => {
			console.log(res.errMsg);
			console.log(res.errCode);
		});
	},
	onPause() {
		const innerAudioContext = this.innerAudioContext;
		if (innerAudioContext) {
			innerAudioContext.pause()
			innerAudioContext.onPause(() => {
				console.log('背景音乐保活:暂停');
			});
		}
	}
}

检测因意外未上传的录音

这个没有完全实现,上面在代码注释中也说了问题所在。在这里也简单提供一个思路,如果能解决问题该方案也是可用的。

思路就是在用户拨打电话时,存储拨打电话的信息(拨出时间,电话号)。从通话记录找到电话接听时间,并以此推算出录音文件的创建时间,找到合适的文件。 以下为简单的代码实现,可供参考:

call.js

/**
 * 获取接听电话时间戳
 */
export function getOnthePhoneTime(makePhoneTime, phoneNumber) {
	if (platform == 'android') {
		return new Promise((resolve) => {
			let onThePhoneTime
			const CallLog = plus.android.importClass('android.provider.CallLog');
			const Main = plus.android.runtimeMainActivity(); // 此处相当于 context
			const Resolver = Main.getContentResolver(); // 获取ContentResolver实例
			plus.android.importClass(Resolver);
			console.log('拨出电话时间', makePhoneTime);
			const cursor = Resolver.query(CallLog.Calls.CONTENT_URI, null, CallLog.Calls.DATE + '>=' + makePhoneTime -
				3000,
				null,
				CallLog.Calls.DATE + "ASC");
			plus.android.importClass(cursor);
			// 从通话记录找到电话接听时间
			cursor.moveToNext()
			const callsNUMBER = cursor.getString(cursor.getColumnIndexOrThrow(CallLog.Calls.NUMBER));
			console.info('callsNUMBER', callsNUMBER)
			const callsDATE = cursor.getString(cursor.getColumnIndexOrThrow(CallLog.Calls.DATE));
			console.info('callsDATE', callsDATE)
			onThePhoneTime = callsDATE
			cursor.close();
			resolve(onThePhoneTime)
		})
	}
}

/**
 * 检查是否有录音文件上传
 */
export async function onCheckRecord() {
	const recordCall = cache.local.getJSON('recordCall')
	console.log('是否有录音文件上传', recordCall);
	if (recordCall && recordCall.length > 0) {
		for (var i = 0; i < recordCall.length; i++) {
			const item = recordCall[i]
			item.onThePhoneTime = await getOnthePhoneTime(item.makePhoneTime, item.telInfo.phoneNumber)
			console.log('makePhoneTime', item.makePhoneTime);
			console.log('onThePhoneTime', item.onThePhoneTime);
		}
		console.log('recordCall', recordCall);
		cache.local.setJSON('recordCall', recordCall)
		readFile()
	}
}

/**
 * 筛选出真正需要的录音文件
 * @param {Object} file 录音文件
 */
async function filterFileUpLoad(file) {
	const fileName = plus.android.invoke(file, "getName");
	const lastModified = plus.android.invoke(file, "lastModified");
	const fileSuffix = fileName.split('.')[1]
	const mediaType = ['mp3', 'm4a', 'flac', 'ogg', 'ape', 'amr', 'wma', 'wav', 'mp4', 'aac']
	// 过滤文件格式
	if (mediaType.includes(fileSuffix)) {
		const recordCall = cache.local.getJSON('recordCall')
		for (var i = 0; i < recordCall.length; i++) {
			const callInfo = recordCall[i]
			const onThePhoneTime = callInfo.onThePhoneTime
			const errorTime = 10 * 1000 // 误差时间
			console.log('onThePhoneTime', onThePhoneTime);
			if (vendor == 'HUAWEI') {
				const diffTime = lastModified - onThePhoneTime
				console.log('HUAWEI creationTime', lastModified);
				console.log('HUAWEI diffTime', diffTime);
				console.log('HUAWEI fileName', fileName);
				// HUAWEI系统的lastModified 为创建时间
				if (diffTime > 0 && diffTime < errorTime) {
					filterNameFile(callInfo)
				} else {
					removeStorageCallInfo(callInfo)
				}
			} else {
				// 其它系统
				const audio = uni.createInnerAudioContext()
				console.log('录音文件路径', `${recordAudioPath}/${fileName}`);
				audio.src = `${recordAudioPath}/${fileName}`
				audio.onCanplay(async () => {
					const audioTime = audio.duration
					// 录音创建时间=完成时间-录音持续时间
					const creationTime = lastModified - (audioTime * 1000)
					const diffTime = creationTime - onThePhoneTime;
					// 如果在拨出时间后,并且在拨出时间后的30秒内,可以认为是我们需要的录音文件
					console.log('creationTime', creationTime);
					console.log('diffTime', diffTime);
					console.log('fileName', fileName);
					if (diffTime > 0 && diffTime < errorTime) {
						// 由于要读取录音时长,此事件为异步。所以要等待录音读取完毕后再执行上传
						filterNameFile(callInfo)
					} else {
						removeStorageCallInfo(callInfo)
					}
				})
			}
		}
	}

	async function filterNameFile(callInfo) {
		// 员工当前拨打的客户电话号码
		const currentMakePhoneCall = callInfo.telInfo.phoneNumber
		const makeCall = uni.$u.trim(currentMakePhoneCall, 'all')
		console.log('员工当前拨打的电话', makeCall);
		const fileNameAdapter = await adapterCallFile(fileName, makeCall)
		console.log('录音文件名', fileNameAdapter);
		const callRegex = new RegExp(`${makeCall}(?!=\d)`)
		const fileCallRegexResult = fileNameAdapter.match(callRegex)
		console.log('电话号码匹配的正则', fileCallRegexResult);
		if (fileCallRegexResult) {
			// 通话-已接通
			// 如果录音文件电话号码与拨打电话号码一致,可以确定是需要上传的电话录音文件
			const fileNameCall = fileCallRegexResult[0]
			console.log('录音文件电话号码', fileNameCall);
			if (fileNameCall == makeCall) {
				// 准备进行上传文件
				const fileName = fileNameAdapter.split('.')[0]
				const fileSuffix = fileNameAdapter.split('.')[1]
				const pathTo =
					`/storage/emulated/0/music/${fileName}_${Date.now()}.${fileSuffix}`
				const backupFile = plus.android.newObject("java.io.File",
					pathTo);
				const getCanonicalPath = plus.android.invoke(backupFile,
					"createNewFile");
				let input
				let output
				try {
					console.log('pathTo', pathTo);
					input = plus.android.newObject("java.io.FileInputStream", file);
					output = plus.android.newObject("java.io.FileOutputStream",
						backupFile);
					const channel1 = plus.android.invoke(input, "getChannel");
					const channel2 = plus.android.invoke(output, "getChannel");
					const size = plus.android.invoke(channel1, "size");
					plus.android.invoke(channel1, "transferTo", 0, size, channel2);
					const uploadFileSrc = await uploadFile(pathTo)
					if (uploadFileSrc) {
						addFile(file, uploadFileSrc, callInfo, backupFile)
					}
				} catch (e) {
					console.log('readFile', e);
				} finally {
					plus.android.invoke(input, "close")
					plus.android.invoke(output, "close")
				}
			} else {
				// 通话-未接通
				removeStorageCallInfo(callInfo)
			}
		} else {
			removeStorageCallInfo(callInfo)
		}
	}
}

/**
 * 清空已处理(成功或者失败)的通话信息
 */
function removeStorageCallInfo(callInfo) {
	let recordCall = cache.local.getJSON('recordCall')
	let callList = []
	for (var i = 0; i < recordCall.length; i++) {
		const item = recordCall[i]
		const makePhoneTime = item.makePhoneTime
		const timeout = 1000 * 60 * 60 * 24 * 30
		const now = Date.now()
		const isTimeout = now - makePhoneTime <= timeout
		const isExist = item.makePhoneTime == callInfo.makePhoneTime && item.telInfo.phoneNumber == callInfo.telInfo
			.phoneNumber && item.followUpId == callInfo.followUpId
		// 当每一项通话信息与校验过的通话信息比对,如果不一致添加,一致删除
		// 当存储的通话信息已经超过30天,那么可以认为此项信息不需要上传了(主要针对于:双卡时会选择卡1,卡2,此时点击取消无法检测,只能每次都存储通话信息,但存储过多会影响性能,所以在此做了超时判断,超过30天的就删除掉不再比对了)
		if (!isExist && isTimeout) {
			callList.push(callInfo)
		}
	}
	console.log('清空通话信息后', callList);
	cache.local.setJSON('recordCall', callList)

}