Home | History | Annotate | Download | only in audio
      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 
     17 package com.android.server.audio;
     18 
     19 import android.content.Context;
     20 import android.content.pm.PackageManager;
     21 import android.media.AudioFormat;
     22 import android.media.AudioManager;
     23 import android.media.AudioPlaybackConfiguration;
     24 import android.media.AudioRecordingConfiguration;
     25 import android.media.AudioSystem;
     26 import android.media.IRecordingConfigDispatcher;
     27 import android.media.MediaRecorder;
     28 import android.os.IBinder;
     29 import android.os.RemoteException;
     30 import android.util.Log;
     31 
     32 import java.io.PrintWriter;
     33 import java.text.DateFormat;
     34 import java.util.ArrayList;
     35 import java.util.Date;
     36 import java.util.HashMap;
     37 import java.util.Iterator;
     38 import java.util.List;
     39 
     40 /**
     41  * Class to receive and dispatch updates from AudioSystem about recording configurations.
     42  */
     43 public 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