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