Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
*/
public class BuildConfigHelper {

private BuildConfigHelper() {
// utility class, prevent instantiation
}

public static boolean isDebugMode() {
return BuildConfig.DEBUG;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import android.Manifest;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
Expand Down Expand Up @@ -33,6 +35,7 @@
import com.batteryhealth.app.service.BatteryMonitorService;
import com.batteryhealth.app.service.ChargingMonitorService;
import com.batteryhealth.app.ui.battery.BatteryHealthFragment;
import com.batteryhealth.app.ui.bugreport.BugReportFragment;
import com.batteryhealth.app.ui.config.DeviceConfigFragment;
import com.batteryhealth.app.ui.endurance.EnduranceFragment;
import com.batteryhealth.app.ui.performance.PerformanceFragment;
Expand Down Expand Up @@ -67,9 +70,12 @@ public class MainActivity extends AppCompatActivity {

private BatteryDataManager batteryDataManager;
private DeviceInfoManager deviceInfoManager;

private Handler mainHandler;
private boolean servicesStarted = false;

// 电池广播接收器:在 onResume 注册,在 onPause 注销,避免内存泄漏
private BroadcastReceiver batteryUpdateReceiver;

@Override
protected void onCreate(Bundle savedInstanceState) {
Expand Down Expand Up @@ -127,8 +133,7 @@ protected void onCreate(Bundle savedInstanceState) {
// 引导用户关闭电池优化
promptBatteryOptimizationIfNeeded();

// 注意:电池广播由BatteryMonitorService统一处理
// 不再在MainActivity中注册电池广播接收器,避免重复监听
// 电池广播在 onResume 中注册,onPause 中注销,避免内存泄漏

// 延迟启动服务,避免启动时闪退
mainHandler.postDelayed(() -> {
Expand All @@ -146,7 +151,10 @@ protected void onCreate(Bundle savedInstanceState) {

} catch (Exception e) {
Log.e(TAG, "Critical error in onCreate: " + e.getMessage(), e);
Toast.makeText(this, getString(R.string.status_app_init_failed, e.getMessage()), Toast.LENGTH_LONG).show();
// status_app_init_failed has %1$s placeholder; safely handle null message.
String safeMessage = (e.getMessage() != null) ? e.getMessage() : e.getClass().getSimpleName();
String toastMessage = getString(R.string.status_app_init_failed, safeMessage);
Toast.makeText(this, toastMessage, Toast.LENGTH_LONG).show();
// 记录详细错误信息
StringBuilder errorDetail = new StringBuilder();
errorDetail.append("Error: ").append(e.getMessage()).append("\n");
Expand All @@ -157,10 +165,78 @@ protected void onCreate(Bundle savedInstanceState) {
}
}

@Override
protected void onResume() {
super.onResume();
registerBatteryUpdateReceiver();
}

@Override
protected void onPause() {
super.onPause();
unregisterBatteryUpdateReceiver();
}

@Override
protected void onDestroy() {
// 注销广播接收器(以防 onPause 未被调用的极端情况)
unregisterBatteryUpdateReceiver();

// 清除 Handler 回调,防止 Activity 销毁后仍执行
if (mainHandler != null) {
mainHandler.removeCallbacksAndMessages(null);
}

// 释放管理器引用
batteryDataManager = null;
deviceInfoManager = null;
viewPager = null;
bottomNavigation = null;
mainHandler = null;

super.onDestroy();
// 不再需要注销广播接收器,因为已经移除了
}

/**
* 注册电池更新广播接收器
*/
private void registerBatteryUpdateReceiver() {
if (batteryUpdateReceiver == null) {
batteryUpdateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (batteryDataManager != null && !isFinishing()) {
batteryDataManager.refreshFromStickyIntent();
}
}
};
}
try {
IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
registerReceiver(batteryUpdateReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else {
registerReceiver(batteryUpdateReceiver, filter);
}
} catch (Exception e) {
Log.e(TAG, "Error registering battery update receiver: " + e.getMessage());
}
}

/**
* 注销电池更新广播接收器
*/
private void unregisterBatteryUpdateReceiver() {
if (batteryUpdateReceiver != null) {
try {
unregisterReceiver(batteryUpdateReceiver);
} catch (IllegalArgumentException ignored) {
// 接收器未注册时忽略
} catch (Exception e) {
Log.e(TAG, "Error unregistering battery update receiver: " + e.getMessage());
}
batteryUpdateReceiver = null;
}
}

/**
Expand Down Expand Up @@ -250,18 +326,20 @@ public Fragment createFragment(int position) {
return new TrendFragment();
case 5:
return new PowerFragment();
case 6:
return new BugReportFragment();
default:
return new BatteryHealthFragment();
}
}

@Override
public int getItemCount() {
return 6;
return 7;
}
});

viewPager.setOffscreenPageLimit(4);
viewPager.setOffscreenPageLimit(1);
// 设置页面切换动画
viewPager.setPageTransformer((page, position) -> {
float absPosition = Math.abs(position);
Expand Down Expand Up @@ -294,7 +372,7 @@ public void onPageSelected(int position) {
}

/**
* 设置底部导航(6项:健康 / 配置 / 性能 / 续航 / 趋势 / 充电)
* 设置底部导航(7项:健康 / 配置 / 性能 / 续航 / 趋势 / 充电 / 分析
*/
private void setupBottomNavigation() {
List<CustomBottomNavigationView.NavItem> navItems = new ArrayList<>();
Expand All @@ -304,6 +382,7 @@ private void setupBottomNavigation() {
navItems.add(new CustomBottomNavigationView.NavItem(getString(R.string.nav_endurance), R.drawable.ic_endurance));
navItems.add(new CustomBottomNavigationView.NavItem(getString(R.string.nav_trend), R.drawable.ic_trend));
navItems.add(new CustomBottomNavigationView.NavItem(getString(R.string.nav_power), R.drawable.ic_power));
navItems.add(new CustomBottomNavigationView.NavItem(getString(R.string.nav_bugreport), R.drawable.ic_bugreport));

bottomNavigation.setItems(navItems);
bottomNavigation.setOnItemSelectedListener(position -> {
Expand All @@ -315,7 +394,9 @@ private void setupBottomNavigation() {
* 更新底部导航状态
*/
private void updateBottomNavigation(int position) {
bottomNavigation.setSelectedPosition(position);
if (bottomNavigation != null) {
bottomNavigation.setSelectedPosition(position);
}
}

/**
Expand Down Expand Up @@ -478,18 +559,11 @@ private void promptBatteryOptimizationIfNeeded() {
* 加载初始数据
*/
private void loadInitialData() {
// 在后台线程加载数据
new Thread(() -> {
try {
// 获取电池信息
if (batteryDataManager != null) {
batteryDataManager.refreshAllDataAsync();
}

// 在主线程更新UI
mainHandler.post(() -> {
// 数据已加载,Fragment会自行获取
});
} catch (Exception e) {
Log.e(TAG, "Error loading initial data: " + e.getMessage());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@

/**
* Room数据库主类
*
* 包含所有数据表的定义
*
* 包含所有数据表的定义。
*
* 注意:
* - schema 文件建议导出({@code exportSchema = true})以便版本管理与回归测试,
* 当前为简化部署关闭该选项。
* - 数据库 schema 变更必须提供 {@code Migration} 并在 {@code Room.databaseBuilder}
* 中显式注册;当前 v3 假设在生产端已通过单独的迁移实现接入。
*/
@Database(
entities = {
Expand All @@ -24,8 +30,8 @@
)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {

public abstract BatteryInfoDao batteryInfoDao();
public abstract PerformanceDataDao performanceDataDao();
public abstract PowerHistoryDao powerHistoryDao();
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.batteryhealth.app.data.database;

import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Transaction;
import androidx.room.Update;

import com.batteryhealth.app.data.model.BatteryInfo;
Expand All @@ -13,49 +16,78 @@

/**
* 电池信息数据访问对象
*
* 注意:
* 1. timestamp 字段建立了索引,以加速按时间范围查询(getSince、getBetween、
* getAverageHealthSince、getCountSince、deleteOlderThan)。
* 2. health_percentage 字段建立了索引,以加速按健康度排序或筛选的查询。
* 3. 默认主键 id 已有 Room 隐式索引。
*
* 实现说明:
* - 聚合查询(AVG)使用 COALESCE 兜底,避免空表时返回 NULL 进而导致 Java 端
* 拆箱 NPE。
* - 批量插入(insertAll)和多语句操作(pruneOlderThan)使用 @Transaction 包裹
* 以保证原子性。
*/
@Dao
public interface BatteryInfoDao {
@Insert

@Insert(onConflict = OnConflictStrategy.REPLACE)
long insert(BatteryInfo batteryInfo);


@Insert(onConflict = OnConflictStrategy.REPLACE)
List<Long> insertAll(List<BatteryInfo> batteryInfos);

@Update
void update(BatteryInfo batteryInfo);

@Delete
void delete(BatteryInfo batteryInfo);

@Query("SELECT * FROM battery_info ORDER BY timestamp DESC")
List<BatteryInfo> getAll();

@Query("SELECT * FROM battery_info ORDER BY timestamp DESC")
LiveData<List<BatteryInfo>> getAllLiveData();

@Query("SELECT * FROM battery_info ORDER BY timestamp DESC LIMIT :limit")
List<BatteryInfo> getRecent(int limit);

@Query("SELECT * FROM battery_info WHERE timestamp >= :startTime ORDER BY timestamp ASC")
List<BatteryInfo> getSince(long startTime);

@Query("SELECT * FROM battery_info WHERE timestamp BETWEEN :startTime AND :endTime ORDER BY timestamp ASC")
List<BatteryInfo> getBetween(long startTime, long endTime);

@Query("SELECT AVG(health_percentage) FROM battery_info WHERE timestamp >= :startTime")

/**
* 返回指定时间之后的平均健康度。空表返回 0,避免 NULL 拆箱。
*/
@Query("SELECT COALESCE(AVG(health_percentage), 0) FROM battery_info WHERE timestamp >= :startTime")
float getAverageHealthSince(long startTime);

@Query("SELECT COUNT(*) FROM battery_info WHERE timestamp >= :startTime")
int getCountSince(long startTime);

@Nullable
@Query("SELECT * FROM battery_info ORDER BY timestamp DESC LIMIT 1")
BatteryInfo getLatest();

@Query("SELECT COUNT(*) FROM battery_info")
int getCount();

@Query("DELETE FROM battery_info WHERE timestamp < :timestamp")
void deleteOlderThan(long timestamp);

@Query("DELETE FROM battery_info")
void deleteAll();
}

/**
* 事务性清理:先删除再统计,保证外部读取到的 count 与实际一致。
*/
@Transaction
default int pruneOlderThan(long timestamp) {
int before = getCount();
deleteOlderThan(timestamp);
return before - getCount();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
* Room数据库类型转换器
*/
public class Converters {

@TypeConverter
public static Date fromTimestamp(Long value) {
return value == null ? null : new Date(value);
}

@TypeConverter
public static Long dateToTimestamp(Date date) {
return date == null ? null : date.getTime();
}
}
}
Loading