Android 12 MtkSettings 存储空间显示错误问题

1,051 阅读3分钟

问题背景

入职新公司,最近被分配了个Bug,一个MTK的Android 12 项目中,测试发现在插入三星的32G sd卡后,且将其格式化内部存储空间时,发现sd卡显示的总内存对应为64GB? 如果只是作为便携式存储时,则显示正常。并且竞品的显示都是正常的。如下图所示,图1 为只作为便携式存储,图2为格式化为内部存储。

图1 4.png

图2 3.png

问题分析

一开始怀疑之所以显示为64G,是因为设备本身有32G,添加sd卡后,sd卡的总存储显示就成了设备原有存储+sd 卡的存储大小,认为应该也不算问题,只是机制不同。but,一番分析后,我错了。

在 Settings 中,如图2中显示内存界面最终在StorageDashboardFragment.java 文件中. StorageDashboardFragment.java 中调用了StorageUsageProgressBarPreferenceController::setSelectedStorageEntry方法

public class StorageUsageProgressBarPreferenceController extends BasePreferenceController {

    /** Set StorageEntry to display. */
    public void setSelectedStorageEntry(StorageEntry storageEntry) {
        mStorageEntry = storageEntry;
        getStorageStatsAndUpdateUi();
    }

    private void getStorageStatsAndUpdateUi() {
        ThreadUtils.postOnBackgroundThread(() -> {
            try {
                if (mStorageEntry == null || !mStorageEntry.isMounted()) {
                    throw new IOException();
                }
                //代码1
                if (mStorageEntry.isPrivate()) {
                    // StorageStatsManager can only query private storages.
                    mTotalBytes = mStorageStatsManager.getTotalBytes(mStorageEntry.getFsUuid());
                    mUsedBytes = mTotalBytes
                            - mStorageStatsManager.getFreeBytes(mStorageEntry.getFsUuid());
                } else {//代码2
                    final File rootFile = mStorageEntry.getPath();
                    if (rootFile == null) {
                        Log.d(TAG, "Mounted public storage has null root path: " + mStorageEntry);
                        throw new IOException();
                    }
                    mTotalBytes = rootFile.getTotalSpace();
                    mUsedBytes = mTotalBytes - rootFile.getFreeSpace();
                }
            } catch (IOException e) {
                // The storage device isn't present.
                mTotalBytes = 0;
                mUsedBytes = 0;
            }

            if (mUsageProgressBarPreference == null) {
                return;
            }
            mIsUpdateStateFromSelectedStorageEntry = true;
            ThreadUtils.postOnMainThread(() -> updateState(mUsageProgressBarPreference));
        });
    }

}
  • 代码1:只有当选择的页面是内部存储,或者是将sd卡作为内部存储后,才会走该代码,通过 mStorageStatsManager.getTotalBytes方法获取总空间和可用空间
  • 代码2: 只有sd卡作为便携存储设备时,走该代码逻辑,通过File对象的方法获取sd卡的总空间和可用空间

显然,导致存储空间显示为 64G的原因就是 mStorageStatsManager.getTotalBytes 这段代码.

接下来我们继续分析该代码调用逻辑。

android.app.usage.StorageStatsManager

class StorageStatsManager {
    private final IStorageStatsManager mService;
    public @BytesLong long getTotalBytes(@NonNull UUID storageUuid) throws IOException {
        try {
            return mService.getTotalBytes(convert(storageUuid), mContext.getOpPackageName());
        } catch (ParcelableException e) {
            e.maybeRethrow(IOException.class);
            throw new RuntimeException(e);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }
}

IStorageStatsManager 对应的service 类为com.android.server.usage.StorageStatsService

class StorageStatsService{
    private final StorageManager mStorage;
    public long getTotalBytes(String volumeUuid, String callingPackage) {
        // NOTE: No permissions required

        if (volumeUuid == StorageManager.UUID_PRIVATE_INTERNAL) {//代码1
            return FileUtils.roundStorageSize(mStorage.getPrimaryStorageSize());
        } else {//代码2
            final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid);
            if (vol == null) {
                throw new ParcelableException(
                        new IOException("Failed to find storage device for UUID " + volumeUuid));
            }
            return FileUtils.roundStorageSize(vol.disk.size);
        }
    }
}

重点就在这个FileUtils.roundStorageSize方法中了

StorageManager 使用UUID_PRIVATE_INTERNAL字段标注了设备本身的内存空间卷的uuid.sd卡作为内部空间将走到代码2 处。在加入日志后

class StorageStatsService{
    private final StorageManager mStorage;
    public long getTotalBytes(String volumeUuid, String callingPackage) {
        // NOTE: No permissions required

        if (volumeUuid == StorageManager.UUID_PRIVATE_INTERNAL) {//代码1
            return FileUtils.roundStorageSize(mStorage.getPrimaryStorageSize());
        } else {//代码2
            final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid);
            if (vol == null) {
                throw new ParcelableException(
                        new IOException("Failed to find storage device for UUID " + volumeUuid));
            }
            Slog.e(TAG," vol.disk.size:"+vol.disk.size);
            Slog.e(TAG," roundStorageSize:"+FileUtils.roundStorageSize(vol.disk.size));
            return FileUtils.roundStorageSize(vol.disk.size);
        }
    }
}


打印结果为

vol.disk.size:32010928128L
roundStorageSize:64000000000

由此发现问题出现在 FileUtils.roundStorageSize方法中

android.os.FileUtils

class FileUtils {
     /**
     * Round the given size of a storage device to a nice round power-of-two
     * value, such as 256MB or 32GB. This avoids showing weird values like
     * "29.5GB" in UI.
     *
     * @hide
     */
    public static long roundStorageSize(long size) {
        long val = 1;
        long pow = 1;
        while ((val * pow) < size) {
            val <<= 1;
            if (val > 512) {
                val = 1;
                pow *= 1000;
            }
        }
        return val * pow;
    }
}

该方法让数据的非0部分都是2的整数倍。但是这种方法就导致了当数据即使只是超过 1 byte 也会出现直接显示双倍的大小。并且,我搜索了下google 自己的Settings代码,好像也是这样的。

至于Google 为什么这么计算,我有个大概的猜想,在市面上一般磁盘的大小、sd卡的大小都是使用 10进制的。所以往往标注了 32GB的存储卡,可能实际大小都是不足 32G的,不仅没有 2^32 byte,甚至没有32E9 byte.因此大部分时侯显示的都是正确的,而这边这张 sd卡过于实在。。。。居然超过了该大小,因此导致了该问题的出现。

问题解决

问题的解决方法,其实就是 将StorageUsageProgressBarPreferenceController 类中的 mStorageStatsManager.getTotalBytes(mStorageEntry.getFsUuid());方法改为如下代码

public static long getTotalBytes(String volumeUuid) {
    storageManager = context.getApplicationContext().getSystemService(StorageManager.class);
    if (Objects.equals(volumeUuid,StorageManager.UUID_PRIVATE_INTERNAL)) {
        return FileUtils.roundStorageSize(storageManager.getPrimaryStorageSize());
    } else {
        final VolumeInfo vol = storageManager.findVolumeByUuid(volumeUuid);
        if (vol == null) {
            throw new ParcelableException(
                    new IOException("Failed to find storage device for UUID " + volumeUuid));
        }
        return vol.disk.size;
    }