1 /* 2 * Copyright (C) 2019 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.internal.compat; 18 19 import static android.text.TextUtils.formatSimple; 20 21 import static java.util.Collections.EMPTY_SET; 22 23 import android.annotation.IntDef; 24 import android.util.Log; 25 import android.util.Slog; 26 27 import com.android.internal.annotations.VisibleForTesting; 28 import com.android.internal.compat.flags.Flags; 29 import com.android.internal.util.FrameworkStatsLog; 30 31 import java.lang.annotation.Retention; 32 import java.lang.annotation.RetentionPolicy; 33 import java.util.Collections; 34 import java.util.HashSet; 35 import java.util.Objects; 36 import java.util.Set; 37 import java.util.concurrent.ConcurrentHashMap; 38 import java.util.function.Function; 39 40 /** 41 * A helper class to report changes to stats log. 42 * 43 * @hide 44 */ 45 @android.ravenwood.annotation.RavenwoodKeepWholeClass 46 public class ChangeReporter { 47 private static final String TAG = "CompatChangeReporter"; 48 private static final Function<Integer, Set<ChangeReport>> NEW_CHANGE_REPORT_SET = 49 uid -> Collections.synchronizedSet(new HashSet<>()); 50 private int mSource; 51 52 private static final class ChangeReport { 53 long mChangeId; 54 int mState; 55 ChangeReport(long changeId, @State int state)56 ChangeReport(long changeId, @State int state) { 57 mChangeId = changeId; 58 mState = state; 59 } 60 61 @Override equals(Object o)62 public boolean equals(Object o) { 63 if (this == o) return true; 64 if (o == null || getClass() != o.getClass()) return false; 65 ChangeReport that = (ChangeReport) o; 66 return mChangeId == that.mChangeId 67 && mState == that.mState; 68 } 69 70 @Override hashCode()71 public int hashCode() { 72 return Objects.hash(mChangeId, mState); 73 } 74 } 75 76 // Maps uid to a set of ChangeReports (that were reported for that uid). 77 private final ConcurrentHashMap<Integer, Set<ChangeReport>> mReportedChanges; 78 79 // When true will of every time to debug (logcat). 80 private boolean mDebugLogAll; 81 ChangeReporter(@ource int source)82 public ChangeReporter(@Source int source) { 83 mSource = source; 84 mReportedChanges = new ConcurrentHashMap<>(); 85 mDebugLogAll = false; 86 } 87 88 /** 89 * Report the change to stats log and to the debug log if the change was not previously 90 * logged already. 91 * 92 * @param uid affected by the change 93 * @param changeId the reported change id 94 * @param state of the reported change - enabled/disabled/only logged 95 * @param isKnownSystemApp do we know that the affected app is a system app? (if true is 96 * definitely a system app, if false it may or may not be a system app) 97 * @param isLoggableBySdk whether debug logging is allowed for this change based on target 98 * SDK version. This is combined with other logic to determine whether 99 * to actually log. If the sdk version does not matter, should be true. 100 */ reportChange(int uid, long changeId, int state, boolean isKnownSystemApp, boolean isLoggableBySdk)101 public void reportChange(int uid, long changeId, int state, boolean isKnownSystemApp, 102 boolean isLoggableBySdk) { 103 boolean isAlreadyReported = 104 checkAndSetIsAlreadyReported(uid, new ChangeReport(changeId, state)); 105 if (shouldWriteToStatsLog(isKnownSystemApp, isAlreadyReported)) { 106 FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPATIBILITY_CHANGE_REPORTED, uid, 107 changeId, state, mSource); 108 } 109 if (shouldWriteToDebug(isAlreadyReported, state, isLoggableBySdk)) { 110 debugLog(uid, changeId, state); 111 } 112 } 113 114 /** 115 * Report the change to stats log and to the debug log if the change was not previously 116 * logged already. 117 * 118 * @param uid affected by the change 119 * @param changeId the reported change id 120 * @param state of the reported change - enabled/disabled/only logged 121 */ reportChange(int uid, long changeId, int state)122 public void reportChange(int uid, long changeId, int state) { 123 reportChange(uid, changeId, state, false, true); 124 } 125 126 /** 127 * Start logging all the time to logcat. 128 */ startDebugLogAll()129 public void startDebugLogAll() { 130 mDebugLogAll = true; 131 } 132 133 /** 134 * Stop logging all the time to logcat. 135 */ stopDebugLogAll()136 public void stopDebugLogAll() { 137 mDebugLogAll = false; 138 } 139 140 /** 141 * Returns whether the next report should be logged to FrameworkStatsLog. 142 * 143 * @param isKnownSystemApp do we know that the affected app is a system app? (if true is 144 * definitely a system app, if false it may or may not be a system app) 145 * @param isAlreadyReported is the change already reported 146 * @return true if the report should be logged 147 */ 148 @VisibleForTesting shouldWriteToStatsLog(boolean isKnownSystemApp, boolean isAlreadyReported)149 boolean shouldWriteToStatsLog(boolean isKnownSystemApp, boolean isAlreadyReported) { 150 // We don't log where we know the source is a system app or is already reported 151 return !isKnownSystemApp && !isAlreadyReported; 152 } 153 154 /** 155 * Returns whether the next report should be logged to logcat. 156 * 157 * @param isAlreadyReported is the change already reported 158 * @param state of the reported change - enabled/disabled/only logged 159 * @param isLoggableBySdk whether debug logging is allowed for this change based on target SDK 160 * version. This is combined with other logic to determine whether to 161 * actually log. If the sdk version does not matter, should be true. 162 * @return true if the report should be logged 163 */ shouldWriteToDebug( boolean isAlreadyReported, int state, boolean isLoggableBySdk)164 private boolean shouldWriteToDebug( 165 boolean isAlreadyReported, int state, boolean isLoggableBySdk) { 166 // If log all bit is on, always return true. 167 if (mDebugLogAll) return true; 168 // If the change has already been reported, do not write. 169 if (isAlreadyReported) return false; 170 171 // If the flag is turned off or the TAG's logging is forced to debug level with 172 // `adb setprop log.tag.CompatChangeReporter=DEBUG`, write to debug since the above checks 173 // have already passed. 174 boolean skipLoggingFlag = Flags.skipOldAndDisabledCompatLogging(); 175 if (!skipLoggingFlag || Log.isLoggable(TAG, Log.DEBUG)) return true; 176 177 // Log if the change is enabled and targets the latest sdk version. 178 return isLoggableBySdk && state != STATE_DISABLED; 179 } 180 181 /** 182 * Returns whether the next report should be logged to logcat. 183 * 184 * @param uid affected by the change 185 * @param changeId the reported change id 186 * @param state of the reported change - enabled/disabled/only logged 187 * 188 * @return true if the report should be logged 189 */ 190 @VisibleForTesting shouldWriteToDebug(int uid, long changeId, int state)191 boolean shouldWriteToDebug(int uid, long changeId, int state) { 192 return shouldWriteToDebug(uid, changeId, state, true); 193 } 194 195 /** 196 * Returns whether the next report should be logged to logcat. 197 * 198 * @param uid affected by the change 199 * @param changeId the reported change id 200 * @param state of the reported change - enabled/disabled/only logged 201 * @param isLoggableBySdk whether debug logging is allowed for this change based on target SDK 202 * version. This is combined with other logic to determine whether to 203 * actually log. If the sdk version does not matter, should be true. 204 * @return true if the report should be logged 205 */ 206 @VisibleForTesting shouldWriteToDebug(int uid, long changeId, int state, boolean isLoggableBySdk)207 boolean shouldWriteToDebug(int uid, long changeId, int state, boolean isLoggableBySdk) { 208 return shouldWriteToDebug( 209 isAlreadyReported(uid, new ChangeReport(changeId, state)), state, isLoggableBySdk); 210 } 211 212 /** 213 * Return if change has been reported. Also mark change as reported if not. 214 * 215 * @param uid affected by the change 216 * @param changeReport change reported to be checked and marked as reported. 217 * 218 * @return true if change has been reported, and vice versa. 219 */ checkAndSetIsAlreadyReported(int uid, ChangeReport changeReport)220 private boolean checkAndSetIsAlreadyReported(int uid, ChangeReport changeReport) { 221 boolean isAlreadyReported = isAlreadyReported(uid, changeReport); 222 if (!isAlreadyReported) { 223 markAsReported(uid, changeReport); 224 } 225 return isAlreadyReported; 226 } 227 isAlreadyReported(int uid, ChangeReport report)228 private boolean isAlreadyReported(int uid, ChangeReport report) { 229 return mReportedChanges.getOrDefault(uid, EMPTY_SET).contains(report); 230 } 231 232 /** 233 * Returns whether the next report should be logged. 234 * 235 * @param uid affected by the change 236 * @param changeId the reported change id 237 * @param state of the reported change - enabled/disabled/only logged 238 * @return true if the report should be logged 239 */ 240 @VisibleForTesting isAlreadyReported(int uid, long changeId, int state)241 boolean isAlreadyReported(int uid, long changeId, int state) { 242 return isAlreadyReported(uid, new ChangeReport(changeId, state)); 243 } 244 markAsReported(int uid, ChangeReport report)245 private void markAsReported(int uid, ChangeReport report) { 246 mReportedChanges.computeIfAbsent(uid, NEW_CHANGE_REPORT_SET).add(report); 247 } 248 249 /** 250 * Clears the saved information about a given uid. Requests to report uid again will be reported 251 * regardless to the past reports. 252 * 253 * <p> Only intended to be called from PlatformCompat. 254 * 255 * @param uid to reset 256 */ resetReportedChanges(int uid)257 public void resetReportedChanges(int uid) { 258 mReportedChanges.remove(uid); 259 } 260 debugLog(int uid, long changeId, int state)261 private void debugLog(int uid, long changeId, int state) { 262 String message = formatSimple("Compat change id reported: %d; UID %d; state: %s", changeId, 263 uid, stateToString(state)); 264 if (mSource == SOURCE_SYSTEM_SERVER) { 265 Slog.d(TAG, message); 266 } else { 267 Log.d(TAG, message); 268 } 269 } 270 271 /** 272 * Transforms {@link #ChangeReporter.State} enum to a string. 273 * 274 * @param state to transform 275 * @return a string representing the state 276 */ stateToString(@tate int state)277 private static String stateToString(@State int state) { 278 switch (state) { 279 case STATE_LOGGED: 280 return "LOGGED"; 281 case STATE_ENABLED: 282 return "ENABLED"; 283 case STATE_DISABLED: 284 return "DISABLED"; 285 default: 286 return "UNKNOWN"; 287 } 288 } 289 290 /** These values should be kept in sync with those in atoms.proto */ 291 public static final int STATE_UNKNOWN_STATE = 292 FrameworkStatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__STATE__UNKNOWN_STATE; 293 public static final int STATE_ENABLED = 294 FrameworkStatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__STATE__ENABLED; 295 public static final int STATE_DISABLED = 296 FrameworkStatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__STATE__DISABLED; 297 public static final int STATE_LOGGED = 298 FrameworkStatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__STATE__LOGGED; 299 public static final int SOURCE_UNKNOWN_SOURCE = 300 FrameworkStatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__SOURCE__UNKNOWN_SOURCE; 301 public static final int SOURCE_APP_PROCESS = 302 FrameworkStatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__SOURCE__APP_PROCESS; 303 public static final int SOURCE_SYSTEM_SERVER = 304 FrameworkStatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__SOURCE__SYSTEM_SERVER; 305 306 @Retention(RetentionPolicy.SOURCE) 307 @IntDef(prefix = { "STATE_" }, value = { 308 STATE_UNKNOWN_STATE, 309 STATE_ENABLED, 310 STATE_DISABLED, 311 STATE_LOGGED 312 }) 313 public @interface State { 314 } 315 316 @Retention(RetentionPolicy.SOURCE) 317 @IntDef(prefix = { "SOURCE_" }, value = { 318 SOURCE_UNKNOWN_SOURCE, 319 SOURCE_APP_PROCESS, 320 SOURCE_SYSTEM_SERVER 321 }) 322 public @interface Source { 323 } 324 } 325