diff --git a/contacts/build.gradle b/contacts/build.gradle index 24e18ae..f30c00a 100644 --- a/contacts/build.gradle +++ b/contacts/build.gradle @@ -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' diff --git a/contacts/build.properties b/contacts/build.properties index 1ae7c04..67d8f38 100644 --- a/contacts/build.properties +++ b/contacts/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Thu Jul 17 09:57:24 HKT 2025 -stageCount=12 +#Sun Aug 31 06:05:42 CST 2025 +stageCount=17 libraryProject= baseVersion=15.3 -publishVersion=15.3.11 +publishVersion=15.3.16 buildCount=0 -baseBetaVersion=15.3.12 +baseBetaVersion=15.3.17 diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/MainActivity.java b/contacts/src/main/java/cc/winboll/studio/contacts/MainActivity.java index 86a546d..1425e54 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/MainActivity.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/MainActivity.java @@ -1,5 +1,10 @@ package cc.winboll.studio.contacts; +/** + * @Author ZhanGSKen&豆包大模型 + * @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 views; //用来存放放进ViewPager里面的布局 - //实例化存储imageView(导航原点)的集合 + private List 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(); + tabTitleList = new ArrayList(); 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 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_DRAGGING(1)表示用户手指“按在屏幕上并且开始拖动”的状态 - (手指按下但是还没有拖动的时候还不是这个状态,只有按下并且手指开始拖动后log才打出。) - SCROLL_STATE_IDLE(0)滑动动画做完的状态。 - SCROLL_STATE_SETTLING(2)在“手指离开屏幕”的状态。*/ + // 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(); } } } } + diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/adapters/CallLogAdapter.java b/contacts/src/main/java/cc/winboll/studio/contacts/adapters/CallLogAdapter.java index 58ed096..54c1347 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/adapters/CallLogAdapter.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/adapters/CallLogAdapter.java @@ -40,6 +40,10 @@ public class CallLogAdapter extends RecyclerView.Adapter - * @Date 2025/02/26 13:37:00 - * @Describe ContactModel + * @Author ZhanGSKen&豆包大模型 + * @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; } diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/bobulltoon/TomCat.java b/contacts/src/main/java/cc/winboll/studio/contacts/bobulltoon/TomCat.java index 19d1d5c..4faab99 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/bobulltoon/TomCat.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/bobulltoon/TomCat.java @@ -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()) { diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/fragments/CallLogFragment.java b/contacts/src/main/java/cc/winboll/studio/contacts/fragments/CallLogFragment.java index d8dac2a..bf17e7b 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/fragments/CallLogFragment.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/fragments/CallLogFragment.java @@ -161,4 +161,12 @@ public class CallLogFragment extends Fragment { _CallLogFragment.triggerUpdate(); } } + + @Override + public void onResume() { + super.onResume(); + //ToastUtils.show("onResume"); + callLogAdapter.relaodContacts(); + readCallLog(); // 窗口回显时更新通话记录 + } } diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/fragments/ContactsFragment.java b/contacts/src/main/java/cc/winboll/studio/contacts/fragments/ContactsFragment.java index dbaec57..04aea51 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/fragments/ContactsFragment.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/fragments/ContactsFragment.java @@ -1,15 +1,18 @@ package cc.winboll.studio.contacts.fragments; + /** - * @Author ZhanGSKen - * @Date 2025/02/20 12:57:50 - * @Describe 联系人 + * @Author ZhanGSKen&豆包大模型 + * @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 contactList = new ArrayList<>(); - private List originalContactList = new ArrayList<>(); private EditText searchEditText; + private Button btnDial; + private boolean isViewInitialized = false; // 标记视图是否已初始化 + + // 静态缓存:全局复用联系人数据 + private static List sCachedOriginalList = new ArrayList(); + private static List sCachedFilteredList = new ArrayList(); + + // 当前页面数据容器 + private List contactList = new ArrayList(); + private List originalContactList = new ArrayList(); + + // 异步工具 + 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,103 +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(); 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 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 readContactsInBackground() { + List tempList = new ArrayList(); + 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"); + // 防抖TextWatcher(Java 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(); } } diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/utils/ContactUtils.java b/contacts/src/main/java/cc/winboll/studio/contacts/utils/ContactUtils.java index 93e814c..48e4aec 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/utils/ContactUtils.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/utils/ContactUtils.java @@ -1,9 +1,9 @@ package cc.winboll.studio.contacts.utils; /** - * @Author ZhanGSKen - * @Date 2025/03/06 21:08:16 - * @Describe ContactUtils + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/08/30 14:32 + * @Describe 联系人工具集 */ import android.content.ContentResolver; import android.content.Context; diff --git a/contacts/src/main/res/values/strings.xml b/contacts/src/main/res/values/strings.xml index 8817cbe..a994a7c 100644 --- a/contacts/src/main/res/values/strings.xml +++ b/contacts/src/main/res/values/strings.xml @@ -2,6 +2,6 @@ Contacts - https://gitea.winboll.cc/Studio/BoBullToon/archive/main.zip + https://gitee.com/zhangsken/bobulltoon/repository/archive/main.zip