Commit f54ad0d2 by zhangchengbo

feat:添加[黑名单]工具类与javaBean

parent a91b8262
package com.simplemobiletools.commons.helpers
import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri
import android.provider.CallLog.Calls
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.models.RecentCall
import com.simplemobiletools.commons.models.SIMAccount
import com.simplemobiletools.commons.models.contacts.Contact
class RecentsHelperCommon(private val context: Context) {
private val COMPARABLE_PHONE_NUMBER_LENGTH = 9
private val QUERY_LIMIT = 200
private val contentUri = Calls.CONTENT_URI
/**
* 获取导出通话记录 .json文件
*/
fun getRecentCalls(groupSubsequentCalls: Boolean, maxSize: Int = QUERY_LIMIT, callback: (MutableList<RecentCall>) -> Unit) {
val privateCursor = context.getMyContactsCursor(favoritesOnly = false, withPhoneNumbersOnly = true)
ensureBackgroundThread {
if (!context.hasPermission(PERMISSION_READ_CALL_LOG)) {
callback(ArrayList())
return@ensureBackgroundThread
}
ContactsHelper(context).getContacts(showOnlyContactsWithNumbers = true) { contacts: java.util.ArrayList<Contact> ->
val privateContacts = MyContactsContentProvider.getContacts(context, privateCursor)
if (privateContacts.isNotEmpty()) {
contacts.addAll(privateContacts)
}
getRecents(contacts, groupSubsequentCalls, maxSize, callback = callback)
}
}
}
@SuppressLint("NewApi")
private inline fun getRecents(contacts: List<Contact>, groupSubsequentCalls: Boolean, maxSize: Int, callback: (MutableList<RecentCall>) -> Unit) {
val recentCalls = mutableListOf<RecentCall>()
var previousRecentCallFrom = ""
var previousStartTS = 0
val contactsNumbersMap = HashMap<String, String>()
val contactPhotosMap = HashMap<String, String>()
val projection = arrayOf(
Calls._ID,
Calls.NUMBER,
Calls.CACHED_NAME,
Calls.CACHED_PHOTO_URI,
Calls.DATE,
Calls.DURATION,
Calls.TYPE,
Calls.PHONE_ACCOUNT_ID,
Calls.COUNTRY_ISO,
Calls.IS_READ,
// Calls.CACHED_MATCHED_NUMBER
)
val accountIdToSimIDMap = HashMap<String, Int>()
context.getAvailableSIMCardLabels().forEach {
accountIdToSimIDMap[it.handle.id] = it.id
}
val cursor = if (isNougatPlus()) {
// https://issuetracker.google.com/issues/175198972?pli=1#comment6
val limitedUri = contentUri.buildUpon()
.appendQueryParameter(Calls.LIMIT_PARAM_KEY, QUERY_LIMIT.toString())
.build()
val sortOrder = "${Calls.DATE} DESC"
context.contentResolver.query(limitedUri, projection, null, null, sortOrder)
} else {
val sortOrder = "${Calls.DATE} DESC LIMIT $QUERY_LIMIT"
context.contentResolver.query(contentUri, projection, null, null, sortOrder)
}
val contactsWithMultipleNumbers = contacts.filter { it.phoneNumbers.size > 1 }
val numbersToContactIDMap = HashMap<String, Int>()
contactsWithMultipleNumbers.forEach { contact ->
contact.phoneNumbers.forEach { phoneNumber ->
numbersToContactIDMap[phoneNumber.value] = contact.contactId
numbersToContactIDMap[phoneNumber.normalizedNumber] = contact.contactId
}
}
cursor?.use {
if (!cursor.moveToFirst()) {
return@use
}
do {
val id = cursor.getIntValue(Calls._ID)
var isUnknownNumber = false
val number = cursor.getStringValueOrNull(Calls.NUMBER)
if (number == null || number == "-1") {
isUnknownNumber = true
}
var name = cursor.getStringValueOrNull(Calls.CACHED_NAME)
if (name.isNullOrEmpty() || name == "-1") {
name = number.orEmpty()
}
if (name == number && !isUnknownNumber) {
if (contactsNumbersMap.containsKey(number)) {
name = contactsNumbersMap[number]!!
} else {
val normalizedNumber = number.normalizePhoneNumber()
if (normalizedNumber!!.length >= COMPARABLE_PHONE_NUMBER_LENGTH) {
name = contacts.filter { it.phoneNumbers.isNotEmpty() }.firstOrNull { contact ->
val curNumber = contact.phoneNumbers.first().normalizedNumber
if (curNumber.length >= COMPARABLE_PHONE_NUMBER_LENGTH) {
if (curNumber.substring(curNumber.length - COMPARABLE_PHONE_NUMBER_LENGTH) == normalizedNumber.substring(
normalizedNumber.length - COMPARABLE_PHONE_NUMBER_LENGTH
)
) {
contactsNumbersMap[number] = contact.getNameToDisplay()
return@firstOrNull true
}
}
false
}?.name ?: number
}
}
}
if (name.isEmpty() || name == "-1") {
name = "未知"
}
var photoUri = cursor.getStringValue(Calls.CACHED_PHOTO_URI) ?: ""
if (photoUri.isEmpty() && !number.isNullOrEmpty()) {
if (contactPhotosMap.containsKey(number)) {
photoUri = contactPhotosMap[number]!!
} else {
val contact = contacts.firstOrNull { it.doesContainPhoneNumber(number) }
if (contact != null) {
photoUri = contact.photoUri
contactPhotosMap[number] = contact.photoUri
}
}
}
val startTS = (cursor.getLongValue(Calls.DATE) / 1000L).toInt()
if (previousStartTS == startTS) {
continue
} else {
previousStartTS = startTS
}
val duration = cursor.getIntValue(Calls.DURATION)
val type = cursor.getIntValue(Calls.TYPE)
val accountId = cursor.getStringValue(Calls.PHONE_ACCOUNT_ID)
val simID = accountIdToSimIDMap[accountId] ?: -1
val neighbourIDs = mutableListOf<Int>()
var specificNumber = ""
var specificType = ""
val sfFlag = cursor.getStringValue(Calls.COUNTRY_ISO)
// val export = cursor.getStringValue(Calls.IS_READ)
// val syncHideNumber = cursor.getStringValue(Calls.CACHED_MATCHED_NUMBER)
val contactIdWithMultipleNumbers = numbersToContactIDMap[number]
if (contactIdWithMultipleNumbers != null) {
val specificPhoneNumber =
contacts.firstOrNull { it.contactId == contactIdWithMultipleNumbers }?.phoneNumbers?.firstOrNull { it.value == number }
if (specificPhoneNumber != null) {
specificNumber = specificPhoneNumber.value
specificType = context.getPhoneNumberTypeText(specificPhoneNumber.type, specificPhoneNumber.label)
}
}
val recentCall = RecentCall(
id = id,
phoneNumber = number.orEmpty(),//正常存储,加密放在结束通话
name = name,
photoUri = photoUri,
startTS = startTS,
duration = duration,
type = type,
neighbourIDs = neighbourIDs,
simID = simID,
specificNumber = specificNumber,
specificType = specificType,
isUnknownNumber = isUnknownNumber,
isShunFeng = sfFlag == "",
sfNumber = number.orEmpty(),
isExport = false,
// isHidePhoneNumber = syncNumber,
)
// if we have multiple missed calls from the same number, show it just once
if (!groupSubsequentCalls || "$number$name$simID" != previousRecentCallFrom) {
recentCalls.add(recentCall)
} else {
recentCalls.lastOrNull()?.neighbourIDs?.add(id)
}
previousRecentCallFrom = "$number$name$simID"
} while (cursor.moveToNext() && recentCalls.size < maxSize)
}
// val blockedNumbers = context.getBlockedNumbers()
//
// val recentResult = recentCalls
// .filter { !context.isNumberBlocked(it.phoneNumber, blockedNumbers) }.toMutableList()
// callback(recentResult)
callback(recentCalls)
}
@SuppressLint("MissingPermission")
fun Context.getAvailableSIMCardLabels(): List<SIMAccount> {
val SIMAccounts = mutableListOf<SIMAccount>()
try {
telecomManager.callCapablePhoneAccounts.forEachIndexed { index, account ->
val phoneAccount = telecomManager.getPhoneAccount(account)
var label = phoneAccount.label.toString()
var address = phoneAccount.address.toString()
if (address.startsWith("tel:") && address.substringAfter("tel:").isNotEmpty()) {
address = Uri.decode(address.substringAfter("tel:"))
label += " ($address)"
}
val SIM = SIMAccount(index + 1, phoneAccount.accountHandle, label, address.substringAfter("tel:"))
SIMAccounts.add(SIM)
}
} catch (ignored: Exception) {
}
return SIMAccounts
}
}
package com.simplemobiletools.commons.models
import android.os.Parcelable
import android.telephony.PhoneNumberUtils
import androidx.annotation.Keep
import com.simplemobiletools.commons.extensions.normalizePhoneNumber
import kotlinx.parcelize.Parcelize
/**
* Used at displaying recent calls.
* For contacts with multiple numbers specify the number and type
*/
@Keep
@kotlinx.serialization.Serializable
@Parcelize
data class RecentCall(
val id: Int,
val phoneNumber: String,
val name: String,
val photoUri: String,
val startTS: Int,
val duration: Int,
val type: Int,
val neighbourIDs: MutableList<Int>,
val simID: Int,
val specificNumber: String,
val specificType: String,
val isUnknownNumber: Boolean,
var isShunFeng: Boolean,
var sfNumber: String,
var isExport: Boolean = false,//是否已经导出过该条记录
) : Parcelable {
fun doesContainPhoneNumber(text: String): Boolean {
val normalizedText = text.normalizePhoneNumber()
return PhoneNumberUtils.compare(phoneNumber.normalizePhoneNumber(), normalizedText) ||
phoneNumber.contains(text) ||
phoneNumber.normalizePhoneNumber().contains(normalizedText) ||
phoneNumber.contains(normalizedText)
}
}
package com.simplemobiletools.commons.models
import android.telecom.PhoneAccountHandle
import androidx.annotation.Keep
@Keep
data class SIMAccount(val id: Int, val handle: PhoneAccountHandle, val label: String, val phoneNumber: String)
package com.simplemobiletools.commons.utils;
import android.os.Build;
import android.util.Base64;
import androidx.annotation.RequiresApi;
import com.secspace.log.Log;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* AES 对称加密算法,加解密工具类
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public class AESCommon {
private static final String TAG = AESCommon.class.getSimpleName() + " --> ";
/**
* 加密算法
*/
private static final String KEY_ALGORITHM = "AES";
/**
* AES 的 密钥长度,32 字节,范围:16 - 32 字节
*/
public static final int SECRET_KEY_LENGTH = 32;
/**
* 字符编码
*/
private static final Charset CHARSET_UTF8 = StandardCharsets.UTF_8;
/**
* 秘钥长度不足 16 个字节时,默认填充位数
*/
private static final String DEFAULT_VALUE = "0";
/**
* 加解密算法/工作模式/填充方式
*/
private static final String CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
/**
* 加密密码,长度:16 或 32 个字符(随便定义)
*/
private static final String secretKey = "qweasd123";
/**
* AES 加密
*
* @param data 待加密内容
* @return 返回Base64转码后的加密数据
*/
public static String encrypt(String data) {
try {
//创建密码器
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
//初始化为加密密码器
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(secretKey));
byte[] encryptByte = cipher.doFinal(data.getBytes(CHARSET_UTF8));
// 将加密以后的数据进行 Base64 编码
return base64Encode(encryptByte);
} catch (Exception e) {
handleException(e);
}
return null;
}
/**
* AES 解密
*
* @param base64Data 加密的密文 Base64 字符串
*/
public static String decrypt(String base64Data) {
try {
byte[] data = base64Decode(base64Data);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
//设置为解密模式
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(secretKey));
//执行解密操作
byte[] result = cipher.doFinal(data);
return new String(result, CHARSET_UTF8);
} catch (Exception e) {
handleException(e);
}
return null;
}
/**
* 使用密码获取 AES 秘钥
*/
public static SecretKeySpec getSecretKey(String secretKey) {
secretKey = toMakeKey(secretKey, SECRET_KEY_LENGTH, DEFAULT_VALUE);
return new SecretKeySpec(secretKey.getBytes(CHARSET_UTF8), KEY_ALGORITHM);
}
/**
* 如果 AES 的密钥小于 {@code length} 的长度,就对秘钥进行补位,保证秘钥安全。
*
* @param secretKey 密钥 key
* @param length 密钥应有的长度
* @param text 默认补的文本
* @return 密钥
*/
private static String toMakeKey(String secretKey, int length, String text) {
// 获取密钥长度
int strLen = secretKey.length();
// 判断长度是否小于应有的长度
if (strLen < length) {
// 补全位数
StringBuilder builder = new StringBuilder();
// 将key添加至builder中
builder.append(secretKey);
// 遍历添加默认文本
for (int i = 0; i < length - strLen; i++) {
builder.append(text);
}
// 赋值
secretKey = builder.toString();
}
return secretKey;
}
/**
* 将 Base64 字符串 解码成 字节数组
*/
public static byte[] base64Decode(String data) {
return Base64.decode(data, Base64.NO_WRAP);
}
/**
* 将 字节数组 转换成 Base64 编码
*/
public static String base64Encode(byte[] data) {
return Base64.encodeToString(data, Base64.NO_WRAP);
}
/**
* 处理异常
*/
private static void handleException(Exception e) {
e.printStackTrace();
Log.e(TAG, TAG + e);
}
/**
* 对文件进行AES加密
*
* @param sourceFile 待加密文件
* @param dir 加密后的文件存储路径
* @param toFileName 加密后的文件名称
* @param secretKey 密钥
* @return 加密后的文件
*/
public static File encryptFile(File sourceFile, String dir, String toFileName, String secretKey) {
try {
// 创建加密后的文件
File encryptFile = new File(dir, toFileName);
// 根据文件创建输出流
FileOutputStream outputStream = new FileOutputStream(encryptFile);
// 初始化 Cipher
Cipher cipher = initFileAESCipher(secretKey, Cipher.ENCRYPT_MODE);
// 以加密流写入文件
CipherInputStream cipherInputStream = new CipherInputStream(
new FileInputStream(sourceFile), cipher);
// 创建缓存字节数组
byte[] buffer = new byte[1024 * 2];
// 读取
int len;
// 读取加密并写入文件
while ((len = cipherInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
outputStream.flush();
}
// 关闭加密输入流
cipherInputStream.close();
closeStream(outputStream);
return encryptFile;
} catch (Exception e) {
handleException(e);
}
return null;
}
/**
* AES解密文件
*
* @param sourceFile 源加密文件
* @param dir 解密后的文件存储路径
* @param toFileName 解密后的文件名称
* @param secretKey 密钥
*/
public static File decryptFile(File sourceFile, String dir, String toFileName, String secretKey) {
try {
// 创建解密文件
File decryptFile = new File(dir, toFileName);
// 初始化Cipher
Cipher cipher = initFileAESCipher(secretKey, Cipher.DECRYPT_MODE);
// 根据源文件创建输入流
FileInputStream inputStream = new FileInputStream(sourceFile);
// 获取解密输出流
CipherOutputStream cipherOutputStream = new CipherOutputStream(
new FileOutputStream(decryptFile), cipher);
// 创建缓冲字节数组
byte[] buffer = new byte[1024 * 2];
int len;
// 读取解密并写入
while ((len = inputStream.read(buffer)) >= 0) {
cipherOutputStream.write(buffer, 0, len);
cipherOutputStream.flush();
}
// 关闭流
cipherOutputStream.close();
closeStream(inputStream);
return decryptFile;
} catch (IOException e) {
handleException(e);
}
return null;
}
/**
* 初始化 AES Cipher
*
* @param secretKey 密钥
* @param cipherMode 加密模式
* @return 密钥
*/
private static Cipher initFileAESCipher(String secretKey, int cipherMode) {
try {
// 创建密钥规格
SecretKeySpec secretKeySpec = getSecretKey(secretKey);
// 获取密钥
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// 初始化
cipher.init(cipherMode, secretKeySpec, new IvParameterSpec(new byte[cipher.getBlockSize()]));
return cipher;
} catch (Exception e) {
handleException(e);
}
return null;
}
/**
* 关闭流
*
* @param closeable 实现Closeable接口
*/
private static void closeStream(Closeable closeable) {
try {
if (closeable != null) closeable.close();
} catch (Exception e) {
handleException(e);
}
}
}
package com.simplemobiletools.commons.utils
import java.lang.reflect.Field
object PhoneFromUtilCommon {
/**
* 获取调用者的包名
*
* @return
*/
// 反射的方式获取mReferrer
fun reflectGetReferrerA(): String {
try {
val activityClass = Class.forName("android.app.Activity");
val refererField = activityClass.getDeclaredField("mReferrer");
refererField.isAccessible = true
val referrer: String = refererField.get(this).toString()
return referrer
} catch (e: Exception) {
return "No referrer"
}
}
fun reflectGetReferrer(): String {
try {
val activityClass = Class.forName("android.app.Activity")
val refererField: Field = activityClass.getDeclaredField("mReferrer")
refererField.isAccessible = true
return refererField.get(this).toString()
} catch (e: ClassNotFoundException) {
e.printStackTrace()
return "No referrer"
} catch (e: IllegalAccessException) {
e.printStackTrace()
return "No referrer"
} catch (e: NoSuchFieldException) {
e.printStackTrace()
return "No referrer"
}
}
/**
* 是否是纯数字 正则
*/
fun isNumeric(str: String): Boolean {
return str.matches("\\d+".toRegex())
}
/**
* 判断字符串是否为纯中文
*
* @param str 输入的字符串
* @return 是否为纯中文
*/
fun isPureChinese(str: String?): Boolean {
if (str.isNullOrEmpty()) {
return false
}
// 定义中文字符的 Unicode 范围
val regex = "[\\u4E00-\\u9FA5|\\u3400-\\u4DBF|\\u20000-\\u2A6DF]+"
return str.matches(regex.toRegex())
}
fun containsChinese(str: String): Boolean {
for (i in 0 until str.length) {
val c = str[i]
if (c >= '\u4e00' && c <= '\u9fff' || c >= '\u3400' && c <= '\u4DBF') {
return true
}
}
return false
}
}
package com.simplemobiletools.commons.utils
/**
* 手机号码处理工具类
* Created by
*/
object PhoneUtilsCommon {
/**
* 电话号掩码规则
*/
fun phoneNumberFormat(number: String, phoneCallBack: (String) -> Unit/*, errorCallback: (String) -> Unit*/) {
if (number.isBlank() || number.length <= 6) return
var phoneResult = ""
val numberResult = number.replace(" ", "")//原始号码
when (numberResult.length) {
7 -> {
val phone = numberResult.substring(3, numberResult.length)
phoneResult = "${"***$phone"}"
phoneCallBack.invoke(phoneResult)
}
8 -> {
val phone = numberResult.substring(4, numberResult.length)
phoneResult = "${"****$phone"}"
phoneCallBack.invoke(phoneResult)
}
11 -> {
if (numberResult.substring(0, 1) == "1") {
phoneResult = numberResult.replace("(\\d{3})\\d{4}(\\d{4})".toRegex(), "$1****$2")
phoneCallBack.invoke(phoneResult)
} else {
val phone1 = numberResult.substring(0, 3)
val phone2 = numberResult.substring(7, numberResult.length)
phoneResult = "${"$phone1****$phone2"}"
phoneCallBack.invoke(phoneResult)
}
}
12 -> {
val phone1 = numberResult.substring(0, 4)
val phone2 = numberResult.substring(9, numberResult.length)
phoneResult = "${"$phone1****$phone2"}"
phoneCallBack.invoke(phoneResult)
}
14 -> {
if (numberResult.contains("+86")) {
val phoneNum = numberResult.substring(3, numberResult.length)
phoneResult = phoneNum.replace("(\\d{3})\\d{4}(\\d{4})".toRegex(), "$1****$2")
phoneCallBack.invoke(phoneResult)
}
}
else -> {
if (number.isNotBlank()) {
phoneCallBack.invoke(number)
}
}
}
// Log.e("shuju", "---phoneNumberFormat---原号码:${numberResult} 掩码后:$phoneResult //手机号长度:${numberResult.length}")
}
/**
* 手机号码脱敏转换
*/
fun phoneConvert(number: String): String {
if (number.isBlank() || number.length <= 6) return ""
var phoneResult = ""
val numberResult = number.replace(" ", "")//原始号码
if (numberResult.substring(0, 1) == "1") {
phoneResult = numberResult.replace("(\\d{3})\\d{4}(\\d{4})".toRegex(), "$1****$2")
} else {
val phone1 = numberResult.substring(0, 3)
val phone2 = numberResult.substring(7, numberResult.length)
phoneResult = "${"$phone1****$phone2"}"
}
return phoneResult
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment