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 android.annotation.IntDef;
20 import android.util.Log;
21 import android.util.Slog;
22 import android.util.StatsLog;
23 
24 import com.android.internal.annotations.GuardedBy;
25 import com.android.internal.annotations.VisibleForTesting;
26 
27 import java.lang.annotation.Retention;
28 import java.lang.annotation.RetentionPolicy;
29 import java.util.HashMap;
30 import java.util.HashSet;
31 import java.util.Map;
32 import java.util.Objects;
33 import java.util.Set;
34 
35 /**
36  * A helper class to report changes to stats log.
37  *
38  * @hide
39  */
40 public final class ChangeReporter {
41     private static final String TAG = "CompatibilityChangeReporter";
42     private int mSource;
43 
44     private static final class ChangeReport {
45         long mChangeId;
46         int mState;
47 
ChangeReport(long changeId, @State int state)48         ChangeReport(long changeId, @State int state) {
49             mChangeId = changeId;
50             mState = state;
51         }
52 
53         @Override
equals(Object o)54         public boolean equals(Object o) {
55             if (this == o) return true;
56             if (o == null || getClass() != o.getClass()) return false;
57             ChangeReport that = (ChangeReport) o;
58             return mChangeId == that.mChangeId
59                     && mState == that.mState;
60         }
61 
62         @Override
hashCode()63         public int hashCode() {
64             return Objects.hash(mChangeId, mState);
65         }
66     }
67 
68     // Maps uid to a set of ChangeReports (that were reported for that uid).
69     @GuardedBy("mReportedChanges")
70     private final Map<Integer, Set<ChangeReport>> mReportedChanges;
71 
72     // When true will of every time to debug (logcat).
73     private boolean mDebugLogAll;
74 
ChangeReporter(@ource int source)75     public ChangeReporter(@Source int source) {
76         mSource = source;
77         mReportedChanges =  new HashMap<>();
78         mDebugLogAll = false;
79     }
80 
81     /**
82      * Report the change to stats log and to the debug log if the change was not previously
83      * logged already.
84      *
85      * @param uid      affected by the change
86      * @param changeId the reported change id
87      * @param state    of the reported change - enabled/disabled/only logged
88      */
reportChange(int uid, long changeId, int state)89     public void reportChange(int uid, long changeId, int state) {
90         if (shouldWriteToStatsLog(uid, changeId, state)) {
91             StatsLog.write(StatsLog.APP_COMPATIBILITY_CHANGE_REPORTED, uid, changeId,
92                     state, mSource);
93         }
94         if (shouldWriteToDebug(uid, changeId, state)) {
95             debugLog(uid, changeId, state);
96         }
97         markAsReported(uid, new ChangeReport(changeId, state));
98     }
99 
100     /**
101      * Start logging all the time to logcat.
102      */
startDebugLogAll()103     public void startDebugLogAll() {
104         mDebugLogAll = true;
105     }
106 
107     /**
108      * Stop logging all the time to logcat.
109      */
stopDebugLogAll()110     public void stopDebugLogAll() {
111         mDebugLogAll = false;
112     }
113 
114 
115     /**
116      * Returns whether the next report should be logged to statsLog.
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      * @return true if the report should be logged
122      */
123     @VisibleForTesting
shouldWriteToStatsLog(int uid, long changeId, int state)124     public boolean shouldWriteToStatsLog(int uid, long changeId, int state) {
125         return !isAlreadyReported(uid, new ChangeReport(changeId, state));
126     }
127 
128     /**
129      * Returns whether the next report should be logged to logcat.
130      *
131      * @param uid      affected by the change
132      * @param changeId the reported change id
133      * @param state    of the reported change - enabled/disabled/only logged
134      * @return true if the report should be logged
135      */
136     @VisibleForTesting
shouldWriteToDebug(int uid, long changeId, int state)137     public boolean shouldWriteToDebug(int uid, long changeId, int state) {
138         return mDebugLogAll || !isAlreadyReported(uid, new ChangeReport(changeId, state));
139     }
140 
isAlreadyReported(int uid, ChangeReport report)141     private boolean isAlreadyReported(int uid, ChangeReport report) {
142         synchronized (mReportedChanges) {
143             Set<ChangeReport> reportedChangesForUid = mReportedChanges.get(uid);
144             if (reportedChangesForUid == null) {
145                 return false;
146             } else {
147                 return reportedChangesForUid.contains(report);
148             }
149         }
150     }
151 
markAsReported(int uid, ChangeReport report)152     private void markAsReported(int uid, ChangeReport report) {
153         synchronized (mReportedChanges) {
154             Set<ChangeReport> reportedChangesForUid = mReportedChanges.get(uid);
155             if (reportedChangesForUid == null) {
156                 mReportedChanges.put(uid, new HashSet<ChangeReport>());
157                 reportedChangesForUid = mReportedChanges.get(uid);
158             }
159             reportedChangesForUid.add(report);
160         }
161     }
162 
163     /**
164      * Clears the saved information about a given uid. Requests to report uid again will be reported
165      * regardless to the past reports.
166      *
167      * <p> Only intended to be called from PlatformCompat.
168      *
169      * @param uid to reset
170      */
resetReportedChanges(int uid)171     public void resetReportedChanges(int uid) {
172         synchronized (mReportedChanges) {
173             mReportedChanges.remove(uid);
174         }
175     }
176 
debugLog(int uid, long changeId, int state)177     private void debugLog(int uid, long changeId, int state) {
178         String message = String.format("Compat change id reported: %d; UID %d; state: %s", changeId,
179                 uid, stateToString(state));
180         if (mSource == SOURCE_SYSTEM_SERVER) {
181             Slog.d(TAG, message);
182         } else {
183             Log.d(TAG, message);
184         }
185 
186     }
187 
188     /**
189      * Transforms {@link #ChangeReporter.State} enum to a string.
190      *
191      * @param state to transform
192      * @return a string representing the state
193      */
stateToString(@tate int state)194     private static String stateToString(@State int state) {
195         switch (state) {
196             case STATE_LOGGED:
197                 return "LOGGED";
198             case STATE_ENABLED:
199                 return "ENABLED";
200             case STATE_DISABLED:
201                 return "DISABLED";
202             default:
203                 return "UNKNOWN";
204         }
205     }
206 
207     /** These values should be kept in sync with those in atoms.proto */
208     public static final int STATE_UNKNOWN_STATE =
209                             StatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__STATE__UNKNOWN_STATE;
210     public static final int STATE_ENABLED =
211                             StatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__STATE__ENABLED;
212     public static final int STATE_DISABLED =
213                             StatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__STATE__DISABLED;
214     public static final int STATE_LOGGED =
215                             StatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__STATE__LOGGED;
216     public static final int SOURCE_UNKNOWN_SOURCE =
217                             StatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__SOURCE__UNKNOWN_SOURCE;
218     public static final int SOURCE_APP_PROCESS =
219                             StatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__SOURCE__APP_PROCESS;
220     public static final int SOURCE_SYSTEM_SERVER =
221                             StatsLog.APP_COMPATIBILITY_CHANGE_REPORTED__SOURCE__SYSTEM_SERVER;
222 
223     @Retention(RetentionPolicy.SOURCE)
224     @IntDef(flag = true, prefix = { "STATE_" }, value = {
225             STATE_UNKNOWN_STATE,
226             STATE_ENABLED,
227             STATE_DISABLED,
228             STATE_LOGGED
229     })
230     public @interface State {
231     }
232 
233     @Retention(RetentionPolicy.SOURCE)
234     @IntDef(flag = true, prefix = { "SOURCE_" }, value = {
235             SOURCE_UNKNOWN_SOURCE,
236             SOURCE_APP_PROCESS,
237             SOURCE_SYSTEM_SERVER
238     })
239     public @interface Source {
240     }
241 }
242