integrate voice recorder

This commit is contained in:
Daniel Gultsch 2018-04-22 20:31:37 +02:00
parent cfaebbdbd1
commit 2f974c3eb2
13 changed files with 369 additions and 86 deletions

View file

@ -35,7 +35,6 @@
privacy
* Rely on existing, well established protocols (XMPP)
* Do not require a Google Account or specifically Google Cloud Messaging (GCM)
* Require as few permissions as possible
## Features

View file

@ -22,6 +22,7 @@
<uses-feature android:name="android.hardware.location.network" android:required="false" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission
android:name="android.permission.READ_PHONE_STATE"
@ -58,12 +59,11 @@
</receiver>
<activity
android:name=".ui.ShareLocationActivity"
android:label="@string/title_activity_share_location" >
<intent-filter>
<action android:name="eu.siacs.conversations.location.request" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
android:label="@string/title_activity_share_location"/>
<activity
android:name=".ui.RecordingActivity"
android:theme="@style/ConversationsTheme.Dialog"
android:configChanges="orientation|screenSize"/>
<activity
android:name=".ui.ShowLocationActivity"
android:label="@string/title_activity_show_location" >

View file

@ -55,6 +55,7 @@ import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.RecordingActivity;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExifHelper;
import eu.siacs.conversations.utils.FileUtils;
@ -76,11 +77,13 @@ public class FileBackend {
this.mXmppConnectionService = service;
}
private void createNoMedia() {
final File nomedia = new File(getConversationsDirectory("Files") + ".nomedia");
if (!nomedia.exists()) {
private void createNoMedia(File diretory) {
final File noMedia = new File(diretory,".nomedia");
if (!noMedia.exists()) {
try {
nomedia.createNewFile();
if (!noMedia.createNewFile()) {
Log.d(Config.LOGTAG,"created nomedia file "+noMedia.getAbsolutePath());
}
} catch (Exception e) {
Log.d(Config.LOGTAG, "could not create nomedia file");
}
@ -88,16 +91,25 @@ public class FileBackend {
}
public void updateMediaScanner(File file) {
String path = file.getAbsolutePath();
if (!path.startsWith(getConversationsDirectory("Files"))) {
if (!isInDirectoryThatShouldNotBeScanned(mXmppConnectionService, file)) {
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(Uri.fromFile(file));
mXmppConnectionService.sendBroadcast(intent);
} else {
createNoMedia();
} else if (file.getAbsolutePath().startsWith(getAppMediaDirectory(mXmppConnectionService))) {
createNoMedia(file.getParentFile());
}
}
private static boolean isInDirectoryThatShouldNotBeScanned(Context context, File file) {
String path = file.getAbsolutePath();
for(String type : new String[]{RecordingActivity.STORAGE_DIRECTORY_TYPE_NAME, "Files"}) {
if (path.startsWith(getConversationsDirectory(context, type))) {
return true;
}
}
return false;
}
public boolean deleteFile(Message message) {
File file = getFile(message);
if (file.delete()) {
@ -186,11 +198,19 @@ public class FileBackend {
}
public String getConversationsDirectory(final String type) {
if (Config.ONLY_INTERNAL_STORAGE) {
return mXmppConnectionService.getFilesDir().getAbsolutePath() + "/" + type + "/";
} else {
return Environment.getExternalStorageDirectory() + "/Conversations/Media/Conversations " + type + "/";
return getConversationsDirectory(mXmppConnectionService, type);
}
public static String getConversationsDirectory(Context context, final String type) {
if (Config.ONLY_INTERNAL_STORAGE) {
return context.getFilesDir().getAbsolutePath() + "/" + type + "/";
} else {
return getAppMediaDirectory(context)+context.getString(R.string.app_name)+" " + type + "/";
}
}
public static String getAppMediaDirectory(Context context) {
return Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+context.getString(R.string.app_name)+"/Media/";
}
public static String getConversationsLogsDirectory() {

View file

@ -279,7 +279,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
return false;
}
}
if (hasStoragePermission(REQUEST_ADD_EDITOR_CONTENT)) {
if (hasPermissions(REQUEST_ADD_EDITOR_CONTENT, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
attachImageToConversation(inputContentInfo.getContentUri());
} else {
mPendingEditorContent = inputContentInfo.getContentUri();
@ -1284,12 +1284,16 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
public void attachFile(final int attachmentChoice) {
if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO || attachmentChoice == ATTACHMENT_CHOICE_RECORD_VIDEO) {
if (!hasStorageAndCameraPermission(attachmentChoice)) {
if (attachmentChoice == ATTACHMENT_CHOICE_RECORD_VOICE) {
if (!hasPermissions(attachmentChoice, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO)) {
return;
}
} else if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO || attachmentChoice == ATTACHMENT_CHOICE_RECORD_VIDEO) {
if (!hasPermissions(attachmentChoice, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA)) {
return;
}
} else if (attachmentChoice != ATTACHMENT_CHOICE_LOCATION) {
if (!Config.ONLY_INTERNAL_STORAGE && !hasStoragePermission(attachmentChoice)) {
if (!hasPermissions(attachmentChoice, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
return;
}
}
@ -1365,7 +1369,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
} else {
@StringRes int res;
if (Manifest.permission.CAMERA.equals(getFirstDenied(grantResults, permissions))) {
String firstDenied = getFirstDenied(grantResults, permissions);
if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) {
res = R.string.no_microphone_permission;
} else if (Manifest.permission.CAMERA.equals(firstDenied)) {
res = R.string.no_camera_permission;
} else {
res = R.string.no_storage_permission;
@ -1375,7 +1382,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
public void startDownloadable(Message message) {
if (!Config.ONLY_INTERNAL_STORAGE && !hasStoragePermission(REQUEST_START_DOWNLOAD)) {
if (!hasPermissions(REQUEST_START_DOWNLOAD, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
this.mPendingDownloadableMessage = message;
return;
}
@ -1443,28 +1450,17 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
builder.create().show();
}
private boolean hasStoragePermission(int requestCode) {
private boolean hasPermissions(int requestCode, String... permissions) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (activity.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
return false;
} else {
return true;
final List<String> missingPermissions = new ArrayList<>();
for(String permission : permissions) {
if (Config.ONLY_INTERNAL_STORAGE && permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
continue;
}
} else {
return true;
if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
missingPermissions.add(permission);
}
}
private boolean hasStorageAndCameraPermission(int requestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
List<String> missingPermissions = new ArrayList<>();
if (!Config.ONLY_INTERNAL_STORAGE && activity.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
missingPermissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
if (activity.checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
missingPermissions.add(Manifest.permission.CAMERA);
}
if (missingPermissions.size() == 0) {
return true;
} else {
@ -1489,7 +1485,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
final PresenceSelector.OnPresenceSelected callback = () -> {
Intent intent = new Intent();
boolean chooser = false;
String fallbackPackageId = null;
switch (attachmentChoice) {
case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
intent.setAction(Intent.ACTION_GET_CONTENT);
@ -1515,12 +1510,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
intent.setAction(Intent.ACTION_GET_CONTENT);
break;
case ATTACHMENT_CHOICE_RECORD_VOICE:
intent.setAction(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
fallbackPackageId = "eu.siacs.conversations.voicerecorder";
intent = new Intent(getActivity(), RecordingActivity.class);
break;
case ATTACHMENT_CHOICE_LOCATION:
intent.setAction("eu.siacs.conversations.location.request");
fallbackPackageId = "eu.siacs.conversations.sharelocation";
intent = new Intent(getActivity(), ShareLocationActivity.class);
break;
}
if (intent.resolveActivity(getActivity().getPackageManager()) != null) {
@ -1531,8 +1524,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
} else {
startActivityForResult(intent, attachmentChoice);
}
} else if (fallbackPackageId != null) {
startActivity(getInstallApkIntent(fallbackPackageId));
}
};
if (account.httpUploadAvailable() || attachmentChoice == ATTACHMENT_CHOICE_LOCATION) {
@ -1543,28 +1534,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
}
private Intent getInstallApkIntent(final String packageId) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("market://details?id=" + packageId));
if (intent.resolveActivity(getActivity().getPackageManager()) != null) {
return intent;
} else {
intent.setData(Uri.parse("http://play.google.com/store/apps/details?id=" + packageId));
return intent;
}
}
@Override
public void onResume() {
new Handler().post(() -> {
final Activity activity = getActivity();
if (activity == null) {
return;
}
final PackageManager packageManager = activity.getPackageManager();
ConversationMenuConfigurator.updateAttachmentAvailability(packageManager);
getActivity().invalidateOptionsMenu();
});
super.onResume();
binding.messagesView.post(this::fireReadEvent);
}

View file

@ -0,0 +1,192 @@
package eu.siacs.conversations.ui;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.media.MediaRecorder;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.FileObserver;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.utils.ThemeHelper;
public class RecordingActivity extends Activity implements View.OnClickListener {
public static String STORAGE_DIRECTORY_TYPE_NAME = "Recordings";
private TextView mTimerTextView;
private Button mCancelButton;
private Button mStopButton;
private MediaRecorder mRecorder;
private long mStartTime = 0;
private Handler mHandler = new Handler();
private Runnable mTickExecutor = new Runnable() {
@Override
public void run() {
tick();
mHandler.postDelayed(mTickExecutor, 100);
}
};
private File mOutputFile;
private boolean mShouldFinishAfterWrite = false;
private FileObserver mFileObserver;
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(ThemeHelper.findDialog(this));
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recording);
this.mTimerTextView = (TextView) this.findViewById(R.id.timer);
this.mCancelButton = (Button) this.findViewById(R.id.cancel_button);
this.mCancelButton.setOnClickListener(this);
this.mStopButton = (Button) this.findViewById(R.id.share_button);
this.mStopButton.setOnClickListener(this);
this.setFinishOnTouchOutside(false);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
@Override
protected void onStart() {
super.onStart();
if (!startRecording()) {
mStopButton.setEnabled(false);
Toast.makeText(this, R.string.unable_to_start_recording, Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onStop() {
super.onStop();
if (mRecorder != null) {
mHandler.removeCallbacks(mTickExecutor);
stopRecording(false);
}
if (mFileObserver != null) {
mFileObserver.stopWatching();
}
}
private boolean startRecording() {
mRecorder = new MediaRecorder();
mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC);
mRecorder.setAudioEncodingBitRate(48000);
mRecorder.setAudioSamplingRate(16000);
setupOutputFile();
mRecorder.setOutputFile(mOutputFile.getAbsolutePath());
try {
mRecorder.prepare();
mRecorder.start();
mStartTime = SystemClock.elapsedRealtime();
mHandler.postDelayed(mTickExecutor, 100);
Log.d("Voice Recorder", "started recording to " + mOutputFile.getAbsolutePath());
return true;
} catch (Exception e) {
Log.e("Voice Recorder", "prepare() failed " + e.getMessage());
return false;
}
}
protected void stopRecording(boolean saveFile) {
mShouldFinishAfterWrite = saveFile;
mRecorder.stop();
mRecorder.release();
mRecorder = null;
mStartTime = 0;
if (!saveFile && mOutputFile != null) {
if (mOutputFile.delete()) {
Log.d(Config.LOGTAG,"deleted canceled recording");
}
}
}
private static File generateOutputFilename(Context context) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US);
String filename = "RECORDING_" + dateFormat.format(new Date()) + ".m4a";
return new File(FileBackend.getConversationsDirectory(context, STORAGE_DIRECTORY_TYPE_NAME) + "/" + filename);
}
private void setupOutputFile() {
mOutputFile = generateOutputFilename(this);
File parentDirectory = mOutputFile.getParentFile();
if (parentDirectory.mkdirs()) {
Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath());
}
File noMedia = new File(parentDirectory, ".nomedia");
if (!noMedia.exists()) {
try {
if (noMedia.createNewFile()) {
Log.d(Config.LOGTAG, "created nomedia file in " + parentDirectory.getAbsolutePath());
}
} catch (IOException e) {
Log.d(Config.LOGTAG, "unable to create nomedia file in " + parentDirectory.getAbsolutePath(), e);
}
}
setupFileObserver(parentDirectory);
}
private void setupFileObserver(File directory) {
mFileObserver = new FileObserver(directory.getAbsolutePath()) {
@Override
public void onEvent(int event, String s) {
if (s != null && s.equals(mOutputFile.getName()) && event == FileObserver.CLOSE_WRITE) {
if (mShouldFinishAfterWrite) {
setResult(Activity.RESULT_OK, new Intent().setData(Uri.fromFile(mOutputFile)));
finish();
}
}
}
};
mFileObserver.startWatching();
}
private void tick() {
long time = (mStartTime < 0) ? 0 : (SystemClock.elapsedRealtime() - mStartTime);
int minutes = (int) (time / 60000);
int seconds = (int) (time / 1000) % 60;
int milliseconds = (int) (time / 100) % 10;
mTimerTextView.setText(minutes + ":" + (seconds < 10 ? "0" + seconds : seconds) + "." + milliseconds);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.cancel_button:
mHandler.removeCallbacks(mTickExecutor);
stopRecording(false);
setResult(RESULT_CANCELED);
finish();
break;
case R.id.share_button:
mStopButton.setEnabled(false);
mStopButton.setText(R.string.please_wait);
mHandler.removeCallbacks(mTickExecutor);
mHandler.postDelayed(() -> stopRecording(true), 500);
break;
}
}
}

View file

@ -45,8 +45,8 @@ public class AttachmentTool {
if (intent == null) {
return uris;
}
Uri uri = intent.getData();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && uri == null) {
final Uri uri = intent.getData();
if (uri == null) {
final ClipData clipData = intent.getClipData();
if (clipData != null) {
for (int i = 0; i < clipData.getItemCount(); ++i) {

View file

@ -45,13 +45,7 @@ import eu.siacs.conversations.entities.Message;
public class ConversationMenuConfigurator {
private static boolean showSoundRecorderAttachment = false;
private static boolean showLocationAttachment = false;
public static void configureAttachmentMenu(@NonNull Conversation conversation, Menu menu) {
final MenuItem menuAttachSoundRecorder = menu.findItem(R.id.attach_record_voice);
final MenuItem menuAttachLocation = menu.findItem(R.id.attach_location);
final MenuItem menuAttach = menu.findItem(R.id.action_attach_file);
final boolean visible;
@ -66,9 +60,6 @@ public class ConversationMenuConfigurator {
if (!visible) {
return;
}
menuAttachLocation.setVisible(showLocationAttachment);
menuAttachSoundRecorder.setVisible(showSoundRecorderAttachment);
}
public static void configureEncryptionMenu(@NonNull Conversation conversation, Menu menu) {
@ -118,9 +109,4 @@ public class ConversationMenuConfigurator {
break;
}
}
public static void updateAttachmentAvailability(PackageManager packageManager) {
showSoundRecorderAttachment = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION).resolveActivity(packageManager) != null;
showLocationAttachment = new Intent("eu.siacs.conversations.location.request").resolveActivity(packageManager) != null;
}
}

View file

@ -61,6 +61,21 @@ public class ThemeHelper {
}
}
public static int findDialog(Context context) {
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
final Resources resources = context.getResources();
final boolean dark = sharedPreferences.getString(SettingsActivity.THEME, resources.getString(R.string.theme)).equals("dark");
final String fontSize = sharedPreferences.getString("font_size", resources.getString(R.string.default_font_size));
switch (fontSize) {
case "medium":
return dark ? R.style.ConversationsTheme_Dark_Dialog_Medium : R.style.ConversationsTheme_Dialog_Medium;
case "large":
return dark ? R.style.ConversationsTheme_Dark_Dialog_Large : R.style.ConversationsTheme_Dialog_Large;
default:
return dark ? R.style.ConversationsTheme_Dark_Dialog : R.style.ConversationsTheme_Dialog;
}
}
public static boolean isDark(@StyleRes int id) {
switch (id) {
case R.style.ConversationsTheme_Dark:

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/color_background_primary">
<LinearLayout
android:id="@+id/button_bar"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/timer">
<Button
android:id="@+id/cancel_button"
style="@style/Widget.Conversations.Button.Borderless"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/cancel"/>
<View
android:layout_width="1dp"
android:layout_height="fill_parent"
android:layout_marginBottom="7dp"
android:layout_marginTop="7dp"
android:background="?attr/divider"/>
<Button
android:id="@+id/share_button"
style="@style/Widget.Conversations.Button.Borderless"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/share"/>
</LinearLayout>
<TextView
android:id="@+id/timer"
android:textAppearance="@style/TextAppearance.Conversations.Display2.Monospace"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:text="0:00.0"/>
</RelativeLayout>

View file

@ -6,6 +6,7 @@
<attr name="TextSizeSubhead" format="dimension" />
<attr name="TextSizeBody1" format="dimension" />
<attr name="TextSizeBody2" format="dimension" />
<attr name="TextSizeDisplay2" format="dimension" />
<attr name="TextSizeInput" format="dimension" />
<attr name="TextSeparation" format="dimension"/>

View file

@ -699,4 +699,7 @@
<string name="title_activity_share_location">Share location</string>
<string name="title_activity_show_location">Show location</string>
<string name="share">Share</string>
<string name="unable_to_start_recording">Unable to start recording</string>
<string name="please_wait">Please wait…</string>
<string name="no_microphone_permission">Conversations needs access to the microphone</string>
</resources>

View file

@ -1,5 +1,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<style name="TextAppearance.Conversations.Display2.Monospace" parent="TextAppearance.AppCompat.Display2">
<item name="android:textSize">?TextSizeDisplay2</item>
<item name="android:fontFamily" tools:targetApi="jelly_bean">monospace</item>
<item name="android:typeface">monospace</item>
</style>
<style name="TextAppearance.Conversations.Title" parent="TextAppearance.AppCompat.Title">
<item name="android:textSize">?TextSizeTitle</item>
</style>

View file

@ -239,4 +239,47 @@
<item name="colorPrimaryDark">@color/grey300</item>
<item name="android:windowBackground">@drawable/background</item>
</style>
<style name="ConversationsTheme.Dialog" parent="@style/Theme.AppCompat.Light.Dialog">
<item name="colorPrimary">@color/primary500</item>
<item name="colorPrimaryDark">@color/primary700</item>
<item name="colorAccent">@color/accent</item>
<item name="color_background_primary">@color/grey50</item>
<item name="divider">@color/black12</item>
<item name="TextSizeBody2">14sp</item>
<item name="TextSizeDisplay2">45sp</item>
<item name="android:windowNoTitle">true</item>
</style>
<style name="ConversationsTheme.Dark.Dialog" parent="@style/Theme.AppCompat.Dialog">
<item name="colorPrimary">@color/primary500</item>
<item name="colorPrimaryDark">@color/primary700</item>
<item name="colorAccent">@color/accent</item>
<item name="color_background_primary">@color/grey800</item>
<item name="divider">@color/white12</item>
<item name="TextSizeBody2">14sp</item>
<item name="TextSizeDisplay2">45sp</item>
<item name="android:windowNoTitle">true</item>
</style>
<style name="ConversationsTheme.Dialog.Medium" parent="ConversationsTheme.Dialog">
<item name="TextSizeBody2">16sp</item>
<item name="TextSizeDisplay2">51sp</item>
</style>
<style name="ConversationsTheme.Dark.Dialog.Medium" parent="ConversationsTheme.Dark.Dialog">
<item name="TextSizeBody2">16sp</item>
<item name="TextSizeDisplay2">51sp</item>
</style>
<style name="ConversationsTheme.Dialog.Large" parent="ConversationsTheme.Dialog">
<item name="TextSizeBody2">18sp</item>
<item name="TextSizeDisplay2">56sp</item>
</style>
<style name="ConversationsTheme.Dark.Dialog.Large" parent="ConversationsTheme.Dark.Dialog">
<item name="TextSizeBody2">18sp</item>
<item name="TextSizeTitle">56sp</item>
</style>
</resources>