Phân tích Lớp PowerUsageSummary: Cách Android hiển thị Mức tiêu thụ Pin

Trong hệ điều hành Android, ứng dụng Cài đặt (Settings) cung cấp một cái nhìn tổng quan về mức tiêu thụ pin, cho phép người dùng xác định các ứng dụng và thành phần phần cứng tiêu hao năng lượng nhiều nhất. Chức năng này được triển khai chủ yếu trong lớp PowerUsageSummary, có thể tìm thấy tại đường dẫn /packages/apps/Settings/src/com/android/settings/fuelgauge/PowerUsageSummary.java trong mã nguồn Android (ví dụ, phiên bản 6.0.1_r10).

Mục tiêu chính của PowerUsageSummary là lọc ra và hiển thị danh sách các ứng dụng hoặc thành phần hệ thống có mức tiêu thụ pin cao nhất, thường giới hạn trong top 10.

Lớp PowerUsageSummary kế thừa từ PowerUsageBase và trong quá trình khởi tạo, nó thiết lập các thành phần giao diện người dùng và định nghĩa một số hằng số quan trọng, ví dụ như:

  • USE_FAKE_DATA: Một cờ để xác định xem có nên sử dụng dữ liệu giả lập cho mục đích kiểm thử hay không.
  • private BatteryHistoryPreference mHistPref;: Đối tượng này dùng để truy xuất dữ liệu lịch sử tiêu thụ pin, thường đọc từ một tệp cấu hình.
  • PreferenceGroup: Một nhóm giao diện để tập hợp và hiển thị tất cả các mục tiêu thụ pin của ứng dụng.

Cơ chế Cập nhật Thống kê Pin (refreshStats)

Một trong những phương thức cốt lõi là refreshStats. Khi được gọi, nó kích hoạt quá trình thu thập và tính toán dữ liệu tiêu thụ pin mới nhất:

protected void refreshStats() {
    super.refreshStats(); // Gọi phương thức của lớp cha
}

Phương thức refreshStats của lớp cha thường gọi đến mStatsHelper.refreshStats() để cập nhật dữ liệu. Ví dụ:

protected void refreshStats() {
    // Cập nhật thống kê pin từ lần sạc đầy gần nhất
    mStatsHelper.refreshStats(BatteryStats.STATS_SINCE_CHARGED, mUm.getUserProfiles());
}

Tại đây, BatteryStats.STATS_SINCE_CHARGED chỉ định rằng chúng ta muốn dữ liệu tiêu thụ pin kể từ lần sạc đầy gần nhất. Các tùy chọn khác có thể là STATS_SINCE_UNPLUGGED (kể từ khi rút cáp USB). Tham số mUm.getUserProfiles() được sử dụng để lấy thông tin về các hồ sơ người dùng hiện có trên thiết bị, một yêu cầu từ cơ chế bảo mật của Android nhằm quản lý mức tiêu thụ pin cho từng người dùng riêng biệt.

Đối tượng mStatsHelper (BatteryStatsHelper) đóng vai trò trung tâm trong việc thu thập và tính toán dữ liệu pin. Nó truy cập thông tin cấu hình pin thông qua PowerProfile và dữ liệu thống kê từ BatteryStats.

final PowerProfile devicePowerProfile = mStatsHelper.getPowerProfile();
final BatteryStats batteryStatsData = mStatsHelper.getStats();
final double fullScreenAveragePower = devicePowerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL);

Đối tượng PowerProfile và power_profile.xml

PowerProfile là một đối tượng chứa thông tin cấu hình về mức tiêu thụ năng lượng của các thành phần phần cứng khác nhau trên thiết bị. Nó được khởi tạo từ một tệp XML:

public PowerProfile(Context context) {
    // Chỉ đọc tệp XML một lần khi chưa có dữ liệu cấu hình
    if (sPowerMap.isEmpty()) {
        loadPowerValuesFromResource(context);
    }
    initializeCpuClusters(); // Khởi tạo các cụm CPU
}

Phương thức loadPowerValuesFromResource chịu trách nhiệm phân tích tệp power_profile.xml. Tệp này nằm trong mã nguồn tại /frameworks/base/core/res/res/xml/power_profile.xml và định nghĩa "giá" điện năng tiêu thụ cho mỗi thành phần phần cứng (ví dụ: CPU, màn hình, Wi-Fi) trong một giây. Đây là một tệp có thể cấu hình được, và mỗi nhà sản xuất thiết bị Android có trách nhiệm cung cấp một tệp power_profile.xml chính xác cho thiết bị của họ.

Cơ chế tính toán mức tiêu thụ pin:

Android tính toán mức tiêu thụ pin của một ứng dụng hoặc thành phần dựa trên một mô hình đơn giản:

  1. "Bảng giá" năng lượng: Tệp power_profile.xml hoạt động như một "bảng giá", liệt kê mức tiêu thụ điện (ví dụ: mAh/giây) cho mỗi thành phần phần cứng.
  2. "Danh sách mua sắm" sử dụng: Hệ thống ghi lại các thành phần phần cứng mà một ứng dụng đã sử dụng và tổng thời gian sử dụng của từng thành phần.

