1 /* 2 * Copyright (C) 2016 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.documentsui; 18 19 import static android.os.Environment.STANDARD_DIRECTORIES; 20 import static com.android.documentsui.DocumentsApplication.acquireUnstableProviderOrThrow; 21 import static com.android.documentsui.base.Shared.DEBUG; 22 23 import android.annotation.IntDef; 24 import android.annotation.Nullable; 25 import android.annotation.StringDef; 26 import android.app.Activity; 27 import android.content.ContentProviderClient; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.pm.ResolveInfo; 31 import android.net.Uri; 32 import android.os.RemoteException; 33 import android.provider.DocumentsContract; 34 import android.provider.DocumentsContract.Path; 35 import android.provider.DocumentsProvider; 36 import android.util.Log; 37 38 import com.android.documentsui.base.DocumentInfo; 39 import com.android.documentsui.base.Providers; 40 import com.android.documentsui.base.RootInfo; 41 import com.android.documentsui.base.State; 42 import com.android.documentsui.base.State.ActionType; 43 import com.android.documentsui.files.LauncherActivity; 44 import com.android.documentsui.roots.ProvidersAccess; 45 import com.android.documentsui.services.FileOperationService; 46 import com.android.documentsui.services.FileOperationService.OpType; 47 import com.android.internal.logging.MetricsLogger; 48 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 49 50 import java.lang.annotation.Retention; 51 import java.lang.annotation.RetentionPolicy; 52 import java.util.List; 53 54 /** @hide */ 55 public final class Metrics { 56 private static final String TAG = "Metrics"; 57 58 // These strings have to be whitelisted in tron. Do not change them. 59 private static final String COUNT_LAUNCH_ACTION = "docsui_launch_action"; 60 private static final String COUNT_ROOT_VISITED_IN_MANAGER 61 = "docsui_root_visited_in_manager"; 62 private static final String COUNT_ROOT_VISITED_IN_PICKER 63 = "docsui_root_visited_in_picker"; 64 private static final String COUNT_OPEN_MIME = "docsui_open_mime"; 65 private static final String COUNT_CREATE_MIME = "docsui_create_mime"; 66 private static final String COUNT_GET_CONTENT_MIME = "docsui_get_content_mime"; 67 private static final String COUNT_BROWSE_ROOT = "docsui_browse_root"; 68 @Deprecated private static final String COUNT_MANAGE_ROOT = "docsui_manage_root"; 69 @Deprecated private static final String COUNT_MULTI_WINDOW = "docsui_multi_window"; 70 private static final String COUNT_FILEOP_SYSTEM = "docsui_fileop_system"; 71 private static final String COUNT_FILEOP_EXTERNAL = "docsui_fileop_external"; 72 private static final String COUNT_FILEOP_CANCELED = "docsui_fileop_canceled"; 73 private static final String COUNT_STARTUP_MS = "docsui_startup_ms"; 74 @Deprecated private static final String COUNT_DRAWER_OPENED = "docsui_drawer_opened"; 75 private static final String COUNT_USER_ACTION = "docsui_menu_action"; 76 private static final String COUNT_BROWSE_AT_LOCATION = "docsui_browse_at_location"; 77 private static final String COUNT_CREATE_AT_LOCATION = "docsui_create_at_location"; 78 private static final String COUNT_OPEN_AT_LOCATION = "docsui_open_at_location"; 79 private static final String COUNT_GET_CONTENT_AT_LOCATION = "docsui_get_content_at_location"; 80 private static final String COUNT_MEDIA_FILEOP_FAILURE = "docsui_media_fileop_failure"; 81 private static final String COUNT_DOWNLOADS_FILEOP_FAILURE = "docsui_downloads_fileop_failure"; 82 private static final String COUNT_INTERNAL_STORAGE_FILEOP_FAILURE 83 = "docsui_internal_storage_fileop_failure"; 84 private static final String COUNT_EXTERNAL_STORAGE_FILEOP_FAILURE 85 = "docsui_external_storage_fileop_failure"; 86 private static final String COUNT_MTP_FILEOP_FAILURE = "docsui_mtp_fileop_failure"; 87 private static final String COUNT_OTHER_FILEOP_FAILURE = "docsui_other_fileop_failure"; 88 private static final String COUNT_FILE_COPIED = "docsui_file_copied"; 89 private static final String COUNT_FILE_MOVED = "docsui_file_moved"; 90 91 // Indices for bucketing roots in the roots histogram. "Other" is the catch-all index for any 92 // root that is not explicitly recognized by the Metrics code (see {@link 93 // #getSanitizedRootIndex}). Apps are also bucketed in this histogram. 94 // Do not change or rearrange these values, that will break historical data. Only add to the end 95 // of the list. 96 // Do not use negative numbers or zero; clearcut only handles positive integers. 97 private static final int ROOT_NONE = 1; 98 private static final int ROOT_OTHER = 2; 99 private static final int ROOT_AUDIO = 3; 100 private static final int ROOT_DEVICE_STORAGE = 4; 101 private static final int ROOT_DOWNLOADS = 5; 102 private static final int ROOT_HOME = 6; 103 private static final int ROOT_IMAGES = 7; 104 private static final int ROOT_RECENTS = 8; 105 private static final int ROOT_VIDEOS = 9; 106 private static final int ROOT_MTP = 10; 107 // Apps aren't really "roots", but they are treated as such in the roots fragment UI and so they 108 // are logged analogously to roots. 109 private static final int ROOT_THIRD_PARTY_APP = 100; 110 111 @IntDef(flag = true, value = { 112 ROOT_NONE, 113 ROOT_OTHER, 114 ROOT_AUDIO, 115 ROOT_DEVICE_STORAGE, 116 ROOT_DOWNLOADS, 117 ROOT_HOME, 118 ROOT_IMAGES, 119 ROOT_RECENTS, 120 ROOT_VIDEOS, 121 ROOT_MTP, 122 ROOT_THIRD_PARTY_APP 123 }) 124 @Retention(RetentionPolicy.SOURCE) 125 public @interface Root {} 126 127 // Indices for bucketing mime types. 128 // Do not change or rearrange these values, that will break historical data. Only add to the end 129 // of the list. 130 // Do not use negative numbers or zero; clearcut only handles positive integers. 131 private static final int MIME_NONE = 1; // null mime 132 private static final int MIME_ANY = 2; // */* 133 private static final int MIME_APPLICATION = 3; // application/* 134 private static final int MIME_AUDIO = 4; // audio/* 135 private static final int MIME_IMAGE = 5; // image/* 136 private static final int MIME_MESSAGE = 6; // message/* 137 private static final int MIME_MULTIPART = 7; // multipart/* 138 private static final int MIME_TEXT = 8; // text/* 139 private static final int MIME_VIDEO = 9; // video/* 140 private static final int MIME_OTHER = 10; // anything not enumerated below 141 142 @IntDef(flag = true, value = { 143 MIME_NONE, 144 MIME_ANY, 145 MIME_APPLICATION, 146 MIME_AUDIO, 147 MIME_IMAGE, 148 MIME_MESSAGE, 149 MIME_MULTIPART, 150 MIME_TEXT, 151 MIME_VIDEO, 152 MIME_OTHER 153 }) 154 @Retention(RetentionPolicy.SOURCE) 155 public @interface Mime {} 156 157 public static final int FILES_SCOPE = 1; 158 public static final int PICKER_SCOPE = 2; 159 160 @IntDef({ FILES_SCOPE, PICKER_SCOPE }) 161 @Retention(RetentionPolicy.SOURCE) 162 public @interface ContextScope {} 163 164 // Codes representing different kinds of file operations. These are used for bucketing 165 // operations in the COUNT_FILEOP_{SYSTEM|EXTERNAL} histograms. 166 // Do not change or rearrange these values, that will break historical data. Only add to the 167 // list. 168 // Do not use negative numbers or zero; clearcut only handles positive integers. 169 private static final int FILEOP_OTHER = 1; // any file operation not listed below 170 private static final int FILEOP_COPY_INTRA_PROVIDER = 2; // Copy within a provider 171 private static final int FILEOP_COPY_SYSTEM_PROVIDER = 3; // Copy to a system provider. 172 private static final int FILEOP_COPY_EXTERNAL_PROVIDER = 4; // Copy to a 3rd-party provider. 173 private static final int FILEOP_MOVE_INTRA_PROVIDER = 5; // Move within a provider. 174 private static final int FILEOP_MOVE_SYSTEM_PROVIDER = 6; // Move to a system provider. 175 private static final int FILEOP_MOVE_EXTERNAL_PROVIDER = 7; // Move to a 3rd-party provider. 176 private static final int FILEOP_DELETE = 8; 177 private static final int FILEOP_RENAME = 9; 178 private static final int FILEOP_CREATE_DIR = 10; 179 private static final int FILEOP_OTHER_ERROR = 100; 180 private static final int FILEOP_DELETE_ERROR = 101; 181 private static final int FILEOP_MOVE_ERROR = 102; 182 private static final int FILEOP_COPY_ERROR = 103; 183 private static final int FILEOP_RENAME_ERROR = 104; 184 private static final int FILEOP_CREATE_DIR_ERROR = 105; 185 private static final int FILEOP_COMPRESS_INTRA_PROVIDER = 106; // Compres within a provider 186 private static final int FILEOP_COMPRESS_SYSTEM_PROVIDER = 107; // Compress to a system provider. 187 private static final int FILEOP_COMPRESS_EXTERNAL_PROVIDER = 108; // Compress to a 3rd-party provider. 188 private static final int FILEOP_EXTRACT_INTRA_PROVIDER = 109; // Extract within a provider 189 private static final int FILEOP_EXTRACT_SYSTEM_PROVIDER = 110; // Extract to a system provider. 190 private static final int FILEOP_EXTRACT_EXTERNAL_PROVIDER = 111; // Extract to a 3rd-party provider. 191 private static final int FILEOP_COMPRESS_ERROR = 112; 192 private static final int FILEOP_EXTRACT_ERROR = 113; 193 194 @IntDef(flag = true, value = { 195 FILEOP_OTHER, 196 FILEOP_COPY_INTRA_PROVIDER, 197 FILEOP_COPY_SYSTEM_PROVIDER, 198 FILEOP_COPY_EXTERNAL_PROVIDER, 199 FILEOP_MOVE_INTRA_PROVIDER, 200 FILEOP_MOVE_SYSTEM_PROVIDER, 201 FILEOP_MOVE_EXTERNAL_PROVIDER, 202 FILEOP_DELETE, 203 FILEOP_RENAME, 204 FILEOP_CREATE_DIR, 205 FILEOP_OTHER_ERROR, 206 FILEOP_DELETE_ERROR, 207 FILEOP_MOVE_ERROR, 208 FILEOP_COPY_ERROR, 209 FILEOP_RENAME_ERROR, 210 FILEOP_CREATE_DIR_ERROR, 211 FILEOP_COMPRESS_INTRA_PROVIDER, 212 FILEOP_COMPRESS_SYSTEM_PROVIDER, 213 FILEOP_COMPRESS_EXTERNAL_PROVIDER, 214 FILEOP_EXTRACT_INTRA_PROVIDER, 215 FILEOP_EXTRACT_SYSTEM_PROVIDER, 216 FILEOP_EXTRACT_EXTERNAL_PROVIDER, 217 FILEOP_COMPRESS_ERROR, 218 FILEOP_EXTRACT_ERROR 219 }) 220 @Retention(RetentionPolicy.SOURCE) 221 public @interface FileOp {} 222 223 // Codes representing different kinds of file operations. These are used for bucketing 224 // operations in the COUNT_FILEOP_CANCELED histogram. 225 // Do not change or rearrange these values, that will break historical data. Only add to the 226 // list. 227 // Do not use negative numbers or zero; clearcut only handles positive integers. 228 private static final int OPERATION_UNKNOWN = 1; 229 private static final int OPERATION_COPY = 2; 230 private static final int OPERATION_MOVE = 3; 231 private static final int OPERATION_DELETE = 4; 232 private static final int OPERATION_COMPRESS = 5; 233 private static final int OPERATION_EXTRACT = 6; 234 235 @IntDef(flag = true, value = { 236 OPERATION_UNKNOWN, 237 OPERATION_COPY, 238 OPERATION_MOVE, 239 OPERATION_DELETE, 240 OPERATION_COMPRESS, 241 OPERATION_EXTRACT 242 }) 243 @Retention(RetentionPolicy.SOURCE) 244 public @interface MetricsOpType {} 245 246 // Codes representing different provider types. Used for sorting file operations when logging. 247 private static final int PROVIDER_INTRA = 0; 248 private static final int PROVIDER_SYSTEM = 1; 249 private static final int PROVIDER_EXTERNAL = 2; 250 251 @IntDef(flag = false, value = { 252 PROVIDER_INTRA, 253 PROVIDER_SYSTEM, 254 PROVIDER_EXTERNAL 255 }) 256 @Retention(RetentionPolicy.SOURCE) 257 public @interface Provider {} 258 259 // Codes representing different types of sub-fileops. These are used for bucketing fileop 260 // failures in COUNT_*_FILEOP_FAILURE. 261 public static final int SUBFILEOP_QUERY_DOCUMENT = 1; 262 public static final int SUBFILEOP_QUERY_CHILDREN = 2; 263 public static final int SUBFILEOP_OPEN_FILE = 3; 264 public static final int SUBFILEOP_READ_FILE = 4; 265 public static final int SUBFILEOP_CREATE_DOCUMENT = 5; 266 public static final int SUBFILEOP_WRITE_FILE = 6; 267 public static final int SUBFILEOP_DELETE_DOCUMENT = 7; 268 public static final int SUBFILEOP_OBTAIN_STREAM_TYPE = 8; 269 public static final int SUBFILEOP_QUICK_MOVE = 9; 270 public static final int SUBFILEOP_QUICK_COPY = 10; 271 272 @IntDef(flag = false, value = { 273 SUBFILEOP_QUERY_DOCUMENT, 274 SUBFILEOP_QUERY_CHILDREN, 275 SUBFILEOP_OPEN_FILE, 276 SUBFILEOP_READ_FILE, 277 SUBFILEOP_CREATE_DOCUMENT, 278 SUBFILEOP_WRITE_FILE, 279 SUBFILEOP_DELETE_DOCUMENT, 280 SUBFILEOP_OBTAIN_STREAM_TYPE, 281 SUBFILEOP_QUICK_MOVE, 282 SUBFILEOP_QUICK_COPY 283 }) 284 @Retention(RetentionPolicy.SOURCE) 285 public @interface SubFileOp {} 286 287 // Codes representing different user actions. These are used for bucketing stats in the 288 // COUNT_USER_ACTION histogram. 289 // The historgram includes action triggered from menu or invoked by keyboard shortcut. 290 // Do not change or rearrange these values, that will break historical data. Only add to the 291 // list. 292 // Do not use negative numbers or zero; clearcut only handles positive integers. 293 public static final int USER_ACTION_OTHER = 1; 294 public static final int USER_ACTION_GRID = 2; 295 public static final int USER_ACTION_LIST = 3; 296 public static final int USER_ACTION_SORT_NAME = 4; 297 public static final int USER_ACTION_SORT_DATE = 5; 298 public static final int USER_ACTION_SORT_SIZE = 6; 299 public static final int USER_ACTION_SEARCH = 7; 300 public static final int USER_ACTION_SHOW_SIZE = 8; 301 public static final int USER_ACTION_HIDE_SIZE = 9; 302 public static final int USER_ACTION_SETTINGS = 10; 303 public static final int USER_ACTION_COPY_TO = 11; 304 public static final int USER_ACTION_MOVE_TO = 12; 305 public static final int USER_ACTION_DELETE = 13; 306 public static final int USER_ACTION_RENAME = 14; 307 public static final int USER_ACTION_CREATE_DIR = 15; 308 public static final int USER_ACTION_SELECT_ALL = 16; 309 public static final int USER_ACTION_SHARE = 17; 310 public static final int USER_ACTION_OPEN = 18; 311 public static final int USER_ACTION_SHOW_ADVANCED = 19; 312 public static final int USER_ACTION_HIDE_ADVANCED = 20; 313 public static final int USER_ACTION_NEW_WINDOW = 21; 314 public static final int USER_ACTION_PASTE_CLIPBOARD = 22; 315 public static final int USER_ACTION_COPY_CLIPBOARD = 23; 316 public static final int USER_ACTION_DRAG_N_DROP = 24; 317 public static final int USER_ACTION_DRAG_N_DROP_MULTI_WINDOW = 25; 318 public static final int USER_ACTION_CUT_CLIPBOARD = 26; 319 public static final int USER_ACTION_COMPRESS = 27; 320 public static final int USER_ACTION_EXTRACT_TO = 28; 321 public static final int USER_ACTION_VIEW_IN_APPLICATION = 29; 322 323 @IntDef(flag = false, value = { 324 USER_ACTION_OTHER, 325 USER_ACTION_GRID, 326 USER_ACTION_LIST, 327 USER_ACTION_SORT_NAME, 328 USER_ACTION_SORT_DATE, 329 USER_ACTION_SORT_SIZE, 330 USER_ACTION_SEARCH, 331 USER_ACTION_SHOW_SIZE, 332 USER_ACTION_HIDE_SIZE, 333 USER_ACTION_SETTINGS, 334 USER_ACTION_COPY_TO, 335 USER_ACTION_MOVE_TO, 336 USER_ACTION_DELETE, 337 USER_ACTION_RENAME, 338 USER_ACTION_CREATE_DIR, 339 USER_ACTION_SELECT_ALL, 340 USER_ACTION_SHARE, 341 USER_ACTION_OPEN, 342 USER_ACTION_SHOW_ADVANCED, 343 USER_ACTION_HIDE_ADVANCED, 344 USER_ACTION_NEW_WINDOW, 345 USER_ACTION_PASTE_CLIPBOARD, 346 USER_ACTION_COPY_CLIPBOARD, 347 USER_ACTION_DRAG_N_DROP, 348 USER_ACTION_DRAG_N_DROP_MULTI_WINDOW, 349 USER_ACTION_CUT_CLIPBOARD, 350 USER_ACTION_COMPRESS, 351 USER_ACTION_EXTRACT_TO, 352 USER_ACTION_VIEW_IN_APPLICATION 353 }) 354 @Retention(RetentionPolicy.SOURCE) 355 public @interface UserAction {} 356 357 // Codes representing different approaches to copy/move a document. OPMODE_PROVIDER indicates 358 // it's an optimized operation provided by providers; OPMODE_CONVERTED means it's converted from 359 // a virtual file; and OPMODE_CONVENTIONAL means it's byte copied. 360 public static final int OPMODE_PROVIDER = 1; 361 public static final int OPMODE_CONVERTED = 2; 362 public static final int OPMODE_CONVENTIONAL = 3; 363 @IntDef({OPMODE_PROVIDER, OPMODE_CONVERTED, OPMODE_CONVENTIONAL}) 364 @Retention(RetentionPolicy.SOURCE) 365 public @interface FileOpMode {} 366 367 // Codes representing different menu actions. These are used for bucketing stats in the 368 // COUNT_MENU_ACTION histogram. 369 // Do not change or rearrange these values, that will break historical data. Only add to the 370 // list. 371 // Do not use negative numbers or zero; clearcut only handles positive integers. 372 private static final int ACTION_OTHER = 1; 373 private static final int ACTION_OPEN = 2; 374 private static final int ACTION_CREATE = 3; 375 private static final int ACTION_GET_CONTENT = 4; 376 private static final int ACTION_OPEN_TREE = 5; 377 @Deprecated private static final int ACTION_MANAGE = 6; 378 private static final int ACTION_BROWSE = 7; 379 private static final int ACTION_PICK_COPY_DESTINATION = 8; 380 381 @IntDef(flag = true, value = { 382 ACTION_OTHER, 383 ACTION_OPEN, 384 ACTION_CREATE, 385 ACTION_GET_CONTENT, 386 ACTION_OPEN_TREE, 387 ACTION_MANAGE, 388 ACTION_BROWSE, 389 ACTION_PICK_COPY_DESTINATION 390 }) 391 @Retention(RetentionPolicy.SOURCE) 392 public @interface MetricsAction {} 393 394 // Codes representing different actions to open the drawer. They are used for bucketing stats in 395 // the COUNT_DRAWER_OPENED histogram. 396 // Do not change or rearrange these values, that will break historical data. Only add to the 397 // list. 398 // Do not use negative numbers or zero; clearcut only handles positive integers. 399 private static final int DRAWER_OPENED_HAMBURGER = 1; 400 private static final int DRAWER_OPENED_SWIPE = 2; 401 402 @IntDef(flag = true, value = { 403 DRAWER_OPENED_HAMBURGER, 404 DRAWER_OPENED_SWIPE 405 }) 406 @Retention(RetentionPolicy.SOURCE) 407 public @interface DrawerTrigger {} 408 409 /** 410 * Logs when DocumentsUI is started, and how. Call this when DocumentsUI first starts up. 411 * 412 * @param context 413 * @param state 414 * @param intent 415 */ logActivityLaunch(Context context, State state, Intent intent)416 public static void logActivityLaunch(Context context, State state, Intent intent) { 417 // Log the launch action. 418 logHistogram(context, COUNT_LAUNCH_ACTION, toMetricsAction(state.action)); 419 // Then log auxiliary data (roots/mime types) associated with some actions. 420 Uri uri = intent.getData(); 421 switch (state.action) { 422 case State.ACTION_OPEN: 423 logHistogram(context, COUNT_OPEN_MIME, sanitizeMime(intent.getType())); 424 break; 425 case State.ACTION_CREATE: 426 logHistogram(context, COUNT_CREATE_MIME, sanitizeMime(intent.getType())); 427 break; 428 case State.ACTION_GET_CONTENT: 429 logHistogram(context, COUNT_GET_CONTENT_MIME, sanitizeMime(intent.getType())); 430 break; 431 case State.ACTION_BROWSE: 432 logHistogram(context, COUNT_BROWSE_ROOT, sanitizeRoot(uri)); 433 break; 434 default: 435 break; 436 } 437 } 438 439 /** 440 * Logs when DocumentsUI are launched with {@link DocumentsContract#EXTRA_INITIAL_URI}. 441 * 442 * @param context 443 * @param state used to resolve action 444 * @param rootUri the resolved rootUri, or {@code null} if the provider doesn't 445 * support {@link DocumentsProvider#findDocumentPath(String, String)} 446 */ logLaunchAtLocation(Context context, State state, @Nullable Uri rootUri)447 public static void logLaunchAtLocation(Context context, State state, @Nullable Uri rootUri) { 448 switch (state.action) { 449 case State.ACTION_BROWSE: 450 logHistogram(context, COUNT_BROWSE_AT_LOCATION, sanitizeRoot(rootUri)); 451 break; 452 case State.ACTION_CREATE: 453 logHistogram(context, COUNT_CREATE_AT_LOCATION, sanitizeRoot(rootUri)); 454 break; 455 case State.ACTION_GET_CONTENT: 456 logHistogram(context, COUNT_GET_CONTENT_AT_LOCATION, sanitizeRoot(rootUri)); 457 break; 458 case State.ACTION_OPEN: 459 logHistogram(context, COUNT_OPEN_AT_LOCATION, sanitizeRoot(rootUri)); 460 break; 461 } 462 } 463 464 /** 465 * Logs a root visited event in file managers. Call this when the user 466 * taps on a root in {@link com.android.documentsui.sidebar.RootsFragment}. 467 * 468 * @param context 469 * @param scope 470 * @param info 471 */ logRootVisited( Context context, @ContextScope int scope, RootInfo info)472 public static void logRootVisited( 473 Context context, @ContextScope int scope, RootInfo info) { 474 switch (scope) { 475 case FILES_SCOPE: 476 logHistogram(context, COUNT_ROOT_VISITED_IN_MANAGER, 477 sanitizeRoot(info)); 478 break; 479 case PICKER_SCOPE: 480 logHistogram(context, COUNT_ROOT_VISITED_IN_PICKER, 481 sanitizeRoot(info)); 482 break; 483 } 484 } 485 486 /** 487 * Logs an app visited event in file pickers. Call this when the user visits 488 * on an app in the RootsFragment. 489 * 490 * @param context 491 * @param info 492 */ logAppVisited(Context context, ResolveInfo info)493 public static void logAppVisited(Context context, ResolveInfo info) { 494 logHistogram(context, COUNT_ROOT_VISITED_IN_PICKER, sanitizeRoot(info)); 495 } 496 497 /** 498 * Logs file operation stats. Call this when a file operation has completed. The given 499 * DocumentInfo is only used to distinguish broad categories of actions (e.g. copying from one 500 * provider to another vs copying within a given provider). No PII is logged. 501 * 502 * @param context 503 * @param operationType 504 * @param srcs 505 * @param dst 506 */ logFileOperation( Context context, @OpType int operationType, List<DocumentInfo> srcs, @Nullable DocumentInfo dst)507 public static void logFileOperation( 508 Context context, 509 @OpType int operationType, 510 List<DocumentInfo> srcs, 511 @Nullable DocumentInfo dst) { 512 513 ProviderCounts counts = new ProviderCounts(); 514 countProviders(counts, srcs, dst); 515 516 if (counts.intraProvider > 0) { 517 logIntraProviderFileOps(context, dst.authority, operationType); 518 } 519 if (counts.systemProvider > 0) { 520 // Log file operations on system providers. 521 logInterProviderFileOps(context, COUNT_FILEOP_SYSTEM, dst, operationType); 522 } 523 if (counts.externalProvider > 0) { 524 // Log file operations on external providers. 525 logInterProviderFileOps(context, COUNT_FILEOP_EXTERNAL, dst, operationType); 526 } 527 } 528 logFileOperated( Context context, @OpType int operationType, @FileOpMode int approach)529 public static void logFileOperated( 530 Context context, @OpType int operationType, @FileOpMode int approach) { 531 switch (operationType) { 532 case FileOperationService.OPERATION_COPY: 533 logHistogram(context, COUNT_FILE_COPIED, approach); 534 break; 535 case FileOperationService.OPERATION_MOVE: 536 logHistogram(context, COUNT_FILE_MOVED, approach); 537 break; 538 } 539 } 540 541 /** 542 * Logs create directory operation. It is a part of file operation stats. We do not 543 * differentiate between internal and external locations, all create directory operations are 544 * logged under COUNT_FILEOP_SYSTEM. Call this when a create directory operation has completed. 545 * 546 * @param context 547 */ logCreateDirOperation(Context context)548 public static void logCreateDirOperation(Context context) { 549 logHistogram(context, COUNT_FILEOP_SYSTEM, FILEOP_CREATE_DIR); 550 } 551 552 /** 553 * Logs rename file operation. It is a part of file operation stats. We do not differentiate 554 * between internal and external locations, all rename operations are logged under 555 * COUNT_FILEOP_SYSTEM. Call this when a rename file operation has completed. 556 * 557 * @param context 558 */ logRenameFileOperation(Context context)559 public static void logRenameFileOperation(Context context) { 560 logHistogram(context, COUNT_FILEOP_SYSTEM, FILEOP_RENAME); 561 } 562 563 /** 564 * Logs some kind of file operation error. Call this when a file operation (e.g. copy, delete) 565 * fails. 566 * 567 * @param context 568 * @param operationType 569 * @param failedFiles 570 */ logFileOperationErrors(Context context, @OpType int operationType, List<DocumentInfo> failedFiles, List<Uri> failedUris)571 public static void logFileOperationErrors(Context context, @OpType int operationType, 572 List<DocumentInfo> failedFiles, List<Uri> failedUris) { 573 574 ProviderCounts counts = new ProviderCounts(); 575 countProviders(counts, failedFiles, null); 576 577 // TODO: Report URI errors separate from file operation errors. 578 countProviders(counts, failedUris); 579 580 @FileOp int opCode = FILEOP_OTHER_ERROR; 581 switch (operationType) { 582 case FileOperationService.OPERATION_COPY: 583 opCode = FILEOP_COPY_ERROR; 584 break; 585 case FileOperationService.OPERATION_COMPRESS: 586 opCode = FILEOP_COMPRESS_ERROR; 587 break; 588 case FileOperationService.OPERATION_EXTRACT: 589 opCode = FILEOP_EXTRACT_ERROR; 590 break; 591 case FileOperationService.OPERATION_DELETE: 592 opCode = FILEOP_DELETE_ERROR; 593 break; 594 case FileOperationService.OPERATION_MOVE: 595 opCode = FILEOP_MOVE_ERROR; 596 break; 597 } 598 if (counts.systemProvider > 0) { 599 logHistogram(context, COUNT_FILEOP_SYSTEM, opCode); 600 } 601 if (counts.externalProvider > 0) { 602 logHistogram(context, COUNT_FILEOP_EXTERNAL, opCode); 603 } 604 } 605 logFileOperationFailure( Context context, @SubFileOp int subFileOp, Uri docUri)606 public static void logFileOperationFailure( 607 Context context, @SubFileOp int subFileOp, Uri docUri) { 608 final String authority = docUri.getAuthority(); 609 switch (authority) { 610 case Providers.AUTHORITY_MEDIA: 611 logHistogram(context, COUNT_MEDIA_FILEOP_FAILURE, subFileOp); 612 break; 613 case Providers.AUTHORITY_STORAGE: 614 logStorageFileOperationFailure(context, subFileOp, docUri); 615 break; 616 case Providers.AUTHORITY_DOWNLOADS: 617 logHistogram(context, COUNT_DOWNLOADS_FILEOP_FAILURE, subFileOp); 618 break; 619 case Providers.AUTHORITY_MTP: 620 logHistogram(context, COUNT_MTP_FILEOP_FAILURE, subFileOp); 621 break; 622 default: 623 logHistogram(context, COUNT_OTHER_FILEOP_FAILURE, subFileOp); 624 break; 625 } 626 } 627 628 /** 629 * Logs create directory operation error. We do not differentiate between internal and external 630 * locations, all create directory errors are logged under COUNT_FILEOP_SYSTEM. Call this when a 631 * create directory operation fails. 632 * 633 * @param context 634 */ logCreateDirError(Context context)635 public static void logCreateDirError(Context context) { 636 logHistogram(context, COUNT_FILEOP_SYSTEM, FILEOP_CREATE_DIR_ERROR); 637 } 638 639 /** 640 * Logs rename file operation error. We do not differentiate between internal and external 641 * locations, all rename errors are logged under COUNT_FILEOP_SYSTEM. Call this 642 * when a rename file operation fails. 643 * 644 * @param context 645 */ logRenameFileError(Context context)646 public static void logRenameFileError(Context context) { 647 logHistogram(context, COUNT_FILEOP_SYSTEM, FILEOP_RENAME_ERROR); 648 } 649 650 /** 651 * Logs the cancellation of a file operation. Call this when a Job is canceled. 652 * @param context 653 * @param operationType 654 */ logFileOperationCancelled(Context context, @OpType int operationType)655 public static void logFileOperationCancelled(Context context, @OpType int operationType) { 656 logHistogram(context, COUNT_FILEOP_CANCELED, toMetricsOpType(operationType)); 657 } 658 659 /** 660 * Logs startup time in milliseconds. 661 * @param context 662 * @param startupMs Startup time in milliseconds. 663 */ logStartupMs(Context context, int startupMs)664 public static void logStartupMs(Context context, int startupMs) { 665 logHistogram(context, COUNT_STARTUP_MS, startupMs); 666 } 667 logInterProviderFileOps( Context context, String histogram, DocumentInfo dst, @OpType int operationType)668 private static void logInterProviderFileOps( 669 Context context, 670 String histogram, 671 DocumentInfo dst, 672 @OpType int operationType) { 673 if (operationType == FileOperationService.OPERATION_DELETE) { 674 logHistogram(context, histogram, FILEOP_DELETE); 675 } else { 676 assert(dst != null); 677 @Provider int providerType = 678 isSystemProvider(dst.authority) ? PROVIDER_SYSTEM : PROVIDER_EXTERNAL; 679 logHistogram(context, histogram, getOpCode(operationType, providerType)); 680 } 681 } 682 logIntraProviderFileOps( Context context, String authority, @OpType int operationType)683 private static void logIntraProviderFileOps( 684 Context context, String authority, @OpType int operationType) { 685 // Find the right histogram to log to, then log the operation. 686 String histogram = isSystemProvider(authority) ? COUNT_FILEOP_SYSTEM : COUNT_FILEOP_EXTERNAL; 687 logHistogram(context, histogram, getOpCode(operationType, PROVIDER_INTRA)); 688 } 689 690 // Types for logInvalidScopedAccessRequest 691 public static final String SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS = 692 "docsui_scoped_directory_access_invalid_args"; 693 public static final String SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY = 694 "docsui_scoped_directory_access_invalid_dir"; 695 public static final String SCOPED_DIRECTORY_ACCESS_ERROR = 696 "docsui_scoped_directory_access_error"; 697 698 @StringDef(value = { 699 SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS, 700 SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY, 701 SCOPED_DIRECTORY_ACCESS_ERROR 702 }) 703 @Retention(RetentionPolicy.SOURCE) 704 public @interface InvalidScopedAccess{} 705 logInvalidScopedAccessRequest(Context context, @InvalidScopedAccess String type)706 public static void logInvalidScopedAccessRequest(Context context, 707 @InvalidScopedAccess String type) { 708 switch (type) { 709 case SCOPED_DIRECTORY_ACCESS_INVALID_ARGUMENTS: 710 case SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY: 711 case SCOPED_DIRECTORY_ACCESS_ERROR: 712 logCount(context, type); 713 break; 714 default: 715 Log.wtf(TAG, "invalid InvalidScopedAccess: " + type); 716 } 717 } 718 719 // Types for logValidScopedAccessRequest 720 public static final int SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED = 0; 721 public static final int SCOPED_DIRECTORY_ACCESS_GRANTED = 1; 722 public static final int SCOPED_DIRECTORY_ACCESS_DENIED = 2; 723 public static final int SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST = 3; 724 public static final int SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED = 4; 725 726 @IntDef(flag = true, value = { 727 SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED, 728 SCOPED_DIRECTORY_ACCESS_GRANTED, 729 SCOPED_DIRECTORY_ACCESS_DENIED, 730 SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST, 731 SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED 732 }) 733 @Retention(RetentionPolicy.SOURCE) 734 public @interface ScopedAccessGrant {} 735 logValidScopedAccessRequest(Activity activity, String directory, @ScopedAccessGrant int type)736 public static void logValidScopedAccessRequest(Activity activity, String directory, 737 @ScopedAccessGrant int type) { 738 int index = -1; 739 if (OpenExternalDirectoryActivity.DIRECTORY_ROOT.equals(directory)) { 740 index = -2; 741 } else { 742 for (int i = 0; i < STANDARD_DIRECTORIES.length; i++) { 743 if (STANDARD_DIRECTORIES[i].equals(directory)) { 744 index = i; 745 break; 746 } 747 } 748 } 749 final String packageName = activity.getCallingPackage(); 750 switch (type) { 751 case SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED: 752 MetricsLogger.action(activity, MetricsEvent 753 .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED_BY_PACKAGE, packageName); 754 MetricsLogger.action(activity, MetricsEvent 755 .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED_BY_FOLDER, index); 756 break; 757 case SCOPED_DIRECTORY_ACCESS_GRANTED: 758 MetricsLogger.action(activity, MetricsEvent 759 .ACTION_SCOPED_DIRECTORY_ACCESS_GRANTED_BY_PACKAGE, packageName); 760 MetricsLogger.action(activity, MetricsEvent 761 .ACTION_SCOPED_DIRECTORY_ACCESS_GRANTED_BY_FOLDER, index); 762 break; 763 case SCOPED_DIRECTORY_ACCESS_DENIED: 764 MetricsLogger.action(activity, MetricsEvent 765 .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_BY_PACKAGE, packageName); 766 MetricsLogger.action(activity, MetricsEvent 767 .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_BY_FOLDER, index); 768 break; 769 case SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST: 770 MetricsLogger.action(activity, MetricsEvent 771 .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST_BY_PACKAGE, packageName); 772 MetricsLogger.action(activity, MetricsEvent 773 .ACTION_SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST_BY_FOLDER, index); 774 break; 775 case SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED: 776 MetricsLogger.action(activity, MetricsEvent 777 .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED_BY_PACKAGE, packageName); 778 MetricsLogger.action(activity, MetricsEvent 779 .ACTION_SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED_BY_FOLDER, index); 780 break; 781 default: 782 Log.wtf(TAG, "invalid ScopedAccessGrant: " + type); 783 } 784 } 785 786 /** 787 * Logs the action that was started by user. 788 * @param context 789 * @param userAction 790 */ logUserAction(Context context, @UserAction int userAction)791 public static void logUserAction(Context context, @UserAction int userAction) { 792 logHistogram(context, COUNT_USER_ACTION, userAction); 793 } 794 logStorageFileOperationFailure( Context context, @SubFileOp int subFileOp, Uri docUri)795 private static void logStorageFileOperationFailure( 796 Context context, @SubFileOp int subFileOp, Uri docUri) { 797 assert(Providers.AUTHORITY_STORAGE.equals(docUri.getAuthority())); 798 799 boolean isInternal; 800 try (ContentProviderClient client = acquireUnstableProviderOrThrow( 801 context.getContentResolver(), Providers.AUTHORITY_STORAGE)) { 802 final Path path = DocumentsContract.findDocumentPath(client, docUri); 803 804 final ProvidersAccess providers = DocumentsApplication.getProvidersCache(context); 805 final RootInfo root = providers.getRootOneshot( 806 Providers.AUTHORITY_STORAGE, path.getRootId()); 807 isInternal = !root.supportsEject(); 808 } catch (RemoteException | RuntimeException e) { 809 Log.e(TAG, "Failed to obtain its root info. Log the metrics as internal.", e); 810 // It's not very likely to have an external storage so log it as internal. 811 isInternal = true; 812 } 813 814 final String histogram = isInternal 815 ? COUNT_INTERNAL_STORAGE_FILEOP_FAILURE 816 : COUNT_EXTERNAL_STORAGE_FILEOP_FAILURE; 817 logHistogram(context, histogram, subFileOp); 818 } 819 820 /** 821 * Internal method for making a MetricsLogger.count call. Increments the given counter by 1. 822 * 823 * @param context 824 * @param name The counter to increment. 825 */ logCount(Context context, String name)826 private static void logCount(Context context, String name) { 827 if (DEBUG) Log.d(TAG, name + ": " + 1); 828 MetricsLogger.count(context, name, 1); 829 } 830 831 /** 832 * Internal method for making a MetricsLogger.histogram call. 833 * 834 * @param context 835 * @param name The name of the histogram. 836 * @param bucket The bucket to increment. 837 */ logHistogram(Context context, String name, @ActionType int bucket)838 private static void logHistogram(Context context, String name, @ActionType int bucket) { 839 if (DEBUG) Log.d(TAG, name + ": " + bucket); 840 MetricsLogger.histogram(context, name, bucket); 841 } 842 843 /** 844 * Generates an integer identifying the given root. For privacy, this function only recognizes a 845 * small set of hard-coded roots (ones provided by the system). Other roots are all grouped into 846 * a single ROOT_OTHER bucket. 847 */ sanitizeRoot(Uri uri)848 private static @Root int sanitizeRoot(Uri uri) { 849 if (uri == null || uri.getAuthority() == null || LauncherActivity.isLaunchUri(uri)) { 850 return ROOT_NONE; 851 } 852 853 switch (uri.getAuthority()) { 854 case Providers.AUTHORITY_MEDIA: 855 switch (DocumentsContract.getRootId(uri)) { 856 case Providers.ROOT_ID_AUDIO: 857 return ROOT_AUDIO; 858 case Providers.ROOT_ID_IMAGES: 859 return ROOT_IMAGES; 860 case Providers.ROOT_ID_VIDEOS: 861 return ROOT_VIDEOS; 862 default: 863 return ROOT_OTHER; 864 } 865 case Providers.AUTHORITY_STORAGE: 866 if (Providers.ROOT_ID_HOME.equals(DocumentsContract.getRootId(uri))) { 867 return ROOT_HOME; 868 } else { 869 return ROOT_DEVICE_STORAGE; 870 } 871 case Providers.AUTHORITY_DOWNLOADS: 872 return ROOT_DOWNLOADS; 873 case Providers.AUTHORITY_MTP: 874 return ROOT_MTP; 875 default: 876 return ROOT_OTHER; 877 } 878 } 879 880 /** @see #sanitizeRoot(Uri) */ sanitizeRoot(RootInfo root)881 private static @Root int sanitizeRoot(RootInfo root) { 882 if (root.isRecents()) { 883 // Recents root is special and only identifiable via this method call. Other roots are 884 // identified by URI. 885 return ROOT_RECENTS; 886 } else { 887 return sanitizeRoot(root.getUri()); 888 } 889 } 890 891 /** @see #sanitizeRoot(Uri) */ sanitizeRoot(ResolveInfo info)892 private static @Root int sanitizeRoot(ResolveInfo info) { 893 // Log all apps under a single bucket in the roots histogram. 894 return ROOT_THIRD_PARTY_APP; 895 } 896 897 /** 898 * Generates an int identifying a mime type. For privacy, this function only recognizes a small 899 * set of hard-coded types. For any other type, this function returns "other". 900 * 901 * @param mimeType 902 * @return 903 */ sanitizeMime(String mimeType)904 private static @Mime int sanitizeMime(String mimeType) { 905 if (mimeType == null) { 906 return MIME_NONE; 907 } else if ("*/*".equals(mimeType)) { 908 return MIME_ANY; 909 } else { 910 String type = mimeType.substring(0, mimeType.indexOf('/')); 911 switch (type) { 912 case "application": 913 return MIME_APPLICATION; 914 case "audio": 915 return MIME_AUDIO; 916 case "image": 917 return MIME_IMAGE; 918 case "message": 919 return MIME_MESSAGE; 920 case "multipart": 921 return MIME_MULTIPART; 922 case "text": 923 return MIME_TEXT; 924 case "video": 925 return MIME_VIDEO; 926 } 927 } 928 // Bucket all other types into one bucket. 929 return MIME_OTHER; 930 } 931 isSystemProvider(String authority)932 private static boolean isSystemProvider(String authority) { 933 switch (authority) { 934 case Providers.AUTHORITY_MEDIA: 935 case Providers.AUTHORITY_STORAGE: 936 case Providers.AUTHORITY_DOWNLOADS: 937 return true; 938 default: 939 return false; 940 } 941 } 942 943 /** 944 * @param operation 945 * @param providerType 946 * @return An opcode, suitable for use as histogram bucket, for the given operation/provider 947 * combination. 948 */ getOpCode(@pType int operation, @Provider int providerType)949 private static @FileOp int getOpCode(@OpType int operation, @Provider int providerType) { 950 switch (operation) { 951 case FileOperationService.OPERATION_COPY: 952 switch (providerType) { 953 case PROVIDER_INTRA: 954 return FILEOP_COPY_INTRA_PROVIDER; 955 case PROVIDER_SYSTEM: 956 return FILEOP_COPY_SYSTEM_PROVIDER; 957 case PROVIDER_EXTERNAL: 958 return FILEOP_COPY_EXTERNAL_PROVIDER; 959 } 960 case FileOperationService.OPERATION_COMPRESS: 961 switch (providerType) { 962 case PROVIDER_INTRA: 963 return FILEOP_COMPRESS_INTRA_PROVIDER; 964 case PROVIDER_SYSTEM: 965 return FILEOP_COMPRESS_SYSTEM_PROVIDER; 966 case PROVIDER_EXTERNAL: 967 return FILEOP_COMPRESS_EXTERNAL_PROVIDER; 968 } 969 case FileOperationService.OPERATION_EXTRACT: 970 switch (providerType) { 971 case PROVIDER_INTRA: 972 return FILEOP_EXTRACT_INTRA_PROVIDER; 973 case PROVIDER_SYSTEM: 974 return FILEOP_EXTRACT_SYSTEM_PROVIDER; 975 case PROVIDER_EXTERNAL: 976 return FILEOP_EXTRACT_EXTERNAL_PROVIDER; 977 } 978 case FileOperationService.OPERATION_MOVE: 979 switch (providerType) { 980 case PROVIDER_INTRA: 981 return FILEOP_MOVE_INTRA_PROVIDER; 982 case PROVIDER_SYSTEM: 983 return FILEOP_MOVE_SYSTEM_PROVIDER; 984 case PROVIDER_EXTERNAL: 985 return FILEOP_MOVE_EXTERNAL_PROVIDER; 986 } 987 case FileOperationService.OPERATION_DELETE: 988 return FILEOP_DELETE; 989 default: 990 Log.w(TAG, "Unrecognized operation type when logging a file operation"); 991 return FILEOP_OTHER; 992 } 993 } 994 995 /** 996 * Maps FileOperationService OpType values, to MetricsOpType values. 997 */ toMetricsOpType(@pType int operation)998 private static @MetricsOpType int toMetricsOpType(@OpType int operation) { 999 switch (operation) { 1000 case FileOperationService.OPERATION_COPY: 1001 return OPERATION_COPY; 1002 case FileOperationService.OPERATION_MOVE: 1003 return OPERATION_MOVE; 1004 case FileOperationService.OPERATION_DELETE: 1005 return OPERATION_DELETE; 1006 case FileOperationService.OPERATION_UNKNOWN: 1007 default: 1008 return OPERATION_UNKNOWN; 1009 } 1010 } 1011 toMetricsAction(int action)1012 private static @MetricsAction int toMetricsAction(int action) { 1013 switch(action) { 1014 case State.ACTION_OPEN: 1015 return ACTION_OPEN; 1016 case State.ACTION_CREATE: 1017 return ACTION_CREATE; 1018 case State.ACTION_GET_CONTENT: 1019 return ACTION_GET_CONTENT; 1020 case State.ACTION_OPEN_TREE: 1021 return ACTION_OPEN_TREE; 1022 case State.ACTION_BROWSE: 1023 return ACTION_BROWSE; 1024 case State.ACTION_PICK_COPY_DESTINATION: 1025 return ACTION_PICK_COPY_DESTINATION; 1026 default: 1027 return ACTION_OTHER; 1028 } 1029 } 1030 1031 /** 1032 * Count the given src documents and provide a tally of how many come from the same provider as 1033 * the dst document (if a dst is provided), how many come from system providers, and how many 1034 * come from external 3rd-party providers. 1035 */ countProviders( ProviderCounts counts, List<DocumentInfo> srcs, @Nullable DocumentInfo dst)1036 private static void countProviders( 1037 ProviderCounts counts, List<DocumentInfo> srcs, @Nullable DocumentInfo dst) { 1038 for (DocumentInfo doc: srcs) { 1039 countForAuthority(counts, doc.authority, dst); 1040 } 1041 } 1042 1043 /** 1044 * Count the given uris and provide a tally of how many come from the same provider as 1045 * the dst document (if a dst is provided), how many come from system providers, and how many 1046 * come from external 3rd-party providers. 1047 */ countProviders(ProviderCounts counts, List<Uri> uris)1048 private static void countProviders(ProviderCounts counts, List<Uri> uris) { 1049 for (Uri uri: uris) { 1050 countForAuthority(counts, uri.getAuthority(), null); 1051 } 1052 } 1053 countForAuthority( ProviderCounts counts, String authority, @Nullable DocumentInfo dst)1054 private static void countForAuthority( 1055 ProviderCounts counts, String authority, @Nullable DocumentInfo dst) { 1056 if (dst != null && authority.equals(dst.authority)) { 1057 counts.intraProvider++; 1058 } else if (isSystemProvider(authority)){ 1059 counts.systemProvider++; 1060 } else { 1061 counts.externalProvider++; 1062 } 1063 } 1064 1065 private static class ProviderCounts { 1066 int intraProvider; 1067 int systemProvider; 1068 int externalProvider; 1069 } 1070 } 1071