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