Compare commits
	
		
			8 Commits
		
	
	
		
			contacts
			...
			powerbell-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 561330697b | |||
|   | f7b2c0d4c0 | ||
|   | c3978a1e3c | ||
| 5e198d9c68 | |||
|   | 963a3bb7cd | ||
| e9bb789daa | |||
| dbff19e7f4 | |||
|   | 44679d0c8a | 
| @@ -1,8 +1,8 @@ | ||||
| #Created by .winboll/winboll_app_build.gradle | ||||
| #Fri Sep 26 05:36:14 HKT 2025 | ||||
| stageCount=9 | ||||
| #Sat Sep 27 21:03:20 HKT 2025 | ||||
| stageCount=10 | ||||
| libraryProject=libappbase | ||||
| baseVersion=15.10 | ||||
| publishVersion=15.10.8 | ||||
| publishVersion=15.10.9 | ||||
| buildCount=0 | ||||
| baseBetaVersion=15.10.9 | ||||
| baseBetaVersion=15.10.10 | ||||
|   | ||||
| @@ -18,8 +18,8 @@ def genVersionName(def versionName){ | ||||
| } | ||||
|  | ||||
| android { | ||||
|     compileSdkVersion 30 | ||||
|     buildToolsVersion "30.0.3" | ||||
|     compileSdkVersion 32 | ||||
|     buildToolsVersion "32.0.0" | ||||
|  | ||||
|     defaultConfig { | ||||
|         applicationId "cc.winboll.studio.contacts" | ||||
| @@ -66,7 +66,7 @@ dependencies { | ||||
|     // 应用介绍页类库 | ||||
|     api 'io.github.medyo:android-about-page:2.0.0' | ||||
|     // 吐司类库 | ||||
|     //api 'com.github.getActivity:ToastUtils:10.5' | ||||
|     api 'com.github.getActivity:ToastUtils:10.5' | ||||
|     // 网络连接类库 | ||||
|     api 'com.squareup.okhttp3:okhttp:4.4.1' | ||||
|      | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| #Created by .winboll/winboll_app_build.gradle | ||||
| #Sat Oct 18 13:43:14 HKT 2025 | ||||
| stageCount=21 | ||||
| #Sun Aug 31 06:05:42 CST 2025 | ||||
| stageCount=17 | ||||
| libraryProject= | ||||
| baseVersion=15.3 | ||||
| publishVersion=15.3.20 | ||||
| publishVersion=15.3.16 | ||||
| buildCount=0 | ||||
| baseBetaVersion=15.3.21 | ||||
| baseBetaVersion=15.3.17 | ||||
|   | ||||
| @@ -7,8 +7,8 @@ package cc.winboll.studio.contacts; | ||||
|  */ | ||||
| import android.view.Gravity; | ||||
| import cc.winboll.studio.libappbase.GlobalApplication; | ||||
| import cc.winboll.studio.libappbase.utils.ToastUtils; | ||||
| import cc.winboll.studio.libappbase.winboll.WinBoLLActivityManager; | ||||
| import com.hjq.toast.ToastUtils; | ||||
|  | ||||
| public class App extends GlobalApplication { | ||||
|  | ||||
| @@ -30,7 +30,7 @@ public class App extends GlobalApplication { | ||||
|         // 设置 Toast 布局样式 | ||||
|         //ToastUtils.setView(R.layout.toast_custom_view); | ||||
|         //ToastUtils.setStyle(new WhiteToastStyle()); | ||||
|         //ToastUtils.setGravity(Gravity.BOTTOM, 0, 200); | ||||
|         ToastUtils.setGravity(Gravity.BOTTOM, 0, 200); | ||||
|          | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -8,7 +8,6 @@ package cc.winboll.studio.contacts; | ||||
| import android.Manifest; | ||||
| import android.app.Activity; | ||||
| import android.app.ActivityManager; | ||||
| import android.app.AlertDialog; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.pm.PackageManager; | ||||
| @@ -32,8 +31,6 @@ import androidx.fragment.app.Fragment; | ||||
| import androidx.fragment.app.FragmentManager; | ||||
| import androidx.fragment.app.FragmentPagerAdapter; | ||||
| import androidx.viewpager.widget.ViewPager; | ||||
| import // 新增:导入 AppGoToSettingsUtil 工具类(确保包路径与项目实际一致) | ||||
| cc.winboll.studio.contacts.utils.AppGoToSettingsUtil; | ||||
| import cc.winboll.studio.contacts.R; | ||||
| import cc.winboll.studio.contacts.activities.SettingsActivity; | ||||
| import cc.winboll.studio.contacts.beans.MainServiceBean; | ||||
| @@ -51,9 +48,10 @@ 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 int REQUEST_HOME_ACTIVITY = 0; | ||||
|     public static final int REQUEST_ABOUT_ACTIVITY = 1; | ||||
|     public static final int REQUEST_APP_SETTINGS = 2; | ||||
|  | ||||
|     public static final String ACTION_SOS = "cc.winboll.studio.libappbase.WinBoLL.ACTION_SOS"; | ||||
|  | ||||
|     static MainActivity _MainActivity; | ||||
| @@ -74,13 +72,6 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct | ||||
|     List<String> tabTitleList; | ||||
|  | ||||
|     private static final int DIALER_REQUEST_CODE = 1; | ||||
|     private static final int REQUEST_REQUIRED_PERMISSIONS = 1002; | ||||
|     // 关键修改1:新增 READ_CALL_LOG 权限到必需权限列表(解决通话记录读取崩溃) | ||||
|     private String[] REQUIRED_PERMISSIONS = new String[]{ | ||||
| 		Manifest.permission.READ_CONTACTS,  // 通讯录读取(原) | ||||
| 		Manifest.permission.CALL_PHONE,     // 电话拨号(原) | ||||
| 		Manifest.permission.READ_CALL_LOG   // 通话记录读取(新增,核心修复) | ||||
|     }; | ||||
|  | ||||
|  | ||||
|     @Override | ||||
| @@ -97,86 +88,9 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         _MainActivity = this; | ||||
|  | ||||
|         // 优先检查所有必需权限(含新增的 READ_CALL_LOG) | ||||
|         if (!checkAllRequiredPermissions()) { | ||||
|             requestAllRequiredPermissions(); | ||||
|         } else { | ||||
|             initUIAndLogic(savedInstanceState); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // 权限检查方法(无需修改,自动包含新增的 READ_CALL_LOG) | ||||
|     private boolean checkAllRequiredPermissions() { | ||||
|         for (String permission : REQUIRED_PERMISSIONS) { | ||||
|             if (ActivityCompat.checkSelfPermission(this, permission)  | ||||
| 				!= PackageManager.PERMISSION_GRANTED) { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     // 权限申请方法(无需修改,自动申请新增的 READ_CALL_LOG) | ||||
|     private void requestAllRequiredPermissions() { | ||||
|         ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_REQUIRED_PERMISSIONS); | ||||
|     } | ||||
|  | ||||
|     // 权限结果回调(无需修改,确保所有权限(含 READ_CALL_LOG)都通过才加载UI) | ||||
|     @Override | ||||
|     public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { | ||||
|         super.onRequestPermissionsResult(requestCode, permissions, grantResults); | ||||
|  | ||||
|         if (requestCode == REQUEST_REQUIRED_PERMISSIONS) { | ||||
|             boolean allPermissionsGranted = true; | ||||
|             for (int result : grantResults) { | ||||
|                 if (result != PackageManager.PERMISSION_GRANTED) { | ||||
|                     allPermissionsGranted = false; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (allPermissionsGranted) { | ||||
|                 initUIAndLogic(null); | ||||
|             } else { | ||||
|                 // 关键修改2:更新提示文案,告知用户新增的“通话记录权限” | ||||
|                 showPermissionDeniedDialogAndExit(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // 核心修改:新增“设置权限”按钮,点击调用 AppGoToSettingsUtil 跳转设置页 | ||||
|     private void showPermissionDeniedDialogAndExit() { | ||||
|         new AlertDialog.Builder(this) | ||||
| 			.setTitle("权限不足,无法使用") | ||||
| 			// 文案修改:明确新增“通话记录读取”权限 | ||||
| 			.setMessage("应用需要「通讯录读取」、「电话」和「通话记录读取」权限才能正常运行,请授予权限后重新打开应用。") | ||||
| 			.setCancelable(false) | ||||
| 			// 新增:左侧“设置权限”按钮(先添加的按钮在左侧) | ||||
| 			.setNegativeButton("设置权限", new AlertDialog.OnClickListener() { | ||||
| 				@Override | ||||
| 				public void onClick(android.content.DialogInterface dialog, int which) { | ||||
| 					dialog.dismiss(); | ||||
| 					// 调用工具类跳转应用设置页(按需求实现) | ||||
| 					AppGoToSettingsUtil appGoToSettingsUtil = new AppGoToSettingsUtil(); | ||||
| 					appGoToSettingsUtil.GoToSetting(MainActivity.this); | ||||
| 				} | ||||
| 			}) | ||||
| 			// 原有:右侧“确定退出”按钮(后添加的按钮在右侧) | ||||
| 			.setPositiveButton("确定退出", new AlertDialog.OnClickListener() { | ||||
| 				@Override | ||||
| 				public void onClick(android.content.DialogInterface dialog, int which) { | ||||
| 					dialog.dismiss(); | ||||
| 					finishAndRemoveTask(); | ||||
| 				} | ||||
| 			}) | ||||
| 			.show(); | ||||
|     } | ||||
|  | ||||
|     // 初始化UI和逻辑(无需修改,权限通过后才加载 CallLogFragment) | ||||
|     private void initUIAndLogic(Bundle savedInstanceState) { | ||||
|         setContentView(R.layout.activity_main); | ||||
|  | ||||
|         // 初始化工具栏(仅加载基础UI) | ||||
|         mToolbar = (Toolbar) findViewById(R.id.activitymainToolbar1); | ||||
|         setSupportActionBar(mToolbar); | ||||
|         getSupportActionBar().setSubtitle(TAG); | ||||
| @@ -184,28 +98,34 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct | ||||
|         tabLayout = (TabLayout) findViewById(R.id.tabLayout); | ||||
|         viewPager = (ViewPager) findViewById(R.id.viewPager); | ||||
|  | ||||
|         // 创建Fragment列表(仅实例化,不加载数据) | ||||
|         fragmentList = new ArrayList<Fragment>(); | ||||
|         tabTitleList = new ArrayList<String>(); | ||||
|         // CallLogFragment 仅在权限通过后才实例化(避免提前触发读取) | ||||
|         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适配器 | ||||
|         MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager(), fragmentList, tabTitleList); | ||||
|         viewPager.setAdapter(adapter); | ||||
|         viewPager.setOffscreenPageLimit(0); // 关闭预加载,避免提前初始化 CallLogFragment | ||||
|  | ||||
|         // 关键:关闭预加载,仅当前页初始化 | ||||
|         viewPager.setOffscreenPageLimit(0); | ||||
|  | ||||
|         // 关联TabLayout和ViewPager | ||||
|         tabLayout.setupWithViewPager(viewPager); | ||||
|  | ||||
|         // 原有服务启动、电话监听等逻辑... | ||||
|         // 初始化服务状态(延迟启动非核心服务) | ||||
|         MainServiceBean mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class); | ||||
|         if (mMainServiceBean == null) { | ||||
|             mMainServiceBean = new MainServiceBean(); | ||||
|             MainServiceBean.saveBean(this, mMainServiceBean); | ||||
|         } | ||||
|         if (mMainServiceBean.isEnable()) { | ||||
|             // 延迟1秒启动服务,避免阻塞启动 | ||||
|             new Handler().postDelayed(new Runnable() { | ||||
| 					@Override | ||||
| 					public void run() { | ||||
| @@ -214,14 +134,16 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct | ||||
| 				}, 1000); | ||||
|         } | ||||
|  | ||||
|         // 初始化电话状态监听(基础功能保留) | ||||
|         telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); | ||||
|         phoneStateListener = new MyPhoneStateListener(); | ||||
|         telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     // 以下为原有代码(无需修改) | ||||
|     // ViewPager适配器(Java 7语法) | ||||
|     private class MyPagerAdapter extends FragmentPagerAdapter { | ||||
|  | ||||
|         private List<Fragment> fragmentList; | ||||
|         private List<String> tabTitleList; | ||||
|  | ||||
| @@ -251,18 +173,21 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct | ||||
|         Intent intent = new Intent(Intent.ACTION_DIAL); | ||||
|         intent.setData(android.net.Uri.parse("tel:" + phoneNumber)); | ||||
|         if (ActivityCompat.checkSelfPermission(_MainActivity, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { | ||||
|             Toast.makeText(_MainActivity, "拨号权限不足", Toast.LENGTH_SHORT).show(); | ||||
|             return; | ||||
|         } | ||||
|         _MainActivity.startActivity(intent); | ||||
|     } | ||||
|  | ||||
|     // OnPageChangeListener接口实现 | ||||
|     @Override | ||||
|     public void onPageScrollStateChanged(int state) {} | ||||
|  | ||||
|     @Override | ||||
|     public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {} | ||||
|  | ||||
|     @Override | ||||
|     public void onPageSelected(int position) {} | ||||
|  | ||||
|     @Override | ||||
|     public void onClick(View v) {} | ||||
|  | ||||
| @@ -314,6 +239,9 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct | ||||
|         super.onResume(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 检查是否是系统默认电话应用 | ||||
|      */ | ||||
|     public boolean isDefaultPhoneCallApp() { | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | ||||
|             TelecomManager manger = (TelecomManager) getSystemService(TELECOM_SERVICE); | ||||
| @@ -344,9 +272,7 @@ final public class MainActivity extends AppCompatActivity implements IWinBoLLAct | ||||
|                 Toast.makeText(MainActivity.this, getString(R.string.app_name) + " 已成为默认电话应用", | ||||
| 							   Toast.LENGTH_SHORT).show(); | ||||
|             } | ||||
|         } else if (requestCode == REQUEST_APP_SETTINGS) { | ||||
| 			recreate(); | ||||
| 		} | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -36,8 +36,8 @@ import cc.winboll.studio.contacts.dun.Rules; | ||||
| import cc.winboll.studio.contacts.services.MainService; | ||||
| import cc.winboll.studio.contacts.views.DuInfoTextView; | ||||
| import cc.winboll.studio.libappbase.LogUtils; | ||||
| import cc.winboll.studio.libappbase.utils.ToastUtils; | ||||
| import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity; | ||||
| import com.hjq.toast.ToastUtils; | ||||
| import java.lang.reflect.Field; | ||||
| import java.util.List; | ||||
|  | ||||
|   | ||||
| @@ -23,7 +23,7 @@ import cc.winboll.studio.contacts.R; | ||||
| import cc.winboll.studio.contacts.beans.CallLogModel; | ||||
| import cc.winboll.studio.contacts.utils.ContactUtils; | ||||
| import cc.winboll.studio.libaes.views.AOHPCTCSeekBar; | ||||
| import cc.winboll.studio.libappbase.utils.ToastUtils; | ||||
| import com.hjq.toast.ToastUtils; | ||||
| import java.text.SimpleDateFormat; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| @@ -76,9 +76,6 @@ public class CallLogAdapter extends RecyclerView.Adapter<CallLogAdapter.CallLogV | ||||
| 									// Set the clipboard's primary clip. | ||||
| 									clipboard.setPrimaryClip(clip); | ||||
| 									Toast.makeText(mContext, "Copy to clipboard.", Toast.LENGTH_SHORT).show(); | ||||
| 								} else if (nItemId == R.id.item_calllog_phonenumber_add_contact) { | ||||
| 									//ToastUtils.show(callLog.getPhoneNumber()); | ||||
| 									ContactUtils.jumpToAddContact(mContext, callLog.getPhoneNumber()); | ||||
| 								} | ||||
|  | ||||
| 								return true; | ||||
|   | ||||
| @@ -21,9 +21,8 @@ import androidx.annotation.NonNull; | ||||
| import androidx.recyclerview.widget.RecyclerView; | ||||
| import cc.winboll.studio.contacts.R; | ||||
| import cc.winboll.studio.contacts.beans.ContactModel; | ||||
| import cc.winboll.studio.contacts.utils.ContactUtils; | ||||
| import cc.winboll.studio.libaes.views.AOHPCTCSeekBar; | ||||
| import cc.winboll.studio.libappbase.utils.ToastUtils; | ||||
| import com.hjq.toast.ToastUtils; | ||||
| import java.util.List; | ||||
|  | ||||
| public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactViewHolder> { | ||||
| @@ -70,11 +69,6 @@ public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactV | ||||
| 									// Set the clipboard's primary clip. | ||||
| 									clipboard.setPrimaryClip(clip); | ||||
| 									Toast.makeText(mContext, "Copy to clipboard.", Toast.LENGTH_SHORT).show(); | ||||
| 								} else if (nItemId == R.id.item_calllog_phonenumber_edit_contact) { | ||||
| 									//ToastUtils.show("Test"); | ||||
| 									Long nContactId = ContactUtils.getContactIdByPhone(mContext, contact.getNumber()); | ||||
| 									//ToastUtils.show(String.format("%d", nContactId)); | ||||
| 									ContactUtils.jumpToEditContact(mContext, contact.getNumber(), nContactId); | ||||
| 								} | ||||
|  | ||||
| 								return true; | ||||
| @@ -118,7 +112,7 @@ public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactV | ||||
|         TextView contactName; | ||||
|         TextView contactNumber; | ||||
|         AOHPCTCSeekBar dialAOHPCTCSeekBar; | ||||
|  | ||||
|          | ||||
|         public ContactViewHolder(@NonNull View itemView) { | ||||
|             super(itemView); | ||||
| 			llPhoneNumberMain = itemView.findViewById(R.id.itemcontactLinearLayout1); | ||||
|   | ||||
| @@ -7,6 +7,7 @@ package cc.winboll.studio.contacts.adapters; | ||||
|  */ | ||||
| import android.content.Context; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.MotionEvent; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.Button; | ||||
| @@ -20,8 +21,9 @@ import cc.winboll.studio.contacts.R; | ||||
| import cc.winboll.studio.contacts.beans.PhoneConnectRuleModel; | ||||
| import cc.winboll.studio.contacts.dun.Rules; | ||||
| import cc.winboll.studio.contacts.views.LeftScrollView; | ||||
| import cc.winboll.studio.libappbase.LogUtils; | ||||
| import cc.winboll.studio.libappbase.dialogs.YesNoAlertDialog; | ||||
| import cc.winboll.studio.libappbase.utils.ToastUtils; | ||||
| import com.hjq.toast.ToastUtils; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import android.content.Context; | ||||
| import cc.winboll.studio.contacts.R; | ||||
| import cc.winboll.studio.contacts.dun.Rules; | ||||
| import cc.winboll.studio.libappbase.LogUtils; | ||||
| import cc.winboll.studio.libappbase.utils.ToastUtils; | ||||
| import com.hjq.toast.ToastUtils; | ||||
| import java.io.File; | ||||
| import java.io.FileFilter; | ||||
| import java.io.FileOutputStream; | ||||
|   | ||||
| @@ -25,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView; | ||||
| import cc.winboll.studio.contacts.R; | ||||
| import cc.winboll.studio.contacts.adapters.CallLogAdapter; | ||||
| import cc.winboll.studio.contacts.beans.CallLogModel; | ||||
| import com.hjq.toast.ToastUtils; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Date; | ||||
| import java.util.List; | ||||
| @@ -160,7 +161,7 @@ public class CallLogFragment extends Fragment { | ||||
|             _CallLogFragment.triggerUpdate(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| 	 | ||||
| 	@Override | ||||
| 	public void onResume() { | ||||
| 		super.onResume(); | ||||
|   | ||||
| @@ -31,7 +31,7 @@ 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 cc.winboll.studio.libappbase.utils.ToastUtils; | ||||
| import com.hjq.toast.ToastUtils; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.concurrent.ExecutorService; | ||||
| @@ -95,7 +95,7 @@ public class ContactsFragment extends Fragment { | ||||
|         recyclerView = (RecyclerView) view.findViewById(R.id.contacts_recycler_view); | ||||
|         recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); | ||||
|         contactList = new ArrayList<ContactModel>(); | ||||
|         contactAdapter = new ContactAdapter(getActivity(), contactList); | ||||
|         contactAdapter = new ContactAdapter(getContext(), contactList); | ||||
|         recyclerView.setAdapter(contactAdapter); | ||||
|         // 初始隐藏列表,数据加载后显示 | ||||
|         recyclerView.setVisibility(View.GONE); | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import androidx.annotation.Nullable; | ||||
| import androidx.fragment.app.Fragment; | ||||
| import cc.winboll.studio.contacts.R; | ||||
| import cc.winboll.studio.libappbase.LogView; | ||||
| import com.hjq.toast.ToastUtils; | ||||
|  | ||||
| public class LogFragment extends Fragment { | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.IntentFilter; | ||||
| import cc.winboll.studio.contacts.services.MainService; | ||||
| import cc.winboll.studio.libappbase.utils.ToastUtils; | ||||
| import com.hjq.toast.ToastUtils; | ||||
| import java.lang.ref.WeakReference; | ||||
|  | ||||
| public class MainReceiver extends BroadcastReceiver { | ||||
|   | ||||
| @@ -1,270 +0,0 @@ | ||||
| package cc.winboll.studio.contacts.utils; | ||||
|  | ||||
| /** | ||||
|  * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> | ||||
|  * @Date 2025/09/27 14:27 | ||||
|  * @Describe 调用应用属性设置页工具类 | ||||
|  *  来源:https://blog.csdn.net/zhuhai__yizhi/article/details/78737593 | ||||
|  *  Created by zyy on 2018/3/12. | ||||
|  *  直接跳转到权限后返回,可以监控权限授权情况,但是,跳转到应用详情页,无法监测权限情况 | ||||
|  *  是否要加以区分,若是应用详情页,则跳转回来后,onRestart检测所求权限,如果授权,则收回提示,如果没授权,则继续提示 | ||||
|  */ | ||||
| import android.app.Activity; | ||||
| import android.content.ComponentName; | ||||
| import android.content.Intent; | ||||
| import android.net.Uri; | ||||
| import android.os.Build; | ||||
| import android.provider.Settings; | ||||
| import cc.winboll.studio.contacts.MainActivity; | ||||
|  | ||||
| public class AppGoToSettingsUtil { | ||||
|  | ||||
|     public static final String TAG = "AppGoToSettingsUtil"; | ||||
|  | ||||
|     public static final int ACTIVITY_RESULT_APP_SETTINGS = MainActivity.REQUEST_APP_SETTINGS; | ||||
|  | ||||
|     /** | ||||
|      * Build.MANUFACTURER判断各大手机厂商品牌 | ||||
|      */ | ||||
|     private static final String MANUFACTURER_HUAWEI = "Huawei";//华为 | ||||
|     private static final String MANUFACTURER_MEIZU = "Meizu";//魅族 | ||||
|     private static final String MANUFACTURER_XIAOMI = "Xiaomi";//小米 | ||||
|     private static final String MANUFACTURER_SONY = "Sony";//索尼 | ||||
|     private static final String MANUFACTURER_OPPO = "OPPO"; | ||||
|     private static final String MANUFACTURER_LG = "LG"; | ||||
|     private static final String MANUFACTURER_VIVO = "vivo"; | ||||
|     private static final String MANUFACTURER_SAMSUNG = "samsung";//三星 | ||||
|     private static final String MANUFACTURER_LETV = "Letv";//乐视 | ||||
|     private static final String MANUFACTURER_ZTE = "ZTE";//中兴 | ||||
|     private static final String MANUFACTURER_YULONG = "YuLong";//酷派 | ||||
|     private static final String MANUFACTURER_LENOVO = "LENOVO";//联想 | ||||
|  | ||||
|     public static boolean isAppSettingOpen=false; | ||||
|     /** | ||||
|      * 跳转到相应品牌手机系统权限设置页,如果跳转不成功,则跳转到应用详情页 | ||||
|      * 这里需要改造成返回true或者false,应用详情页:true,应用权限页:false | ||||
|      * @param activity | ||||
|      */ | ||||
|     public static void GoToSetting(Activity activity) { | ||||
|         switch (Build.MANUFACTURER) { | ||||
|             case MANUFACTURER_HUAWEI://华为 | ||||
|                 Huawei(activity); | ||||
|                 break; | ||||
|             case MANUFACTURER_MEIZU://魅族 | ||||
|                 Meizu(activity); | ||||
|                 break; | ||||
|             case MANUFACTURER_XIAOMI://小米 | ||||
|                 Xiaomi(activity); | ||||
|                 break; | ||||
|             case MANUFACTURER_SONY://索尼 | ||||
|                 Sony(activity); | ||||
|                 break; | ||||
|             case MANUFACTURER_OPPO://oppo | ||||
|                 OPPO(activity); | ||||
|                 break; | ||||
|             case MANUFACTURER_LG://lg | ||||
|                 LG(activity); | ||||
|                 break; | ||||
|             case MANUFACTURER_LETV://乐视 | ||||
|                 Letv(activity); | ||||
|                 break; | ||||
|             default://其他 | ||||
|                 try {//防止应用详情页也找不到,捕获异常后跳转到设置,这里跳转最好是两级,太多用户也会觉得麻烦,还不如不跳 | ||||
|                     openAppDetailSetting(activity); | ||||
|                     //activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT); | ||||
|                 } catch (Exception e) { | ||||
|                     SystemConfig(activity); | ||||
|                 } | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 华为跳转权限设置页 | ||||
|      * @param activity | ||||
|      */ | ||||
|     public static void Huawei(Activity activity) { | ||||
|         try { | ||||
|             Intent intent = new Intent(); | ||||
|             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|             intent.putExtra("packageName", activity.getPackageName()); | ||||
|             ComponentName comp = new ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity"); | ||||
|             intent.setComponent(comp); | ||||
|             activity.startActivityForResult(intent, ACTIVITY_RESULT_APP_SETTINGS); | ||||
|             isAppSettingOpen = false; | ||||
|         } catch (Exception e) { | ||||
|             openAppDetailSetting(activity); | ||||
|             //activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 魅族跳转权限设置页,测试时,点击无反应,具体原因不明 | ||||
|      * @param activity | ||||
|      */ | ||||
|     public static void Meizu(Activity activity) { | ||||
|         try { | ||||
|             Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC"); | ||||
|             intent.addCategory(Intent.CATEGORY_DEFAULT); | ||||
|             intent.putExtra("packageName", activity.getPackageName()); | ||||
|             activity.startActivity(intent); | ||||
|             isAppSettingOpen = false; | ||||
|         } catch (Exception e) { | ||||
|             openAppDetailSetting(activity); | ||||
|             //activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 小米,功能正常 | ||||
|      * @param activity | ||||
|      */ | ||||
|     public static void Xiaomi(Activity activity) { | ||||
|         try { //MIUI 8 9 | ||||
|             Intent localIntent = new Intent("miui.intent.action.APP_PERM_EDITOR"); | ||||
|             localIntent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity"); | ||||
|             localIntent.putExtra("extra_pkgname", activity.getPackageName()); | ||||
|             activity.startActivityForResult(localIntent, ACTIVITY_RESULT_APP_SETTINGS); | ||||
|             isAppSettingOpen = false; | ||||
|             //activity.startActivity(localIntent); | ||||
|         } catch (Exception e) { | ||||
|             try { //MIUI 5/6/7 | ||||
|                 Intent localIntent = new Intent("miui.intent.action.APP_PERM_EDITOR"); | ||||
|                 localIntent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity"); | ||||
|                 localIntent.putExtra("extra_pkgname", activity.getPackageName()); | ||||
|                 activity.startActivityForResult(localIntent, ACTIVITY_RESULT_APP_SETTINGS); | ||||
|                 isAppSettingOpen = false; | ||||
|                 //activity.startActivity(localIntent); | ||||
|             } catch (Exception e1) { //否则跳转到应用详情 | ||||
|                 openAppDetailSetting(activity); | ||||
|                 //activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT); | ||||
|                 //这里有个问题,进入活动后需要再跳一级活动,就检测不到返回结果 | ||||
|                 //activity.startActivity(getAppDetailSettingIntent()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 索尼,6.0以上的手机非常少,基本没看见 | ||||
|      * @param activity | ||||
|      */ | ||||
|     public static void Sony(Activity activity) { | ||||
|         try { | ||||
|             Intent intent = new Intent(); | ||||
|             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|             intent.putExtra("packageName", activity.getPackageName()); | ||||
|             ComponentName comp = new ComponentName("com.sonymobile.cta", "com.sonymobile.cta.SomcCTAMainActivity"); | ||||
|             intent.setComponent(comp); | ||||
|             activity.startActivity(intent); | ||||
|             isAppSettingOpen = false; | ||||
|         } catch (Exception e) { | ||||
|             openAppDetailSetting(activity); | ||||
|             //activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * OPPO | ||||
|      * @param activity | ||||
|      */ | ||||
|     public static void OPPO(Activity activity) { | ||||
|         try { | ||||
|             Intent intent = new Intent(); | ||||
|             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|             intent.putExtra("packageName", activity.getPackageName()); | ||||
|             ComponentName comp = new ComponentName("com.color.safecenter", "com.color.safecenter.permission.PermissionManagerActivity"); | ||||
|             intent.setComponent(comp); | ||||
|             activity.startActivity(intent); | ||||
|             isAppSettingOpen = false; | ||||
|         } catch (Exception e) { | ||||
|             openAppDetailSetting(activity); | ||||
|             //activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * LG经过测试,正常使用 | ||||
|      * @param activity | ||||
|      */ | ||||
|     public static void LG(Activity activity) { | ||||
|         try { | ||||
|             Intent intent = new Intent("android.intent.action.MAIN"); | ||||
|             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|             intent.putExtra("packageName", activity.getPackageName()); | ||||
|             ComponentName comp = new ComponentName("com.android.settings", "com.android.settings.Settings$AccessLockSummaryActivity"); | ||||
|             intent.setComponent(comp); | ||||
|             activity.startActivity(intent); | ||||
|             isAppSettingOpen = false; | ||||
|         } catch (Exception e) { | ||||
|             openAppDetailSetting(activity); | ||||
|             //activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 乐视6.0以上很少,基本都可以忽略了,现在乐视手机不多 | ||||
|      * @param activity | ||||
|      */ | ||||
|     public static void Letv(Activity activity) { | ||||
|         try { | ||||
|             Intent intent = new Intent(); | ||||
|             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|             intent.putExtra("packageName", activity.getPackageName()); | ||||
|             ComponentName comp = new ComponentName("com.letv.android.letvsafe", "com.letv.android.letvsafe.PermissionAndApps"); | ||||
|             intent.setComponent(comp); | ||||
|             activity.startActivity(intent); | ||||
|             isAppSettingOpen = false; | ||||
|         } catch (Exception e) { | ||||
|             openAppDetailSetting(activity); | ||||
|             //activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 只能打开到自带安全软件 | ||||
|      * @param activity | ||||
|      */ | ||||
|     public static void _360(Activity activity) { | ||||
|         try { | ||||
|             Intent intent = new Intent("android.intent.action.MAIN"); | ||||
|             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|             intent.putExtra("packageName", activity.getPackageName()); | ||||
|             ComponentName comp = new ComponentName("com.qihoo360.mobilesafe", "com.qihoo360.mobilesafe.ui.index.AppEnterActivity"); | ||||
|             intent.setComponent(comp); | ||||
|             activity.startActivity(intent); | ||||
|         } catch (Exception e) { | ||||
|             openAppDetailSetting(activity); | ||||
|             //activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT); | ||||
|         } | ||||
|     } | ||||
|     /** | ||||
|      * 系统设置界面 | ||||
|      * @param activity | ||||
|      */ | ||||
|     public static void SystemConfig(Activity activity) { | ||||
|         Intent intent = new Intent(Settings.ACTION_SETTINGS); | ||||
|         activity.startActivity(intent); | ||||
|     } | ||||
|     /** | ||||
|      * 获取应用详情页面 | ||||
|      * @return | ||||
|      */ | ||||
|     private static Intent getAppDetailSettingIntent(Activity activity) { | ||||
|         Intent localIntent = new Intent(); | ||||
|         localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|         //if (Build.VERSION.SDK_INT >= 9) { | ||||
|         localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS"); | ||||
|         localIntent.setData(Uri.fromParts("package", activity.getPackageName(), null)); | ||||
|         /*} else if (Build.VERSION.SDK_INT <= 8) { | ||||
|          localIntent.setAction(Intent.ACTION_VIEW); | ||||
|          localIntent.setClassName("com.android.settings", "com.android.settings.InstalledAppDetails"); | ||||
|          localIntent.putExtra("com.android.settings.ApplicationPkgName", activity.getPackageName()); | ||||
|          }*/ | ||||
|         return localIntent; | ||||
|     } | ||||
|  | ||||
|     public static void openAppDetailSetting(Activity activity) { | ||||
|         activity.startActivityForResult(getAppDetailSettingIntent(activity), ACTIVITY_RESULT_APP_SETTINGS); | ||||
|         isAppSettingOpen = true; | ||||
|     } | ||||
| } | ||||
| @@ -6,14 +6,10 @@ package cc.winboll.studio.contacts.utils; | ||||
|  * @Describe 联系人工具集 | ||||
|  */ | ||||
| import android.content.ContentResolver; | ||||
| import android.content.ContentUris; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.database.Cursor; | ||||
| import android.net.Uri; | ||||
| import android.provider.ContactsContract; | ||||
| import cc.winboll.studio.libappbase.LogUtils; | ||||
| import cc.winboll.studio.libappbase.utils.ToastUtils; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
|  | ||||
| @@ -124,92 +120,4 @@ public class ContactUtils { | ||||
|         } | ||||
|         return sbSpaceNumber.toString(); | ||||
|     } | ||||
|  | ||||
|  | ||||
| 	/** | ||||
| 	 * 跳转至系统添加联系人界面的工具函数 | ||||
| 	 * @param context 上下文(如 PhoneCallService、Activity、Fragment 均可,需传入有效上下文) | ||||
| 	 * @param phoneNumber 可选参数:预填的联系人电话(传 null 则跳转空表单) | ||||
| 	 */ | ||||
| 	public static void jumpToAddContact(Context mContext, String phoneNumber) { | ||||
| 		Intent intent = new Intent(Intent.ACTION_INSERT); | ||||
| 		intent.setType("vnd.android.cursor.dir/person"); | ||||
| 		intent.putExtra(android.provider.ContactsContract.Intents.Insert.PHONE, phoneNumber); | ||||
| 		mContext.startActivity(intent); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * 跳转至系统编辑联系人界面(适配小米等定制机型) | ||||
| 	 * @param context 上下文(Activity/Service/Fragment) | ||||
| 	 * @param phoneNumber 待编辑联系人的电话号码(用于匹配已有联系人,必传) | ||||
| 	 * @param contactId 可选:已有联系人的ID(通过 ContactsContract 获取,传null则自动匹配号码) | ||||
| 	 */ | ||||
| 	public static void jumpToEditContact(Context context, String phoneNumber, Long contactId) { | ||||
| 		Intent intent = new Intent(Intent.ACTION_EDIT); | ||||
| 		// 关键:小米等机型需明确设置数据类型为“单个联系人”,避免参数丢失 | ||||
| 		intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE); | ||||
|  | ||||
| 		// 场景A:已知联系人ID(精准定位,优先用此方式,参数传递最稳定) | ||||
| 		if (contactId != null && contactId > 0) { | ||||
| 			// 构建联系人的Uri(格式:content://contacts/people/[contactId],系统标准格式) | ||||
| 			Uri contactUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId); | ||||
| 			intent.setData(contactUri); | ||||
| 			//ToastUtils.show("1"); | ||||
| 		} else if (phoneNumber != null && !phoneNumber.isEmpty()) { | ||||
| 			// 方式1:小米等机型兼容的“通过号码定位联系人”参数(部分系统认此参数) | ||||
| 			//intent.putExtra(ContactsContract.Intents.Insert.PHONE_NUMBER, phoneNumber); | ||||
| 			// 方式2:补充系统标准的“数据Uri”,强化匹配(避免参数被定制系统忽略) | ||||
| 			Uri phoneUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(phoneNumber)); | ||||
| 			intent.setData(phoneUri); | ||||
| 		} else { | ||||
| 			LogUtils.d(TAG, "编辑联系人失败:电话号码和联系人ID均为空"); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// 可选:预填最新号码(覆盖原有号码,若用户修改了号码,编辑时自动更新) | ||||
| 		if (phoneNumber != null && !phoneNumber.isEmpty()) { | ||||
| 			intent.putExtra(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber); | ||||
| 			intent.putExtra(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE); | ||||
| 		} | ||||
|  | ||||
| 		// 启动活动(加防护,避免无联系人应用崩溃) | ||||
| 		// 小米机型在Service/非Activity中调用,需加NEW_TASK标志,否则可能无法启动 | ||||
| 		intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
| 		context.startActivity(intent); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * 通过电话号码查询联系人ID(适配小米机型,解决编辑时匹配不稳定问题) | ||||
| 	 * @param context 上下文 | ||||
| 	 * @param phoneNumber 待查询的电话号码 | ||||
| 	 * @return 联系人ID(无匹配时返回-1) | ||||
| 	 */ | ||||
| 	public static Long getContactIdByPhone(Context context, String phoneNumber) { | ||||
| 		if (phoneNumber == null || phoneNumber.isEmpty()) { | ||||
| 			return -1L; | ||||
| 		} | ||||
|  | ||||
| 		ContentResolver cr = context.getContentResolver(); | ||||
| 		// 1. 构建电话查询Uri(系统标准:通过号码过滤联系人数据) | ||||
| 		Uri queryUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(phoneNumber)); | ||||
| 		// 2. 只查询“联系人ID”字段(高效,避免冗余数据) | ||||
| 		String[] projection = {ContactsContract.CommonDataKinds.Phone.CONTACT_ID}; | ||||
| 		Cursor cursor = null; | ||||
|  | ||||
| 		try { | ||||
| 			cursor = cr.query(queryUri, projection, null, null, null); | ||||
| 			if (cursor != null && cursor.moveToFirst()) { | ||||
| 				// 3. 读取联系人ID(返回Long类型,避免int溢出) | ||||
| 				return cursor.getLong(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.CONTACT_ID)); | ||||
| 			} | ||||
| 		} catch (Exception e) { | ||||
| 			LogUtils.d(TAG, "查询联系人ID失败。" + e); | ||||
| 		} finally { | ||||
| 			if (cursor != null) { | ||||
| 				cursor.close(); // 关闭游标,避免内存泄漏 | ||||
| 			} | ||||
| 		} | ||||
| 		return -1L; // 无匹配联系人 | ||||
| 	} | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -13,8 +13,9 @@ import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.widget.RemoteViews; | ||||
| import cc.winboll.studio.contacts.R; | ||||
| import cc.winboll.studio.contacts.threads.MainServiceThread; | ||||
| import cc.winboll.studio.libappbase.LogUtils; | ||||
| import cc.winboll.studio.libappbase.utils.ToastUtils; | ||||
| import com.hjq.toast.ToastUtils; | ||||
|  | ||||
| public class APPStatusWidget extends AppWidgetProvider { | ||||
|  | ||||
|   | ||||
| @@ -5,8 +5,5 @@ | ||||
|     <item | ||||
|         android:id="@+id/item_calllog_phonenumber_copy" | ||||
|         android:title="Copy"/> | ||||
|     <item | ||||
|         android:id="@+id/item_calllog_phonenumber_add_contact" | ||||
|         android:title="Add Contact"/> | ||||
|      | ||||
| </menu> | ||||
|   | ||||
| @@ -5,8 +5,5 @@ | ||||
|     <item | ||||
|         android:id="@+id/item_contact_phonenumber_copy" | ||||
|         android:title="Copy"/> | ||||
|     <item | ||||
|         android:id="@+id/item_calllog_phonenumber_edit_contact" | ||||
|         android:title="Edit Contact"/> | ||||
|      | ||||
| </menu> | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| #Created by .winboll/winboll_app_build.gradle | ||||
| #Fri Sep 26 05:36:14 HKT 2025 | ||||
| stageCount=9 | ||||
| #Sat Sep 27 21:03:08 HKT 2025 | ||||
| stageCount=10 | ||||
| libraryProject=libappbase | ||||
| baseVersion=15.10 | ||||
| publishVersion=15.10.8 | ||||
| publishVersion=15.10.9 | ||||
| buildCount=0 | ||||
| baseBetaVersion=15.10.9 | ||||
| baseBetaVersion=15.10.10 | ||||
|   | ||||
| @@ -22,12 +22,12 @@ public class GlobalApplication extends Application { | ||||
|         GlobalApplication.isDebuging = isDebuging; | ||||
|     } | ||||
|      | ||||
|     public static void saveDebugStatus(GlobalApplication application) { | ||||
|         APPModel.saveBeanToFile(application.getAPPModelFilePath(application), new APPModel(GlobalApplication.isDebuging)); | ||||
|     public static void saveDebugStatus(Context context) { | ||||
|         APPModel.saveBeanToFile(getAPPModelFilePath(context), new APPModel(GlobalApplication.isDebuging)); | ||||
|     } | ||||
| 	 | ||||
|     static String getAPPModelFilePath(GlobalApplication application) { | ||||
|         return application.getDataDir().getPath() + "/APPModel.json"; | ||||
|     static String getAPPModelFilePath(Context context) { | ||||
|         return context.getDataDir().getPath() + "/APPModel.json"; | ||||
|     } | ||||
|  | ||||
|     public static boolean isDebuging() { | ||||
|   | ||||
| @@ -41,6 +41,17 @@ android { | ||||
|             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | ||||
|         } | ||||
|     } | ||||
| 	 | ||||
| 	// 允许使用系统隐藏API | ||||
|     lintOptions { | ||||
|         checkReleaseBuilds false | ||||
|         abortOnError false | ||||
|     } | ||||
|     // 针对PowerProfile的依赖配置 | ||||
|     dependenciesInfo { | ||||
|         includeInApk = false | ||||
|         includeInBundle = false | ||||
|     } | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| #Created by .winboll/winboll_app_build.gradle | ||||
| #Wed Sep 03 20:59:53 HKT 2025 | ||||
| stageCount=13 | ||||
| #Wed Oct 22 16:57:21 HKT 2025 | ||||
| stageCount=15 | ||||
| libraryProject= | ||||
| baseVersion=15.4 | ||||
| publishVersion=15.4.12 | ||||
| publishVersion=15.4.14 | ||||
| buildCount=0 | ||||
| baseBetaVersion=15.4.13 | ||||
| baseBetaVersion=15.4.15 | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| <?xml version='1.0' encoding='utf-8'?> | ||||
| <manifest | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
| 	xmlns:tools="http://schemas.android.com/tools" | ||||
|     package="cc.winboll.studio.powerbell"> | ||||
|  | ||||
|     <!-- 拍摄照片和视频 --> | ||||
| @@ -24,10 +25,29 @@ | ||||
|     <!-- 显示通知 --> | ||||
|     <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> | ||||
|  | ||||
|     <!-- PACKAGE_USAGE_STATS --> | ||||
|     <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/> | ||||
|  | ||||
|     <!-- BATTERY_STATS --> | ||||
|     <uses-permission android:name="android.permission.BATTERY_STATS"/> | ||||
|  | ||||
|     <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> | ||||
|  | ||||
|     <uses-feature android:name="android.hardware.camera"/> | ||||
|  | ||||
|     <uses-feature android:name="android.hardware.camera.autofocus"/> | ||||
| 		 | ||||
| 	<!-- 1. 基础应用信息读取权限(Android 11 及以下) --> | ||||
| 	<uses-permission android:name="android.permission.GET_PACKAGE_SIZE" /> | ||||
|  | ||||
| 	<!-- 2. Android 11+ 应用列表读取权限(必须声明,否则无法获取全部应用) --> | ||||
| 	<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" | ||||
| 		tools:ignore="QueryAllPackagesPermission" /> | ||||
|  | ||||
| 	<!-- 3. 可选:若需读取系统应用,添加此权限(部分机型需要) --> | ||||
| 	<uses-permission android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS" | ||||
| 		tools:ignore="ProtectedPermissions" /> | ||||
| 	 | ||||
|     <application | ||||
|         android:name=".App" | ||||
|         android:allowBackup="true" | ||||
| @@ -123,6 +143,8 @@ | ||||
|  | ||||
|         <activity android:name="cc.winboll.studio.powerbell.activities.PixelPickerActivity"/> | ||||
|  | ||||
|         <activity android:name="cc.winboll.studio.powerbell.activities.BatteryReportActivity"/> | ||||
|  | ||||
|     </application> | ||||
|  | ||||
| </manifest> | ||||
| </manifest> | ||||
|   | ||||
| @@ -17,7 +17,7 @@ import cc.winboll.studio.libappbase.LogUtils; | ||||
| import cc.winboll.studio.powerbell.MainActivity; | ||||
| import cc.winboll.studio.powerbell.activities.AboutActivity; | ||||
| import cc.winboll.studio.powerbell.activities.BackgroundPictureActivity; | ||||
| import cc.winboll.studio.powerbell.activities.BatteryReporterActivity; | ||||
| import cc.winboll.studio.powerbell.activities.BatteryReportActivity; | ||||
| import cc.winboll.studio.powerbell.activities.ClearRecordActivity; | ||||
| import cc.winboll.studio.powerbell.activities.WinBoLLActivity; | ||||
| import cc.winboll.studio.powerbell.beans.BackgroundPictureBean; | ||||
| @@ -161,7 +161,7 @@ public class MainActivity extends WinBoLLActivity { | ||||
|             startActivity(intent); | ||||
|         } else if (menuItemId == R.id.action_battery_reporter) { | ||||
|             Intent intent = new Intent(); | ||||
|             intent.setClass(this, BatteryReporterActivity.class); | ||||
|             intent.setClass(this, BatteryReportActivity.class); | ||||
|             startActivity(intent); | ||||
|         } else if (menuItemId == R.id.action_clearrecord) { | ||||
|             Intent intent = new Intent(); | ||||
|   | ||||
| @@ -0,0 +1,520 @@ | ||||
| package cc.winboll.studio.powerbell.activities; | ||||
|  | ||||
| /** | ||||
|  * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> | ||||
|  * @Date 2025/10/22 13:21 | ||||
|  * @Describe BatteryReportActivity | ||||
|  */ | ||||
| import android.app.Activity; | ||||
| import android.content.BroadcastReceiver; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.IntentFilter; | ||||
| import android.content.pm.ApplicationInfo; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.os.Build; | ||||
| import android.os.Bundle; | ||||
| import android.provider.Settings; | ||||
| import android.text.Editable; | ||||
| import android.text.TextWatcher; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.EditText; | ||||
| import android.widget.TextView; | ||||
| import android.widget.Toast; | ||||
| import androidx.recyclerview.widget.LinearLayoutManager; | ||||
| import androidx.recyclerview.widget.RecyclerView; | ||||
| import cc.winboll.studio.powerbell.R; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.Comparator; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import cc.winboll.studio.libappbase.LogUtils; | ||||
|  | ||||
| public class BatteryReportActivity extends Activity { | ||||
|     public static final String TAG = "BatteryReportActivity"; | ||||
|     // 目标应用包名常量(统一维护,数据计算核心标识) | ||||
|     private static final String TARGET_APP_PACKAGE = "cc.winboll.studio.mymessagemanager"; | ||||
|  | ||||
|     private RecyclerView rvBatteryReport; | ||||
|     private BatteryReportAdapter adapter; | ||||
|     // 数据列表:仅存储包名、耗电、时长(不存储名称,计算全程用包名) | ||||
|     private List<AppBatteryModel> dataList = new ArrayList<AppBatteryModel>();  | ||||
|     private List<AppBatteryModel> filteredList = new ArrayList<AppBatteryModel>();  | ||||
|     private BroadcastReceiver batteryReceiver; | ||||
|     private int batteryCapacity = 5400; // 电池容量(mAh) | ||||
|     private float lastBatteryPercent = 100.0f; | ||||
|     private long lastCheckTime = System.currentTimeMillis(); | ||||
|     private EditText etSearch; | ||||
|     // 应用运行时长缓存(key:包名,value:时长ms) | ||||
|     private Map<String, Long> appRunTimeCache = new HashMap<String, Long>(); | ||||
|     // 包管理工具(全局持有,避免重复获取) | ||||
|     private PackageManager mPackageManager; | ||||
|  | ||||
|     @Override | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setContentView(R.layout.activity_batteryreport); | ||||
|         // 初始化包管理工具(仅一次,供后续查询名称使用) | ||||
|         mPackageManager = getPackageManager(); | ||||
|  | ||||
|         // 权限检查(Java7 传统条件判断) | ||||
|         if (!hasUsageStatsPermission(this)) { | ||||
|             Toast.makeText(this, "请进入设置-应用-权限-特殊访问权限-使用情况访问权限,开启本应用的权限", Toast.LENGTH_LONG).show(); | ||||
|             startActivity(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         etSearch = (EditText) findViewById(R.id.et_search);  | ||||
|         rvBatteryReport = (RecyclerView) findViewById(R.id.rv_battery_report);  | ||||
|         rvBatteryReport.setLayoutManager(new LinearLayoutManager(this)); | ||||
|  | ||||
|         // 1. 加载所有应用(仅存包名,不查名称) | ||||
|         loadAllAppPackage(); | ||||
|         // 2. 强制添加目标应用(包名兜底) | ||||
|         //forceAddTargetPackage(); | ||||
|         // 3. 首次获取运行时长(key:包名) | ||||
|         appRunTimeCache = getAppRunTime(); | ||||
|         // 4. 更新时长到数据模型(用包名匹配) | ||||
|         updateAppRunTimeToModel(); | ||||
|         // 5. 初始化过滤列表和适配器(传入包管理工具供显示时查名称) | ||||
|         filteredList.addAll(dataList); | ||||
|         adapter = new BatteryReportAdapter(this, filteredList, mPackageManager); | ||||
|         rvBatteryReport.setAdapter(adapter); | ||||
|  | ||||
|         // 搜索监听(按包名过滤,不涉及名称) | ||||
|         etSearch.addTextChangedListener(new TextWatcher() { | ||||
| 				@Override | ||||
| 				public void beforeTextChanged(CharSequence s, int start, int count, int after) {} | ||||
|  | ||||
| 				@Override | ||||
| 				public void onTextChanged(CharSequence s, int start, int before, int count) { | ||||
| 					filterAppsByPackage(s.toString()); | ||||
| 				} | ||||
|  | ||||
| 				@Override | ||||
| 				public void afterTextChanged(Editable s) {} | ||||
| 			}); | ||||
|  | ||||
|         // 电池广播(耗电计算全程用包名) | ||||
|         batteryReceiver = new BroadcastReceiver() { | ||||
|             @Override | ||||
|             public void onReceive(Context context, Intent intent) { | ||||
|                 int level = intent.getIntExtra("level", 100); | ||||
|                 int scale = intent.getIntExtra("scale", 100); | ||||
|                 float currentPercent = (float) level / scale * 100; | ||||
|                 LogUtils.d(TAG, "电池百分比变化:" + lastBatteryPercent + " -> " + currentPercent); | ||||
|  | ||||
|                 // 计算电池消耗并更新数据(全程用包名) | ||||
|                 if (currentPercent < lastBatteryPercent) { | ||||
|                     float dropPercent = lastBatteryPercent - currentPercent; | ||||
|                     long duration = System.currentTimeMillis() - lastCheckTime; | ||||
|                     LogUtils.d(TAG, "电池消耗:" + dropPercent + "%,时长:" + duration + "ms"); | ||||
|                     // 重新获取最新运行时长(key:包名) | ||||
|                     appRunTimeCache = getAppRunTime(); | ||||
|                     // 更新模型中的运行时长(包名匹配) | ||||
|                     updateAppRunTimeToModel(); | ||||
|                     // 分配耗电(包名计算) | ||||
|                     distributeBatteryConsumption(dropPercent, appRunTimeCache); | ||||
|                 } | ||||
|  | ||||
|                 lastBatteryPercent = currentPercent; | ||||
|                 lastCheckTime = System.currentTimeMillis(); | ||||
|             } | ||||
|         }; | ||||
|         registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         // Java7 显式非空判断 | ||||
|         if (batteryReceiver != null) { | ||||
|             unregisterReceiver(batteryReceiver); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 加载所有应用(仅获取包名,不查询应用名称,数据计算核心步骤) | ||||
|      */ | ||||
|     private void loadAllAppPackage() { | ||||
|         List<ApplicationInfo> appList = mPackageManager.getInstalledApplications(PackageManager.GET_META_DATA); | ||||
|         dataList.clear(); // 清空列表避免重复 | ||||
|  | ||||
|         LogUtils.d(TAG, "开始加载应用包名列表,共找到" + appList.size() + "个应用"); | ||||
|          | ||||
|         // 仅遍历包名,不处理名称(避免名称获取异常影响) | ||||
|         for (ApplicationInfo appInfo : appList) { | ||||
|             String packageName = appInfo.packageName; | ||||
| 			/*if (packageName.equals("cc.winboll.studio.powerbell.beta")) { | ||||
| 			//if (packageName.equals("aidepro.top")) { | ||||
| 				LogUtils.d(TAG, "aidepro.top OK5"); | ||||
| 			}*/ | ||||
|             // 模型仅存包名、初始耗电0、初始时长0(名称显示时再查) | ||||
|             dataList.add(new AppBatteryModel(packageName, "0.0", 0)); | ||||
|         } | ||||
| 		 | ||||
| 		/*for (AppBatteryModel appBatteryModel : dataList) { | ||||
| 			if (appBatteryModel.getPackageName().equals("aidepro.top")) { | ||||
| 				LogUtils.d(TAG, "aidepro.top OK4"); | ||||
| 			} | ||||
| 		}*/ | ||||
|  | ||||
|         LogUtils.d(TAG, "应用包名列表加载完成,共添加" + dataList.size() + "个包名。" ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 强制添加目标包名(兜底逻辑,仅操作包名) | ||||
|      */ | ||||
|     private void forceAddTargetPackage() { | ||||
|         // 1. 先检查列表中是否已存在目标包名 | ||||
|         boolean isTargetExists = false; | ||||
|         for (AppBatteryModel model : dataList) { | ||||
|             if (TARGET_APP_PACKAGE.equals(model.getPackageName())) { | ||||
|                 isTargetExists = true; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (isTargetExists) { | ||||
|             LogUtils.d(TAG, "目标包名已在列表中,无需重复添加"); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // 2. 仅用包名创建模型,强制添加(不依赖名称查询) | ||||
|         dataList.add(0, new AppBatteryModel(TARGET_APP_PACKAGE, "0.0", 0)); | ||||
|         LogUtils.d(TAG, "强制添加目标包名成功:" + TARGET_APP_PACKAGE); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 更新运行时长到模型(用包名匹配,不涉及名称) | ||||
|      */ | ||||
|     private void updateAppRunTimeToModel() { | ||||
| 		/*if(appRunTimeCache.containsKey("aidepro.top")) | ||||
| 		 { | ||||
| 		 LogUtils.d(TAG, "aidepro.top OK2"); | ||||
| 		 }*/ | ||||
|  | ||||
| 		int nCount = 0; | ||||
|         for (AppBatteryModel model : dataList) { | ||||
|             String packageName = model.getPackageName(); | ||||
| 			/*if(packageName.equals("aidepro.top")) | ||||
| 			 { | ||||
| 			 LogUtils.d(TAG, "aidepro.top OK3"); | ||||
| 			 }*/ | ||||
|  | ||||
|             Long runTime; | ||||
|             // Java7 显式判断包名是否在缓存中 | ||||
|             if (appRunTimeCache.containsKey(packageName)) { | ||||
| 				/*if(packageName.equals("cc.winboll.studio.powerbell.beta")) { | ||||
| 				 LogUtils.d(TAG, String.format("特殊查询 %s 查询有结果。", packageName)); | ||||
| 				 }*/ | ||||
|                 runTime = appRunTimeCache.get(packageName); | ||||
| 				LogUtils.d(TAG, String.format("应用包 %s 运行时长已更新。", packageName)); | ||||
| 				nCount++; | ||||
|             } else { | ||||
|                 runTime = 0L; | ||||
|             } | ||||
|             model.setRunTime(runTime); | ||||
|         } | ||||
| 		LogUtils.d(TAG, String.format("dataList.size() %d, appRunTimeCache.size() %d。", dataList.size(), appRunTimeCache.size())); | ||||
| 		LogUtils.d(TAG, String.format("updateAppRunTimeToModel() 更新的数据量为:%d", nCount)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 按包名过滤应用(搜索逻辑仅操作包名) | ||||
|      */ | ||||
|     private void filterAppsByPackage(String keyword) { | ||||
|         filteredList.clear(); | ||||
|         if (keyword == null || keyword.isEmpty()) { | ||||
|             filteredList.addAll(dataList); | ||||
|         } else { | ||||
|             String lowerKeyword = keyword.toLowerCase(); | ||||
|             boolean isTargetMatched = false; | ||||
|  | ||||
|             for (AppBatteryModel model : dataList) { | ||||
|                 String packageNameLower = model.getPackageName().toLowerCase(); | ||||
|                 // 仅按包名包含关键词过滤 | ||||
|                 boolean isMatched = packageNameLower.contains(lowerKeyword); | ||||
|  | ||||
|                 if (isMatched) { | ||||
|                     filteredList.add(model); | ||||
|                     // 检查是否是目标包名 | ||||
|                     if (TARGET_APP_PACKAGE.equals(model.getPackageName())) { | ||||
|                         isTargetMatched = true; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // 兜底:目标包名未匹配时强制添加 | ||||
|             if (!isTargetMatched) { | ||||
|                 for (AppBatteryModel model : dataList) { | ||||
|                     if (TARGET_APP_PACKAGE.equals(model.getPackageName())) { | ||||
|                         filteredList.add(0, model); | ||||
|                         LogUtils.d(TAG, "搜索兜底:强制添加目标包名到结果列表"); | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         adapter.notifyDataSetChanged(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取应用运行时长(返回包名-时长映射,不处理名称) | ||||
|      */ | ||||
|     private Map<String, Long> getAppRunTime() { | ||||
|         Map<String, Long> runTimeMap = new HashMap<String, Long>(); | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||
|             try { | ||||
|                 android.app.usage.UsageStatsManager manager = | ||||
| 					(android.app.usage.UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE); | ||||
|                 long endTime = System.currentTimeMillis(); | ||||
|                 long startTime = endTime - 24 * 3600 * 1000; // 近24小时 | ||||
|                 List<android.app.usage.UsageStats> statsList = manager.queryUsageStats( | ||||
| 					android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime); | ||||
|  | ||||
|                 // 仅存储包名和时长(不查名称) | ||||
|                 for (android.app.usage.UsageStats stats : statsList) { | ||||
|                     long runTimeMs = stats.getTotalTimeInForeground(); | ||||
|                     String packageName = stats.getPackageName(); | ||||
|                     LogUtils.d(TAG, "包名" + packageName + "运行时长:" + formatRunTime(runTimeMs)); | ||||
|                     runTimeMap.put(packageName, runTimeMs); | ||||
| 					if (packageName.equals("aidepro.top")) { | ||||
| 						LogUtils.d(TAG, String.format("runTimeMap.put(packageName, runTimeMs) 特殊查询 %s 查询有结果。", packageName)); | ||||
| 					} | ||||
|                 } | ||||
|             } catch (Exception e) { | ||||
|                 LogUtils.e(TAG, "获取应用运行时长失败:" + e.getMessage()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| 		/*if(runTimeMap.containsKey("aidepro.top")) | ||||
| 		 { | ||||
| 		 LogUtils.d(TAG, "aidepro.top OK"); | ||||
| 		 }*/ | ||||
|  | ||||
| 		LogUtils.d(TAG, String.format("应用运行时长列表数量%d。", runTimeMap.size())); | ||||
|         return runTimeMap; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 格式化运行时长(工具方法,与包名无关) | ||||
|      */ | ||||
|     private String formatRunTime(long runTimeMs) { | ||||
|         if (runTimeMs <= 0) { | ||||
|             return "0秒"; | ||||
|         } | ||||
|         long seconds = runTimeMs / 1000; | ||||
|         long hours = seconds / 3600; | ||||
|         long minutes = (seconds % 3600) / 60; | ||||
|         seconds = seconds % 60; | ||||
|  | ||||
|         if (hours > 0) { | ||||
|             return String.format("%d时%d分%d秒", hours, minutes, seconds); | ||||
|         } else if (minutes > 0) { | ||||
|             return String.format("%d分%d秒", minutes, seconds); | ||||
|         } else { | ||||
|             return String.format("%d秒", seconds); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 分配电池消耗(全程用包名计算,不涉及名称) | ||||
|      */ | ||||
|     private void distributeBatteryConsumption(float totalDropPercent, Map<String, Long> runTimeMap) { | ||||
|         long totalRunTime = 0; | ||||
|         // 遍历包名计算总时长 | ||||
|         for (Map.Entry<String, Long> entry : runTimeMap.entrySet()) { | ||||
|             totalRunTime += entry.getValue(); | ||||
|         } | ||||
|  | ||||
|         // 按包名匹配计算各应用耗电 | ||||
|         for (AppBatteryModel model : dataList) { | ||||
|             String packageName = model.getPackageName(); | ||||
|             Long appRunTime; | ||||
|             // Java7 显式判断包名是否存在 | ||||
|             if (runTimeMap.containsKey(packageName)) { | ||||
|                 appRunTime = runTimeMap.get(packageName); | ||||
|             } else { | ||||
|                 appRunTime = 0L; | ||||
|             } | ||||
|  | ||||
|             float ratio = (totalRunTime > 0) ? (float) appRunTime / totalRunTime : 0; | ||||
|             float consumption = batteryCapacity * totalDropPercent / 100 * ratio; | ||||
|             model.setConsumption(String.format("%.1f", consumption)); | ||||
|             model.setRunTime(appRunTime); // 同步更新时长 | ||||
|         } | ||||
|  | ||||
|         // 按耗电排序(用包名对应的耗电值排序) | ||||
|         Collections.sort(dataList, new Comparator<AppBatteryModel>() { | ||||
| 				@Override | ||||
| 				public int compare(AppBatteryModel m1, AppBatteryModel m2) { | ||||
| 					float c1 = Float.parseFloat(m1.getConsumption()); | ||||
| 					float c2 = Float.parseFloat(m2.getConsumption()); | ||||
| 					return Float.compare(c2, c1); // 逆序(耗电从高到低) | ||||
| 				} | ||||
| 			}); | ||||
|  | ||||
|         // 重新应用包名过滤 | ||||
|         filterAppsByPackage(etSearch.getText().toString()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 权限检查(仅操作系统服务,与包名无关) | ||||
|      */ | ||||
|     private boolean hasUsageStatsPermission(Context context) { | ||||
|         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         android.app.usage.UsageStatsManager manager = | ||||
| 			(android.app.usage.UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE); | ||||
|         if (manager == null) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         long endTime = System.currentTimeMillis(); | ||||
|         long startTime = endTime - 1000 * 60; | ||||
|         List<android.app.usage.UsageStats> statsList = manager.queryUsageStats( | ||||
| 			android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime); | ||||
|  | ||||
|         return statsList != null && !statsList.isEmpty(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 数据模型(仅存储包名、耗电、时长,不存名称) | ||||
|      */ | ||||
|     public static class AppBatteryModel { | ||||
|         private String packageName; // 核心标识:应用包名 | ||||
|         private String consumption; // 电量消耗(mAh) | ||||
|         private long runTime;       // 运行时长(ms) | ||||
|  | ||||
|         // Java7 显式构造(仅传入包名、初始耗电、初始时长) | ||||
|         public AppBatteryModel(String packageName, String consumption, long runTime) { | ||||
|             this.packageName = packageName; | ||||
|             this.consumption = consumption; | ||||
|             this.runTime = runTime; | ||||
|         } | ||||
|  | ||||
|         // Getter/Setter(仅操作包名、耗电、时长) | ||||
|         public String getPackageName() { | ||||
|             return packageName; | ||||
|         } | ||||
|  | ||||
|         public String getConsumption() { | ||||
|             return consumption; | ||||
|         } | ||||
|  | ||||
|         public void setConsumption(String consumption) { | ||||
|             this.consumption = consumption; | ||||
|         } | ||||
|  | ||||
|         public long getRunTime() { | ||||
|             return runTime; | ||||
|         } | ||||
|  | ||||
|         public void setRunTime(long runTime) { | ||||
|             this.runTime = runTime; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * RecyclerView 适配器(仅在显示时通过包名查询应用名称) | ||||
|      */ | ||||
|     public static class BatteryReportAdapter extends RecyclerView.Adapter<BatteryReportAdapter.ViewHolder> { | ||||
|         private Context mContext; | ||||
|         private List<AppBatteryModel> mDataList; | ||||
|         private PackageManager mPm; // 用于显示时查询应用名称 | ||||
|  | ||||
|         // Java7 显式构造(传入包管理工具) | ||||
|         public BatteryReportAdapter(Context context, List<AppBatteryModel> dataList, PackageManager pm) { | ||||
|             this.mContext = context; | ||||
|             this.mDataList = dataList; | ||||
|             this.mPm = pm; // 持有包管理工具,供显示时查名称 | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { | ||||
|             View itemView = LayoutInflater.from(mContext) | ||||
| 				.inflate(android.R.layout.simple_list_item_2, parent, false); | ||||
|             return new ViewHolder(itemView); | ||||
|         } | ||||
|  | ||||
|         // 绑定数据:核心修改——仅在此处通过包名查询应用名称并显示 | ||||
|         @Override | ||||
|         public void onBindViewHolder(ViewHolder holder, int position) { | ||||
|             // Java7 显式非空判断(避免空指针) | ||||
|             if (mDataList == null || mDataList.isEmpty() || position >= mDataList.size()) { | ||||
|                 holder.tvAppName.setText("未知应用"); | ||||
|                 holder.tvConsumption.setText("耗电:0.0 mAh | 运行时长:0秒"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             AppBatteryModel model = mDataList.get(position); | ||||
|             String packageName = model.getPackageName(); | ||||
|             String appName = ""; | ||||
|  | ||||
|             // 关键步骤:通过包名查询应用名称(仅显示时执行,不影响数据计算) | ||||
|             try { | ||||
|                 ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, 0); | ||||
|                 appName = mPm.getApplicationLabel(appInfo).toString(); | ||||
|             } catch (PackageManager.NameNotFoundException e) { | ||||
|                 // 包名不存在(如应用已卸载),用包名作为名称兜底 | ||||
|                 appName = packageName; | ||||
|                 LogUtils.e("Adapter", "包名" + packageName + "对应的应用未找到:" + e.getMessage()); | ||||
|             } catch (Exception e) { | ||||
|                 // 其他异常(如权限问题),同样用包名兜底 | ||||
|                 appName = packageName; | ||||
|                 LogUtils.e("Adapter", "查询应用名称失败(包名:" + packageName + "):" + e.getMessage()); | ||||
|             } | ||||
|  | ||||
|             // 显示应用名称(查询到则显示名称,否则显示包名) | ||||
|             holder.tvAppName.setText(appName); | ||||
|  | ||||
|             // 格式化运行时长(调用 Activity 的工具方法) | ||||
|             String runTimeStr = ((BatteryReportActivity) mContext).formatRunTime(model.getRunTime()); | ||||
|             // 显示耗电+运行时长(基于包名计算的结果) | ||||
|             String consumptionText = String.format("耗电:%s mAh | 运行时长:%s", | ||||
| 												   model.getConsumption(), runTimeStr); | ||||
|             holder.tvConsumption.setText(consumptionText); | ||||
|  | ||||
|             // 优化显示:目标应用文字标蓝(通过包名匹配) | ||||
|             if (BatteryReportActivity.TARGET_APP_PACKAGE.equals(packageName)) { | ||||
|                 holder.tvAppName.setTextColor(mContext.getResources().getColor(android.R.color.holo_blue_dark)); | ||||
|                 holder.tvConsumption.setTextColor(mContext.getResources().getColor(android.R.color.holo_blue_dark)); | ||||
|             } else { | ||||
|                 holder.tvAppName.setTextColor(mContext.getResources().getColor(android.R.color.black)); | ||||
|                 holder.tvConsumption.setTextColor(mContext.getResources().getColor(android.R.color.darker_gray)); | ||||
|             } | ||||
|  | ||||
|             // 调整文字大小(提升可读性) | ||||
|             holder.tvAppName.setTextSize(16); | ||||
|             holder.tvConsumption.setTextSize(14); | ||||
|         } | ||||
|  | ||||
|         // 获取列表长度(Java7 三元运算符) | ||||
|         @Override | ||||
|         public int getItemCount() { | ||||
|             return mDataList == null ? 0 : mDataList.size(); | ||||
|         } | ||||
|  | ||||
|         // ViewHolder(绑定系统布局的两个 TextView) | ||||
|         public static class ViewHolder extends RecyclerView.ViewHolder { | ||||
|             TextView tvAppName;     // 显示应用名称(通过包名查询) | ||||
|             TextView tvConsumption; // 显示耗电+运行时长(包名计算结果) | ||||
|  | ||||
|             // Java7 显式构造 | ||||
|             public ViewHolder(View itemView) { | ||||
|                 super(itemView); | ||||
|                 // 强制类型转换(系统布局控件 ID:text1 和 text2) | ||||
|                 tvAppName = (TextView) itemView.findViewById(android.R.id.text1); | ||||
|                 tvConsumption = (TextView) itemView.findViewById(android.R.id.text2); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -1,51 +0,0 @@ | ||||
| package cc.winboll.studio.powerbell.activities; | ||||
|  | ||||
| /** | ||||
|  * @Author ZhanGSKen<zhangsken@qq.com> | ||||
|  * @Date 2025/03/22 14:20:15 | ||||
|  */ | ||||
| import android.app.Activity; | ||||
| import android.os.Bundle; | ||||
| import androidx.recyclerview.widget.DividerItemDecoration; | ||||
| import androidx.recyclerview.widget.LinearLayoutManager; | ||||
| import androidx.recyclerview.widget.RecyclerView; | ||||
| import cc.winboll.studio.powerbell.R; | ||||
| import cc.winboll.studio.powerbell.adapters.BatteryAdapter; | ||||
| import cc.winboll.studio.powerbell.beans.BatteryData; | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
|  | ||||
| public class BatteryReporterActivity extends Activity { | ||||
|     public static final String TAG = "BatteryReporterActivity"; | ||||
|  | ||||
|     private RecyclerView rvBatteryReport; | ||||
|     private BatteryAdapter adapter; | ||||
|  | ||||
|     @Override | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setContentView(R.layout.activity_battery_reporter); | ||||
|  | ||||
|         rvBatteryReport = findViewById(R.id.rvBatteryReport); | ||||
|         setupRecyclerView(); | ||||
|         loadSampleData(); | ||||
|     } | ||||
|  | ||||
|     private void setupRecyclerView() { | ||||
|         adapter = new BatteryAdapter(); | ||||
|         rvBatteryReport.setLayoutManager(new LinearLayoutManager(this)); | ||||
|         rvBatteryReport.setAdapter(adapter); | ||||
|         rvBatteryReport.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)); | ||||
|     } | ||||
|  | ||||
|     private void loadSampleData() { | ||||
|         List<BatteryData> dataList = Arrays.asList( | ||||
|             new BatteryData(95, "01:23:45", "00:05:12"), | ||||
|             new BatteryData(80, "02:15:30", "00:10:00"), | ||||
|             new BatteryData(65, "03:45:15", "00:15:30"), | ||||
|             new BatteryData(50, "05:00:00", "00:20:45") | ||||
|         ); | ||||
|         adapter.updateData(dataList); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -1,24 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout  | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:orientation="vertical" | ||||
|     android:padding="16dp"> | ||||
|  | ||||
|     <TextView | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:text="电池使用报告" | ||||
|         android:textSize="24sp" | ||||
|         android:fontFamily="sans-serif-medium" | ||||
|         android:layout_marginBottom="16dp"/> | ||||
|  | ||||
|     <androidx.recyclerview.widget.RecyclerView | ||||
|         android:id="@+id/rvBatteryReport" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" | ||||
|         android:divider="@drawable/divider_line" | ||||
|         android:dividerHeight="1dp"/> | ||||
|  | ||||
| </LinearLayout> | ||||
							
								
								
									
										31
									
								
								powerbell/src/main/res/layout/activity_batteryreport.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								powerbell/src/main/res/layout/activity_batteryreport.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout  | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:orientation="vertical" | ||||
|     android:background="@android:color/white"> | ||||
|  | ||||
|     <!-- 搜索框:使用系统兼容背景 --> | ||||
|     <EditText | ||||
|         android:id="@+id/et_search" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_margin="8dp" | ||||
|         android:padding="12dp" | ||||
|         android:hint="搜索应用包名" | ||||
|         android:background="@android:drawable/btn_default_small" | ||||
|         android:inputType="text" | ||||
|         android:textSize="16sp"/> | ||||
|  | ||||
|     <!-- 应用列表:不依赖系统分割线,避免资源缺失 --> | ||||
|     <androidx.recyclerview.widget.RecyclerView | ||||
|         android:id="@+id/rv_battery_report" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="0dp" | ||||
|         android:layout_weight="1" | ||||
|         android:layout_marginLeft="8dp" | ||||
|         android:layout_marginRight="8dp"/> | ||||
|  | ||||
| </LinearLayout> | ||||
|  | ||||
		Reference in New Issue
	
	Block a user