diff options
author | Danny Baumann <dannybaumann@web.de> | 2016-02-01 14:06:44 +0100 |
---|---|---|
committer | Danny Baumann <dannybaumann@web.de> | 2016-02-10 13:18:21 +0100 |
commit | 9cf4db0d7d71989d8ad70d27ac1665ed72046d14 (patch) | |
tree | 0240f2f9ee89bbff0f1b187d100b800752ab7aab /src/com/android/services | |
parent | 8f517b2c28cb29620fb32f1f8904b24528dde18d (diff) | |
download | android_packages_apps_Dialer-9cf4db0d7d71989d8ad70d27ac1665ed72046d14.tar.gz android_packages_apps_Dialer-9cf4db0d7d71989d8ad70d27ac1665ed72046d14.tar.bz2 android_packages_apps_Dialer-9cf4db0d7d71989d8ad70d27ac1665ed72046d14.zip |
Re-add call recording feature.
Change-Id: I47e9c49db56c75e5c5f491d2ef923039f7a7f519
Diffstat (limited to 'src/com/android/services')
5 files changed, 538 insertions, 0 deletions
diff --git a/src/com/android/services/callrecorder/CallRecorderService.java b/src/com/android/services/callrecorder/CallRecorderService.java new file mode 100644 index 000000000..7b5602365 --- /dev/null +++ b/src/com/android/services/callrecorder/CallRecorderService.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.services.callrecorder; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.media.MediaRecorder; +import android.media.MediaScannerConnection; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.text.TextUtils; +import android.provider.Settings; +import android.util.Log; + +import com.android.services.callrecorder.common.CallRecording; +import com.android.services.callrecorder.common.ICallRecorderService; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import com.android.dialer.R; + +public class CallRecorderService extends Service { + private static final String TAG = "CallRecorderService"; + private static final boolean DBG = false; + + private static enum RecorderState { + IDLE, + RECORDING + }; + + private MediaRecorder mMediaRecorder = null; + private RecorderState mState = RecorderState.IDLE; + private CallRecording mCurrentRecording = null; + + private static final String AUDIO_SOURCE_PROPERTY = "persist.call_recording.src"; + + private SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyMMdd_HHmmssSSS"); + + private final ICallRecorderService.Stub mBinder = new ICallRecorderService.Stub() { + @Override + public CallRecording stopRecording() { + if (getState() == RecorderState.RECORDING) { + stopRecordingInternal(); + return mCurrentRecording; + } + return null; + } + + @Override + public boolean startRecording(String phoneNumber, long creationTime) + throws RemoteException { + String fileName = generateFilename(phoneNumber); + mCurrentRecording = new CallRecording(phoneNumber, creationTime, + fileName, System.currentTimeMillis()); + return startRecordingInternal(mCurrentRecording.getFile()); + + } + + @Override + public boolean isRecording() throws RemoteException { + return getState() == RecorderState.RECORDING; + } + + @Override + public CallRecording getActiveRecording() throws RemoteException { + return mCurrentRecording; + } + }; + + @Override + public void onCreate() { + if (DBG) Log.d(TAG, "Creating CallRecorderService"); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + private int getAudioSource() { + int defaultValue = getResources().getInteger(R.integer.call_recording_audio_source); + return SystemProperties.getInt(AUDIO_SOURCE_PROPERTY, defaultValue); + } + + private int getAudioFormatChoice() { + // This replicates PreferenceManager.getDefaultSharedPreferences, except + // that we need multi process preferences, as the pref is written in a separate + // process (com.android.dialer vs. com.android.incallui) + final String prefName = getPackageName() + "_preferences"; + final SharedPreferences prefs = getSharedPreferences(prefName, MODE_MULTI_PROCESS); + + try { + String value = prefs.getString(getString(R.string.call_recording_format_key), null); + if (value != null) { + return Integer.parseInt(value); + } + } catch (NumberFormatException e) { + // ignore and fall through + } + return 0; + } + + private synchronized boolean startRecordingInternal(File file) { + if (mMediaRecorder != null) { + if (DBG) { + Log.d(TAG, "Start called with recording in progress, stopping current recording"); + } + stopRecordingInternal(); + } + + if (checkSelfPermission(android.Manifest.permission.RECORD_AUDIO) + != PackageManager.PERMISSION_GRANTED) { + Log.w(TAG, "Record audio permission not granted, can't record call"); + return false; + } + if (checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + Log.w(TAG, "External storage permission not granted, can't save recorded call"); + return false; + } + + if (DBG) Log.d(TAG, "Starting recording"); + + mMediaRecorder = new MediaRecorder(); + try { + int audioSource = getAudioSource(); + int formatChoice = getAudioFormatChoice(); + if (DBG) Log.d(TAG, "Creating media recorder with audio source " + audioSource); + mMediaRecorder.setAudioSource(audioSource); + mMediaRecorder.setOutputFormat(formatChoice == 0 + ? MediaRecorder.OutputFormat.AMR_WB : MediaRecorder.OutputFormat.MPEG_4); + mMediaRecorder.setAudioEncoder(formatChoice == 0 + ? MediaRecorder.AudioEncoder.AMR_WB : MediaRecorder.AudioEncoder.HE_AAC); + } catch (IllegalStateException e) { + Log.w(TAG, "Error initializing media recorder", e); + return false; + } + + file.getParentFile().mkdirs(); + String outputPath = file.getAbsolutePath(); + if (DBG) Log.d(TAG, "Writing output to file " + outputPath); + + try { + mMediaRecorder.setOutputFile(outputPath); + mMediaRecorder.prepare(); + mMediaRecorder.start(); + mState = RecorderState.RECORDING; + return true; + } catch (IOException e) { + Log.w(TAG, "Could not start recording for file " + outputPath, e); + } catch (IllegalStateException e) { + Log.w(TAG, "Could not start recording for file " + outputPath, e); + } catch (RuntimeException e) { + // only catch exceptions thrown by the MediaRecorder JNI code + if (e.getMessage().indexOf("start failed") >= 0) { + Log.w(TAG, "Could not start recording for file " + outputPath, e); + } else { + throw e; + } + } + + mMediaRecorder.reset(); + mMediaRecorder.release(); + mMediaRecorder = null; + + return false; + } + + private synchronized void stopRecordingInternal() { + if (DBG) Log.d(TAG, "Stopping current recording"); + if (mMediaRecorder != null) { + try { + if (getState() == RecorderState.RECORDING) { + mMediaRecorder.stop(); + mMediaRecorder.reset(); + mMediaRecorder.release(); + } + } catch (IllegalStateException e) { + Log.e(TAG, "Exception closing media recorder", e); + } + MediaScannerConnection.scanFile(this, new String[] { + mCurrentRecording.fileName + }, null, null); + mMediaRecorder = null; + mState = RecorderState.IDLE; + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (DBG) Log.d(TAG, "Destroying CallRecorderService"); + } + + private synchronized RecorderState getState() { + return mState; + } + + private String generateFilename(String number) { + String timestamp = DATE_FORMAT.format(new Date()); + + if (TextUtils.isEmpty(number)) { + number = "unknown"; + } + + int formatChoice = getAudioFormatChoice(); + String extension = formatChoice == 0 ? ".amr" : ".m4a"; + return number + "_" + timestamp + extension; + } + + public static boolean isEnabled(Context context) { + return context.getResources().getBoolean(R.bool.call_recording_enabled); + } +} diff --git a/src/com/android/services/callrecorder/CallRecordingDataStore.java b/src/com/android/services/callrecorder/CallRecordingDataStore.java new file mode 100644 index 000000000..4d148a4fb --- /dev/null +++ b/src/com/android/services/callrecorder/CallRecordingDataStore.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.services.callrecorder; + +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteStatement; +import android.provider.BaseColumns; +import android.util.Log; + +import com.android.services.callrecorder.common.CallRecording; + +import java.util.ArrayList; +import java.util.List; + +/** + * Persistent data store for call recordings. Usage: + * open() + * read/write operations + * close() + */ +public class CallRecordingDataStore { + private static final String TAG = "CallRecordingStore"; + private SQLiteOpenHelper mOpenHelper = null; + private SQLiteDatabase mDatabase = null; + + /** + * Open before reading/writing. Will not open handle if one is already open. + */ + public void open(Context context) { + if (mDatabase == null) { + mOpenHelper = new CallRecordingSQLiteOpenHelper(context); + mDatabase = mOpenHelper.getWritableDatabase(); + } + } + + /** + * close when finished reading/writing + */ + public void close() { + if (mDatabase != null) { + mDatabase.close(); + } + if (mOpenHelper != null) { + mOpenHelper.close(); + } + mDatabase = null; + mOpenHelper = null; + } + + /** + * Save a recording in the data store + * + * @param recording the recording to store + */ + public void putRecording(CallRecording recording) { + final String insertSql = "INSERT INTO " + + CallRecordingsContract.CallRecording.TABLE_NAME + " (" + + CallRecordingsContract.CallRecording.COLUMN_NAME_PHONE_NUMBER + ", " + + CallRecordingsContract.CallRecording.COLUMN_NAME_CALL_DATE + ", " + + CallRecordingsContract.CallRecording.COLUMN_NAME_RECORDING_FILENAME + ", " + + CallRecordingsContract.CallRecording.COLUMN_NAME_CREATION_DATE + ") " + + " VALUES (?, ?, ?, ?)"; + + try { + SQLiteStatement stmt = mDatabase.compileStatement(insertSql); + int idx = 1; + stmt.bindString(idx++, recording.phoneNumber); + stmt.bindLong(idx++, recording.creationTime); + stmt.bindString(idx++, recording.fileName); + stmt.bindLong(idx++, System.currentTimeMillis()); + long id = stmt.executeInsert(); + Log.i(TAG, "Saved recording " + recording + " with id " + id); + } catch (SQLiteException e) { + Log.w(TAG, "Failed to save recording " + recording, e); + } + } + + /** + * Get all recordings associated with a phone call + * + * @param phoneNumber phone number no spaces + * @param callCreationDate time that the call was created + * @return list of recordings + */ + public List<CallRecording> getRecordings(String phoneNumber, long callCreationDate) { + List<CallRecording> resultList = new ArrayList<CallRecording>(); + + final String query = "SELECT " + + CallRecordingsContract.CallRecording.COLUMN_NAME_RECORDING_FILENAME + "," + + CallRecordingsContract.CallRecording.COLUMN_NAME_CREATION_DATE + + " FROM " + CallRecordingsContract.CallRecording.TABLE_NAME + + " WHERE " + CallRecordingsContract.CallRecording.COLUMN_NAME_PHONE_NUMBER + " = ?" + + " AND " + CallRecordingsContract.CallRecording.COLUMN_NAME_CALL_DATE + " = ?" + + " ORDER BY " + CallRecordingsContract.CallRecording.COLUMN_NAME_CREATION_DATE; + + String args[] = { + phoneNumber, String.valueOf(callCreationDate) + }; + + try { + Cursor cursor = mDatabase.rawQuery(query, args); + while (cursor.moveToNext()) { + String fileName = cursor.getString(0); + long creationDate = cursor.getLong(1); + CallRecording recording = + new CallRecording(phoneNumber, callCreationDate, fileName, creationDate); + if (recording.getFile().exists()) { + resultList.add(recording); + } + } + cursor.close(); + } catch (SQLiteException e) { + Log.w(TAG, "Failed to fetch recordings for number " + phoneNumber + + ", date " + callCreationDate, e); + } + + return resultList; + } + + static class CallRecordingsContract { + static interface CallRecording extends BaseColumns { + static final String TABLE_NAME = "call_recordings"; + static final String COLUMN_NAME_PHONE_NUMBER = "phone_number"; + static final String COLUMN_NAME_CALL_DATE = "call_date"; + static final String COLUMN_NAME_RECORDING_FILENAME = "recording_filename"; + static final String COLUMN_NAME_CREATION_DATE = "creation_date"; + } + } + + static class CallRecordingSQLiteOpenHelper extends SQLiteOpenHelper { + private static final int VERSION = 1; + private static final String DB_NAME = "callrecordings.db"; + + public CallRecordingSQLiteOpenHelper(Context context) { + super(context, DB_NAME, null, VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + CallRecordingsContract.CallRecording.TABLE_NAME + " (" + + CallRecordingsContract.CallRecording._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + CallRecordingsContract.CallRecording.COLUMN_NAME_PHONE_NUMBER + " TEXT," + + CallRecordingsContract.CallRecording.COLUMN_NAME_CALL_DATE + " LONG," + + CallRecordingsContract.CallRecording.COLUMN_NAME_RECORDING_FILENAME + " TEXT, " + + CallRecordingsContract.CallRecording.COLUMN_NAME_CREATION_DATE + " LONG" + + ");" + ); + + db.execSQL("CREATE INDEX IF NOT EXISTS phone_number_call_date_index ON " + + CallRecordingsContract.CallRecording.TABLE_NAME + " (" + + CallRecordingsContract.CallRecording.COLUMN_NAME_PHONE_NUMBER + ", " + + CallRecordingsContract.CallRecording.COLUMN_NAME_CALL_DATE + ");" + ); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // implement if we change the schema + } + } +} diff --git a/src/com/android/services/callrecorder/common/CallRecording.aidl b/src/com/android/services/callrecorder/common/CallRecording.aidl new file mode 100644 index 000000000..5ab84a09e --- /dev/null +++ b/src/com/android/services/callrecorder/common/CallRecording.aidl @@ -0,0 +1,3 @@ +package com.android.services.callrecorder.common; + +parcelable CallRecording; diff --git a/src/com/android/services/callrecorder/common/CallRecording.java b/src/com/android/services/callrecorder/common/CallRecording.java new file mode 100644 index 000000000..0bb192f8d --- /dev/null +++ b/src/com/android/services/callrecorder/common/CallRecording.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.services.callrecorder.common; + +import android.os.Environment; +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.File; + +public final class CallRecording implements Parcelable { + public String phoneNumber; + public long creationTime; + public String fileName; + public long startRecordingTime; + + private static final String PUBLIC_DIRECTORY_NAME = "CallRecordings"; + + public static final Parcelable.Creator<CallRecording> CREATOR = new + Parcelable.Creator<CallRecording>() { + public CallRecording createFromParcel(Parcel in) { + return new CallRecording(in); + } + + public CallRecording[] newArray(int size) { + return new CallRecording[size]; + } + }; + + public CallRecording(String phoneNumber, long creationTime, + String fileName, long startRecordingTime) { + this.phoneNumber = phoneNumber; + this.creationTime = creationTime; + this.fileName = fileName; + this.startRecordingTime = startRecordingTime; + } + + public CallRecording(Parcel in) { + phoneNumber = in.readString(); + creationTime = in.readLong(); + fileName = in.readString(); + startRecordingTime = in.readLong(); + } + + public File getFile() { + File dir = Environment.getExternalStoragePublicDirectory(PUBLIC_DIRECTORY_NAME); + return new File(dir, fileName); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeString(phoneNumber); + out.writeLong(creationTime); + out.writeString(fileName); + out.writeLong(startRecordingTime); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "phoneNumber=" + phoneNumber + ", creationTime=" + creationTime + + ", fileName=" + fileName + ", startRecordingTime=" + startRecordingTime; + } +} diff --git a/src/com/android/services/callrecorder/common/ICallRecorderService.aidl b/src/com/android/services/callrecorder/common/ICallRecorderService.aidl new file mode 100644 index 000000000..d6d96d712 --- /dev/null +++ b/src/com/android/services/callrecorder/common/ICallRecorderService.aidl @@ -0,0 +1,39 @@ +package com.android.services.callrecorder.common; + +import com.android.services.callrecorder.common.CallRecording; + +/** + * Service for recording phone calls. Only one recording may be active at a time + * (i.e. every call to startRecording should be followed by a call to stopRecording). + */ +interface ICallRecorderService { + + /** + * Start a recording. + * + * @return true if recording started successfully + */ + boolean startRecording(String phoneNumber, long creationTime); + + /** + * stops the current recording + * + * @return call recording data including the output filename + */ + CallRecording stopRecording(); + + /** + * Recording status + * + * @return true if there is an active recording + */ + boolean isRecording(); + + /** + * Get recording currently in progress + * + * @return call recording object + */ + CallRecording getActiveRecording(); + +} |