diff --git a/app/src/main/java/org/linphone/LinphoneActivity.java b/app/src/main/java/org/linphone/LinphoneActivity.java index b9b6b24b9..4af08aa41 100644 --- a/app/src/main/java/org/linphone/LinphoneActivity.java +++ b/app/src/main/java/org/linphone/LinphoneActivity.java @@ -111,6 +111,7 @@ import org.linphone.purchase.InAppPurchaseActivity; import org.linphone.views.AddressText; import org.linphone.utils.LinphoneGenericActivity; import org.linphone.utils.LinphoneUtils; +import org.linphone.recording.RecordingListFragment; import org.linphone.xmlrpc.XmlRpcHelper; import org.linphone.xmlrpc.XmlRpcListenerBase; @@ -136,7 +137,7 @@ public class LinphoneActivity extends LinphoneGenericActivity implements OnClick private StatusFragment statusFragment; private TextView missedCalls, missedChats; private RelativeLayout contacts, history, dialer, chat; - private View contacts_selected, history_selected, dialer_selected, chat_selected; + private View contacts_selected, history_selected, dialer_selected, chat_selected, record_selected; private LinearLayout mTopBar; private TextView mTopBarTitle; private ImageView cancel; @@ -433,6 +434,9 @@ public class LinphoneActivity extends LinphoneGenericActivity implements OnClick case CONTACT_DEVICES: fragment = new DevicesFragment(); break; + case RECORDING_LIST: + fragment = new RecordingListFragment(); + break; default: break; } @@ -642,6 +646,10 @@ public class LinphoneActivity extends LinphoneGenericActivity implements OnClick changeCurrentFragment(FragmentsAvailable.ABOUT, null); } + public void displayRecordings() { + changeCurrentFragment(FragmentsAvailable.RECORDING_LIST, null); + } + public void displayContactsForEdition(String sipAddress, String displayName) { Bundle extras = new Bundle(); extras.putBoolean("EditOnClick", true); @@ -1611,6 +1619,7 @@ public class LinphoneActivity extends LinphoneGenericActivity implements OnClick if (getResources().getBoolean(R.bool.enable_in_app_purchase)) { sideMenuItems.add(new MenuItem(getResources().getString(R.string.inapp), R.drawable.menu_options)); } + sideMenuItems.add(new MenuItem(getResources().getString(R.string.menu_recordings), R.drawable.menu_recordings)); sideMenuItems.add(new MenuItem(getResources().getString(R.string.menu_about), R.drawable.menu_about)); sideMenuContent = findViewById(R.id.side_menu_content); sideMenuItemList = findViewById(R.id.item_list); @@ -1634,6 +1643,9 @@ public class LinphoneActivity extends LinphoneGenericActivity implements OnClick LinphoneActivity.instance().displayInapp(); } } + if (sideMenuItemList.getAdapter().getItem(i).toString().equals(getString(R.string.menu_recordings))) { + LinphoneActivity.instance().displayRecordings(); + } openOrCloseSideMenu(false); } }); diff --git a/app/src/main/java/org/linphone/LinphoneManager.java b/app/src/main/java/org/linphone/LinphoneManager.java index 620f2805e..29fb0b6a4 100644 --- a/app/src/main/java/org/linphone/LinphoneManager.java +++ b/app/src/main/java/org/linphone/LinphoneManager.java @@ -779,7 +779,7 @@ public class LinphoneManager implements CoreListener, SensorEventListener, Accou public void setHandsetMode(Boolean on) { if (mLc.isIncomingInvitePending() && on) { handsetON = true; - mLc.acceptCall(mLc.getCurrentCall()); + acceptCall(mLc.getCurrentCall()); LinphoneActivity.instance().startIncallActivity(); } else if (on && CallActivity.isInstanciated()) { handsetON = true; @@ -1148,6 +1148,10 @@ public class LinphoneManager implements CoreListener, SensorEventListener, Accou return null; } + public void setAudioManagerModeNormal() { + mAudioManager.setMode(AudioManager.MODE_NORMAL); + } + public void setAudioManagerInCallMode() { if (mAudioManager.getMode() == AudioManager.MODE_IN_COMMUNICATION) { Log.w("[AudioManager] already in MODE_IN_COMMUNICATION, skipping..."); @@ -1179,7 +1183,7 @@ public class LinphoneManager implements CoreListener, SensorEventListener, Accou public void run() { if (mLc != null) { if (mLc.getCallsNb() > 0) { - mLc.acceptCall(call); + acceptCall(call); if (LinphoneManager.getInstance() != null) { LinphoneManager.getInstance().routeAudioToReceiver(); if (LinphoneActivity.instance() != null) @@ -1471,12 +1475,22 @@ public class LinphoneManager implements CoreListener, SensorEventListener, Accou return reinviteWithVideo(); } - public boolean acceptCallIfIncomingPending() throws CoreException { - if (mLc.isIncomingInvitePending()) { - mLc.acceptCall(mLc.getCurrentCall()); - return true; + public boolean acceptCall(Call call) { + if (call == null) return false; + + CallParams params = LinphoneManager.getLc().createCallParams(call); + + boolean isLowBandwidthConnection = !LinphoneUtils.isHighBandwidthConnection(LinphoneService.instance().getApplicationContext()); + + if (params != null) { + params.enableLowBandwidth(isLowBandwidthConnection); + params.setRecordFile(FileUtils.getCallRecordingFilename(getContext(), call.getRemoteAddress())); + } else { + Log.e("Could not create call params for call"); + return false; } - return false; + + return acceptCallWithParams(call, params); } public boolean acceptCallWithParams(Call call, CallParams params) { diff --git a/app/src/main/java/org/linphone/call/CallActivity.java b/app/src/main/java/org/linphone/call/CallActivity.java index 1704586f6..4f569a441 100644 --- a/app/src/main/java/org/linphone/call/CallActivity.java +++ b/app/src/main/java/org/linphone/call/CallActivity.java @@ -101,6 +101,7 @@ public class CallActivity extends LinphoneGenericActivity implements OnClickList private static final int PERMISSIONS_REQUEST_CAMERA = 202; private static final int PERMISSIONS_ENABLED_CAMERA = 203; private static final int PERMISSIONS_ENABLED_MIC = 204; + private static final int PERMISSIONS_EXTERNAL_STORAGE = 205; private static CallActivity instance; @@ -109,14 +110,14 @@ public class CallActivity extends LinphoneGenericActivity implements OnClickList private ImageView switchCamera; private TextView missedChats; private RelativeLayout mActiveCallHeader, sideMenuContent, avatar_layout; - private ImageView pause, hangUp, dialer, video, micro, speaker, options, addCall, transfer, conference, conferenceStatus, contactPicture; + private ImageView pause, hangUp, dialer, video, micro, speaker, options, addCall, transfer, conference, conferenceStatus, contactPicture, recordCall, recording; private ImageView audioRoute, routeSpeaker, routeEarpiece, routeBluetooth, menu, chat; private LinearLayout mNoCurrentCall, callInfo, mCallPaused; private ProgressBar videoProgress; private StatusFragment status; private CallAudioFragment audioCallFragment; private CallVideoFragment videoCallFragment; - private boolean isSpeakerEnabled = false, isMicMuted = false, isTransferAllowed, isVideoAsk; + private boolean isSpeakerEnabled = false, isMicMuted = false, isTransferAllowed, isVideoAsk, isRecording = false; private LinearLayout mControlsLayout; private Numpad numpad; private int cameraNumber; @@ -194,7 +195,7 @@ public class CallActivity extends LinphoneGenericActivity implements OnClickList return; } else if (state == State.Paused || state == State.PausedByRemote || state == State.Pausing) { if (LinphoneManager.getLc().getCurrentCall() != null) { - enabledVideoButton(false); + video.setEnabled(false); } if (isVideoEnabled(call)) { showAudioView(); @@ -207,7 +208,7 @@ public class CallActivity extends LinphoneGenericActivity implements OnClickList } } if (LinphoneManager.getLc().getCurrentCall() != null) { - enabledVideoButton(true); + video.setEnabled(true); } } else if (state == State.StreamsRunning) { switchVideo(isVideoEnabled(call)); @@ -344,7 +345,7 @@ public class CallActivity extends LinphoneGenericActivity implements OnClickList //TopBar video = findViewById(R.id.video); video.setOnClickListener(this); - enabledVideoButton(false); + video.setEnabled(false); videoProgress = findViewById(R.id.video_in_progress); videoProgress.setVisibility(View.GONE); @@ -380,7 +381,7 @@ public class CallActivity extends LinphoneGenericActivity implements OnClickList pause = findViewById(R.id.pause); pause.setOnClickListener(this); - enabledPauseButton(false); + pause.setEnabled(false); mActiveCallHeader = findViewById(R.id.active_call); mNoCurrentCall = findViewById(R.id.no_current_call); @@ -402,6 +403,15 @@ public class CallActivity extends LinphoneGenericActivity implements OnClickList conference.setEnabled(false); conference.setOnClickListener(this); + recordCall = findViewById(R.id.record_call); + recordCall.setOnClickListener(this); + recordCall.setEnabled(false); + + recording = findViewById(R.id.recording); + recording.setOnClickListener(this); + recording.setEnabled(false); + recording.setVisibility(View.GONE); + try { audioRoute = findViewById(R.id.audio_route); audioRoute.setOnClickListener(this); @@ -489,6 +499,15 @@ public class CallActivity extends LinphoneGenericActivity implements OnClickList } }); break; + case PERMISSIONS_EXTERNAL_STORAGE: + LinphoneUtils.dispatchOnUIThread(new Runnable() { + @Override + public void run() { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + toggleCallRecording(!isRecording); + } + } + }); } } @@ -528,7 +547,7 @@ public class CallActivity extends LinphoneGenericActivity implements OnClickList public void refreshInCallActions() { if (!LinphonePreferences.instance().isVideoEnabled() || isConferenceRunning) { - enabledVideoButton(false); + video.setEnabled(false); } else { if (video.isEnabled()) { if (isVideoEnabled(LinphoneManager.getLc().getCurrentCall())) { @@ -582,33 +601,27 @@ public class CallActivity extends LinphoneGenericActivity implements OnClickList } //Enabled transfer button - if (isTransferAllowed && !LinphoneManager.getLc().soundResourcesLocked()) - enabledTransferButton(true); + transfer.setEnabled(isTransferAllowed && !LinphoneManager.getLc().soundResourcesLocked()); //Enable conference button - if (LinphoneManager.getLc().getCallsNb() > 1 && LinphoneManager.getLc().getCallsNb() > confsize && !LinphoneManager.getLc().soundResourcesLocked()) { - enabledConferenceButton(true); - } else { - enabledConferenceButton(false); - } + conference.setEnabled(LinphoneManager.getLc().getCallsNb() > 1 && LinphoneManager.getLc().getCallsNb() > confsize && !LinphoneManager.getLc().soundResourcesLocked()); addCall.setEnabled(LinphoneManager.getLc().getCallsNb() < LinphoneManager.getLc().getMaxCalls() && !LinphoneManager.getLc().soundResourcesLocked()); options.setEnabled(!getResources().getBoolean(R.bool.disable_options_in_call) && (addCall.isEnabled() || transfer.isEnabled())); - if (LinphoneManager.getLc().getCurrentCall() != null && LinphonePreferences.instance().isVideoEnabled() && !LinphoneManager.getLc().getCurrentCall().mediaInProgress()) { - enabledVideoButton(true); - } else { - enabledVideoButton(false); - } - if (LinphoneManager.getLc().getCurrentCall() != null && !LinphoneManager.getLc().getCurrentCall().mediaInProgress()) { - enabledPauseButton(true); - } else { - enabledPauseButton(false); - } + recordCall.setEnabled(!LinphoneManager.getLc().soundResourcesLocked()); + recordCall.setImageResource(isRecording ? R.drawable.options_rec_selected : R.drawable.options_rec_default); + + recording.setEnabled(isRecording); + recording.setVisibility(isRecording ? View.VISIBLE : View.GONE); + + + video.setEnabled(LinphoneManager.getLc().getCurrentCall() != null && LinphonePreferences.instance().isVideoEnabled() && !LinphoneManager.getLc().getCurrentCall().mediaInProgress()); + + pause.setEnabled(LinphoneManager.getLc().getCurrentCall() != null && !LinphoneManager.getLc().getCurrentCall().mediaInProgress()); + micro.setEnabled(true); - if (!isTablet()) { - speaker.setEnabled(true); - } + speaker.setEnabled(!isTablet()); transfer.setEnabled(true); pause.setEnabled(true); dialer.setEnabled(true); @@ -649,6 +662,17 @@ public class CallActivity extends LinphoneGenericActivity implements OnClickList toggleSpeaker(); } else if (id == R.id.add_call) { goBackToDialer(); + } else if (id == R.id.record_call) { + int externalStorage = getPackageManager().checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, getPackageName()); + Log.i("[Permission] External storage permission is " + (externalStorage == PackageManager.PERMISSION_GRANTED ? "granted" : "denied")); + + if (externalStorage == PackageManager.PERMISSION_GRANTED) { + toggleCallRecording(!isRecording); + } else { + checkAndRequestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, PERMISSIONS_EXTERNAL_STORAGE); + } + } else if (id == R.id.recording) { + toggleCallRecording(false); } else if (id == R.id.pause) { pauseOrResumeCall(LinphoneManager.getLc().getCurrentCall()); } else if (id == R.id.hang_up) { @@ -700,35 +724,31 @@ public class CallActivity extends LinphoneGenericActivity implements OnClickList } } - private void enabledVideoButton(boolean enabled) { - if (enabled) { - video.setEnabled(true); - } else { - video.setEnabled(false); - } - } + private void toggleCallRecording(boolean enable) { + Call call = LinphoneManager.getLc().getCurrentCall(); - private void enabledPauseButton(boolean enabled) { - if (enabled) { - pause.setEnabled(true); - } else { - pause.setEnabled(false); - } - } + if (call == null) return; - private void enabledTransferButton(boolean enabled) { - if (enabled) { - transfer.setEnabled(true); - } else { - transfer.setEnabled(false); - } - } + if (enable && !isRecording) { + call.startRecording(); + Log.d("start call recording"); - private void enabledConferenceButton(boolean enabled) { - if (enabled) { - conference.setEnabled(true); - } else { - conference.setEnabled(false); + recordCall.setImageResource(R.drawable.options_rec_selected); + + recording.setVisibility(View.VISIBLE); + recording.setEnabled(true); + + isRecording = true; + } else if (!enable && isRecording) { + call.stopRecording(); + Log.d("stop call recording"); + + recordCall.setImageResource(R.drawable.options_rec_default); + + recording.setVisibility(View.GONE); + recording.setEnabled(false); + + isRecording = false; } } @@ -910,6 +930,10 @@ public class CallActivity extends LinphoneGenericActivity implements OnClickList Core lc = LinphoneManager.getLc(); Call currentCall = lc.getCurrentCall(); + if (isRecording) { + toggleCallRecording(false); + } + if (currentCall != null) { lc.terminateCall(currentCall); } else if (lc.isInConference()) { @@ -963,6 +987,7 @@ public class CallActivity extends LinphoneGenericActivity implements OnClickList transfer.setVisibility(View.INVISIBLE); addCall.setVisibility(View.INVISIBLE); conference.setVisibility(View.INVISIBLE); + recordCall.setVisibility(View.INVISIBLE); displayVideoCall(false); numpad.setVisibility(View.GONE); options.setSelected(false); @@ -1023,12 +1048,14 @@ public class CallActivity extends LinphoneGenericActivity implements OnClickList } addCall.setVisibility(View.INVISIBLE); conference.setVisibility(View.INVISIBLE); + recordCall.setVisibility(View.INVISIBLE); } else { //Display options if (isTransferAllowed) { transfer.setVisibility(View.VISIBLE); } addCall.setVisibility(View.VISIBLE); conference.setVisibility(View.VISIBLE); + recordCall.setVisibility(View.VISIBLE); options.setSelected(true); transfer.setEnabled(LinphoneManager.getLc().getCurrentCall() != null); } diff --git a/app/src/main/java/org/linphone/call/CallIncomingActivity.java b/app/src/main/java/org/linphone/call/CallIncomingActivity.java index a8db7baf8..80235380e 100644 --- a/app/src/main/java/org/linphone/call/CallIncomingActivity.java +++ b/app/src/main/java/org/linphone/call/CallIncomingActivity.java @@ -245,17 +245,7 @@ public class CallIncomingActivity extends LinphoneGenericActivity { } alreadyAcceptedOrDeniedCall = true; - CallParams params = LinphoneManager.getLc().createCallParams(mCall); - - boolean isLowBandwidthConnection = !LinphoneUtils.isHighBandwidthConnection(LinphoneService.instance().getApplicationContext()); - - if (params != null) { - params.enableLowBandwidth(isLowBandwidthConnection); - } else { - Log.e("Could not create call params for call"); - } - - if (params == null || !LinphoneManager.getInstance().acceptCallWithParams(mCall, params)) { + if (!LinphoneManager.getInstance().acceptCall(mCall)) { // the above method takes care of Samsung Galaxy S Toast.makeText(this, R.string.couldnt_accept_call, Toast.LENGTH_LONG).show(); } else { diff --git a/app/src/main/java/org/linphone/call/CallManager.java b/app/src/main/java/org/linphone/call/CallManager.java index 956c90537..47b087e15 100644 --- a/app/src/main/java/org/linphone/call/CallManager.java +++ b/app/src/main/java/org/linphone/call/CallManager.java @@ -20,6 +20,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import org.linphone.LinphoneManager; +import org.linphone.utils.FileUtils; import org.linphone.core.Address; import org.linphone.core.Call; import org.linphone.core.CallParams; @@ -63,6 +64,9 @@ public class CallManager { Log.d("Low bandwidth enabled in call params"); } + String recordFile = FileUtils.getCallRecordingFilename(LinphoneManager.getInstance().getContext(), lAddress); + params.setRecordFile(recordFile); + lc.inviteAddressWithParams(lAddress, params); } diff --git a/app/src/main/java/org/linphone/fragments/FragmentsAvailable.java b/app/src/main/java/org/linphone/fragments/FragmentsAvailable.java index d59fc5df0..9ed1af3da 100644 --- a/app/src/main/java/org/linphone/fragments/FragmentsAvailable.java +++ b/app/src/main/java/org/linphone/fragments/FragmentsAvailable.java @@ -37,7 +37,8 @@ public enum FragmentsAvailable { INFO_GROUP_CHAT, GROUP_CHAT, MESSAGE_IMDN, - CONTACT_DEVICES; + CONTACT_DEVICES, + RECORDING_LIST; public boolean shouldAddItselfToTheRightOf(FragmentsAvailable fragment) { switch (this) { diff --git a/app/src/main/java/org/linphone/history/HistoryDetailFragment.java b/app/src/main/java/org/linphone/history/HistoryDetailFragment.java index 8672eaaa9..4bd22f9f1 100644 --- a/app/src/main/java/org/linphone/history/HistoryDetailFragment.java +++ b/app/src/main/java/org/linphone/history/HistoryDetailFragment.java @@ -45,6 +45,7 @@ import org.linphone.core.ChatRoomListenerStub; import org.linphone.core.Core; import org.linphone.core.Factory; import org.linphone.core.ProxyConfig; +import org.linphone.fragments.FragmentsAvailable; import org.linphone.mediastream.Log; public class HistoryDetailFragment extends Fragment implements OnClickListener { diff --git a/app/src/main/java/org/linphone/history/HistoryListFragment.java b/app/src/main/java/org/linphone/history/HistoryListFragment.java index 8b3d102c8..d076a84e2 100644 --- a/app/src/main/java/org/linphone/history/HistoryListFragment.java +++ b/app/src/main/java/org/linphone/history/HistoryListFragment.java @@ -45,6 +45,7 @@ import org.linphone.core.Call; import org.linphone.core.CallLog; import org.linphone.utils.SelectableHelper; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; diff --git a/app/src/main/java/org/linphone/recording/Recording.java b/app/src/main/java/org/linphone/recording/Recording.java new file mode 100644 index 000000000..bb8d75514 --- /dev/null +++ b/app/src/main/java/org/linphone/recording/Recording.java @@ -0,0 +1,161 @@ +package org.linphone.recording; + +/* +Recording.java +Copyright (C) 2018 Belledonne Communications, Grenoble, France + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Handler; +import android.support.annotation.NonNull; + +import org.linphone.LinphoneManager; +import org.linphone.core.Player; +import org.linphone.core.PlayerListener; +import org.linphone.mediastream.Log; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Recording implements PlayerListener, Comparable { + private String recordPath, name; + private Date recordDate; + private Player player; + private RecordingListener listener; + private Handler handler; + private Runnable updateCurrentPositionTimer; + + public static final Pattern RECORD_PATTERN = Pattern.compile(".*/(.*)_(\\d{2}-\\d{2}-\\d{4}-\\d{2}-\\d{2}-\\d{2})\\..*"); + + @SuppressLint("SimpleDateFormat") + public Recording(Context context, String recordPath) { + this.recordPath = recordPath; + + Matcher m = RECORD_PATTERN.matcher(recordPath); + if (m.matches()) { + name = m.group(1); + + try { + recordDate = new SimpleDateFormat("dd-MM-yyyy-HH-mm-ss").parse(m.group(2)); + } catch (ParseException e) { + Log.e(e); + } + } + + handler = new Handler(context.getMainLooper()); + updateCurrentPositionTimer = new Runnable() { + @Override + public void run() { + if (listener != null) listener.currentPositionChanged(getCurrentPosition()); + if (isPlaying()) handler.postDelayed(updateCurrentPositionTimer, 20); + } + }; + + player = LinphoneManager.getLc().createLocalPlayer(null, null, null); + player.setListener(this); + } + + public String getRecordPath() { + return recordPath; + } + + public String getName() { + return name; + } + + public Date getRecordDate() { + return recordDate; + } + + public boolean isClosed() { + return player.getState() == Player.State.Closed; + } + + public void play() { + if (isClosed()) { + player.open(recordPath); + } + + player.start(); + handler.post(updateCurrentPositionTimer); + } + + public boolean isPlaying() { + return player.getState() == Player.State.Playing; + } + + public void pause() { + if (!isClosed()) { + player.pause(); + } + } + + public boolean isPaused() { + return player.getState() == Player.State.Paused; + } + + public void seek(int i) { + if (!isClosed()) player.seek(i); + } + + public int getCurrentPosition() { + if (isClosed()) { + player.open(recordPath); + } + + return player.getCurrentPosition(); + } + + public int getDuration() { + if (isClosed()) { + player.open(recordPath); + } + + return player.getDuration(); + } + + public void close() { + player.close(); + } + + public void setRecordingListener(RecordingListener listener) { + this.listener = listener; + } + + @Override + public void onEofReached(Player player) { + if (listener != null) listener.endOfRecordReached(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Recording) { + Recording r = (Recording) o; + return recordPath.equals(r.getRecordPath()); + } + return false; + } + + @Override + public int compareTo(@NonNull Recording o) { + return -recordDate.compareTo(o.getRecordDate()); + } +} diff --git a/app/src/main/java/org/linphone/recording/RecordingAdapter.java b/app/src/main/java/org/linphone/recording/RecordingAdapter.java new file mode 100644 index 000000000..3a62fc77a --- /dev/null +++ b/app/src/main/java/org/linphone/recording/RecordingAdapter.java @@ -0,0 +1,219 @@ +package org.linphone.recording; + +/* +RecordingAdapter.java +Copyright (C) 2018 Belledonne Communications, Grenoble, France + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +import android.annotation.SuppressLint; +import android.content.Context; +import android.support.annotation.NonNull; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.SeekBar; + +import org.linphone.R; +import org.linphone.utils.SelectableAdapter; +import org.linphone.utils.SelectableHelper; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class RecordingAdapter extends SelectableAdapter { + private List recordings; + private Context context; + private RecordingViewHolder.ClickListener clickListener; + + public RecordingAdapter(Context context, List recordings, RecordingViewHolder.ClickListener listener, SelectableHelper helper) { + super(helper); + + this.recordings = recordings; + this.context = context; + this.clickListener = listener; + } + + @Override + public Object getItem(int position) { + return recordings.get(position); + } + + @NonNull + @Override + public RecordingViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { + View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.recording_cell, viewGroup, false); + return new RecordingViewHolder(v, clickListener); + } + + @SuppressLint("SimpleDateFormat") + @Override + public void onBindViewHolder(@NonNull final RecordingViewHolder viewHolder, int i) { + final Recording record = recordings.get(i); + + viewHolder.name.setSelected(true); // For automated horizontal scrolling of long texts + + Calendar recordTime = Calendar.getInstance(); + recordTime.setTime(record.getRecordDate()); + viewHolder.separatorText.setText(DateToHumanDate(recordTime)); + viewHolder.select.setVisibility(isEditionEnabled() ? View.VISIBLE : View.GONE); + viewHolder.select.setChecked(isSelected(i)); + + if (i > 0) { + Recording previousRecord = recordings.get(i - 1); + Date previousRecordDate = previousRecord.getRecordDate(); + Calendar previousRecordTime = Calendar.getInstance(); + previousRecordTime.setTime(previousRecordDate); + + if (isSameDay(previousRecordTime, recordTime)) { + viewHolder.separator.setVisibility(View.GONE); + } else { + viewHolder.separator.setVisibility(View.VISIBLE); + } + } else { + viewHolder.separator.setVisibility(View.VISIBLE); + } + + if (record.isPlaying()) { + viewHolder.playButton.setImageResource(R.drawable.record_pause); + } else { + viewHolder.playButton.setImageResource(R.drawable.record_play); + } + viewHolder.playButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (record.isPaused()) { + record.play(); + viewHolder.playButton.setImageResource(R.drawable.record_pause); + } else { + record.pause(); + viewHolder.playButton.setImageResource(R.drawable.record_play); + } + } + }); + + viewHolder.name.setText(record.getName()); + viewHolder.date.setText(new SimpleDateFormat("HH:mm").format(record.getRecordDate())); + + int position = record.getCurrentPosition(); + viewHolder.currentPosition.setText(String.format("%02d:%02d", + TimeUnit.MILLISECONDS.toMinutes(position), + TimeUnit.MILLISECONDS.toSeconds(position) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(position)) + )); + + int duration = record.getDuration(); + viewHolder.duration.setText(String.format("%02d:%02d", + TimeUnit.MILLISECONDS.toMinutes(duration), + TimeUnit.MILLISECONDS.toSeconds(duration) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(duration)) + )); + + viewHolder.progressionBar.setMax(record.getDuration()); + viewHolder.progressionBar.setProgress(0); + viewHolder.progressionBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + int progressToSet = progress > 0 && progress < seekBar.getMax() ? progress : 0; + + if (progress == seekBar.getMax()) { + if (record.isPlaying()) record.pause(); + } + + record.seek(progressToSet); + seekBar.setProgress(progressToSet); + + int currentPosition = record.getCurrentPosition(); + viewHolder.currentPosition.setText(String.format("%02d:%02d", + TimeUnit.MILLISECONDS.toMinutes(currentPosition), + TimeUnit.MILLISECONDS.toSeconds(currentPosition) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(currentPosition)) + )); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + + } + }); + + record.setRecordingListener(new RecordingListener() { + @Override + public void currentPositionChanged(int currentPosition) { + viewHolder.currentPosition.setText(String.format("%02d:%02d", + TimeUnit.MILLISECONDS.toMinutes(currentPosition), + TimeUnit.MILLISECONDS.toSeconds(currentPosition) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(currentPosition)) + )); + viewHolder.progressionBar.setProgress(currentPosition); + } + + @Override + public void endOfRecordReached() { + record.pause(); + record.seek(0); + viewHolder.progressionBar.setProgress(0); + viewHolder.currentPosition.setText("00:00"); + viewHolder.playButton.setImageResource(R.drawable.record_play); + } + }); + } + + @Override + public int getItemCount() { + return recordings.size(); + } + + @SuppressLint("SimpleDateFormat") + private String DateToHumanDate(Calendar cal) { + SimpleDateFormat dateFormat; + if (isToday(cal)) { + return context.getString(R.string.today); + } else if (isYesterday(cal)) { + return context.getString(R.string.yesterday); + } else { + dateFormat = new SimpleDateFormat(context.getResources().getString(R.string.history_date_format)); + } + + return dateFormat.format(cal.getTime()); + } + + private boolean isSameDay(Calendar cal1, Calendar cal2) { + if (cal1 == null || cal2 == null) { + return false; + } + + return (cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) && + cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && + cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR)); + } + + private boolean isToday(Calendar cal) { + return isSameDay(cal, Calendar.getInstance()); + } + + private boolean isYesterday(Calendar cal) { + Calendar yesterday = Calendar.getInstance(); + yesterday.roll(Calendar.DAY_OF_MONTH, -1); + return isSameDay(cal, yesterday); + } +} diff --git a/app/src/main/java/org/linphone/recording/RecordingListFragment.java b/app/src/main/java/org/linphone/recording/RecordingListFragment.java new file mode 100644 index 000000000..040008e80 --- /dev/null +++ b/app/src/main/java/org/linphone/recording/RecordingListFragment.java @@ -0,0 +1,225 @@ +package org.linphone.recording; + +/* +RecordingListFragment.java +Copyright (C) 2018 Belledonne Communications, Grenoble, France + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; +import android.support.v7.widget.DividerItemDecoration; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.TextView; + +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.utils.FileUtils; +import org.linphone.utils.SelectableHelper; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class RecordingListFragment extends Fragment implements AdapterView.OnItemClickListener, RecordingViewHolder.ClickListener, SelectableHelper.DeleteListener { + private RecyclerView recordingList; + private List recordings; + private TextView noRecordings; + private RecordingAdapter recordingAdapter; + private LinearLayoutManager layoutManager; + private Context context; + private SelectableHelper selectableHelper; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.recordings_list, container, false); + + context = getActivity().getApplicationContext(); + selectableHelper = new SelectableHelper(view, this); + + recordingList = view.findViewById(R.id.recording_list); + noRecordings = view.findViewById(R.id.no_recordings); + + layoutManager = new LinearLayoutManager(context); + recordingList.setLayoutManager(layoutManager); + + //Divider between items + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(recordingList.getContext(), + layoutManager.getOrientation()); + dividerItemDecoration.setDrawable(context.getResources().getDrawable(R.drawable.divider)); + recordingList.addItemDecoration(dividerItemDecoration); + + recordings = new ArrayList<>(); + + return view; + } + + private void hideRecordingListAndDisplayMessageIfEmpty() { + if (recordings == null || recordings.isEmpty()) { + noRecordings.setVisibility(View.VISIBLE); + recordingList.setVisibility(View.GONE); + } else { + noRecordings.setVisibility(View.GONE); + recordingList.setVisibility(View.VISIBLE); + } + } + + public void removeDeletedRecordings() { + String recordingsDirectory = FileUtils.getRecordingsDirectory(context); + File directory = new File(recordingsDirectory); + + if (directory.exists() && directory.isDirectory()) { + File[] existingRecordings = directory.listFiles(); + + for(Recording r : recordings) { + boolean exists = false; + for(File f : existingRecordings) { + if (f.getPath().equals(r.getRecordPath())) { + exists = true; + break; + } + } + + if (!exists) recordings.remove(r); + } + + Collections.sort(recordings); + } + } + + public void searchForRecordings() { + String recordingsDirectory = FileUtils.getRecordingsDirectory(context); + File directory = new File(recordingsDirectory); + + if (directory.exists() && directory.isDirectory()) { + File[] existingRecordings = directory.listFiles(); + + for(File f : existingRecordings) { + boolean exists = false; + for(Recording r : recordings) { + if (r.getRecordPath().equals(f.getPath())) { + exists = true; + break; + } + } + + if (!exists) { + if (Recording.RECORD_PATTERN.matcher(f.getPath()).matches()) { + recordings.add(new Recording(context, f.getPath())); + } + } + } + + Collections.sort(recordings); + } + } + + @Override + public void onResume() { + super.onResume(); + + // This is necessary, without it you won't be able to remove recordings as you won't be allowed to. + LinphoneActivity.instance().checkAndRequestExternalStoragePermission(); + + LinphoneManager.getInstance().setAudioManagerModeNormal(); + LinphoneManager.getInstance().routeAudioToSpeaker(); + + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance().selectMenu(FragmentsAvailable.RECORDING_LIST); + } + + removeDeletedRecordings(); + searchForRecordings(); + + hideRecordingListAndDisplayMessageIfEmpty(); + recordingAdapter = new RecordingAdapter(getActivity().getApplicationContext(), recordings, this, selectableHelper); + recordingList.setAdapter(recordingAdapter); + selectableHelper.setAdapter(recordingAdapter); + selectableHelper.setDialogMessage(R.string.recordings_delete_dialog); + } + + @Override + public void onPause() { + super.onPause(); + + LinphoneManager.getInstance().routeAudioToReceiver(); + + // Close all opened recordings + for (Recording r : recordings) { + if (!r.isClosed()) { + if (r.isPlaying()) r.pause(); + r.close(); + } + } + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (recordingAdapter.isEditionEnabled()) { + Recording record = recordings.get(position); + + if (record.isPlaying()) record.pause(); + record.close(); + + File recordingFile = new File(record.getRecordPath()); + if (recordingFile.delete()) { + recordings.remove(record); + } + } + } + + @Override + public void onItemClicked(int position) { + if (recordingAdapter.isEditionEnabled()) { + recordingAdapter.toggleSelection(position); + } + } + + @Override + public boolean onItemLongClicked(int position) { + if (!recordingAdapter.isEditionEnabled()) { + selectableHelper.enterEditionMode(); + } + recordingAdapter.toggleSelection(position); + return true; + } + + @Override + public void onDeleteSelection(Object[] objectsToDelete) { + int size = recordingAdapter.getSelectedItemCount(); + for (int i = 0; i < size; i++) { + Recording record = (Recording) objectsToDelete[i]; + + if (record.isPlaying()) record.pause(); + record.close(); + + File recordingFile = new File(record.getRecordPath()); + if (recordingFile.delete()) { + recordings.remove(record); + } + } + } +} diff --git a/app/src/main/java/org/linphone/recording/RecordingListener.java b/app/src/main/java/org/linphone/recording/RecordingListener.java new file mode 100644 index 000000000..8853d8c14 --- /dev/null +++ b/app/src/main/java/org/linphone/recording/RecordingListener.java @@ -0,0 +1,26 @@ +package org.linphone.recording; + +/* +RecordingListener.java +Copyright (C) 2018 Belledonne Communications, Grenoble, France + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +public interface RecordingListener { + void currentPositionChanged(int currentPosition); + + void endOfRecordReached(); +} diff --git a/app/src/main/java/org/linphone/recording/RecordingViewHolder.java b/app/src/main/java/org/linphone/recording/RecordingViewHolder.java new file mode 100644 index 000000000..7e3389038 --- /dev/null +++ b/app/src/main/java/org/linphone/recording/RecordingViewHolder.java @@ -0,0 +1,80 @@ +package org.linphone.recording; + + +/* + RecordingViewHolder.java + Copyright (C) 2018 Belledonne Communications, Grenoble, France + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.TextView; + +import org.linphone.R; + +public class RecordingViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { + public ImageView playButton; + public TextView name, date, currentPosition, duration; + public SeekBar progressionBar; + public CheckBox select; + public LinearLayout separator; + public TextView separatorText; + private RecordingViewHolder.ClickListener listener; + + public RecordingViewHolder(View view, RecordingViewHolder.ClickListener listener) { + super(view); + + playButton = view.findViewById(R.id.record_play); + name = view.findViewById(R.id.record_name); + date = view.findViewById(R.id.record_date); + currentPosition = view.findViewById(R.id.record_current_time); + duration = view.findViewById(R.id.record_duration); + progressionBar = view.findViewById(R.id.record_progression_bar); + select = view.findViewById(R.id.delete); + separator = view.findViewById(R.id.separator); + separatorText = view.findViewById(R.id.separator_text); + + this.listener = listener; + view.setOnClickListener(this); + view.setOnLongClickListener(this); + } + + @Override + public void onClick(View view) { + if (listener != null) { + listener.onItemClicked(getAdapterPosition()); + } + } + + @Override + public boolean onLongClick(View view) { + if (listener != null) { + return listener.onItemLongClicked(getAdapterPosition()); + } + return false; + } + + public interface ClickListener { + void onItemClicked(int position); + + boolean onItemLongClicked(int position); + } +} diff --git a/app/src/main/java/org/linphone/utils/FileUtils.java b/app/src/main/java/org/linphone/utils/FileUtils.java index 4a5ec57f6..4ec2eeafc 100644 --- a/app/src/main/java/org/linphone/utils/FileUtils.java +++ b/app/src/main/java/org/linphone/utils/FileUtils.java @@ -19,6 +19,7 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import android.annotation.SuppressLint; import android.content.Context; import android.database.Cursor; import android.net.Uri; @@ -28,6 +29,7 @@ import android.provider.OpenableColumns; import android.text.TextUtils; import org.linphone.LinphoneManager; +import org.linphone.core.Address; import org.linphone.core.ChatMessage; import org.linphone.core.Content; import org.linphone.core.Friend; @@ -40,6 +42,7 @@ import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; @@ -197,6 +200,30 @@ public class FileUtils { return storageDir; } + public static String getRecordingsDirectory(Context mContext) { + String recordingsDir = Environment.getExternalStorageDirectory() + "/" + mContext.getString(mContext.getResources().getIdentifier("app_name", "string", mContext.getPackageName())) + "/recordings"; + File file = new File(recordingsDir); + if (!file.isDirectory() || !file.exists()) { + Log.w("Directory " + file + " doesn't seem to exists yet, let's create it"); + file.mkdirs(); + LinphoneManager.getInstance().getMediaScanner().scanFile(file); + } + return recordingsDir; + } + + @SuppressLint("SimpleDateFormat") + public static String getCallRecordingFilename(Context context, Address address) { + String fileName = getRecordingsDirectory(context) + "/"; + + String name = address.getDisplayName() == null ? address.getUsername() : address.getDisplayName(); + fileName += name + "_"; + + DateFormat format = new SimpleDateFormat("dd-MM-yyyy-HH-mm-ss"); + fileName += format.format(new Date()) + ".mkv"; + + return fileName; + } + public static void scanFile(ChatMessage message) { String appData = message.getAppdata(); if (appData == null) { diff --git a/app/src/main/java/org/linphone/views/CallButton.java b/app/src/main/java/org/linphone/views/CallButton.java index e53a660b8..db9201030 100644 --- a/app/src/main/java/org/linphone/views/CallButton.java +++ b/app/src/main/java/org/linphone/views/CallButton.java @@ -58,38 +58,31 @@ public class CallButton extends ImageView implements OnClickListener, AddressAwa } public void onClick(View v) { - try { - if (!LinphoneManager.getInstance().acceptCallIfIncomingPending()) { - if (mAddress.getText().length() > 0) { - LinphoneManager.getInstance().newOutgoingCall(mAddress); - } else { - if (LinphonePreferences.instance().isBisFeatureEnabled()) { - CallLog[] logs = LinphoneManager.getLc().getCallLogs(); - CallLog log = null; - for (CallLog l : logs) { - if (l.getDir() == Call.Dir.Outgoing) { - log = l; - break; - } - } - if (log == null) { - return; - } - - ProxyConfig lpc = LinphoneManager.getLc().getDefaultProxyConfig(); - if (lpc != null && log.getToAddress().getDomain().equals(lpc.getDomain())) { - mAddress.setText(log.getToAddress().getUsername()); - } else { - mAddress.setText(log.getToAddress().asStringUriOnly()); - } - mAddress.setSelection(mAddress.getText().toString().length()); - mAddress.setDisplayedName(log.getToAddress().getDisplayName()); + if (mAddress.getText().length() > 0) { + LinphoneManager.getInstance().newOutgoingCall(mAddress); + } else { + if (LinphonePreferences.instance().isBisFeatureEnabled()) { + CallLog[] logs = LinphoneManager.getLc().getCallLogs(); + CallLog log = null; + for (CallLog l : logs) { + if (l.getDir() == Call.Dir.Outgoing) { + log = l; + break; } } + if (log == null) { + return; + } + + ProxyConfig lpc = LinphoneManager.getLc().getDefaultProxyConfig(); + if (lpc != null && log.getToAddress().getDomain().equals(lpc.getDomain())) { + mAddress.setText(log.getToAddress().getUsername()); + } else { + mAddress.setText(log.getToAddress().asStringUriOnly()); + } + mAddress.setSelection(mAddress.getText().toString().length()); + mAddress.setDisplayedName(log.getToAddress().getDisplayName()); } - } catch (CoreException e) { - LinphoneManager.getInstance().terminateCall(); - onWrongDestinationAddress(); } } diff --git a/app/src/main/res/drawable-xhdpi/menu_recordings.png b/app/src/main/res/drawable-xhdpi/menu_recordings.png new file mode 100644 index 000000000..3bef81d13 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/menu_recordings.png differ diff --git a/app/src/main/res/drawable-xhdpi/options_rec_default.png b/app/src/main/res/drawable-xhdpi/options_rec_default.png new file mode 100644 index 000000000..075c09314 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/options_rec_default.png differ diff --git a/app/src/main/res/drawable-xhdpi/options_rec_selected.png b/app/src/main/res/drawable-xhdpi/options_rec_selected.png new file mode 100644 index 000000000..0ef1ca322 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/options_rec_selected.png differ diff --git a/app/src/main/res/drawable-xhdpi/record_pause.png b/app/src/main/res/drawable-xhdpi/record_pause.png new file mode 100644 index 000000000..3b9311e43 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/record_pause.png differ diff --git a/app/src/main/res/drawable-xhdpi/record_play.png b/app/src/main/res/drawable-xhdpi/record_play.png new file mode 100644 index 000000000..486dfb0de Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/record_play.png differ diff --git a/app/src/main/res/drawable-xhdpi/recording.png b/app/src/main/res/drawable-xhdpi/recording.png new file mode 100644 index 000000000..fb4e82669 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/recording.png differ diff --git a/app/src/main/res/layout-land/call.xml b/app/src/main/res/layout-land/call.xml index 552b953ea..3b11b9054 100644 --- a/app/src/main/res/layout-land/call.xml +++ b/app/src/main/res/layout-land/call.xml @@ -181,6 +181,19 @@ android:layout_gravity="center"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/recordings_list.xml b/app/src/main/res/layout/recordings_list.xml new file mode 100644 index 000000000..a76d1297d --- /dev/null +++ b/app/src/main/res/layout/recordings_list.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aa73976e8..9c96b2eb5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -252,6 +252,7 @@ Assistant Settings + My recordings About Quit @@ -288,6 +289,10 @@ Display filter: Call + + No recordings. + Do you really want to delete and leave the selected recordings? + Send log Reset log @@ -558,4 +563,5 @@ Linphone instant messages notifications Group chat room subject Group chat room info + Record call