/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.appsearch; import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.getPackageName; import android.annotation.NonNull; import android.app.appsearch.checker.initialization.qual.UnderInitialization; import android.app.appsearch.checker.initialization.qual.UnknownInitialization; import android.app.appsearch.checker.nullness.qual.RequiresNonNull; import android.app.appsearch.exceptions.AppSearchException; import android.app.appsearch.util.ExceptionUtil; import android.util.ArrayMap; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.server.appsearch.external.localstorage.AppSearchImpl; import com.google.android.icing.proto.DocumentStorageInfoProto; import com.google.android.icing.proto.NamespaceStorageInfoProto; import com.google.android.icing.proto.StorageInfoProto; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** Saves the storage info read from file for a user. */ public final class UserStorageInfo { public static final String STORAGE_INFO_FILE = "appsearch_storage"; private static final String TAG = "AppSearchUserStorage"; private final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock(); private final File mStorageInfoFile; // Saves storage usage byte size for each package under the user, keyed by package name. private Map mPackageStorageSizeMap; // Saves storage usage byte size for all packages under the user. private long mTotalStorageSizeBytes; public UserStorageInfo(@NonNull File fileParentPath) { Objects.requireNonNull(fileParentPath); mStorageInfoFile = new File(fileParentPath, STORAGE_INFO_FILE); readStorageInfoFromFile(); } /** * Updates storage info file with the latest storage info queried through {@link AppSearchImpl}. */ public void updateStorageInfoFile(@NonNull AppSearchImpl appSearchImpl) { Objects.requireNonNull(appSearchImpl); mReadWriteLock.writeLock().lock(); try (FileOutputStream out = new FileOutputStream(mStorageInfoFile)) { appSearchImpl.getRawStorageInfoProto().writeTo(out); } catch (IOException | AppSearchException | RuntimeException e) { Log.w(TAG, "Failed to dump storage info into file", e); ExceptionUtil.handleException(e); } finally { mReadWriteLock.writeLock().unlock(); } } /** * Gets storage usage byte size for a package with a given package name. * *

Please note the storage info cached in file may be stale. */ public long getSizeBytesForPackage(@NonNull String packageName) { Objects.requireNonNull(packageName); return mPackageStorageSizeMap.getOrDefault(packageName, 0L); } /** * Gets total storage usage byte size for all packages under the user. * *

Please note the storage info cached in file may be stale. */ public long getTotalSizeBytes() { return mTotalStorageSizeBytes; } @RequiresNonNull("mStorageInfoFile") @VisibleForTesting void readStorageInfoFromFile(@UnderInitialization UserStorageInfo this) { if (mStorageInfoFile.exists()) { mReadWriteLock.readLock().lock(); try (InputStream in = new FileInputStream(mStorageInfoFile)) { StorageInfoProto storageInfo = StorageInfoProto.parseFrom(in); mTotalStorageSizeBytes = storageInfo.getTotalStorageSize(); mPackageStorageSizeMap = calculatePackageStorageInfoMap(storageInfo); return; } catch (IOException | RuntimeException e) { Log.w(TAG, "Failed to read storage info from file", e); ExceptionUtil.handleException(e); } finally { mReadWriteLock.readLock().unlock(); } } mTotalStorageSizeBytes = 0; mPackageStorageSizeMap = Collections.emptyMap(); } /** Calculates storage usage byte size for packages from a {@link StorageInfoProto}. */ // TODO(b/198553756): Storage cache effort has created two copies of the storage // calculation/interpolation logic. @NonNull @VisibleForTesting Map calculatePackageStorageInfoMap( @UnknownInitialization UserStorageInfo this, @NonNull StorageInfoProto storageInfo) { Map packageStorageInfoMap = new ArrayMap<>(); if (storageInfo.hasDocumentStorageInfo()) { DocumentStorageInfoProto documentStorageInfo = storageInfo.getDocumentStorageInfo(); List namespaceStorageInfoList = documentStorageInfo.getNamespaceStorageInfoList(); Map packageDocumentCountMap = new ArrayMap<>(); long totalDocuments = 0; for (int i = 0; i < namespaceStorageInfoList.size(); i++) { NamespaceStorageInfoProto namespaceStorageInfo = namespaceStorageInfoList.get(i); String packageName = getPackageName(namespaceStorageInfo.getNamespace()); int namespaceDocuments = namespaceStorageInfo.getNumAliveDocuments() + namespaceStorageInfo.getNumExpiredDocuments(); totalDocuments += namespaceDocuments; packageDocumentCountMap.put( packageName, packageDocumentCountMap.getOrDefault(packageName, 0) + namespaceDocuments); } long totalStorageSize = storageInfo.getTotalStorageSize(); for (Map.Entry entry : packageDocumentCountMap.entrySet()) { // Since we don't have the exact size of all the documents, we do an estimation. // Note that while the total storage takes into account schema, index, etc. in // addition to documents, we'll only calculate the percentage based on number of // documents under packages. packageStorageInfoMap.put( entry.getKey(), (long) (entry.getValue() * 1.0 / totalDocuments * totalStorageSize)); } } return Collections.unmodifiableMap(packageStorageInfoMap); } }