Ví dụ, nếu một ứng dụng sử dụng CPU trong tổng cộng 60 giây và CPU tiêu thụ 1 mAh/giây, thì ứng dụng đó được tính là đã tiêu thụ 60 mAh điện. Do sự đa dạng của phần cứng, mỗi thiết bị Android có một "bảng giá" riêng biệt. Người dùng có thể trích xuất tệp power_profile.xml từ thiết bị của mình bằng cách giải nén tệp /system/framework/framework-res.apk.

Đối tượng BatteryStats

Phương thức mStatsHelper.getStats() trả về một đối tượng BatteryStats (thường là BatteryStatsImpl). Đối tượng này chứa tất cả thông tin chi tiết về mức tiêu thụ năng lượng của mọi ứng dụng và thành phần hệ thống.

Giá trị fullScreenAveragePower được dùng để so sánh với một ngưỡng tối thiểu. Nếu mức tiêu thụ pin trung bình của thiết bị không đạt ngưỡng này (và không sử dụng dữ liệu giả), thì dữ liệu tiêu thụ của ứng dụng có thể không được hiển thị.

if (fullScreenAveragePower >= MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP || USE_FAKE_DATA) {
    // ... tiến hành hiển thị dữ liệu ...
}

Tổng hợp và Lọc Dữ liệu

Tiếp theo, dữ liệu sử dụng pin được tổng hợp. Phương thức getCoalescedUsageList nhóm các mục tiêu thụ pin dựa trên ID người dùng (UID). Khi kiểm thử, có thể sử dụng dữ liệu giả lập từ generateDummyPowerData():

final List<BatterySipper> usageEntries = USE_FAKE_DATA ? generateDummyPowerData() : mStatsHelper.getUsageList();

Đây là một ví dụ về phương thức tạo dữ liệu giả lập:

private static List<BatterySipper> generateDummyPowerData() {
    ArrayList<BatterySipper> dummyEntries = new ArrayList<>();
    float baseUsage = 8.0f; // Mức sử dụng pin cơ bản ban đầu

    // Thêm các loại tiêu hao hệ thống giả lập
    for (BatterySipper.DrainType type : BatterySipper.DrainType.values()) {
        if (type == BatterySipper.DrainType.APP) { // Bỏ qua loại ứng dụng trong vòng lặp này
            continue;
        }
        dummyEntries.add(new BatterySipper(type, null, baseUsage));
        baseUsage += 4.5f; // Tăng mức sử dụng cho mục tiếp theo
    }

    // Thêm một số mục ứng dụng giả lập
    dummyEntries.add(new BatterySipper(BatterySipper.DrainType.APP,
                                       new FakeUid(android.os.Process.FIRST_APPLICATION_UID + 101), baseUsage + 6.0f));
    dummyEntries.add(new BatterySipper(BatterySipper.DrainType.APP,
                                       new FakeUid(android.os.Process.FIRST_APPLICATION_UID + 202), baseUsage + 10.0f));

    // Giả lập các tiến trình hệ thống nền
    BatterySipper systemProcess1 = new BatterySipper(BatterySipper.DrainType.APP,
                                                    new FakeUid(android.os.UserHandle.getSharedAppGid(android.os.Process.SYSTEM_UID + 1)), 15.2f);
    systemProcess1.packageWithHighestDrain = "com.android.background.sync";
    dummyEntries.add(systemProcess1);

    BatterySipper systemProcess2 = new BatterySipper(BatterySipper.DrainType.APP,
                                                    new FakeUid(android.os.UserHandle.getSharedAppGid(android.os.Process.LOG_UID)), 13.8f);
    systemProcess2.packageWithHighestDrain = "com.google.android.gms";
    dummyEntries.add(systemProcess2);

    return dummyEntries;
}

Giá trị dischargeAmount biểu thị tổng lượng pin đã tiêu thụ kể từ lần sạc đầy gần nhất (hoặc lần rút cáp). Điều này được xác định bởi mStatsType, thường là BatteryStats.STATS_SINCE_CHARGED.

final int totalDischarge = USE_FAKE_DATA ? 5000 :
                           batteryStatsData != null ? batteryStatsData.getDischargeAmount(mStatsType) : 0;

Hệ thống sau đó lặp qua từng mục BatterySipper để lọc và tính toán mức tiêu thụ:

