blob: ce54e3f3d1322e219b828bdb565882d4d42742d1 [file] [log] [blame]
/*
* 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.providers.media.metrics;
import static com.android.providers.media.MediaProviderStatsLog.GENERAL_EXTERNAL_STORAGE_ACCESS_STATS;
import static java.util.stream.Collectors.toList;
import android.os.Process;
import android.os.SystemClock;
import android.provider.MediaStore;
import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
import android.util.StatsEvent;
import android.util.proto.ProtoOutputStream;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.providers.media.MediaProviderStatsLog;
import com.android.providers.media.util.FileUtils;
import com.android.providers.media.util.MimeUtils;
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Metrics for {@link MediaProviderStatsLog#GENERAL_EXTERNAL_STORAGE_ACCESS_STATS}. This class
* gathers stats separately for each UID that accesses external storage.
*/
class StorageAccessMetrics {
private static final String TAG = "StorageAccessMetrics";
@VisibleForTesting
static final int UID_SAMPLES_COUNT_LIMIT = 50;
private final int mMyUid = Process.myUid();
@GuardedBy("mLock")
private final SparseArray<PackageStorageAccessStats> mAccessStatsPerPackage =
new SparseArray<>();
@GuardedBy("mLock")
private long mStartTimeMillis = SystemClock.uptimeMillis();
private final Object mLock = new Object();
/**
* Logs the mime type that was accessed by the given {@code uid}.
*/
void logMimeType(int uid, @NonNull String mimeType) {
if (mimeType == null) {
Log.w(TAG, "Attempted to log null mime type access");
return;
}
synchronized (mLock) {
getOrGeneratePackageStatsObjectLocked(uid).mMimeTypes.add(mimeType);
}
}
/**
* Logs the storage access and attributes it to the given {@code uid}.
*
* <p>Should only be called from a FUSE thread.
*/
void logAccessViaFuse(int uid, @NonNull String file) {
// We don't log the access if it's MediaProvider accessing.
if (mMyUid == uid) {
return;
}
incrementFilePathAccesses(uid);
final String volumeName = MediaStore.getVolumeName(
FileUtils.getContentUriForPath(file));
logGeneralExternalStorageAccess(uid, volumeName);
logMimeTypeFromFile(uid, file);
}
/**
* Logs the storage access and attributes it to the given {@code uid}.
*/
void logAccessViaMediaProvider(int uid, @NonNull String volumeName) {
// We also don't log the access if it's MediaProvider accessing.
if (mMyUid == uid) {
return;
}
logGeneralExternalStorageAccess(uid, volumeName);
}
/**
* Use this to log whenever a package accesses external storage via ContentResolver or FUSE.
* The given volume name helps us determine whether this was an access on primary or secondary
* storage.
*/
private void logGeneralExternalStorageAccess(int uid, @NonNull String volumeName) {
switch (volumeName) {
case MediaStore.VOLUME_EXTERNAL:
case MediaStore.VOLUME_EXTERNAL_PRIMARY:
incrementTotalAccesses(uid);
break;
case MediaStore.VOLUME_INTERNAL:
case MediaStore.VOLUME_DEMO:
case MediaStore.MEDIA_SCANNER_VOLUME:
break;
default:
// Secondary external storage
incrementTotalAccesses(uid);
incrementSecondaryStorageAccesses(uid);
}
}
/**
* Logs that the mime type of the given {@param file} was accessed by the given {@param uid}.
*/
private void logMimeTypeFromFile(int uid, @Nullable String file) {
logMimeType(uid, MimeUtils.resolveMimeType(new File(file)));
}
private void incrementTotalAccesses(int uid) {
synchronized (mLock) {
getOrGeneratePackageStatsObjectLocked(uid).mTotalAccesses += 1;
}
}
private void incrementFilePathAccesses(int uid) {
synchronized (mLock) {
getOrGeneratePackageStatsObjectLocked(uid).mFilePathAccesses += 1;
}
}
private void incrementSecondaryStorageAccesses(int uid) {
synchronized (mLock) {
getOrGeneratePackageStatsObjectLocked(uid).mSecondaryStorageAccesses += 1;
}
}
@GuardedBy("mLock")
private PackageStorageAccessStats getOrGeneratePackageStatsObjectLocked(int uid) {
PackageStorageAccessStats stats = mAccessStatsPerPackage.get(uid);
if (stats == null) {
stats = new PackageStorageAccessStats(uid);
mAccessStatsPerPackage.put(uid, stats);
}
return stats;
}
/**
* Returns the list of {@link StatsEvent} since latest reset, for a random subset of tracked
* uids if there are more than {@link #UID_SAMPLES_COUNT_LIMIT} in total. Returns {@code null}
* when the time since reset is non-positive.
*/
@Nullable
List<StatsEvent> pullStatsEvents() {
synchronized (mLock) {
final long timeInterval = SystemClock.uptimeMillis() - mStartTimeMillis;
List<PackageStorageAccessStats> stats = getSampleStats();
resetStats();
return stats
.stream()
.map(s -> s.toNormalizedStats(timeInterval).toStatsEvent())
.collect(toList());
}
}
@VisibleForTesting
List<PackageStorageAccessStats> getSampleStats() {
synchronized (mLock) {
List<PackageStorageAccessStats> result = new ArrayList<>();
List<Integer> sampledUids = new ArrayList<>();
for (int i = 0; i < mAccessStatsPerPackage.size(); i++) {
sampledUids.add(mAccessStatsPerPackage.keyAt(i));
}
if (sampledUids.size() > UID_SAMPLES_COUNT_LIMIT) {
Collections.shuffle(sampledUids);
sampledUids = sampledUids.subList(0, UID_SAMPLES_COUNT_LIMIT);
}
for (Integer uid : sampledUids) {
PackageStorageAccessStats stats = mAccessStatsPerPackage.get(uid);
result.add(stats);
}
return result;
}
}
private void resetStats() {
synchronized (mLock) {
mAccessStatsPerPackage.clear();
mStartTimeMillis = SystemClock.uptimeMillis();
}
}
@VisibleForTesting
static class PackageStorageAccessStats {
private final int mUid;
int mTotalAccesses = 0;
int mFilePathAccesses = 0;
int mSecondaryStorageAccesses = 0;
final ArraySet<String> mMimeTypes = new ArraySet<>();
PackageStorageAccessStats(int uid) {
this.mUid = uid;
}
PackageStorageAccessStats toNormalizedStats(long timeInterval) {
this.mTotalAccesses = normalizeAccessesPerDay(mTotalAccesses, timeInterval);
this.mFilePathAccesses = normalizeAccessesPerDay(mFilePathAccesses, timeInterval);
this.mSecondaryStorageAccesses =
normalizeAccessesPerDay(mSecondaryStorageAccesses, timeInterval);
return this;
}
StatsEvent toStatsEvent() {
return StatsEvent.newBuilder()
.setAtomId(GENERAL_EXTERNAL_STORAGE_ACCESS_STATS)
.writeInt(mUid)
.writeInt(mTotalAccesses)
.writeInt(mFilePathAccesses)
.writeInt(mSecondaryStorageAccesses)
.writeByteArray(getMimeTypesAsProto().getBytes())
.build();
}
private ProtoOutputStream getMimeTypesAsProto() {
ProtoOutputStream proto = new ProtoOutputStream();
for (int i = 0; i < mMimeTypes.size(); i++) {
String mime = mMimeTypes.valueAt(i);
proto.write(/*fieldId*/ProtoOutputStream.FIELD_TYPE_STRING
| ProtoOutputStream.FIELD_COUNT_REPEATED
| 1,
mime);
}
return proto;
}
private static int normalizeAccessesPerDay(int value, long interval) {
if (interval <= 0) {
return -1;
}
double multiplier = Double.valueOf(TimeUnit.DAYS.toMillis(1)) / interval;
double normalizedValue = value * multiplier;
return Double.valueOf(normalizedValue).intValue();
}
@VisibleForTesting
int getUid() {
return mUid;
}
}
}