Compare commits

...

10 Commits

Author SHA1 Message Date
ZhanGSKen
50d4cd830b <contacts>APK 15.3.16 release Publish. 2025-08-31 06:05:42 +08:00
ZhanGSKen
2079822c00 更新类库 2025-08-31 06:04:54 +08:00
ZhanGSKen
297c76f328 <contacts>Start New Stage Version. 2025-08-31 06:02:02 +08:00
ZhanGSKen
43b18ee662 <contacts>Start New Stage Version. 2025-08-31 06:01:23 +08:00
ZhanGSKen
d581cd9842 <contacts>APK 15.3.15 release Publish. 2025-08-30 15:04:01 +08:00
ZhanGSKen
cef50d087d 联系人搜索框添加拼音首字母简单搜索功能,如“周星星”输入“zxx”即可搜索到。 2025-08-30 15:02:37 +08:00
ZhanGSKen
04df902b6b <contacts>APK 15.3.14 release Publish. 2025-08-26 01:37:58 +08:00
ZhanGSKen
33c71ea868 添加手机端BoBullToon数据文件夹查找模块。 2025-08-25 20:47:49 +08:00
ZhanGSKen
ba861d910e <contacts>APK 15.3.13 release Publish. 2025-08-20 20:19:22 +08:00
ZhanGSKen
f5d9aafe43 更新默认BoBullToon数据源URL 2025-08-20 20:17:53 +08:00
8 changed files with 420 additions and 353 deletions

View File

