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.providers.media.metrics;
18 
19 import static com.android.providers.media.MediaProviderStatsLog.GENERAL_EXTERNAL_STORAGE_ACCESS_STATS;
20 
21 import static java.util.stream.Collectors.toList;
22 
23 import android.os.Process;
24 import android.os.SystemClock;
25 import android.provider.MediaStore;
26 import android.util.ArraySet;
27 import android.util.Log;
28 import android.util.SparseArray;
29 import android.util.StatsEvent;
30 import android.util.proto.ProtoOutputStream;
31 
32 import androidx.annotation.GuardedBy;
33 import androidx.annotation.NonNull;
34 import androidx.annotation.Nullable;
35 
36 import com.android.providers.media.MediaProviderStatsLog;
37 import com.android.providers.media.util.FileUtils;
38 import com.android.providers.media.util.MimeUtils;
39 
40 import com.google.common.annotations.VisibleForTesting;
41 
42 import java.io.File;
43 import java.util.ArrayList;
44 import java.util.Collections;
45 import java.util.List;
46 import java.util.concurrent.TimeUnit;
47 
48 /**
49  * Metrics for {@link MediaProviderStatsLog#GENERAL_EXTERNAL_STORAGE_ACCESS_STATS}. This class
50  * gathers stats separately for each UID that accesses external storage.
51  */
52 class StorageAccessMetrics {
53 
54     private static final String TAG = "StorageAccessMetrics";
55 
56     @VisibleForTesting
57     static final int UID_SAMPLES_COUNT_LIMIT = 50;
58 
59     private final int mMyUid = Process.myUid();
60 
61     @GuardedBy("mLock")
62     private final SparseArray<PackageStorageAccessStats> mAccessStatsPerPackage =
63             new SparseArray<>();
64     @GuardedBy("mLock")
65     private long mStartTimeMillis = SystemClock.uptimeMillis();
66 
67     private final Object mLock = new Object();
68 
69 
70     /**
71      * Logs the mime type that was accessed by the given {@code uid}.
72      */
logMimeType(int uid, @NonNull String mimeType)73     void logMimeType(int uid, @NonNull String mimeType) {
74         if (mimeType == null) {
75             Log.w(TAG, "Attempted to log null mime type access");
76             return;
77         }
78 
79         synchronized (mLock) {
80             getOrGeneratePackageStatsObjectLocked(uid).mMimeTypes.add(mimeType);
81         }
82     }
83 
84     /**
85      * Logs the storage access and attributes it to the given {@code uid}.
86      *
87      * <p>Should only be called from a FUSE thread.
88      */
logAccessViaFuse(int uid, @NonNull String file)89     void logAccessViaFuse(int uid, @NonNull String file) {
90         // We don't log the access if it's MediaProvider accessing.
91         if (mMyUid == uid) {
92             return;
93         }
94 
95         incrementFilePathAccesses(uid);
96         final String volumeName = MediaStore.getVolumeName(
97                 FileUtils.getContentUriForPath(file));
98         logGeneralExternalStorageAccess(uid, volumeName);
99         logMimeTypeFromFile(uid, file);
100     }
101 
102 
103     /**
104      * Logs the storage access and attributes it to the given {@code uid}.
105      */
logAccessViaMediaProvider(int uid, @NonNull String volumeName)106     void logAccessViaMediaProvider(int uid, @NonNull String volumeName) {
107         // We also don't log the access if it's MediaProvider accessing.
108         if (mMyUid == uid) {
109             return;
110         }
111 
112         logGeneralExternalStorageAccess(uid, volumeName);
113     }
114 
115     /**
116      * Use this to log whenever a package accesses external storage via ContentResolver or FUSE.
117      * The given volume name helps us determine whether this was an access on primary or secondary
118      * storage.
119      */
logGeneralExternalStorageAccess(int uid, @NonNull String volumeName)120     private void logGeneralExternalStorageAccess(int uid, @NonNull String volumeName) {
121         switch (volumeName) {
122             case MediaStore.VOLUME_EXTERNAL:
123             case MediaStore.VOLUME_EXTERNAL_PRIMARY:
124                 incrementTotalAccesses(uid);
125                 break;
126             case MediaStore.VOLUME_INTERNAL:
127             case MediaStore.VOLUME_DEMO:
128             case MediaStore.MEDIA_SCANNER_VOLUME:
129                 break;
130             default:
131                 // Secondary external storage
132                 incrementTotalAccesses(uid);
133                 incrementSecondaryStorageAccesses(uid);
134         }
135     }
136 
137     /**
138      * Logs that the mime type of the given {@param file} was accessed by the given {@param uid}.
139      */
logMimeTypeFromFile(int uid, @Nullable String file)140     private void logMimeTypeFromFile(int uid, @Nullable String file) {
141         logMimeType(uid, MimeUtils.resolveMimeType(new File(file)));
142     }
143 
incrementTotalAccesses(int uid)144     private void incrementTotalAccesses(int uid) {
145         synchronized (mLock) {
146             getOrGeneratePackageStatsObjectLocked(uid).mTotalAccesses += 1;
147         }
148     }
149 
incrementFilePathAccesses(int uid)150     private void incrementFilePathAccesses(int uid) {
151         synchronized (mLock) {
152             getOrGeneratePackageStatsObjectLocked(uid).mFilePathAccesses += 1;
153         }
154     }
155 
incrementSecondaryStorageAccesses(int uid)156     private void incrementSecondaryStorageAccesses(int uid) {
157         synchronized (mLock) {
158             getOrGeneratePackageStatsObjectLocked(uid).mSecondaryStorageAccesses += 1;
159         }
160     }
161 
162     @GuardedBy("mLock")
getOrGeneratePackageStatsObjectLocked(int uid)163     private PackageStorageAccessStats getOrGeneratePackageStatsObjectLocked(int uid) {
164         PackageStorageAccessStats stats = mAccessStatsPerPackage.get(uid);
165         if (stats == null) {
166             stats = new PackageStorageAccessStats(uid);
167             mAccessStatsPerPackage.put(uid, stats);
168         }
169         return stats;
170     }
171 
172     /**
173      * Returns the list of {@link StatsEvent} since latest reset, for a random subset of tracked
174      * uids if there are more than {@link #UID_SAMPLES_COUNT_LIMIT} in total. Returns {@code null}
175      * when the time since reset is non-positive.
176      */
177     @Nullable
pullStatsEvents()178     List<StatsEvent> pullStatsEvents() {
179         synchronized (mLock) {
180             final long timeInterval = SystemClock.uptimeMillis() - mStartTimeMillis;
181             List<PackageStorageAccessStats> stats = getSampleStats();
182             resetStats();
183             return stats
184                     .stream()
185                     .map(s -> s.toNormalizedStats(timeInterval).toStatsEvent())
186                     .collect(toList());
187         }
188     }
189 
190     @VisibleForTesting
getSampleStats()191     List<PackageStorageAccessStats> getSampleStats() {
192         synchronized (mLock) {
193             List<PackageStorageAccessStats> result = new ArrayList<>();
194 
195             List<Integer> sampledUids = new ArrayList<>();
196             for (int i = 0; i < mAccessStatsPerPackage.size(); i++) {
197                 sampledUids.add(mAccessStatsPerPackage.keyAt(i));
198             }
199 
200             if (sampledUids.size() > UID_SAMPLES_COUNT_LIMIT) {
201                 Collections.shuffle(sampledUids);
202                 sampledUids = sampledUids.subList(0, UID_SAMPLES_COUNT_LIMIT);
203             }
204             for (Integer uid : sampledUids) {
205                 PackageStorageAccessStats stats = mAccessStatsPerPackage.get(uid);
206                 result.add(stats);
207             }
208 
209             return result;
210         }
211     }
212 
resetStats()213     private void resetStats() {
214         synchronized (mLock) {
215             mAccessStatsPerPackage.clear();
216             mStartTimeMillis = SystemClock.uptimeMillis();
217         }
218     }
219 
220     @VisibleForTesting
221     static class PackageStorageAccessStats {
222         private final int mUid;
223         int mTotalAccesses = 0;
224         int mFilePathAccesses = 0;
225         int mSecondaryStorageAccesses = 0;
226 
227         final ArraySet<String> mMimeTypes = new ArraySet<>();
228 
PackageStorageAccessStats(int uid)229         PackageStorageAccessStats(int uid) {
230             this.mUid = uid;
231         }
232 
toNormalizedStats(long timeInterval)233         PackageStorageAccessStats toNormalizedStats(long timeInterval) {
234             this.mTotalAccesses = normalizeAccessesPerDay(mTotalAccesses, timeInterval);
235             this.mFilePathAccesses = normalizeAccessesPerDay(mFilePathAccesses, timeInterval);
236             this.mSecondaryStorageAccesses =
237                     normalizeAccessesPerDay(mSecondaryStorageAccesses, timeInterval);
238             return this;
239         }
240 
toStatsEvent()241         StatsEvent toStatsEvent() {
242             return StatsEvent.newBuilder()
243                     .setAtomId(GENERAL_EXTERNAL_STORAGE_ACCESS_STATS)
244                     .writeInt(mUid)
245                     .writeInt(mTotalAccesses)
246                     .writeInt(mFilePathAccesses)
247                     .writeInt(mSecondaryStorageAccesses)
248                     .writeByteArray(getMimeTypesAsProto().getBytes())
249                     .build();
250         }
251 
getMimeTypesAsProto()252         private ProtoOutputStream getMimeTypesAsProto() {
253             ProtoOutputStream proto = new ProtoOutputStream();
254             for (int i = 0; i < mMimeTypes.size(); i++) {
255                 String mime = mMimeTypes.valueAt(i);
256                 proto.write(/*fieldId*/ProtoOutputStream.FIELD_TYPE_STRING
257                                 | ProtoOutputStream.FIELD_COUNT_REPEATED
258                                 | 1,
259                         mime);
260             }
261             return proto;
262         }
263 
normalizeAccessesPerDay(int value, long interval)264         private static int normalizeAccessesPerDay(int value, long interval) {
265             if (interval <= 0) {
266                 return -1;
267             }
268 
269             double multiplier = Double.valueOf(TimeUnit.DAYS.toMillis(1)) / interval;
270             double normalizedValue = value * multiplier;
271             return Double.valueOf(normalizedValue).intValue();
272         }
273 
274         @VisibleForTesting
getUid()275         int getUid() {
276             return mUid;
277         }
278     }
279 }
280