for (int i = 0; i < usageEntries.size(); i++) {
    final BatterySipper currentSipper = usageEntries.get(i);
    // Bỏ qua nếu mức tiêu thụ dưới ngưỡng tối thiểu
    if ((currentSipper.totalPowerMah * SECONDS_IN_HOUR) < MIN_POWER_THRESHOLD_MILLI_AMP) {
        continue;
    }

    // Tính tổng công suất tiêu thụ của thiết bị
    double totalDevicePower = USE_FAKE_DATA ? 4000 : mStatsHelper.getTotalPower();
    // Tính phần trăm của mục này trên tổng lượng pin đã tiêu thụ
    final double percentageOfTotalDrain = ((currentSipper.totalPowerMah / totalDevicePower) * totalDischarge);

    // Bỏ qua nếu phần trăm quá nhỏ
    if (((int) (percentageOfTotalDrain + 0.5)) < 1) {
        continue;
    }

    // Các quy tắc lọc bổ sung cho các loại tiêu hao đặc biệt
    if (currentSipper.drainType == BatterySipper.DrainType.OVERCOUNTED) {
        // Ví dụ: Bỏ qua nếu OVERCOUNTED quá nhỏ so với mức tiêu thụ thực cao nhất
        if (currentSipper.totalPowerMah < ((mStatsHelper.getMaxRealPower() * 2) / 3) || percentageOfTotalDrain < 10) {
            continue;
        }
    }
    if (currentSipper.drainType == BatterySipper.DrainType.UNACCOUNTED) {
        // Ví dụ: Bỏ qua nếu UNACCOUNTED quá nhỏ so với mức tiêu thụ thực cao nhất
        if (currentSipper.totalPowerMah < (mStatsHelper.getMaxRealPower() / 2) || percentageOfTotalDrain < 5) {
            continue;
        }
    }

    // ... (Thêm mục vào danh sách hiển thị) ...
}

Các ngưỡng như MIN_POWER_THRESHOLD_MILLI_AMP giúp loại bỏ các mục tiêu thụ pin quá nhỏ, không đáng kể. Tương tự, các mục OVERCOUNTED (được tính quá mức) và UNACCOUNTED (không rõ nguyên nhân) cũng có các quy tắc lọc riêng để đảm bảo chỉ hiển thị thông tin hữu ích cho người dùng.

Cập nhật Giao diện Người dùng (UI)

Sau khi lọc, mỗi mục tiêu thụ pin được chuyển đổi thành một đối tượng hiển thị trên UI. Quá trình này bao gồm việc lấy biểu tượng (icon) và nhãn (label) của ứng dụng, có tính đến các hồ sơ người dùng (ví dụ: hồ sơ công việc) thông qua UserManager:

final UserHandle targetUser = new UserHandle(UserHandle.getUserId(currentSipper.getUid()));
final BatteryEntry uiEntry = new BatteryEntry(getActivity(), mHandler, mUm, currentSipper);
final Drawable appIcon = mUm.getBadgedIconForUser(uiEntry.getIcon(), targetUser);
final CharSequence appLabel = mUm.getBadgedLabelForUser(uiEntry.getLabel(), targetUser);
final PowerGaugePreference powerPref = new PowerGaugePreference(getActivity(), appIcon, appLabel, uiEntry);

Mỗi PowerGaugePreference sau đó được cấu hình với tiêu đề, thứ tự, và phần trăm tiêu thụ pin. Hệ thống cũng tính toán phần trăm tiêu thụ của một ứng dụng so với mức tiêu thụ cao nhất của bất kỳ ứng dụng nào khác (percentOfMax) và phần trăm của tổng lượng pin đã tiêu thụ (percentOfTotalDrain).

final double maxDrainPercentage = (currentSipper.totalPowerMah * 100) / mStatsHelper.getMaxPower();
currentSipper.percent = percentageOfTotalDrain; // Cập nhật phần trăm đã tính

powerPref.setTitle(uiEntry.getLabel());
powerPref.setOrder(i + 1);
powerPref.setPercent(maxDrainPercentage, percentageOfTotalDrain);
// ... các thiết lập UI khác ...
mAppListGroup.addPreference(powerPref);

Một giới hạn quan trọng được áp dụng là số lượng mục hiển thị. Thường thì chỉ MAX_ITEMS_TO_LIST (mặc định là 10) ứng dụng tiêu thụ pin nhiều nhất sẽ được hiển thị. Nếu không có ứng dụng nào đáp ứng tiêu chí hiển thị, một thông báo "Không có dữ liệu" sẽ được hiển thị.

if (mAppListGroup.getPreferenceCount() > (MAX_ITEMS_TO_LIST + 1)) {
    break; // Dừng hiển thị nếu đã đạt giới hạn
}
// ...
if (!addedAnyItems) {
    displayNotAvailableMessage();
}

Quyền truy cập và Ký ứng dụng

Để truy cập các API liên quan đến thống kê pin như BatteryStats và PowerProfile, một ứng dụng thông thường không có đủ quyền. Việc cố gắng gọi các API này từ một ứng dụng không có đặc quyền sẽ dẫn đến SecurityException (ví dụ: java.lang.SecurityException: uid 10089 does not have android.permission.BATTERY_STATS).

Để có quyền truy cập, ứng dụng cần phải là một ứng dụng hệ thống, được cấu hình với android:sharedUserId="android.uid.system" trong tệp AndroidManifest.xml và được ký bằng khóa hệ thống của nền tảng Android. Đối với các bản dựng tùy chỉnh hoặc phát triển nội bộ, điều này thường yêu cầu thay thế tệp android.jar bằng phiên bản chứa các API nội bộ hoặc sử dụng các kỹ thuật biên dịch đặc biệt để cho phép truy cập các giao diện ẩn (@hide).

Thẻ: Android Battery Power Management PowerUsageSummary BatteryStats

Đăng vào ngày 5 tháng 10 lúc 01:39