@@ -45,9 +45,9 @@ android {
dependencies {
api fileTree(dir: 'libs', include: ['*.jar'])
api 'cc.winboll.studio:libaes:15.9.2'
api 'cc.winboll.studio:libapputils:15.8.4'
api 'cc.winboll.studio:libappbase:15.8.4'
api 'cc.winboll.studio:libaes:15.9.3'
api 'cc.winboll.studio:libapputils:15.8.5'
api 'cc.winboll.studio:libappbase:15.9.5'
// 权限请求框架https://github.com/getActivity/XXPermissions
api 'com.github.getActivity:XXPermissions:18.63'

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sun Jul 27 15:39:49 HKT 2025
stageCount=13
#Sun Aug 31 06:05:42 CST 2025
stageCount=17
libraryProject=
baseVersion=15.3
publishVersion=15.3.12
publishVersion=15.3.16
buildCount=0
baseBetaVersion=15.3.13
baseBetaVersion=15.3.17

View File

@@ -1,5 +1,10 @@
package cc.winboll.studio.contacts;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@188.com>
* @Date 2025/08/30 14:32
* @Describe 主窗口
*/
import android.Manifest;
import android.app.Activity;
import android.app.ActivityManager;
@@ -8,6 +13,7 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.telecom.TelecomManager;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
@@ -41,7 +47,7 @@ import java.util.List;
final public class MainActivity extends AppCompatActivity implements IWinBoLLActivity, ViewPager.OnPageChangeListener, View.OnClickListener {
public static final String TAG = "MainActivity";
public static final String TAG = "MainActivity";
public static final int REQUEST_HOME_ACTIVITY = 0;
public static final int REQUEST_ABOUT_ACTIVITY = 1;
@@ -55,13 +61,10 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
MainServiceBean mMainServiceBean;
private TabLayout tabLayout;
private ViewPager viewPager;
private List<View> views; //用来存放放进ViewPager里面的布局
//实例化存储imageView导航原点的集合
private List<View> views;
ImageView[] imageViews;
//MyPagerAdapter adapter;//适配器
MyPagerAdapter pagerAdapter;
LinearLayout linearLayout;//下标所在在LinearLayout布局里
int currentPoint = 0;//当前被选中中页面的下标
LinearLayout linearLayout;
int currentPoint = 0;
private TelephonyManager telephonyManager;
private MyPhoneStateListener phoneStateListener;
@@ -70,30 +73,6 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
private static final int DIALER_REQUEST_CODE = 1;
// @Override
// public Activity getActivity() {
// return this;
// }
// @Override
// public APPInfo getAppInfo() {
// String szBranchName = "contacts";
//
// APPInfo appInfo = AboutActivityFactory.buildDefaultAPPInfo();
// appInfo.setAppName("Contacts");
// appInfo.setAppIcon(cc.winboll.studio.libapputils.R.drawable.ic_winboll);
// appInfo.setAppDescription("Contacts Description");
// appInfo.setAppGitName("APP");
// appInfo.setAppGitOwner("Studio");
// appInfo.setAppGitAPPBranch(szBranchName);
// appInfo.setAppGitAPPSubProjectFolder(szBranchName);
// appInfo.setAppHomePage("https://www.winboll.cc/studio/details.php?app=Contacts");
// appInfo.setAppAPKName("Contacts");
// appInfo.setAppAPKFolderName("Contacts");
// return appInfo;
// return null;
// }
@Override
public Activity getActivity() {
@@ -107,89 +86,62 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
@Override
protected void onCreate(Bundle savedInstanceState) {
// 接收并处理 Intent 数据,函数 Intent 处理接收就直接返回
//if (prosessIntents(getIntent())) return;
// 以下正常创建主窗口
super.onCreate(savedInstanceState);
_MainActivity = this;
setContentView(R.layout.activity_main);
// 初始化工具栏
mToolbar = findViewById(R.id.activitymainToolbar1);
// 初始化工具栏仅加载基础UI
mToolbar = (Toolbar) findViewById(R.id.activitymainToolbar1);
setSupportActionBar(mToolbar);
// if (isEnableDisplayHomeAsUp()) {
// // 显示后退按钮
// getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// }
getSupportActionBar().setSubtitle(TAG);
tabLayout = findViewById(R.id.tabLayout);
viewPager = findViewById(R.id.viewPager);
tabLayout = (TabLayout) findViewById(R.id.tabLayout);
viewPager = (ViewPager) findViewById(R.id.viewPager);
// 创建Fragment列表和标题列表
fragmentList = new ArrayList<>();
tabTitleList = new ArrayList<>();
// 创建Fragment列表(仅实例化,不加载数据)
fragmentList = new ArrayList<Fragment>();
tabTitleList = new ArrayList<String>();
fragmentList.add(CallLogFragment.newInstance(0));
fragmentList.add(ContactsFragment.newInstance(1));
fragmentList.add(ContactsFragment.newInstance(1)); // 延迟加载联系人数据
fragmentList.add(LogFragment.newInstance(2));
tabTitleList.add("通话记录");
tabTitleList.add("联系人");
tabTitleList.add("应用日志");
// 设置ViewPager适配器
// 设置ViewPager适配器
MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager(), fragmentList, tabTitleList);
viewPager.setAdapter(adapter);
// 关键:关闭预加载,仅当前页初始化
viewPager.setOffscreenPageLimit(0);
// 关联TabLayout和ViewPager
tabLayout.setupWithViewPager(viewPager);
// initData();
// initView();
// //initPoint();//调用初始化导航原点的方法
// viewPager.addOnPageChangeListener(this);//滑动事件
//ViewPager viewPager = findViewById(R.id.activitymainViewPager1);
//MyPagerAdapter pagerAdapter = new MyPagerAdapter(getSupportFragmentManager());
//viewPager.setAdapter(pagerAdapter);
//TabLayout tabLayout = findViewById(R.id.activitymainTabLayout1);
//tabLayout.setupWithViewPager(viewPager);
// mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
// if (mMainServiceBean == null) {
// mMainServiceBean = new MainServiceBean();
// }
// cbMainService = findViewById(R.id.activitymainCheckBox1);
// cbMainService.setChecked(mMainServiceBean.isEnable());
// cbMainService.setOnClickListener(new View.OnClickListener(){
// @Override
// public void onClick(View view) {
// if (cbMainService.isChecked()) {
// MainService.startMainService(MainActivity.this);
// } else {
// MainService.stopMainService(MainActivity.this);
// }
// }
// });
// 初始化服务状态(延迟启动非核心服务)
MainServiceBean mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
if (mMainServiceBean == null) {
mMainServiceBean = new MainServiceBean();
MainServiceBean.saveBean(this, mMainServiceBean);
}
if (mMainServiceBean.isEnable()) {
MainService.startMainService(this);
// 延迟1秒启动服务避免阻塞启动
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
MainService.startMainService(MainActivity.this);
}
}, 1000);
}
// 初始化TelephonyManager和PhoneStateListener
// 初始化电话状态监听(基础功能保留)
telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
phoneStateListener = new MyPhoneStateListener();
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
}
// ViewPager适配器
// ViewPager适配器Java 7语法
private class MyPagerAdapter extends FragmentPagerAdapter {
private List<Fragment> fragmentList;
@@ -226,91 +178,22 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
_MainActivity.startActivity(intent);
}
//初始化view即显示的图片
// void initView() {
// viewPager = findViewById(R.id.activitymainViewPager1);
// pagerAdapter = new MyPagerAdapter(getSupportFragmentManager());
// viewPager.setAdapter(pagerAdapter);
// //adapter = new MyPagerAdapter(views);
// //viewPager = findViewById(R.id.activitymainViewPager1);
// //viewPager.setAdapter(adapter);
// //linearLayout = findViewById(R.id.activitymainLinearLayout1);
// //initPoint();//初始化页面下方的点
// viewPager.setOnPageChangeListener(this);
//
// }
//初始化所要显示的布局
// void initData() {
// LayoutInflater inflater = LayoutInflater.from(getActivity());
// View view1 = inflater.inflate(R.layout.fragment_call_log, viewPager, false);
// View view2 = inflater.inflate(R.layout.fragment_contacts, viewPager, false);
// View view3 = inflater.inflate(R.layout.fragment_log, viewPager, false);
//
// views = new ArrayList<>();
// views.add(view1);
// views.add(view2);
// views.add(view3);
// }
// void initPoint() {
// imageViews = new ImageView[5];//实例化5个图片
// for (int i = 0; i < linearLayout.getChildCount(); i++) {
// imageViews[i] = (ImageView) linearLayout.getChildAt(i);
// imageViews[i].setImageResource(R.drawable.ic_launcher);
// imageViews[i].setOnClickListener(this);//点击导航点,即可跳转
// imageViews[i].setTag(i);//重复利用实例化的对象
// }
// currentPoint = 0;//默认第一个坐标
// imageViews[currentPoint].setImageResource(R.drawable.ic_launcher);
// }
//OnPageChangeListener接口要实现的三个方法
/* onPageScrollStateChanged(int state)
此方法是在状态改变的时候调用其中state这个参数有三种状态
SCROLL_STATE_DRAGGING1表示用户手指“按在屏幕上并且开始拖动”的状态
手指按下但是还没有拖动的时候还不是这个状态只有按下并且手指开始拖动后log才打出。
SCROLL_STATE_IDLE0滑动动画做完的状态。
SCROLL_STATE_SETTLING2在“手指离开屏幕”的状态。*/
// OnPageChangeListener接口实现
@Override
public void onPageScrollStateChanged(int state) {
public void onPageScrollStateChanged(int state) {}
}
/* onPageScrolled(int position, float positionOffset, int positionOffsetPixels)
当页面在滑动的时候会调用此方法,在滑动被停止之前,此方法回一直得到调用。其中三个参数的含义分别为:
position :当前页面即你点击滑动的页面从A滑B则是A页面的position。
positionOffset:当前页面偏移的百分比
positionOffsetPixels:当前页面偏移的像素位置*/
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
}
/* onPageSelected(int position)
此方法是页面滑动完后得到调用position是你当前选中的页面的Position位置编号
(从A滑动到B就是B的position)*/
public void onPageSelected(int position) {
// ImageView preView = imageViews[currentPoint];
// preView.setImageResource(R.drawable.ic_launcher);
// ImageView currView = imageViews[position];
// currView.setImageResource(R.drawable.ic_launcher);
// currentPoint = position;
}
//小圆点点击事件
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
//通过getTag(),可以判断是哪个控件
// int i = (Integer) v.getTag();
// viewPager.setCurrentItem(i);//直接跳转到某一个页面的情况
}
public void onPageSelected(int position) {}
@Override
public void onClick(View v) {}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
//setSubTitle("");
}
private class MyPhoneStateListener extends PhoneStateListener {
@@ -336,109 +219,31 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
LogUtils.d(TAG, "onDestroy() SOS");
}
//
// 处理传入的 Intent 数据
//
// boolean prosessIntents(Intent intent) {
// if (intent == null
// || intent.getAction() == null
// || intent.getAction().equals(""))
// return false;
//
// if (intent.getAction().equals(StringToQrCodeView.ACTION_UNITTEST_QRCODE)) {
// try {
// WinBoLLActivity clazzActivity = UnitTestActivity.class.newInstance();
// String tag = clazzActivity.getTag();
// LogUtils.d(TAG, "String tag = clazzActivity.getTag(); tag " + tag);
// Intent subIntent = new Intent(this, UnitTestActivity.class);
// subIntent.setAction(intent.getAction());
// File file = new File(getCacheDir(), UUID.randomUUID().toString());
// //取出文件uri
// Uri uri = intent.getData();
// if (uri == null) {
// uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
// }
// //获取文件真实地址
// String szSrcPath = UriUtils.getFileFromUri(getApplication(), uri);
// if (TextUtils.isEmpty(szSrcPath)) {
// return false;
// }
//
// Files.copy(Paths.get(szSrcPath), Paths.get(file.getPath()));
// //startWinBoLLActivity(subIntent, tag);
// WinBoLLActivityManager.getInstance(this).startWinBoLLActivity(this, subIntent, UnitTestActivity.class);
// } catch (IllegalAccessException | InstantiationException | IOException e) {
// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
// // 函数处理异常返回失败
// return false;
// }
// } else {
// LogUtils.d(TAG, "prosessIntents|" + intent.getAction() + "|yet");
// return false;
// }
// return true;
// }
// @Override
// public String getTag() {
// return TAG;
// }
// @Override
// public void onBackPressed() {
// exit();
// }
//
// void exit() {
// YesNoAlertDialog.OnDialogResultListener listener = new YesNoAlertDialog.OnDialogResultListener(){
//
// @Override
// public void onYes() {
// WinBoLLActivityManager.getInstance(getApplicationContext()).finishAll();
// }
//
// @Override
// public void onNo() {
// }
// };
// YesNoAlertDialog.show(this, "[ " + getString(R.string.app_name) + " ]", "Exit(Yes/No).\nIs close all activity?", listener);
// }
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.toolbar_main, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.item_settings) {
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
//WinBoLLActivityManager.getInstance(this).startWinBoLLActivity(this, CallActivity.class);
}
// } else
// if (item.getItemId() == R.id.item_exit) {
// exit();
// return true;
// }
return super.onOptionsItemSelected(item);
}
@Override
protected void onResume() {
super.onResume();
}
/**
* Android M 及以上检查是否是系统默认电话应用
* 检查是否是系统默认电话应用
*/
public boolean isDefaultPhoneCallApp() {
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
TelecomManager manger = (TelecomManager) getSystemService(TELECOM_SERVICE);
if (manger != null && manger.getDefaultDialerPackage() != null) {
return manger.getDefaultDialerPackage().equals(getPackageName());
@@ -452,35 +257,22 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct
if (manager == null) return false;
for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(
Integer.MAX_VALUE)) {
Integer.MAX_VALUE)) {
if (serviceClass.getName().equals(service.service.getClassName())) {
return true;
}
}
return false;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// switch (resultCode) {
// case REQUEST_HOME_ACTIVITY : {
// LogUtils.d(TAG, "REQUEST_HOME_ACTIVITY");
// break;
// }
// case REQUEST_ABOUT_ACTIVITY : {
// LogUtils.d(TAG, "REQUEST_ABOUT_ACTIVITY");
// break;
// }
// default : {
// super.onActivityResult(requestCode, resultCode, data);
// }
// }
if (requestCode == DIALER_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
Toast.makeText(MainActivity.this, getString(R.string.app_name) + " 已成为默认电话应用",
Toast.LENGTH_SHORT).show();
Toast.LENGTH_SHORT).show();
}
}
}
}

