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