Merge remote-tracking branch 'gitee/contacts' into appbase
This commit is contained in:
		| @@ -45,9 +45,9 @@ android { | |||||||
|  |  | ||||||
| dependencies { | dependencies { | ||||||
|     api fileTree(dir: 'libs', include: ['*.jar']) |     api fileTree(dir: 'libs', include: ['*.jar']) | ||||||
|     api 'cc.winboll.studio:libaes:15.9.2' |     api 'cc.winboll.studio:libaes:15.9.3' | ||||||
|     api 'cc.winboll.studio:libapputils:15.8.4' |     api 'cc.winboll.studio:libapputils:15.8.5' | ||||||
|     api 'cc.winboll.studio:libappbase:15.8.4' |     api 'cc.winboll.studio:libappbase:15.9.5' | ||||||
|      |      | ||||||
|     // 权限请求框架:https://github.com/getActivity/XXPermissions |     // 权限请求框架:https://github.com/getActivity/XXPermissions | ||||||
|     api 'com.github.getActivity:XXPermissions:18.63' |     api 'com.github.getActivity:XXPermissions:18.63' | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| #Created by .winboll/winboll_app_build.gradle | #Created by .winboll/winboll_app_build.gradle | ||||||
| #Thu Jul 17 09:57:24 HKT 2025 | #Sun Aug 31 06:05:42 CST 2025 | ||||||
| stageCount=12 | stageCount=17 | ||||||
| libraryProject= | libraryProject= | ||||||
| baseVersion=15.3 | baseVersion=15.3 | ||||||
| publishVersion=15.3.11 | publishVersion=15.3.16 | ||||||
| buildCount=0 | buildCount=0 | ||||||
| baseBetaVersion=15.3.12 | baseBetaVersion=15.3.17 | ||||||
|   | |||||||
| @@ -1,5 +1,10 @@ | |||||||
| package cc.winboll.studio.contacts; | package cc.winboll.studio.contacts; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @Author ZhanGSKen&豆包大模型<zhangsken@188.com> | ||||||
|  |  * @Date 2025/08/30 14:32 | ||||||
|  |  * @Describe 主窗口 | ||||||
|  |  */ | ||||||
| import android.Manifest; | import android.Manifest; | ||||||
| import android.app.Activity; | import android.app.Activity; | ||||||
| import android.app.ActivityManager; | import android.app.ActivityManager; | ||||||
| @@ -8,6 +13,7 @@ import android.content.Intent; | |||||||
| import android.content.pm.PackageManager; | import android.content.pm.PackageManager; | ||||||
| import android.os.Build; | import android.os.Build; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
|  | import android.os.Handler; | ||||||
| import android.telecom.TelecomManager; | import android.telecom.TelecomManager; | ||||||
| import android.telephony.PhoneStateListener; | import android.telephony.PhoneStateListener; | ||||||
| import android.telephony.TelephonyManager; | import android.telephony.TelephonyManager; | ||||||
| @@ -41,7 +47,7 @@ import java.util.List; | |||||||
|  |  | ||||||
| final public class MainActivity extends AppCompatActivity implements IWinBoLLActivity, ViewPager.OnPageChangeListener, View.OnClickListener { | 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_HOME_ACTIVITY = 0; | ||||||
|     public static final int REQUEST_ABOUT_ACTIVITY = 1; |     public static final int REQUEST_ABOUT_ACTIVITY = 1; | ||||||
| @@ -55,13 +61,10 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct | |||||||
|     MainServiceBean mMainServiceBean; |     MainServiceBean mMainServiceBean; | ||||||
|     private TabLayout tabLayout; |     private TabLayout tabLayout; | ||||||
|     private ViewPager viewPager; |     private ViewPager viewPager; | ||||||
|     private List<View> views; //用来存放放进ViewPager里面的布局 |     private List<View> views;  | ||||||
|     //实例化存储imageView(导航原点)的集合 |  | ||||||
|     ImageView[] imageViews; |     ImageView[] imageViews; | ||||||
|     //MyPagerAdapter adapter;//适配器 |     LinearLayout linearLayout; | ||||||
|     MyPagerAdapter pagerAdapter; |     int currentPoint = 0; | ||||||
|     LinearLayout linearLayout;//下标所在在LinearLayout布局里 |  | ||||||
|     int currentPoint = 0;//当前被选中中页面的下标 |  | ||||||
|  |  | ||||||
|     private TelephonyManager telephonyManager; |     private TelephonyManager telephonyManager; | ||||||
|     private MyPhoneStateListener phoneStateListener; |     private MyPhoneStateListener phoneStateListener; | ||||||
| @@ -70,30 +73,6 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct | |||||||
|  |  | ||||||
|     private static final int DIALER_REQUEST_CODE = 1; |     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 |     @Override | ||||||
|     public Activity getActivity() { |     public Activity getActivity() { | ||||||
| @@ -107,89 +86,62 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct | |||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     protected void onCreate(Bundle savedInstanceState) { |     protected void onCreate(Bundle savedInstanceState) { | ||||||
|         // 接收并处理 Intent 数据,函数 Intent 处理接收就直接返回 |  | ||||||
|         //if (prosessIntents(getIntent())) return; |  | ||||||
|         // 以下正常创建主窗口 |  | ||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
|         _MainActivity = this; |         _MainActivity = this; | ||||||
|         setContentView(R.layout.activity_main); |         setContentView(R.layout.activity_main); | ||||||
|  |  | ||||||
|         // 初始化工具栏 |         // 初始化工具栏(仅加载基础UI) | ||||||
|         mToolbar = findViewById(R.id.activitymainToolbar1); |         mToolbar = (Toolbar) findViewById(R.id.activitymainToolbar1); | ||||||
|         setSupportActionBar(mToolbar); |         setSupportActionBar(mToolbar); | ||||||
| //        if (isEnableDisplayHomeAsUp()) { |  | ||||||
| //            // 显示后退按钮 |  | ||||||
| //            getSupportActionBar().setDisplayHomeAsUpEnabled(true); |  | ||||||
| //        } |  | ||||||
|         getSupportActionBar().setSubtitle(TAG); |         getSupportActionBar().setSubtitle(TAG); | ||||||
|  |  | ||||||
|         tabLayout = findViewById(R.id.tabLayout); |         tabLayout = (TabLayout) findViewById(R.id.tabLayout); | ||||||
|         viewPager = findViewById(R.id.viewPager); |         viewPager = (ViewPager) findViewById(R.id.viewPager); | ||||||
|  |  | ||||||
|         // 创建Fragment列表和标题列表 |         // 创建Fragment列表(仅实例化,不加载数据) | ||||||
|         fragmentList = new ArrayList<>(); |         fragmentList = new ArrayList<Fragment>(); | ||||||
|         tabTitleList = new ArrayList<>(); |         tabTitleList = new ArrayList<String>(); | ||||||
|         fragmentList.add(CallLogFragment.newInstance(0)); |         fragmentList.add(CallLogFragment.newInstance(0)); | ||||||
|         fragmentList.add(ContactsFragment.newInstance(1)); |         fragmentList.add(ContactsFragment.newInstance(1)); // 延迟加载联系人数据 | ||||||
|         fragmentList.add(LogFragment.newInstance(2)); |         fragmentList.add(LogFragment.newInstance(2)); | ||||||
|         tabTitleList.add("通话记录"); |         tabTitleList.add("通话记录"); | ||||||
|         tabTitleList.add("联系人"); |         tabTitleList.add("联系人"); | ||||||
|         tabTitleList.add("应用日志"); |         tabTitleList.add("应用日志"); | ||||||
|  |  | ||||||
|         // 设置ViewPager的适配器 |         // 设置ViewPager适配器 | ||||||
|         MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager(), fragmentList, tabTitleList); |         MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager(), fragmentList, tabTitleList); | ||||||
|         viewPager.setAdapter(adapter); |         viewPager.setAdapter(adapter); | ||||||
|  |  | ||||||
|  |         // 关键:关闭预加载,仅当前页初始化 | ||||||
|  |         viewPager.setOffscreenPageLimit(0); | ||||||
|  |  | ||||||
|         // 关联TabLayout和ViewPager |         // 关联TabLayout和ViewPager | ||||||
|         tabLayout.setupWithViewPager(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); |         MainServiceBean mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class); | ||||||
|         if (mMainServiceBean == null) { |         if (mMainServiceBean == null) { | ||||||
|             mMainServiceBean = new MainServiceBean(); |             mMainServiceBean = new MainServiceBean(); | ||||||
|             MainServiceBean.saveBean(this, mMainServiceBean); |             MainServiceBean.saveBean(this, mMainServiceBean); | ||||||
|         } |         } | ||||||
|         if (mMainServiceBean.isEnable()) { |         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); |         telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); | ||||||
|         phoneStateListener = new MyPhoneStateListener(); |         phoneStateListener = new MyPhoneStateListener(); | ||||||
|         telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); |         telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     // ViewPager的适配器 |     // ViewPager适配器(Java 7语法) | ||||||
|     private class MyPagerAdapter extends FragmentPagerAdapter { |     private class MyPagerAdapter extends FragmentPagerAdapter { | ||||||
|  |  | ||||||
|         private List<Fragment> fragmentList; |         private List<Fragment> fragmentList; | ||||||
| @@ -226,91 +178,22 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct | |||||||
|         _MainActivity.startActivity(intent); |         _MainActivity.startActivity(intent); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     //初始化view,即显示的图片 |     // OnPageChangeListener接口实现 | ||||||
| //    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)在“手指离开屏幕”的状态。*/ |  | ||||||
|     @Override |     @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 |     @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 |     @Override | ||||||
|     public void onClick(View v) { |     public void onPageSelected(int position) {} | ||||||
|         // TODO Auto-generated method stub |  | ||||||
|         //通过getTag(),可以判断是哪个控件 |     @Override | ||||||
| //        int i = (Integer) v.getTag(); |     public void onClick(View v) {} | ||||||
| //        viewPager.setCurrentItem(i);//直接跳转到某一个页面的情况 |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     protected void onPostCreate(Bundle savedInstanceState) { |     protected void onPostCreate(Bundle savedInstanceState) { | ||||||
|         super.onPostCreate(savedInstanceState); |         super.onPostCreate(savedInstanceState); | ||||||
|         //setSubTitle(""); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private class MyPhoneStateListener extends PhoneStateListener { |     private class MyPhoneStateListener extends PhoneStateListener { | ||||||
| @@ -336,109 +219,31 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct | |||||||
|         LogUtils.d(TAG, "onDestroy() SOS"); |         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 |     @Override | ||||||
|     public boolean onCreateOptionsMenu(Menu menu) { |     public boolean onCreateOptionsMenu(Menu menu) { | ||||||
|         getMenuInflater().inflate(R.menu.toolbar_main, menu); |         getMenuInflater().inflate(R.menu.toolbar_main, menu); | ||||||
|         return super.onCreateOptionsMenu(menu); |         return super.onCreateOptionsMenu(menu); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public boolean onOptionsItemSelected(MenuItem item) { |     public boolean onOptionsItemSelected(MenuItem item) { | ||||||
|         if (item.getItemId() == R.id.item_settings) { |         if (item.getItemId() == R.id.item_settings) { | ||||||
|             Intent intent = new Intent(this, SettingsActivity.class); |             Intent intent = new Intent(this, SettingsActivity.class); | ||||||
|             startActivity(intent); |             startActivity(intent); | ||||||
|             //WinBoLLActivityManager.getInstance(this).startWinBoLLActivity(this, CallActivity.class); |  | ||||||
|         } |         } | ||||||
| //        } else  |  | ||||||
| //        if (item.getItemId() == R.id.item_exit) { |  | ||||||
| //            exit(); |  | ||||||
| //            return true; |  | ||||||
| //        } |  | ||||||
|         return super.onOptionsItemSelected(item); |         return super.onOptionsItemSelected(item); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     protected void onResume() { |     protected void onResume() { | ||||||
|         super.onResume(); |         super.onResume(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Android M 及以上检查是否是系统默认电话应用 |      * 检查是否是系统默认电话应用 | ||||||
|      */ |      */ | ||||||
|     public boolean isDefaultPhoneCallApp() { |     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); |             TelecomManager manger = (TelecomManager) getSystemService(TELECOM_SERVICE); | ||||||
|             if (manger != null && manger.getDefaultDialerPackage() != null) { |             if (manger != null && manger.getDefaultDialerPackage() != null) { | ||||||
|                 return manger.getDefaultDialerPackage().equals(getPackageName()); |                 return manger.getDefaultDialerPackage().equals(getPackageName()); | ||||||
| @@ -452,35 +257,22 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct | |||||||
|         if (manager == null) return false; |         if (manager == null) return false; | ||||||
|  |  | ||||||
|         for (ActivityManager.RunningServiceInfo service : manager.getRunningServices( |         for (ActivityManager.RunningServiceInfo service : manager.getRunningServices( | ||||||
|             Integer.MAX_VALUE)) { | 			Integer.MAX_VALUE)) { | ||||||
|             if (serviceClass.getName().equals(service.service.getClassName())) { |             if (serviceClass.getName().equals(service.service.getClassName())) { | ||||||
|                 return true; |                 return true; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     protected void onActivityResult(int requestCode, int resultCode, Intent data) { |     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 (requestCode == DIALER_REQUEST_CODE) { | ||||||
|             if (resultCode == Activity.RESULT_OK) { |             if (resultCode == Activity.RESULT_OK) { | ||||||
|                 Toast.makeText(MainActivity.this, getString(R.string.app_name) + " 已成为默认电话应用", |                 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.mContactUtils = ContactUtils.getInstance(mContext); | ||||||
|         this.callLogList = callLogList; |         this.callLogList = callLogList; | ||||||
|     } |     } | ||||||
|  | 	 | ||||||
|  | 	public void relaodContacts() { | ||||||
|  | 		this.mContactUtils.relaodContacts(); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|     @NonNull |     @NonNull | ||||||
|     @Override |     @Override | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| package cc.winboll.studio.contacts.beans; | package cc.winboll.studio.contacts.beans; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @Author ZhanGSKen<zhangsken@188.com> |  * @Author ZhanGSKen&豆包大模型<zhangsken@188.com> | ||||||
|  * @Date 2025/02/26 13:37:00 |  * @Date 2025/08/30 14:32 | ||||||
|  * @Describe ContactModel |  * @Describe 联系人信息数据模型 | ||||||
|  */ |  */ | ||||||
| import net.sourceforge.pinyin4j.PinyinHelper; | import net.sourceforge.pinyin4j.PinyinHelper; | ||||||
| import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType; | import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType; | ||||||
| @@ -18,13 +18,18 @@ public class ContactModel { | |||||||
|     private String name; |     private String name; | ||||||
|     private String number; |     private String number; | ||||||
|     private String pinyin; |     private String pinyin; | ||||||
|  |     // 新增:存储姓名的拼音首字母(如"啊牛"→"an") | ||||||
|  |     private String pinyinFirstLetter; | ||||||
|  |  | ||||||
|     public ContactModel(String name, String number) { |     public ContactModel(String name, String number) { | ||||||
|         this.name = name; |         this.name = name; | ||||||
|         this.number = number.replaceAll("\\s", ""); |         this.number = number.replaceAll("\\s", ""); | ||||||
|         this.pinyin = convertToPinyin(name); |         this.pinyin = convertToPinyin(name); | ||||||
|  |         // 初始化时生成拼音首字母 | ||||||
|  |         this.pinyinFirstLetter = convertToPinyinFirstLetter(name); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // 原方法:转换为全拼(如"啊牛"→"aniu") | ||||||
|     private String convertToPinyin(String chinese) { |     private String convertToPinyin(String chinese) { | ||||||
|         HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat(); |         HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat(); | ||||||
|         format.setCaseType(HanyuPinyinCaseType.LOWERCASE); |         format.setCaseType(HanyuPinyinCaseType.LOWERCASE); | ||||||
| @@ -33,22 +38,55 @@ public class ContactModel { | |||||||
|         StringBuilder pinyin = new StringBuilder(); |         StringBuilder pinyin = new StringBuilder(); | ||||||
|         for (int i = 0; i < chinese.length(); i++) { |         for (int i = 0; i < chinese.length(); i++) { | ||||||
|             char ch = chinese.charAt(i); |             char ch = chinese.charAt(i); | ||||||
|             if (Character.toString(ch).matches("[\\u4e00-\\u9fa5]")) { |             if (Character.toString(ch).matches("[\\u4e00-\\u9fa5]")) { // 仅处理汉字 | ||||||
|                 try { |                 try { | ||||||
|                     String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(ch, format); |                     String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(ch, format); | ||||||
|                     if (pinyinArray != null) { |                     if (pinyinArray != null && pinyinArray.length > 0) { | ||||||
|                         pinyin.append(pinyinArray[0]); |                         pinyin.append(pinyinArray[0]); // 取第一个拼音(多音字默认首选项) | ||||||
|                     } |                     } | ||||||
|                 } catch (BadHanyuPinyinOutputFormatCombination e) { |                 } catch (BadHanyuPinyinOutputFormatCombination e) { | ||||||
|                     e.printStackTrace(); |                     e.printStackTrace(); | ||||||
|                 } |                 } | ||||||
|             } else { |             } else { | ||||||
|                 pinyin.append(ch); |                 pinyin.append(ch); // 非汉字直接拼接(如字母、数字、符号) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         return pinyin.toString(); |         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() { |     public String getName() { | ||||||
|         return name; |         return name; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ import cc.winboll.studio.contacts.dun.Rules; | |||||||
| import cc.winboll.studio.libappbase.LogUtils; | import cc.winboll.studio.libappbase.LogUtils; | ||||||
| import com.hjq.toast.ToastUtils; | import com.hjq.toast.ToastUtils; | ||||||
| import java.io.File; | import java.io.File; | ||||||
|  | import java.io.FileFilter; | ||||||
| import java.io.FileOutputStream; | import java.io.FileOutputStream; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.io.InputStream; | import java.io.InputStream; | ||||||
| @@ -154,6 +155,62 @@ public class TomCat { | |||||||
|     File getWorkingFolder() { |     File getWorkingFolder() { | ||||||
|         return mContext.getExternalFilesDir(TAG); |         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() { | 	public void cleanBoBullToon() { | ||||||
| 		String destinationFolder = getWorkingFolder().getPath(); // 替换为实际的目标文件夹路径 | 		String destinationFolder = getWorkingFolder().getPath(); // 替换为实际的目标文件夹路径 | ||||||
| @@ -170,9 +227,9 @@ public class TomCat { | |||||||
|  |  | ||||||
|     public boolean loadPhoneBoBullToon() { |     public boolean loadPhoneBoBullToon() { | ||||||
|         listPhoneBoBullToon.clear(); |         listPhoneBoBullToon.clear(); | ||||||
|         File fBoBullToon = new File(getWorkingFolder(), "bobulltoon"); |         File fBoBullToon = getBoBullToonDataFolder(); | ||||||
|         if (fBoBullToon.exists()) { |         if (fBoBullToon.exists()) { | ||||||
|             LogUtils.d(TAG, String.format("getWorkingFolder() %s", getWorkingFolder())); |             LogUtils.d(TAG, String.format("getBoBullToonDataFolder() %s", getWorkingFolder())); | ||||||
|             for (File userFolder : fBoBullToon.listFiles()) { |             for (File userFolder : fBoBullToon.listFiles()) { | ||||||
|                 if (userFolder.isDirectory()) { |                 if (userFolder.isDirectory()) { | ||||||
|                     for (File recordFile : userFolder.listFiles()) { |                     for (File recordFile : userFolder.listFiles()) { | ||||||
|   | |||||||
| @@ -161,4 +161,12 @@ public class CallLogFragment extends Fragment { | |||||||
|             _CallLogFragment.triggerUpdate(); |             _CallLogFragment.triggerUpdate(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 	 | ||||||
|  | 	@Override | ||||||
|  | 	public void onResume() { | ||||||
|  | 		super.onResume(); | ||||||
|  | 		//ToastUtils.show("onResume"); | ||||||
|  | 		callLogAdapter.relaodContacts(); | ||||||
|  | 		readCallLog();  // 窗口回显时更新通话记录 | ||||||
|  | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,15 +1,18 @@ | |||||||
| package cc.winboll.studio.contacts.fragments; | package cc.winboll.studio.contacts.fragments; | ||||||
|  |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @Author ZhanGSKen<zhangsken@188.com> |  * @Author ZhanGSKen&豆包大模型<zhangsken@188.com> | ||||||
|  * @Date 2025/02/20 12:57:50 |  * @Date 2025/08/30 14:32 | ||||||
|  * @Describe 联系人 |  * @Describe 联系人视图 | ||||||
|  */ |  */ | ||||||
| import android.Manifest; | import android.Manifest; | ||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
| import android.content.pm.PackageManager; | import android.content.pm.PackageManager; | ||||||
| import android.database.Cursor; | import android.database.Cursor; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
|  | import android.os.Handler; | ||||||
|  | import android.os.Looper; | ||||||
| import android.provider.ContactsContract; | import android.provider.ContactsContract; | ||||||
| import android.text.Editable; | import android.text.Editable; | ||||||
| import android.text.TextWatcher; | import android.text.TextWatcher; | ||||||
| @@ -27,24 +30,39 @@ import androidx.recyclerview.widget.RecyclerView; | |||||||
| import cc.winboll.studio.contacts.R; | import cc.winboll.studio.contacts.R; | ||||||
| import cc.winboll.studio.contacts.adapters.ContactAdapter; | import cc.winboll.studio.contacts.adapters.ContactAdapter; | ||||||
| import cc.winboll.studio.contacts.beans.ContactModel; | import cc.winboll.studio.contacts.beans.ContactModel; | ||||||
|  | import cc.winboll.studio.libappbase.LogUtils; | ||||||
| import com.hjq.toast.ToastUtils; | import com.hjq.toast.ToastUtils; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  | import java.util.concurrent.ExecutorService; | ||||||
|  | import java.util.concurrent.Executors; | ||||||
|  |  | ||||||
| public class ContactsFragment extends Fragment { | public class ContactsFragment extends Fragment { | ||||||
|  |  | ||||||
|     public static final String TAG = "ContactsFragment"; |     public static final String TAG = "ContactsFragment"; | ||||||
|  |  | ||||||
|     private static final String ARG_PAGE = "ARG_PAGE"; |     private static final String ARG_PAGE = "ARG_PAGE"; | ||||||
|     private int mPage; |  | ||||||
|  |  | ||||||
|     private static final int REQUEST_READ_CONTACTS = 1; |     private static final int REQUEST_READ_CONTACTS = 1; | ||||||
|  |  | ||||||
|  |     private int mPage; | ||||||
|     private RecyclerView recyclerView; |     private RecyclerView recyclerView; | ||||||
|     private ContactAdapter contactAdapter; |     private ContactAdapter contactAdapter; | ||||||
|     private List<ContactModel> contactList = new ArrayList<>(); |  | ||||||
|     private List<ContactModel> originalContactList = new ArrayList<>(); |  | ||||||
|     private EditText searchEditText; |     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) { |     public static ContactsFragment newInstance(int page) { | ||||||
|         Bundle args = new Bundle(); |         Bundle args = new Bundle(); | ||||||
| @@ -65,103 +83,272 @@ public class ContactsFragment extends Fragment { | |||||||
|     @Nullable |     @Nullable | ||||||
|     @Override |     @Override | ||||||
|     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { |     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 |     @Override | ||||||
|     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { |     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { | ||||||
|         super.onViewCreated(view, 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())); |         recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); | ||||||
|  |         contactList = new ArrayList<ContactModel>(); | ||||||
|         contactAdapter = new ContactAdapter(getContext(), contactList); |         contactAdapter = new ContactAdapter(getContext(), contactList); | ||||||
|         recyclerView.setAdapter(contactAdapter); |         recyclerView.setAdapter(contactAdapter); | ||||||
|  |         // 初始隐藏列表,数据加载后显示 | ||||||
|  |         recyclerView.setVisibility(View.GONE); | ||||||
|  |  | ||||||
|         searchEditText = view.findViewById(R.id.search_edit_text); |         // 绑定搜索框和拨号按钮 | ||||||
|         searchEditText.addTextChangedListener(new TextWatcher() { |         searchEditText = (EditText) view.findViewById(R.id.search_edit_text); | ||||||
|                 @Override |         btnDial = (Button) view.findViewById(R.id.btn_dial); | ||||||
|                 public void beforeTextChanged(CharSequence s, int start, int count, int after) { |         // 初始隐藏搜索相关控件,延迟到首次可见时显示 | ||||||
|                 } |         searchEditText.setVisibility(View.GONE); | ||||||
|  |         btnDial.setVisibility(View.GONE); | ||||||
|  |     } | ||||||
|  |  | ||||||
|                 @Override |     // 首次可见时初始化资源 | ||||||
|                 public void onTextChanged(CharSequence s, int start, int before, int count) { |     @Override | ||||||
|                     filterContacts(s.toString()); |     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) { |         if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { | ||||||
|             ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_READ_CONTACTS); |             ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_READ_CONTACTS); | ||||||
|         } else { |         } 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 |     @Override | ||||||
|     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { |     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { | ||||||
|         super.onRequestPermissionsResult(requestCode, permissions, grantResults); |         super.onRequestPermissionsResult(requestCode, permissions, grantResults); | ||||||
|         if (requestCode == REQUEST_READ_CONTACTS) { |         if (requestCode == REQUEST_READ_CONTACTS) { | ||||||
|             if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { |             if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { | ||||||
|                 readContacts(); |                 loadContacts(); // 授权后加载联系人 | ||||||
|  |             } else { | ||||||
|  |                 ToastUtils.show("请授予联系人权限以查看联系人列表"); | ||||||
|  |                 recyclerView.setVisibility(View.VISIBLE); // 显示空列表 | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void readContacts() { |     // 防抖TextWatcher(Java 7实现) | ||||||
|         contactList.clear(); |     public abstract static class DebounceTextWatcher implements TextWatcher { | ||||||
|         originalContactList.clear(); |         private final long debounceDelay; | ||||||
|         Cursor cursor = requireContext().getContentResolver().query( |         private Handler handler = new Handler(Looper.getMainLooper()); | ||||||
|             ContactsContract.CommonDataKinds.Phone.CONTENT_URI, |         private Runnable pendingRunnable; | ||||||
|             null, |  | ||||||
|             null, |  | ||||||
|             null, |  | ||||||
|             ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " ASC"); |  | ||||||
|  |  | ||||||
|         if (cursor != null) { |         public DebounceTextWatcher(long debounceDelay) { | ||||||
|             while (cursor.moveToNext()) { |             this.debounceDelay = debounceDelay; | ||||||
|                 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(); |  | ||||||
|         } |         } | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void filterContacts(String query) { |         @Override | ||||||
|         contactList.clear(); |         public void beforeTextChanged(CharSequence s, int start, int count, int after) { | ||||||
|         if (query.isEmpty()) { |             // 无需处理 | ||||||
|             contactList.addAll(originalContactList); |         } | ||||||
|         } else { |  | ||||||
|             for (ContactModel contact : originalContactList) { |         @Override | ||||||
|                 if (contact.getName().toLowerCase().contains(query.toLowerCase()) || |         public void onTextChanged(final CharSequence s, int start, int before, int count) { | ||||||
|                     contact.getPinyin().toLowerCase().contains(query.toLowerCase()) || |             // 移除之前的延迟任务 | ||||||
|                     contact.getNumber().toLowerCase().contains(query.toLowerCase())) { |             if (pendingRunnable != null) { | ||||||
|                     contactList.add(contact); |                 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; | package cc.winboll.studio.contacts.utils; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @Author ZhanGSKen<zhangsken@188.com> |  * @Author ZhanGSKen&豆包大模型<zhangsken@188.com> | ||||||
|  * @Date 2025/03/06 21:08:16 |  * @Date 2025/08/30 14:32 | ||||||
|  * @Describe ContactUtils |  * @Describe 联系人工具集 | ||||||
|  */ |  */ | ||||||
| import android.content.ContentResolver; | import android.content.ContentResolver; | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
|   | |||||||
| @@ -2,6 +2,6 @@ | |||||||
| <resources> | <resources> | ||||||
|  |  | ||||||
|     <string name="app_name">Contacts</string> |     <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> | </resources> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 ZhanGSKen
					ZhanGSKen