View File

@@ -1,9 +1,9 @@
package cc.winboll.studio.contacts.beans;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/02/26 13:37:00
* @Describe ContactModel
* @Author ZhanGSKen&豆包大模型<zhangsken@188.com>
* @Date 2025/08/30 14:32
* @Describe 联系人信息数据模型
*/
import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
@@ -18,13 +18,18 @@ public class ContactModel {
private String name;
private String number;
private String pinyin;
// 新增:存储姓名的拼音首字母(如"啊牛"→"an"
private String pinyinFirstLetter;
public ContactModel(String name, String number) {
this.name = name;
this.number = number.replaceAll("\\s", "");
this.pinyin = convertToPinyin(name);
// 初始化时生成拼音首字母
this.pinyinFirstLetter = convertToPinyinFirstLetter(name);
}
// 原方法:转换为全拼(如"啊牛"→"aniu"
private String convertToPinyin(String chinese) {
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
@@ -33,22 +38,55 @@ public class ContactModel {
StringBuilder pinyin = new StringBuilder();
for (int i = 0; i < chinese.length(); i++) {
char ch = chinese.charAt(i);
if (Character.toString(ch).matches("[\\u4e00-\\u9fa5]")) {
if (Character.toString(ch).matches("[\\u4e00-\\u9fa5]")) { // 仅处理汉字
try {
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(ch, format);
if (pinyinArray != null) {
pinyin.append(pinyinArray[0]);
if (pinyinArray != null && pinyinArray.length > 0) {
pinyin.append(pinyinArray[0]); // 取第一个拼音(多音字默认首选项)
}
} catch (BadHanyuPinyinOutputFormatCombination e) {
e.printStackTrace();
}
} else {
pinyin.append(ch);
pinyin.append(ch); // 非汉字直接拼接(如字母、数字、符号)
}
}
return pinyin.toString();
}
// 新增:转换为拼音首字母(如"啊牛"→"an"
private String convertToPinyinFirstLetter(String chinese) {
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
StringBuilder firstLetters = new StringBuilder();
for (int i = 0; i < chinese.length(); i++) {
char ch = chinese.charAt(i);
if (Character.toString(ch).matches("[\\u4e00-\\u9fa5]")) { // 仅处理汉字
try {
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(ch, format);
if (pinyinArray != null && pinyinArray.length > 0) {
// 取拼音的第一个字母(如"a"、"niu"→"a"、"n"
firstLetters.append(pinyinArray[0].charAt(0));
}
} catch (BadHanyuPinyinOutputFormatCombination e) {
e.printStackTrace();
}
} else {
// 非汉字可根据需求处理:此处保留原字符(如"李3"→"l3""张A"→"za"
firstLetters.append(ch);
}
}
return firstLetters.toString();
}
// 新增:获取拼音首字母
public String getPinyinFirstLetter() {
return pinyinFirstLetter;
}
// 原有getter方法
public String getName() {
return name;
}

View File

@@ -11,6 +11,7 @@ import cc.winboll.studio.contacts.dun.Rules;
import cc.winboll.studio.libappbase.LogUtils;
import com.hjq.toast.ToastUtils;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -154,6 +155,62 @@ public class TomCat {
File getWorkingFolder() {
return mContext.getExternalFilesDir(TAG);
}
public File getBoBullToonDataFolder() {
File fCheckRoot = getWorkingFolder();
if (fCheckRoot == null || !fCheckRoot.exists()) {
return fCheckRoot;
}
// 递归查找符合条件的文件夹
File targetFolder = findTargetFolder(fCheckRoot);
return targetFolder != null ? targetFolder : fCheckRoot;
}
/**
* 递归查找同时包含LICENSE和README.md文件的文件夹
*/
private File findTargetFolder(File currentFolder) {
// 检查当前文件夹是否符合条件
if (hasRequiredFiles(currentFolder)) {
return currentFolder;
}
// 查找子文件夹Java 7不支持方法引用用匿名内部类过滤
File[] subFolders = currentFolder.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isDirectory(); // 仅保留子文件夹
}
});
if (subFolders != null) {
for (File subFolder : subFolders) {
File result = findTargetFolder(subFolder);
if (result != null) {
return result;
}
}
}
return null;
}
/**
* 检查文件夹中是否同时存在LICENSE和README.md文件
*/
private boolean hasRequiredFiles(File folder) {
if (folder == null || !folder.isDirectory()) {
return false;
}
// 检查两个文件是否同时存在且均为文件(非文件夹)
File licenseFile = new File(folder, "LICENSE");
File readmeFile = new File(folder, "README.md");
return licenseFile.exists() && licenseFile.isFile()
&& readmeFile.exists() && readmeFile.isFile();
}
public void cleanBoBullToon() {
String destinationFolder = getWorkingFolder().getPath(); // 替换为实际的目标文件夹路径
@@ -170,9 +227,9 @@ public class TomCat {
public boolean loadPhoneBoBullToon() {
listPhoneBoBullToon.clear();
File fBoBullToon = new File(getWorkingFolder(), "bobulltoon");
File fBoBullToon = getBoBullToonDataFolder();
if (fBoBullToon.exists()) {
LogUtils.d(TAG, String.format("getWorkingFolder() %s", getWorkingFolder()));
LogUtils.d(TAG, String.format("getBoBullToonDataFolder() %s", getWorkingFolder()));
for (File userFolder : fBoBullToon.listFiles()) {
if (userFolder.isDirectory()) {
for (File recordFile : userFolder.listFiles()) {

View File

@@ -1,15 +1,18 @@
package cc.winboll.studio.contacts.fragments;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/02/20 12:57:50
* @Describe 联系人
* @Author ZhanGSKen&豆包大模型<zhangsken@188.com>
* @Date 2025/08/30 14:32
* @Describe 联系人视图
*/
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.ContactsContract;
import android.text.Editable;
import android.text.TextWatcher;
@@ -27,24 +30,39 @@ import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.contacts.R;
import cc.winboll.studio.contacts.adapters.ContactAdapter;
import cc.winboll.studio.contacts.beans.ContactModel;
import cc.winboll.studio.libappbase.LogUtils;
import com.hjq.toast.ToastUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ContactsFragment extends Fragment {
public static final String TAG = "ContactsFragment";
private static final String ARG_PAGE = "ARG_PAGE";
private int mPage;
private static final int REQUEST_READ_CONTACTS = 1;
private int mPage;
private RecyclerView recyclerView;
private ContactAdapter contactAdapter;
private List<ContactModel> contactList = new ArrayList<>();
private List<ContactModel> originalContactList = new ArrayList<>();
private EditText searchEditText;
private Button btnDial;
private boolean isViewInitialized = false; // 标记视图是否已初始化
// 静态缓存:全局复用联系人数据
private static List<ContactModel> sCachedOriginalList = new ArrayList<ContactModel>();
private static List<ContactModel> sCachedFilteredList = new ArrayList<ContactModel>();
// 当前页面数据容器
private List<ContactModel> contactList = new ArrayList<ContactModel>();
private List<ContactModel> originalContactList = new ArrayList<ContactModel>();
// 异步工具
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final Handler mainHandler = new Handler(Looper.getMainLooper());
private boolean isDataLoaded = false;
public static ContactsFragment newInstance(int page) {
Bundle args = new Bundle();
@@ -65,110 +83,272 @@ public class ContactsFragment extends Fragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_contacts, container, false);
// 加载布局(已移除进度条相关代码)
View view = inflater.inflate(R.layout.fragment_contacts, container, false);
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
recyclerView = view.findViewById(R.id.contacts_recycler_view);
// 初始化RecyclerView
recyclerView = (RecyclerView) view.findViewById(R.id.contacts_recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
contactList = new ArrayList<ContactModel>();
contactAdapter = new ContactAdapter(getContext(), contactList);
recyclerView.setAdapter(contactAdapter);
// 初始隐藏列表,数据加载后显示
recyclerView.setVisibility(View.GONE);
searchEditText = view.findViewById(R.id.search_edit_text);
searchEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
// 绑定搜索框和拨号按钮
searchEditText = (EditText) view.findViewById(R.id.search_edit_text);
btnDial = (Button) view.findViewById(R.id.btn_dial);
// 初始隐藏搜索相关控件,延迟到首次可见时显示
searchEditText.setVisibility(View.GONE);
btnDial.setVisibility(View.GONE);
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
filterContacts(s.toString());
}
// 首次可见时初始化资源
@Override
public void onResume() {
super.onResume();
if (!isViewInitialized) {
initSearchAndDial(); // 初始化搜索和拨号功能
checkContactPermission(); // 检查权限并加载数据
isViewInitialized = true;
}
}
@Override
public void afterTextChanged(Editable s) {
}
});
// 初始化搜索框和拨号按钮
private void initSearchAndDial() {
// 显示搜索相关控件
searchEditText.setVisibility(View.VISIBLE);
btnDial.setVisibility(View.VISIBLE);
// 搜索框防抖监听
searchEditText.addTextChangedListener(new DebounceTextWatcher(300) {
@Override
public void onDebounceTextChanged(String query) {
filterContacts(query);
}
});
// 拨号按钮点击事件
btnDial.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String phoneNumber = searchEditText.getText().toString().replaceAll("\\s", "");
if (phoneNumber.isEmpty()) {
ToastUtils.show("请输入号码");
return;
}
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
});
}
// 权限检查
private void checkContactPermission() {
if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_READ_CONTACTS);
} else {
readContacts();
loadContacts();
}
Button btnDial = view.findViewById(R.id.btn_dial);
btnDial.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View p1) {
String phoneNumber = searchEditText.getText().toString().replaceAll("\\s", "");
//phoneNumber = "+8616769764848";
ToastUtils.show(phoneNumber);
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
// 添加 FLAG_ACTIVITY_NEW_TASK 标志
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
});
}
// 加载联系人(延迟到首次可见时)
private void loadContacts() {
// 若有缓存,直接复用
if (!sCachedOriginalList.isEmpty() && !sCachedFilteredList.isEmpty()) {
originalContactList.clear();
originalContactList.addAll(sCachedOriginalList);
contactList.clear();
contactList.addAll(sCachedFilteredList);
contactAdapter.notifyDataSetChanged();
recyclerView.setVisibility(View.VISIBLE); // 显示列表
isDataLoaded = true;
return;
}
// 无缓存时异步加载
if (!isDataLoaded) {
recyclerView.setVisibility(View.GONE); // 加载中隐藏列表
executor.execute(new Runnable() {
@Override
public void run() {
// 子线程读取联系人
final List<ContactModel> tempList = readContactsInBackground();
// 主线程更新UI
mainHandler.post(new Runnable() {
@Override
public void run() {
// 更新缓存
sCachedOriginalList.clear();
sCachedOriginalList.addAll(tempList);
sCachedFilteredList.clear();
sCachedFilteredList.addAll(tempList);
// 更新当前列表
originalContactList.clear();
originalContactList.addAll(sCachedOriginalList);
contactList.clear();
contactList.addAll(sCachedFilteredList);
contactAdapter.notifyDataSetChanged();
LogUtils.d(TAG, String.format("联系人加载完成,共%d条数据", contactList.size()));
// 数据加载后显示列表
recyclerView.setVisibility(View.VISIBLE);
isDataLoaded = true;
}
});
}
});
}
}
// 子线程读取联系人
private List<ContactModel> readContactsInBackground() {
List<ContactModel> tempList = new ArrayList<ContactModel>();
Cursor cursor = null;
try {
// 查询联系人姓名和号码
cursor = requireContext().getContentResolver().query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
new String[]{
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER
},
null,
null,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " ASC"
);
if (cursor != null && cursor.moveToFirst()) {
int nameIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
int numberIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
do {
String name = cursor.getString(nameIndex);
String number = cursor.getString(numberIndex).replaceAll("\\s", ""); // 去除空格
tempList.add(new ContactModel(name, number));
} while (cursor.moveToNext());
}
} catch (Exception e) {
LogUtils.d(TAG, "读取联系人失败:" + e);
} finally {
if (cursor != null) {
cursor.close(); // 关闭游标,避免内存泄漏
}
}
return tempList;
}
// 过滤联系人
private void filterContacts(String query) {
contactList.clear();
if (query.isEmpty()) {
contactList.addAll(originalContactList);
sCachedFilteredList.clear();
sCachedFilteredList.addAll(originalContactList);
} else {
String lowerQuery = query.toLowerCase();
for (ContactModel contact : originalContactList) {
// 匹配姓名、全拼、简拼、号码
boolean matchName = contact.getName().toLowerCase().contains(lowerQuery);
boolean matchPinyin = contact.getPinyin().toLowerCase().contains(lowerQuery);
boolean matchFirstLetter = contact.getPinyinFirstLetter().toLowerCase().contains(lowerQuery);
boolean matchNumber = contact.getNumber().contains(lowerQuery);
if (matchName || matchPinyin || matchFirstLetter || matchNumber) {
contactList.add(contact);
}
}
sCachedFilteredList.clear();
sCachedFilteredList.addAll(contactList);
}
contactAdapter.notifyDataSetChanged();
// 过滤后确保列表可见
recyclerView.setVisibility(View.VISIBLE);
}
// 权限回调
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_READ_CONTACTS) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
readContacts();
loadContacts(); // 授权后加载联系人
} else {
ToastUtils.show("请授予联系人权限以查看联系人列表");
recyclerView.setVisibility(View.VISIBLE); // 显示空列表
}
}
}
private void readContacts() {
contactList.clear();
originalContactList.clear();
Cursor cursor = requireContext().getContentResolver().query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null,
null,
null,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " ASC");
// 防抖TextWatcherJava 7实现
public abstract static class DebounceTextWatcher implements TextWatcher {
private final long debounceDelay;
private Handler handler = new Handler(Looper.getMainLooper());
private Runnable pendingRunnable;
if (cursor != null) {
while (cursor.moveToNext()) {
String name = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
ContactModel contact = new ContactModel(name, number);
contactList.add(contact);
originalContactList.add(contact);
}
cursor.close();
contactAdapter.notifyDataSetChanged();
public DebounceTextWatcher(long debounceDelay) {
this.debounceDelay = debounceDelay;
}
}
private void filterContacts(String query) {
contactList.clear();
if (query.isEmpty()) {
contactList.addAll(originalContactList);
} else {
for (ContactModel contact : originalContactList) {
if (contact.getName().toLowerCase().contains(query.toLowerCase()) ||
contact.getPinyin().toLowerCase().contains(query.toLowerCase()) ||
contact.getNumber().toLowerCase().contains(query.toLowerCase())) {
contactList.add(contact);
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// 无需处理
}
@Override
public void onTextChanged(final CharSequence s, int start, int before, int count) {
// 移除之前的延迟任务
if (pendingRunnable != null) {
handler.removeCallbacks(pendingRunnable);
}
// 延迟执行过滤
pendingRunnable = new Runnable() {
@Override
public void run() {
onDebounceTextChanged(s.toString());
}
}
};
handler.postDelayed(pendingRunnable, debounceDelay);
}
@Override
public void afterTextChanged(Editable s) {
// 无需处理
}
// 抽象方法:防抖后的回调
public abstract void onDebounceTextChanged(String query);
}
// 资源释放
@Override
public void onDestroy() {
super.onDestroy();
executor.shutdown(); // 关闭线程池
mainHandler.removeCallbacksAndMessages(null); // 清除未执行任务
}
// Fragment隐藏/显示时的处理
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (!hidden && isDataLoaded) {
// 复用缓存数据并显示列表
contactList.clear();
contactList.addAll(sCachedFilteredList);
contactAdapter.notifyDataSetChanged();
recyclerView.setVisibility(View.VISIBLE);
}
contactAdapter.notifyDataSetChanged();
}
@Override
public void onResume() {
super.onResume();
//ToastUtils.show("onResume");
readContacts(); // 窗口回显时更新联系人列表
}
}

View File

@@ -1,9 +1,9 @@
package cc.winboll.studio.contacts.utils;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/03/06 21:08:16
* @Describe ContactUtils
* @Author ZhanGSKen&豆包大模型<zhangsken@188.com>
* @Date 2025/08/30 14:32
* @Describe 联系人工具集
*/
import android.content.ContentResolver;
import android.content.Context;

View File

@@ -2,6 +2,6 @@
<resources>
<string name="app_name">Contacts</string>
<string name="default_bobulltoon_url">https://gitea.winboll.cc/Studio/BoBullToon/archive/main.zip</string>
<string name="default_bobulltoon_url">https://gitee.com/zhangsken/bobulltoon/repository/archive/main.zip</string>
</resources>