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
17package com.android.server.audio;
18
19import android.content.Context;
20import android.content.pm.PackageManager;
21import android.media.AudioFormat;
22import android.media.AudioManager;
23import android.media.AudioPlaybackConfiguration;
24import android.media.AudioRecordingConfiguration;
25import android.media.AudioSystem;
26import android.media.IRecordingConfigDispatcher;
27import android.media.MediaRecorder;
28import android.os.IBinder;
29import android.os.RemoteException;
30import android.util.Log;
31
32import java.io.PrintWriter;
33import java.text.DateFormat;
34import java.util.ArrayList;
35import java.util.Date;
36import java.util.HashMap;
37import java.util.Iterator;
38import java.util.List;
39
40/**
41 * Class to receive and dispatch updates from AudioSystem about recording configurations.
42 */
43public final class RecordingActivityMonitor implements AudioSystem.AudioRecordingCallback {
44
45    public final static String TAG = "AudioService.RecordingActivityMonitor";
46
47    private ArrayList<RecMonitorClient> mClients = new ArrayList<RecMonitorClient>();
48    // a public client is one that needs an anonymized version of the playback configurations, we
49    // keep track of whether there is at least one to know when we need to create the list of
50    // playback configurations that do not contain uid/package name information.
51    private boolean mHasPublicClients = false;
52
53    private HashMap<Integer, AudioRecordingConfiguration> mRecordConfigs =
54            new HashMap<Integer, AudioRecordingConfiguration>();
55
56    private final PackageManager mPackMan;
57
58    RecordingActivityMonitor(Context ctxt) {
59        RecMonitorClient.sMonitor = this;
60        mPackMan = ctxt.getPackageManager();
61    }
62
63    /**
64     * Implementation of android.media.AudioSystem.AudioRecordingCallback
65     */
66    public void onRecordingConfigurationChanged(int event, int uid, int session, int source,
67            int[] recordingInfo, String packName) {
68        if (MediaRecorder.isSystemOnlyAudioSource(source)) {
69            return;
70        }
71        final List<AudioRecordingConfiguration> configsSystem =
72                updateSnapshot(event, uid, session, source, recordingInfo);
73        if (configsSystem != null){
74            synchronized (mClients) {
75                // list of recording configurations for "public consumption". It is only computed if
76                // there are non-system recording activity listeners.
77                final List<AudioRecordingConfiguration> configsPublic = mHasPublicClients ?
78                        anonymizeForPublicConsumption(configsSystem) :
79                            new ArrayList<AudioRecordingConfiguration>();
80                final Iterator<RecMonitorClient> clientIterator = mClients.iterator();
81                while (clientIterator.hasNext()) {
82                    final RecMonitorClient rmc = clientIterator.next();
83                    try {
84                        if (rmc.mIsPrivileged) {
85                            rmc.mDispatcherCb.dispatchRecordingConfigChange(configsSystem);
86                        } else {
87                            rmc.mDispatcherCb.dispatchRecordingConfigChange(configsPublic);
88                        }
89                    } catch (RemoteException e) {
90                        Log.w(TAG, "Could not call dispatchRecordingConfigChange() on client", e);
91                    }
92                }
93            }
94        }
95    }
96
97    protected void dump(PrintWriter pw) {
98        // players
99        pw.println("\nRecordActivityMonitor dump time: "
100                + DateFormat.getTimeInstance().format(new Date()));
101        synchronized(mRecordConfigs) {
102            for (AudioRecordingConfiguration conf : mRecordConfigs.values()) {
103                conf.dump(pw);
104            }
105        }
106        pw.println("\n");
107        // log
108        sEventLogger.dump(pw);
109    }
110
111    private ArrayList<AudioRecordingConfiguration> anonymizeForPublicConsumption(
112            List<AudioRecordingConfiguration> sysConfigs) {
113        ArrayList<AudioRecordingConfiguration> publicConfigs =
114                new ArrayList<AudioRecordingConfiguration>();
115        // only add active anonymized configurations,
116        for (AudioRecordingConfiguration config : sysConfigs) {
117            publicConfigs.add(AudioRecordingConfiguration.anonymizedCopy(config));
118        }
119        return publicConfigs;
120    }
121
122    void initMonitor() {
123        AudioSystem.setRecordingCallback(this);
124    }
125
126    void registerRecordingCallback(IRecordingConfigDispatcher rcdb, boolean isPrivileged) {
127        if (rcdb == null) {
128            return;
129        }
130        synchronized (mClients) {
131            final RecMonitorClient rmc = new RecMonitorClient(rcdb, isPrivileged);
132            if (rmc.init()) {
133                if (!isPrivileged) {
134                    mHasPublicClients = true;
135                }
136                mClients.add(rmc);
137            }
138        }
139    }
140
141    void unregisterRecordingCallback(IRecordingConfigDispatcher rcdb) {
142        if (rcdb == null) {
143            return;
144        }
145        synchronized (mClients) {
146            final Iterator<RecMonitorClient> clientIterator = mClients.iterator();
147            boolean hasPublicClients = false;
148            while (clientIterator.hasNext()) {
149                RecMonitorClient rmc = clientIterator.next();
150                if (rcdb.equals(rmc.mDispatcherCb)) {
151                    rmc.release();
152                    clientIterator.remove();
153                } else {
154                    if (!rmc.mIsPrivileged) {
155                        hasPublicClients = true;
156                    }
157                }
158            }
159            mHasPublicClients = hasPublicClients;
160        }
161    }
162
163    List<AudioRecordingConfiguration> getActiveRecordingConfigurations(boolean isPrivileged) {
164        synchronized(mRecordConfigs) {
165            if (isPrivileged) {
166                return new ArrayList<AudioRecordingConfiguration>(mRecordConfigs.values());
167            } else {
168                final List<AudioRecordingConfiguration> configsPublic =
169                        anonymizeForPublicConsumption(
170                            new ArrayList<AudioRecordingConfiguration>(mRecordConfigs.values()));
171                return configsPublic;
172            }
173        }
174    }
175
176    /**
177     * Update the internal "view" of the active recording sessions
178     * @param event
179     * @param session
180     * @param source
181     * @param recordingFormat see
182     *     {@link AudioSystem.AudioRecordingCallback#onRecordingConfigurationChanged(int, int, int, int[])}
183     *     for the definition of the contents of the array
184     * @return null if the list of active recording sessions has not been modified, a list
185     *     with the current active configurations otherwise.
186     */
187    private List<AudioRecordingConfiguration> updateSnapshot(int event, int uid, int session,
188            int source, int[] recordingInfo) {
189        final boolean configChanged;
190        final ArrayList<AudioRecordingConfiguration> configs;
191        synchronized(mRecordConfigs) {
192            switch (event) {
193            case AudioManager.RECORD_CONFIG_EVENT_STOP:
194                // return failure if an unknown recording session stopped
195                configChanged = (mRecordConfigs.remove(new Integer(session)) != null);
196                if (configChanged) {
197                    sEventLogger.log(new RecordingEvent(event, uid, session, source, null));
198                }
199                break;
200            case AudioManager.RECORD_CONFIG_EVENT_START:
201                final AudioFormat clientFormat = new AudioFormat.Builder()
202                        .setEncoding(recordingInfo[0])
203                        // FIXME this doesn't support index-based masks
204                        .setChannelMask(recordingInfo[1])
205                        .setSampleRate(recordingInfo[2])
206                        .build();
207                final AudioFormat deviceFormat = new AudioFormat.Builder()
208                        .setEncoding(recordingInfo[3])
209                        // FIXME this doesn't support index-based masks
210                        .setChannelMask(recordingInfo[4])
211                        .setSampleRate(recordingInfo[5])
212                        .build();
213                final int patchHandle = recordingInfo[6];
214                final Integer sessionKey = new Integer(session);
215
216                final String[] packages = mPackMan.getPackagesForUid(uid);
217                final String packageName;
218                if (packages != null && packages.length > 0) {
219                    packageName = packages[0];
220                } else {
221                    packageName = "";
222                }
223                final AudioRecordingConfiguration updatedConfig =
224                        new AudioRecordingConfiguration(uid, session, source,
225                                clientFormat, deviceFormat, patchHandle, packageName);
226
227                if (mRecordConfigs.containsKey(sessionKey)) {
228                    if (updatedConfig.equals(mRecordConfigs.get(sessionKey))) {
229                        configChanged = false;
230                    } else {
231                        // config exists but has been modified
232                        mRecordConfigs.remove(sessionKey);
233                        mRecordConfigs.put(sessionKey, updatedConfig);
234                        configChanged = true;
235                    }
236                } else {
237                    mRecordConfigs.put(sessionKey, updatedConfig);
238                    configChanged = true;
239                }
240                if (configChanged) {
241                    sEventLogger.log(new RecordingEvent(event, uid, session, source, packageName));
242                }
243                break;
244            default:
245                Log.e(TAG, String.format("Unknown event %d for session %d, source %d",
246                        event, session, source));
247                configChanged = false;
248            }
249            if (configChanged) {
250                configs = new ArrayList<AudioRecordingConfiguration>(mRecordConfigs.values());
251            } else {
252                configs = null;
253            }
254        }
255        return configs;
256    }
257
258    /**
259     * Inner class to track clients that want to be notified of recording updates
260     */
261    private final static class RecMonitorClient implements IBinder.DeathRecipient {
262
263        // can afford to be static because only one RecordingActivityMonitor ever instantiated
264        static RecordingActivityMonitor sMonitor;
265
266        final IRecordingConfigDispatcher mDispatcherCb;
267        final boolean mIsPrivileged;
268
269        RecMonitorClient(IRecordingConfigDispatcher rcdb, boolean isPrivileged) {
270            mDispatcherCb = rcdb;
271            mIsPrivileged = isPrivileged;
272        }
273
274        public void binderDied() {
275            Log.w(TAG, "client died");
276            sMonitor.unregisterRecordingCallback(mDispatcherCb);
277        }
278
279        boolean init() {
280            try {
281                mDispatcherCb.asBinder().linkToDeath(this, 0);
282                return true;
283            } catch (RemoteException e) {
284                Log.w(TAG, "Could not link to client death", e);
285                return false;
286            }
287        }
288
289        void release() {
290            mDispatcherCb.asBinder().unlinkToDeath(this, 0);
291        }
292    }
293
294    /**
295     * Inner class for recording event logging
296     */
297    private static final class RecordingEvent extends AudioEventLogger.Event {
298        private final int mRecEvent;
299        private final int mClientUid;
300        private final int mSession;
301        private final int mSource;
302        private final String mPackName;
303
304        RecordingEvent(int event, int uid, int session, int source, String packName) {
305            mRecEvent = event;
306            mClientUid = uid;
307            mSession = session;
308            mSource = source;
309            mPackName = packName;
310        }
311
312        @Override
313        public String eventToString() {
314            return new StringBuilder("rec ").append(
315                        mRecEvent == AudioManager.RECORD_CONFIG_EVENT_START ? "start" : "stop ")
316                    .append(" uid:").append(mClientUid)
317                    .append(" session:").append(mSession)
318                    .append(" src:").append(MediaRecorder.toLogFriendlyAudioSource(mSource))
319                    .append(mPackName == null ? "" : " pack:" + mPackName).toString();
320        }
321    }
322
323    private static final AudioEventLogger sEventLogger = new AudioEventLogger(50,
324            "recording activity as reported through AudioSystem.AudioRecordingCallback");
325}
326