/* * Copyright (C) 2016 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.documentsui; import static android.os.Environment.STANDARD_DIRECTORIES; import static com.android.documentsui.DocumentsApplication.acquireUnstableProviderOrThrow; import static com.android.documentsui.base.Shared.DEBUG; import android.annotation.IntDef; import android.annotation.Nullable; import android.annotation.StringDef; import android.app.Activity; import android.content.ContentProviderClient; import android.content.Context; import android.content.Intent; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.RemoteException; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Path; import android.provider.DocumentsProvider; import android.util.Log; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.Providers; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.State; import com.android.documentsui.base.State.ActionType; import com.android.documentsui.files.LauncherActivity; import com.android.documentsui.roots.ProvidersAccess; import com.android.documentsui.services.FileOperationService; import com.android.documentsui.services.FileOperationService.OpType; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; /** @hide */ public final class Metrics { private static final String TAG = "Metrics"; // These strings have to be whitelisted in tron. Do not change them. private static final String COUNT_LAUNCH_ACTION = "docsui_launch_action"; private static final String COUNT_ROOT_VISITED_IN_MANAGER = "docsui_root_visited_in_manager"; private static final String COUNT_ROOT_VISITED_IN_PICKER = "docsui_root_visited_in_picker"; private static final String COUNT_OPEN_MIME = "docsui_open_mime"; private static final String COUNT_CREATE_MIME = "docsui_create_mime"; private static final String COUNT_GET_CONTENT_MIME = "docsui_get_content_mime"; private static final String COUNT_BROWSE_ROOT = "docsui_browse_root"; @Deprecated private static final String COUNT_MANAGE_ROOT = "docsui_manage_root"; @Deprecated private static final String COUNT_MULTI_WINDOW = "docsui_multi_window"; private static final String COUNT_FILEOP_SYSTEM = "docsui_fileop_system"; private static final String COUNT_FILEOP_EXTERNAL = "docsui_fileop_external"; private static final String COUNT_FILEOP_CANCELED = "docsui_fileop_canceled"; private static final String COUNT_STARTUP_MS = "docsui_startup_ms"; @Deprecated private static final String COUNT_DRAWER_OPENED = "docsui_drawer_opened"; private static final String COUNT_USER_ACTION = "docsui_menu_action"; private static final String COUNT_BROWSE_AT_LOCATION = "docsui_browse_at_location"; private static final String COUNT_CREATE_AT_LOCATION = "docsui_create_at_location"; private static final String COUNT_OPEN_AT_LOCATION = "docsui_open_at_location"; private static final String COUNT_GET_CONTENT_AT_LOCATION = "docsui_get_content_at_location"; private static final String COUNT_MEDIA_FILEOP_FAILURE = "docsui_media_fileop_failure"; private static final String COUNT_DOWNLOADS_FILEOP_FAILURE = "docsui_downloads_fileop_failure"; private static final String COUNT_INTERNAL_STORAGE_FILEOP_FAILURE = "docsui_internal_storage_fileop_failure"; private static final String COUNT_EXTERNAL_STORAGE_FILEOP_FAILURE = "docsui_external_storage_fileop_failure"; private static final String COUNT_MTP_FILEOP_FAILURE = "docsui_mtp_fileop_failure"; private static final String COUNT_OTHER_FILEOP_FAILURE = "docsui_other_fileop_failure"; private static final String COUNT_FILE_COPIED = "docsui_file_copied"; private static final String COUNT_FILE_MOVED = "docsui_file_moved"; // Indices for bucketing roots in the roots histogram. "Other" is the catch-all index for any // root that is not explicitly recognized by the Metrics code (see {@link // #getSanitizedRootIndex}). Apps are also bucketed in this histogram. // Do not change or rearrange these values, that will break historical data. Only add to the end // of the list. // Do not use negative numbers or zero; clearcut only handles positive integers. private static final int ROOT_NONE = 1; private static final int ROOT_OTHER = 2; private static final int ROOT_AUDIO = 3; private static final int ROOT_DEVICE_STORAGE = 4; private static final int ROOT_DOWNLOADS = 5; private static final int ROOT_HOME = 6; private static final int ROOT_IMAGES = 7; private static final int ROOT_RECENTS = 8; private static final int ROOT_VIDEOS = 9; private static final int ROOT_MTP = 10; // Apps aren't really "roots", but they are treated as such in the roots fragment UI and so they // are logged analogously to roots. private static final int ROOT_THIRD_PARTY_APP = 100; @IntDef(flag = true, value = { ROOT_NONE, ROOT_OTHER, ROOT_AUDIO, ROOT_DEVICE_STORAGE, ROOT_DOWNLOADS, ROOT_HOME, ROOT_IMAGES, ROOT_RECENTS, ROOT_VIDEOS, ROOT_MTP, ROOT_THIRD_PARTY_APP }) @Retention(RetentionPolicy.SOURCE) public @interface Root {} // Indices for bucketing mime types. // Do not change or rearrange these values, that will break historical data. Only add to the end // of the list. // Do not use negative numbers or zero; clearcut only handles positive integers. private static final int MIME_NONE = 1; // null mime private static final int MIME_ANY = 2; // */* private static final int MIME_APPLICATION = 3; // application/* private static final int MIME_AUDIO = 4; // audio/* private static final int MIME_IMAGE = 5; // image/* private static final int MIME_MESSAGE = 6; // message/* private static final int MIME_MULTIPART = 7; // multipart/* private static final int MIME_TEXT = 8; // text/* private static final int MIME_VIDEO = 9; // video/* private static final int MIME_OTHER = 10; // anything not enumerated below @IntDef(flag = true, value = { MIME_NONE, MIME_ANY, MIME_APPLICATION, MIME_AUDIO, MIME_IMAGE, MIME_MESSAGE, MIME_MULTIPART, MIME_TEXT, MIME_VIDEO, MIME_OTHER }) @Retention(RetentionPolicy.SOURCE) public @interface Mime {} public static final int FILES_SCOPE = 1; public static final int PICKER_SCOPE = 2; @IntDef({ FILES_SCOPE, PICKER_SCOPE }) @Retention(RetentionPolicy.SOURCE) public @interface ContextScope {} // Codes representing different kinds of file operations. These are used for bucketing // operations in the COUNT_FILEOP_{SYSTEM|EXTERNAL} histograms. // Do not change or rearrange these values, that will break historical data. Only add to the // list. // Do not use negative numbers or zero; clearcut only handles positive integers. private static final int FILEOP_OTHER = 1; // any file operation not listed below private static final int FILEOP_COPY_INTRA_PROVIDER = 2; // Copy within a provider private static final int FILEOP_COPY_SYSTEM_PROVIDER = 3; // Copy to a system provider. private static final int FILEOP_COPY_EXTERNAL_PROVIDER = 4; // Copy to a 3rd-party provider. private static final int FILEOP_MOVE_INTRA_PROVIDER = 5; // Move within a provider. private static final int FILEOP_MOVE_SYSTEM_PROVIDER = 6; // Move to a system provider. private static final int FILEOP_MOVE_EXTERNAL_PROVIDER = 7; // Move to a 3rd-party provider. private static final int FILEOP_DELETE = 8; private static final int FILEOP_RENAME = 9; private static final int FILEOP_CREATE_DIR = 10; private static final int FILEOP_OTHER_ERROR = 100; private static final int FILEOP_DELETE_ERROR = 101; private static final int FILEOP_MOVE_ERROR = 102; private static final int FILEOP_COPY_ERROR = 103; private static final int FILEOP_RENAME_ERROR = 104; private static final int FILEOP_CREATE_DIR_ERROR = 105; private static final int FILEOP_COMPRESS_INTRA_PROVIDER = 106; // Compres within a provider private static final int FILEOP_COMPRESS_SYSTEM_PROVIDER = 107; // Compress to a system provider. private static final int FILEOP_COMPRESS_EXTERNAL_PROVIDER = 108; // Compress to a 3rd-party provider. private static final int FILEOP_EXTRACT_INTRA_PROVIDER = 109; // Extract within a provider private static final int FILEOP_EXTRACT_SYSTEM_PROVIDER = 110; // Extract to a system provider. private static final int FILEOP_EXTRACT_EXTERNAL_PROVIDER = 111; // Extract to a 3rd-party provider. private static final int FILEOP_COMPRESS_ERROR = 112; private static final int FILEOP_EXTRACT_ERROR = 113; @IntDef(flag = true, value = { FILEOP_OTHER, FILEOP_COPY_INTRA_PROVIDER, FILEOP_COPY_SYSTEM_PROVIDER, FILEOP_COPY_EXTERNAL_PROVIDER, FILEOP_MOVE_INTRA_PROVIDER, FILEOP_MOVE_SYSTEM_PROVIDER, FILEOP_MOVE_EXTERNAL_PROVIDER, FILEOP_DELETE, FILEOP_RENAME, FILEOP_CREATE_DIR, FILEOP_OTHER_ERROR, FILEOP_DELETE_ERROR, FILEOP_MOVE_ERROR, FILEOP_COPY_ERROR, FILEOP_RENAME_ERROR, FILEOP_CREATE_DIR_ERROR, FILEOP_COMPRESS_INTRA_PROVIDER, FILEOP_COMPRESS_SYSTEM_PROVIDER, FILEOP_COMPRESS_EXTERNAL_PROVIDER, FILEOP_EXTRACT_INTRA_PROVIDER, FILEOP_EXTRACT_SYSTEM_PROVIDER, FILEOP_EXTRACT_EXTERNAL_PROVIDER, FILEOP_COMPRESS_ERROR, FILEOP_EXTRACT_ERROR }) @Retention(RetentionPolicy.SOURCE) public @interface FileOp {} // Codes representing different kinds of file operations. These are used for bucketing // operations in the COUNT_FILEOP_CANCELED histogram. // Do not change or rearrange these values, that will break historical data. Only add to the // list. // Do not use negative numbers or zero; clearcut only handles positive integers. private static final int OPERATION_UNKNOWN = 1; private static final int OPERATION_COPY = 2; private static final int OPERATION_MOVE = 3; private static final int OPERATION_DELETE = 4; private static final int OPERATION_COMPRESS = 5; private static final int OPERATION_EXTRACT = 6; @IntDef(flag = true, value = { OPERATION_UNKNOWN, OPERATION_COPY, OPERATION_MOVE, OPERATION_DELETE, OPERATION_COMPRESS, OPERATION_EXTRACT }) @Retention(RetentionPolicy.SOURCE) public @interface MetricsOpType {} // Codes representing different provider types. Used for sorting file operations when logging. private static final int PROVIDER_INTRA = 0; private static final int PROVIDER_SYSTEM = 1; private static final int PROVIDER_EXTERNAL = 2; @IntDef(flag = false, value = { PROVIDER_INTRA, PROVIDER_SYSTEM, PROVIDER_EXTERNAL }) @Retention(RetentionPolicy.SOURCE) public @interface Provider {} // Codes representing different types of sub-fileops. These are used for bucketing fileop // failures in COUNT_*_FILEOP_FAILURE. public static final int SUBFILEOP_QUERY_DOCUMENT = 1; public static final int SUBFILEOP_QUERY_CHILDREN = 2; public static final int SUBFILEOP_OPEN_FILE = 3; public static final int SUBFILEOP_READ_FILE = 4; public static final int SUBFILEOP_CREATE_DOCUMENT = 5; public static final int SUBFILEOP_WRITE_FILE = 6; public static final int SUBFILEOP_DELETE_DOCUMENT = 7; public static final int SUBFILEOP_OBTAIN_STREAM_TYPE = 8; public static final int SUBFILEOP_QUICK_MOVE = 9; public static final int SUBFILEOP_QUICK_COPY = 10; @IntDef(flag = false, value = { SUBFILEOP_QUERY_DOCUMENT, SUBFILEOP_QUERY_CHILDREN, SUBFILEOP_OPEN_FILE, SUBFILEOP_READ_FILE, SUBFILEOP_CREATE_DOCUMENT, SUBFILEOP_WRITE_FILE, SUBFILEOP_DELETE_DOCUMENT, SUBFILEOP_OBTAIN_STREAM_TYPE, SUBFILEOP_QUICK_MOVE, SUBFILEOP_QUICK_COPY }) @Retention(RetentionPolicy.SOURCE) public @interface SubFileOp {} // Codes representing different user actions. These are used for bucketing stats in the // COUNT_USER_ACTION histogram. // The historgram includes action triggered from menu or invoked by keyboard shortcut. // Do not change or rearrange these values, that will break historical data. Only add to the // list. // Do not use negative numbers or zero; clearcut only handles positive integers. public static final int USER_ACTION_OTHER = 1; public static final int USER_ACTION_GRID = 2; public static final int USER_ACTION_LIST = 3; public static final int USER_ACTION_SORT_NAME = 4; public static final int USER_ACTION_SORT_DATE = 5; public static final int USER_ACTION_SORT_SIZE = 6; public static final int USER_ACTION_SEARCH = 7; public static final int USER_ACTION_SHOW_SIZE = 8; public static final int USER_ACTION_HIDE_SIZE = 9; public static final int USER_ACTION_SETTINGS = 10; public static final int USER_ACTION_COPY_TO = 11; public static final int USER_ACTION_MOVE_TO = 12; public static final int USER_ACTION_DELETE = 13; public static final int USER_ACTION_RENAME = 14; public static final int USER_ACTION_CREATE_DIR = 15; public static final int USER_ACTION_SELECT_ALL = 16; public static final int USER_ACTION_SHARE = 17; public static final int USER_ACTION_OPEN = 18; public static final int USER_ACTION_SHOW_ADVANCED = 19; public static final int USER_ACTION_HIDE_ADVANCED = 20; public static final int USER_ACTION_NEW_WINDOW = 21; public static final int USER_ACTION_PASTE_CLIPBOARD = 22; public static final int USER_ACTION_COPY_CLIPBOARD = 23; public static final int USER_ACTION_DRAG_N_DROP = 24; public static final int USER_ACTION_DRAG_N_DROP_MULTI_WINDOW = 25; public static final int USER_ACTION_CUT_CLIPBOARD = 26; public static final int USER_ACTION_COMPRESS = 27; public static final int USER_ACTION_EXTRACT_TO = 28; public static final int USER_ACTION_VIEW_IN_APPLICATION = 29; public static final int USER_ACTION_INSPECTOR = 30; @IntDef(flag = false, value = { USER_ACTION_OTHER, USER_ACTION_GRID, USER_ACTION_LIST, USER_ACTION_SORT_NAME, USER_ACTION_SORT_DATE, USER_ACTION_SORT_SIZE, USER_ACTION_SEARCH, USER_ACTION_SHOW_SIZE, USER_ACTION_HIDE_SIZE, USER_ACTION_SETTINGS, USER_ACTION_COPY_TO, USER_ACTION_MOVE_TO, USER_ACTION_DELETE, USER_ACTION_RENAME, USER_ACTION_CREATE_DIR, USER_ACTION_SELECT_ALL, USER_ACTION_SHARE, USER_ACTION_OPEN, USER_ACTION_SHOW_ADVANCED, USER_ACTION_HIDE_ADVANCED, USER_ACTION_NEW_WINDOW, USER_ACTION_PASTE_CLIPBOARD, USER_ACTION_COPY_CLIPBOARD, USER_ACTION_DRAG_N_DROP, USER_ACTION_DRAG_N_DROP_MULTI_WINDOW, USER_ACTION_CUT_CLIPBOARD, USER_ACTION_COMPRESS, USER_ACTION_EXTRACT_TO, USER_ACTION_VIEW_IN_APPLICATION, USER_ACTION_INSPECTOR }) @Retention(RetentionPolicy.SOURCE) public @interface UserAction {} // Codes representing different approaches to copy/move a document. OPMODE_PROVIDER indicates // it's an optimized operation provided by providers; OPMODE_CONVERTED means it's converted from // a virtual file; and OPMODE_CONVENTIONAL means it's byte copied. public static final int OPMODE_PROVIDER = 1; public static final int OPMODE_CONVERTED = 2; public static final int OPMODE_CONVENTIONAL = 3; @IntDef({OPMODE_PROVIDER, OPMODE_CONVERTED, OPMODE_CONVENTIONAL}) @Retention(RetentionPolicy.SOURCE) public @interface FileOpMode {} // Codes representing different menu actions. These are used for bucketing stats in the // COUNT_MENU_ACTION histogram. // Do not change or rearrange these values, that will break historical data. Only add to the // list. // Do not use negative numbers or zero; clearcut only handles positive integers. private static final int ACTION_OTHER = 1; private static final int ACTION_OPEN = 2; private static final int ACTION_CREATE = 3; private static final int ACTION_GET_CONTENT = 4; private static final int ACTION_OPEN_TREE = 5; @Deprecated private static final int ACTION_MANAGE = 6; private static final int ACTION_BROWSE = 7; private static final int ACTION_PICK_COPY_DESTINATION = 8; @IntDef(flag = true, value = { ACTION_OTHER, ACTION_OPEN, ACTION_CREATE, ACTION_GET_CONTENT, ACTION_OPEN_TREE, ACTION_MANAGE, ACTION_BROWSE, ACTION_PICK_COPY_DESTINATION }) @Retention(RetentionPolicy.SOURCE) public @interface MetricsAction {} // Codes representing different actions to open the drawer. They are used for bucketing stats in // the COUNT_DRAWER_OPENED histogram. // Do not change or rearrange these values, that will break historical data. Only add to the // list. // Do not use negative numbers or zero; clearcut only handles positive integers. private static final int DRAWER_OPENED_HAMBURGER = 1; private static final int DRAWER_OPENED_SWIPE = 2; @IntDef(flag = true, value = { DRAWER_OPENED_HAMBURGER, DRAWER_OPENED_SWIPE }) @Retention(RetentionPolicy.SOURCE) public @interface DrawerTrigger {} /** * Logs when DocumentsUI is started, and how. Call this when DocumentsUI first starts up. * * @param context * @param state * @param intent */ public static void logActivityLaunch(Context context, State state, Intent intent) { // Log the launch action. logHistogram(context, COUNT_LAUNCH_ACTION, toMetricsAction(state.action)); // Then log auxiliary data (roots/mime types) associated with some actions. Uri uri = intent.getData(); switch (state.action) { case State.ACTION_OPEN: logHistogram(context, COUNT_OPEN_MIME, sanitizeMime(intent.getType())); break; case State.ACTION_CREATE: logHistogram(context, COUNT_CREATE_MIME, sanitizeMime(intent.getType())); break; case State.ACTION_GET_CONTENT: logHistogram(context, COUNT_GET_CONTENT_MIME, sanitizeMime(intent.getType())); break; case State.ACTION_BROWSE: logHistogram(context, COUNT_BROWSE_ROOT, sanitizeRoot(uri)); break; default: break; } } /** * Logs when DocumentsUI are launched with {@link DocumentsContract#EXTRA_INITIAL_URI}. * * @param context * @param state used to resolve action * @param rootUri the resolved rootUri, or {@code null} if the provider doesn't * support {@link DocumentsProvider#findDocumentPath(String, String)} */ public static void logLaunchAtLocation(Context context, State state, @Nullable Uri rootUri) { switch (state.action) { case State.ACTION_BROWSE: logHistogram(context, COUNT_BROWSE_AT_LOCATION, sanitizeRoot(rootUri)); break; case State.ACTION_CREATE: logHistogram(context, COUNT_CREATE_AT_LOCATION, sanitizeRoot(rootUri)); break; case State.ACTION_GET_CONTENT: logHistogram(context, COUNT_GET_CONTENT_AT_LOCATION, sanitizeRoot(rootUri)); break; case State.ACTION_OPEN: logHistogram(context, COUNT_OPEN_AT_LOCATION, sanitizeRoot(rootUri)); break; } } /** * Logs a root visited event in file managers. Call this when the user * taps on a root in {@link com.android.documentsui.sidebar.RootsFragment}. * * @param context * @param scope * @param info */ public static void logRootVisited( Context context, @ContextScope int scope, RootInfo info) { switch (scope) { case FILES_SCOPE: logHistogram(context, COUNT_ROOT_VISITED_IN_MANAGER, sanitizeRoot(info)); break; case PICKER_SCOPE: logHistogram(context, COUNT_ROOT_VISITED_IN_PICKER, sanitizeRoot(info)); break; } } /** * Logs an app visited event in file pickers. Call this when the user visits * on an app in the RootsFragment. * * @param context * @param info */ public static void logAppVisited(Context context, ResolveInfo info) { logHistogram(context, COUNT_ROOT_VISITED_IN_PICKER, sanitizeRoot(info)); } /** * Logs file operation stats. Call this when a file operation has completed. The given * DocumentInfo is only used to distinguish broad categories of actions (e.g. copying from one * provider to another vs copying within a given provider). No PII is logged. * * @param context * @param operationType * @param srcs * @param dst */ public static void logFileOperation( Context context, @OpType int operationType, List srcs, @Nullable DocumentInfo dst) { ProviderCounts counts = new ProviderCounts(); countProviders(counts, srcs, dst); if (counts.intraProvider > 0) { logIntraProviderFileOps(context, dst.authority, operationType); } if (counts.systemProvider > 0) { // Log file operations on system providers. logInterProviderFileOps(context, COUNT_FILEOP_SYSTEM, dst, operationType); } if (counts.externalProvider > 0) { // Log file operations on external providers. logInterProviderFileOps(context, COUNT_FILEOP_EXTERNAL, dst, operationType); } } public static void logFileOperated( Context context, @OpType int operationType, @FileOpMode int approach) { switch (operationType) { case FileOperationService.OPERATION_COPY: logHistogram(context, COUNT_FILE_COPIED, approach); break; case FileOperationService.OPERATION_MOVE: logHistogram(context, COUNT_FILE_MOVED, approach); break; } } /** * Logs create directory operation. It is a part of file operation stats. We do not * differentiate between internal and external locations, all create directory operations are * logged under COUNT_FILEOP_SYSTEM. Call this when a create directory operation has completed. * * @param context */ public static void logCreateDirOperation(Context context) { logHistogram(context, COUNT_FILEOP_SYSTEM, FILEOP_CREATE_DIR); } /** * Logs rename file operation. It is a part of file operation stats. We do not differentiate * between internal and external locations, all rename operations are logged under * COUNT_FILEOP_SYSTEM. Call this when a rename file operation has completed. * * @param context */ public static void logRenameFileOperation(Context context) { logHistogram(context, COUNT_FILEOP_SYSTEM, FILEOP_RENAME); } /** * Logs some kind of file operation error. Call this when a file operation (e.g. copy, delete) * fails. * * @param context * @param operationType * @param failedFiles */ public static void logFileOperationErrors(Context context, @OpType int operationType, List failedFiles, List failedUris) { ProviderCounts counts = new ProviderCounts(); countProviders(counts, failedFiles, null); // TODO: Report URI errors separate from file operation errors. countProviders(counts, failedUris); @FileOp int opCode = FILEOP_OTHER_ERROR; switch (operationType) { case FileOperationService.OPERATION_COPY: opCode = FILEOP_COPY_ERROR; break; case FileOperationService.OPERATION_COMPRESS: opCode = FILEOP_COMPRESS_ERROR; break; case FileOperationService.OPERATION_EXTRACT: opCode = FILEOP_EXTRACT_ERROR; break; case FileOperationService.OPERATION_DELETE: opCode = FILEOP_DELETE_ERROR; break; case FileOperationService.OPERATION_MOVE: opCode = FILEOP_MOVE_ERROR; break; } if (counts.systemProvider > 0) { logHistogram(context, COUNT_FILEOP_SYSTEM, opCode); } if (counts.externalProvider > 0) { logHistogram(context, COUNT_FILEOP_EXTERNAL, opCode); } } public static void logFileOperationFailure( Context context, @SubFileOp int subFileOp, Uri docUri) { final String authority = docUri.getAuthority(); switch (authority) { case Providers.AUTHORITY_MEDIA: logHistogram(context, COUNT_MEDIA_FILEOP_FAILURE, subFileOp); break; case Providers.AUTHORITY_STORAGE: logStorageFileOperationFailure(context, subFileOp, docUri); break; case Providers.AUTHORITY_DOWNLOADS: logHistogram(context, COUNT_DOWNLOADS_FILEOP_FAILURE, subFileOp); break; case Providers.AUTHORITY_MTP: logHistogram(context, COUNT_MTP_FILEOP_FAILURE, subFileOp); break; default: logHistogram(context, COUNT_OTHER_FILEOP_FAILURE, subFileOp); break; } } /** * Logs create directory operation error. We do not differentiate between internal and external * locations, all create directory errors are logged under COUNT_FILEOP_SYSTEM. Call this when a * create directory operation fails. * * @param context */ public static void logCreateDirError(Context context) { logHistogram(context, COUNT_FILEOP_SYSTEM, FILEOP_CREATE_DIR_ERROR); } /** * Logs rename file operation error. We do not differentiate between internal and external * locations, all rename errors are logged under COUNT_FILEOP_SYSTEM. Call this * when a rename file operation fails. * * @param context */ public static void logRenameFileError(Context context) { logHistogram(context, COUNT_FILEOP_SYSTEM, FILEOP_RENAME_ERROR); } /** * Logs the cancellation of a file operation. Call this when a Job is canceled. * @param context * @param operationType */ public static void logFileOperationCancelled(Context context, @OpType int operationType) { logHistogram(context, COUNT_FILEOP_CANCELED, toMetricsOpType(operationType)); } /** * Logs startup time in milliseconds. * @param context * @param startupMs Startup time in milliseconds. */ public static void logStartupMs(Context context, int startupMs) { logHistogram(context, COUNT_STARTUP_MS, startupMs); } private static void logInterProviderFileOps( Context context, String histogram, DocumentInfo dst, @OpType int operationType) { if (operationType == FileOperationService.OPERATION_DELETE) { logHistogram(context, histogram, FILEOP_DELETE); } else { assert(dst != null); @Provider int providerType = isSystemProvider(dst.authority) ? PROVIDER_SYSTEM : PROVIDER_EXTERNAL; logHistogram(context, histogram, getOpCode(operationType, providerType)); } } private static void logIntraProviderFileOps( Context context, String authority, @OpType int operationType) { // Find the right histogram to log to, then log the operation. String histogram = isSystemProvider(authority) ? COUNT_FILEOP_SYSTEM : COUNT_FILEOP_EXTERNAL; logHistogram(context, histogram, getOpCode(operationType, PROVIDER_INTRA)); } // Types for logInvalidScopedAccessRequest public static final String SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS = "docsui_scoped_directory_access_invalid_args"; public static final String SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY = "docsui_scoped_directory_access_invalid_dir"; public static final String SCOPED_DIRECTORY_ACCESS_ERROR = "docsui_scoped_directory_access_error"; @StringDef(value = { SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS, SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY, SCOPED_DIRECTORY_ACCESS_ERROR }) @Retention(RetentionPolicy.SOURCE) public @interface InvalidScopedAccess{} public static void logInvalidScopedAccessRequest(Context context, @InvalidScopedAccess String type) { switch (type) { case SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS: case SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY: case SCOPED_DIRECTORY_ACCESS_ERROR: logCount(context, type); break; default: Log.wtf(TAG, "invalid InvalidScopedAccess: " + type); } } // Types for logValidScopedAccessRequest public static final int SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED = 0; public static final int SCOPED_DIRECTORY_ACCESS_GRANTED = 1; public static final int SCOPED_DIRECTORY_ACCESS_DENIED = 2; public static final int SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST = 3; public static final int SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED = 4; @IntDef(flag = true, value = { SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED, SCOPED_DIRECTORY_ACCESS_GRANTED, SCOPED_DIRECTORY_ACCESS_DENIED, SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST, SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED }) @Retention(RetentionPolicy.SOURCE) public @interface ScopedAccessGrant {} public static void logValidScopedAccessRequest(Activity activity, String directory, @ScopedAccessGrant int type) { int index = -1; if (OpenExternalDirectoryActivity.DIRECTORY_ROOT.equals(directory)) { index = -2; } else { for (int i = 0; i < STANDARD_DIRECTORIES.length; i++) { if (STANDARD_DIRECTORIES[i].equals(directory)) { index = i; break; } } } final String packageName = activity.getCallingPackage(); switch (type) { case SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED: MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED_BY_FOLDER, index); break; case SCOPED_DIRECTORY_ACCESS_GRANTED: MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_GRANTED_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_GRANTED_BY_FOLDER, index); break; case SCOPED_DIRECTORY_ACCESS_DENIED: MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_BY_FOLDER, index); break; case SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST: MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST_BY_FOLDER, index); break; case SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED: MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED_BY_PACKAGE, packageName); MetricsLogger.action(activity, MetricsEvent .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED_BY_FOLDER, index); break; default: Log.wtf(TAG, "invalid ScopedAccessGrant: " + type); } } /** * Logs the action that was started by user. * @param context * @param userAction */ public static void logUserAction(Context context, @UserAction int userAction) { logHistogram(context, COUNT_USER_ACTION, userAction); } private static void logStorageFileOperationFailure( Context context, @SubFileOp int subFileOp, Uri docUri) { assert(Providers.AUTHORITY_STORAGE.equals(docUri.getAuthority())); boolean isInternal; try (ContentProviderClient client = acquireUnstableProviderOrThrow( context.getContentResolver(), Providers.AUTHORITY_STORAGE)) { final Path path = DocumentsContract.findDocumentPath(client, docUri); final ProvidersAccess providers = DocumentsApplication.getProvidersCache(context); final RootInfo root = providers.getRootOneshot( Providers.AUTHORITY_STORAGE, path.getRootId()); isInternal = !root.supportsEject(); } catch (RemoteException | RuntimeException e) { Log.e(TAG, "Failed to obtain its root info. Log the metrics as internal.", e); // It's not very likely to have an external storage so log it as internal. isInternal = true; } final String histogram = isInternal ? COUNT_INTERNAL_STORAGE_FILEOP_FAILURE : COUNT_EXTERNAL_STORAGE_FILEOP_FAILURE; logHistogram(context, histogram, subFileOp); } /** * Internal method for making a MetricsLogger.count call. Increments the given counter by 1. * * @param context * @param name The counter to increment. */ private static void logCount(Context context, String name) { if (DEBUG) Log.d(TAG, name + ": " + 1); MetricsLogger.count(context, name, 1); } /** * Internal method for making a MetricsLogger.histogram call. * * @param context * @param name The name of the histogram. * @param bucket The bucket to increment. */ private static void logHistogram(Context context, String name, @ActionType int bucket) { if (DEBUG) Log.d(TAG, name + ": " + bucket); MetricsLogger.histogram(context, name, bucket); } /** * Generates an integer identifying the given root. For privacy, this function only recognizes a * small set of hard-coded roots (ones provided by the system). Other roots are all grouped into * a single ROOT_OTHER bucket. */ private static @Root int sanitizeRoot(Uri uri) { if (uri == null || uri.getAuthority() == null || LauncherActivity.isLaunchUri(uri)) { return ROOT_NONE; } switch (uri.getAuthority()) { case Providers.AUTHORITY_MEDIA: switch (DocumentsContract.getRootId(uri)) { case Providers.ROOT_ID_AUDIO: return ROOT_AUDIO; case Providers.ROOT_ID_IMAGES: return ROOT_IMAGES; case Providers.ROOT_ID_VIDEOS: return ROOT_VIDEOS; default: return ROOT_OTHER; } case Providers.AUTHORITY_STORAGE: if (Providers.ROOT_ID_HOME.equals(DocumentsContract.getRootId(uri))) { return ROOT_HOME; } else { return ROOT_DEVICE_STORAGE; } case Providers.AUTHORITY_DOWNLOADS: return ROOT_DOWNLOADS; case Providers.AUTHORITY_MTP: return ROOT_MTP; default: return ROOT_OTHER; } } /** @see #sanitizeRoot(Uri) */ private static @Root int sanitizeRoot(RootInfo root) { if (root.isRecents()) { // Recents root is special and only identifiable via this method call. Other roots are // identified by URI. return ROOT_RECENTS; } else { return sanitizeRoot(root.getUri()); } } /** @see #sanitizeRoot(Uri) */ private static @Root int sanitizeRoot(ResolveInfo info) { // Log all apps under a single bucket in the roots histogram. return ROOT_THIRD_PARTY_APP; } /** * Generates an int identifying a mime type. For privacy, this function only recognizes a small * set of hard-coded types. For any other type, this function returns "other". * * @param mimeType * @return */ private static @Mime int sanitizeMime(String mimeType) { if (mimeType == null) { return MIME_NONE; } else if ("*/*".equals(mimeType)) { return MIME_ANY; } else { String type = mimeType.substring(0, mimeType.indexOf('/')); switch (type) { case "application": return MIME_APPLICATION; case "audio": return MIME_AUDIO; case "image": return MIME_IMAGE; case "message": return MIME_MESSAGE; case "multipart": return MIME_MULTIPART; case "text": return MIME_TEXT; case "video": return MIME_VIDEO; } } // Bucket all other types into one bucket. return MIME_OTHER; } private static boolean isSystemProvider(String authority) { switch (authority) { case Providers.AUTHORITY_MEDIA: case Providers.AUTHORITY_STORAGE: case Providers.AUTHORITY_DOWNLOADS: return true; default: return false; } } /** * @param operation * @param providerType * @return An opcode, suitable for use as histogram bucket, for the given operation/provider * combination. */ private static @FileOp int getOpCode(@OpType int operation, @Provider int providerType) { switch (operation) { case FileOperationService.OPERATION_COPY: switch (providerType) { case PROVIDER_INTRA: return FILEOP_COPY_INTRA_PROVIDER; case PROVIDER_SYSTEM: return FILEOP_COPY_SYSTEM_PROVIDER; case PROVIDER_EXTERNAL: return FILEOP_COPY_EXTERNAL_PROVIDER; } case FileOperationService.OPERATION_COMPRESS: switch (providerType) { case PROVIDER_INTRA: return FILEOP_COMPRESS_INTRA_PROVIDER; case PROVIDER_SYSTEM: return FILEOP_COMPRESS_SYSTEM_PROVIDER; case PROVIDER_EXTERNAL: return FILEOP_COMPRESS_EXTERNAL_PROVIDER; } case FileOperationService.OPERATION_EXTRACT: switch (providerType) { case PROVIDER_INTRA: return FILEOP_EXTRACT_INTRA_PROVIDER; case PROVIDER_SYSTEM: return FILEOP_EXTRACT_SYSTEM_PROVIDER; case PROVIDER_EXTERNAL: return FILEOP_EXTRACT_EXTERNAL_PROVIDER; } case FileOperationService.OPERATION_MOVE: switch (providerType) { case PROVIDER_INTRA: return FILEOP_MOVE_INTRA_PROVIDER; case PROVIDER_SYSTEM: return FILEOP_MOVE_SYSTEM_PROVIDER; case PROVIDER_EXTERNAL: return FILEOP_MOVE_EXTERNAL_PROVIDER; } case FileOperationService.OPERATION_DELETE: return FILEOP_DELETE; default: Log.w(TAG, "Unrecognized operation type when logging a file operation"); return FILEOP_OTHER; } } /** * Maps FileOperationService OpType values, to MetricsOpType values. */ private static @MetricsOpType int toMetricsOpType(@OpType int operation) { switch (operation) { case FileOperationService.OPERATION_COPY: return OPERATION_COPY; case FileOperationService.OPERATION_MOVE: return OPERATION_MOVE; case FileOperationService.OPERATION_DELETE: return OPERATION_DELETE; case FileOperationService.OPERATION_UNKNOWN: default: return OPERATION_UNKNOWN; } } private static @MetricsAction int toMetricsAction(int action) { switch(action) { case State.ACTION_OPEN: return ACTION_OPEN; case State.ACTION_CREATE: return ACTION_CREATE; case State.ACTION_GET_CONTENT: return ACTION_GET_CONTENT; case State.ACTION_OPEN_TREE: return ACTION_OPEN_TREE; case State.ACTION_BROWSE: return ACTION_BROWSE; case State.ACTION_PICK_COPY_DESTINATION: return ACTION_PICK_COPY_DESTINATION; default: return ACTION_OTHER; } } /** * Count the given src documents and provide a tally of how many come from the same provider as * the dst document (if a dst is provided), how many come from system providers, and how many * come from external 3rd-party providers. */ private static void countProviders( ProviderCounts counts, List srcs, @Nullable DocumentInfo dst) { for (DocumentInfo doc: srcs) { countForAuthority(counts, doc.authority, dst); } } /** * Count the given uris and provide a tally of how many come from the same provider as * the dst document (if a dst is provided), how many come from system providers, and how many * come from external 3rd-party providers. */ private static void countProviders(ProviderCounts counts, List uris) { for (Uri uri: uris) { countForAuthority(counts, uri.getAuthority(), null); } } private static void countForAuthority( ProviderCounts counts, String authority, @Nullable DocumentInfo dst) { if (dst != null && authority.equals(dst.authority)) { counts.intraProvider++; } else if (isSystemProvider(authority)){ counts.systemProvider++; } else { counts.externalProvider++; } } private static class ProviderCounts { int intraProvider; int systemProvider; int externalProvider; } }