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