Merge remote-tracking branch 'gitee/contacts' into appbase
This commit is contained in:
		| @@ -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' | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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_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(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -40,6 +40,10 @@ public class CallLogAdapter extends RecyclerView.Adapter<CallLogAdapter.CallLogV | ||||
|         this.mContactUtils = ContactUtils.getInstance(mContext); | ||||
|         this.callLogList = callLogList; | ||||
|     } | ||||
| 	 | ||||
| 	public void relaodContacts() { | ||||
| 		this.mContactUtils.relaodContacts(); | ||||
| 	} | ||||
|  | ||||
|     @NonNull | ||||
|     @Override | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
|   | ||||
| @@ -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()) { | ||||
|   | ||||
| @@ -161,4 +161,12 @@ public class CallLogFragment extends Fragment { | ||||
|             _CallLogFragment.triggerUpdate(); | ||||
|         } | ||||
|     } | ||||
| 	 | ||||
| 	@Override | ||||
| 	public void onResume() { | ||||
| 		super.onResume(); | ||||
| 		//ToastUtils.show("onResume"); | ||||
| 		callLogAdapter.relaodContacts(); | ||||
| 		readCallLog();  // 窗口回显时更新通话记录 | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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,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<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"); | ||||
|     // 防抖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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 ZhanGSKen
					ZhanGSKen