1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.appsearch; 18 19 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.getPackageName; 20 21 import android.annotation.NonNull; 22 import android.app.appsearch.checker.initialization.qual.UnderInitialization; 23 import android.app.appsearch.checker.initialization.qual.UnknownInitialization; 24 import android.app.appsearch.checker.nullness.qual.RequiresNonNull; 25 import android.app.appsearch.exceptions.AppSearchException; 26 import android.app.appsearch.util.ExceptionUtil; 27 import android.util.ArrayMap; 28 import android.util.Log; 29 30 import com.android.internal.annotations.VisibleForTesting; 31 import com.android.server.appsearch.external.localstorage.AppSearchImpl; 32 33 import com.google.android.icing.proto.DocumentStorageInfoProto; 34 import com.google.android.icing.proto.NamespaceStorageInfoProto; 35 import com.google.android.icing.proto.StorageInfoProto; 36 37 import java.io.File; 38 import java.io.FileInputStream; 39 import java.io.FileOutputStream; 40 import java.io.IOException; 41 import java.io.InputStream; 42 import java.util.Collections; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Objects; 46 import java.util.concurrent.locks.ReadWriteLock; 47 import java.util.concurrent.locks.ReentrantReadWriteLock; 48 49 /** Saves the storage info read from file for a user. */ 50 public final class UserStorageInfo { 51 public static final String STORAGE_INFO_FILE = "appsearch_storage"; 52 private static final String TAG = "AppSearchUserStorage"; 53 private final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock(); 54 private final File mStorageInfoFile; 55 56 // Saves storage usage byte size for each package under the user, keyed by package name. 57 private Map<String, Long> mPackageStorageSizeMap; 58 // Saves storage usage byte size for all packages under the user. 59 private long mTotalStorageSizeBytes; 60 UserStorageInfo(@onNull File fileParentPath)61 public UserStorageInfo(@NonNull File fileParentPath) { 62 Objects.requireNonNull(fileParentPath); 63 mStorageInfoFile = new File(fileParentPath, STORAGE_INFO_FILE); 64 readStorageInfoFromFile(); 65 } 66 67 /** 68 * Updates storage info file with the latest storage info queried through {@link AppSearchImpl}. 69 */ updateStorageInfoFile(@onNull AppSearchImpl appSearchImpl)70 public void updateStorageInfoFile(@NonNull AppSearchImpl appSearchImpl) { 71 Objects.requireNonNull(appSearchImpl); 72 mReadWriteLock.writeLock().lock(); 73 try (FileOutputStream out = new FileOutputStream(mStorageInfoFile)) { 74 appSearchImpl.getRawStorageInfoProto().writeTo(out); 75 } catch (IOException | AppSearchException | RuntimeException e) { 76 Log.w(TAG, "Failed to dump storage info into file", e); 77 ExceptionUtil.handleException(e); 78 } finally { 79 mReadWriteLock.writeLock().unlock(); 80 } 81 } 82 83 /** 84 * Gets storage usage byte size for a package with a given package name. 85 * 86 * <p>Please note the storage info cached in file may be stale. 87 */ getSizeBytesForPackage(@onNull String packageName)88 public long getSizeBytesForPackage(@NonNull String packageName) { 89 Objects.requireNonNull(packageName); 90 return mPackageStorageSizeMap.getOrDefault(packageName, 0L); 91 } 92 93 /** 94 * Gets total storage usage byte size for all packages under the user. 95 * 96 * <p>Please note the storage info cached in file may be stale. 97 */ getTotalSizeBytes()98 public long getTotalSizeBytes() { 99 return mTotalStorageSizeBytes; 100 } 101 102 @RequiresNonNull("mStorageInfoFile") 103 @VisibleForTesting readStorageInfoFromFile(@nderInitialization UserStorageInfo this)104 void readStorageInfoFromFile(@UnderInitialization UserStorageInfo this) { 105 if (mStorageInfoFile.exists()) { 106 mReadWriteLock.readLock().lock(); 107 try (InputStream in = new FileInputStream(mStorageInfoFile)) { 108 StorageInfoProto storageInfo = StorageInfoProto.parseFrom(in); 109 mTotalStorageSizeBytes = storageInfo.getTotalStorageSize(); 110 mPackageStorageSizeMap = calculatePackageStorageInfoMap(storageInfo); 111 return; 112 } catch (IOException | RuntimeException e) { 113 Log.w(TAG, "Failed to read storage info from file", e); 114 ExceptionUtil.handleException(e); 115 } finally { 116 mReadWriteLock.readLock().unlock(); 117 } 118 } 119 mTotalStorageSizeBytes = 0; 120 mPackageStorageSizeMap = Collections.emptyMap(); 121 } 122 123 /** Calculates storage usage byte size for packages from a {@link StorageInfoProto}. */ 124 // TODO(b/198553756): Storage cache effort has created two copies of the storage 125 // calculation/interpolation logic. 126 @NonNull 127 @VisibleForTesting calculatePackageStorageInfoMap( @nknownInitialization UserStorageInfo this, @NonNull StorageInfoProto storageInfo)128 Map<String, Long> calculatePackageStorageInfoMap( 129 @UnknownInitialization UserStorageInfo this, @NonNull StorageInfoProto storageInfo) { 130 Map<String, Long> packageStorageInfoMap = new ArrayMap<>(); 131 if (storageInfo.hasDocumentStorageInfo()) { 132 DocumentStorageInfoProto documentStorageInfo = storageInfo.getDocumentStorageInfo(); 133 List<NamespaceStorageInfoProto> namespaceStorageInfoList = 134 documentStorageInfo.getNamespaceStorageInfoList(); 135 136 Map<String, Integer> packageDocumentCountMap = new ArrayMap<>(); 137 long totalDocuments = 0; 138 for (int i = 0; i < namespaceStorageInfoList.size(); i++) { 139 NamespaceStorageInfoProto namespaceStorageInfo = namespaceStorageInfoList.get(i); 140 String packageName = getPackageName(namespaceStorageInfo.getNamespace()); 141 int namespaceDocuments = 142 namespaceStorageInfo.getNumAliveDocuments() 143 + namespaceStorageInfo.getNumExpiredDocuments(); 144 totalDocuments += namespaceDocuments; 145 packageDocumentCountMap.put( 146 packageName, 147 packageDocumentCountMap.getOrDefault(packageName, 0) + namespaceDocuments); 148 } 149 150 long totalStorageSize = storageInfo.getTotalStorageSize(); 151 for (Map.Entry<String, Integer> entry : packageDocumentCountMap.entrySet()) { 152 // Since we don't have the exact size of all the documents, we do an estimation. 153 // Note that while the total storage takes into account schema, index, etc. in 154 // addition to documents, we'll only calculate the percentage based on number of 155 // documents under packages. 156 packageStorageInfoMap.put( 157 entry.getKey(), 158 (long) (entry.getValue() * 1.0 / totalDocuments * totalStorageSize)); 159 } 160 } 161 return Collections.unmodifiableMap(packageStorageInfoMap); 162 } 163 } 164