From 518fe5e7125ea39ff8adfcacd8e59faf08ebf1a5 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 13 Aug 2024 11:04:04 +1000 Subject: [PATCH 01/12] Bump version name --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 861b94a843..c99404f898 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,7 +32,7 @@ configurations.all { } def canonicalVersionCode = 379 -def canonicalVersionName = "1.19.0" +def canonicalVersionName = "1.19.1" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1, From 62873ee77389489475681c7ca40d56ad504d05b1 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 8 Aug 2024 17:04:59 +1000 Subject: [PATCH 02/12] Removed com.amulyakhare.textdrawable Cleaned up references to TextDrawable. Also cleaned up the way we load layered drawables, used in ProfilePictureView for the load state as the icons were stretched across and didn't look nice. --- app/src/main/AndroidManifest.xml | 2 +- .../securesms/calls/WebRtcCallActivity.kt | 8 - .../securesms/components/AvatarImageView.java | 198 ------------------ .../components/ProfilePictureView.kt | 7 +- .../SingleRecipientNotificationBuilder.java | 8 +- libsession/build.gradle | 1 - .../avatars/FallbackContactPhoto.java | 2 +- .../avatars/GeneratedContactPhoto.java | 83 -------- .../avatars/ResourceContactPhoto.java | 30 ++- .../avatars/TransparentContactPhoto.java | 10 +- 10 files changed, 40 insertions(+), 309 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java delete mode 100644 libsession/src/main/java/org/session/libsession/avatars/GeneratedContactPhoto.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d70c9e080e..9ed5bc0b53 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> - + { - if (recipient.getContactUri() != null) { - ContactsContract.QuickContact.showQuickContact(getContext(), AvatarImageView.this, recipient.getContactUri(), ContactsContract.QuickContact.MODE_LARGE, null); - } else { - getContext().startActivity(RecipientExporter.export(recipient).asAddContactIntent()); - } - }); - } else { - super.setOnClickListener(listener); - } - } - - private static class RecipientContactPhoto { - - private final @NonNull Recipient recipient; - private final @Nullable ContactPhoto contactPhoto; - private final boolean ready; - - RecipientContactPhoto(@NonNull Recipient recipient) { - this.recipient = recipient; - this.ready = !recipient.isResolving(); - this.contactPhoto = recipient.getContactPhoto(); - } - - public boolean equals(@Nullable RecipientContactPhoto other) { - if (other == null) return false; - - return other.recipient.equals(recipient) && - other.recipient.getColor().equals(recipient.getColor()) && - other.ready == ready && - Objects.equals(other.contactPhoto, contactPhoto); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt index 6d59bbfc92..5ec9bc096c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt @@ -38,10 +38,13 @@ class ProfilePictureView @JvmOverloads constructor( var additionalDisplayName: String? = null private val profilePicturesCache = mutableMapOf() + private val resourcePadding by lazy { + context.resources.getDimensionPixelSize(R.dimen.normal_padding).toFloat() + } private val unknownRecipientDrawable by lazy { ResourceContactPhoto(R.drawable.ic_profile_default) - .asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) } + .asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false, resourcePadding) } private val unknownOpenGroupDrawable by lazy { ResourceContactPhoto(R.drawable.ic_notification) - .asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) } + .asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false, resourcePadding) } constructor(context: Context, sender: Recipient): this(context) { update(sender) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java index d5aeba6022..146021ac68 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java @@ -28,7 +28,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy; import org.session.libsession.avatars.ContactColors; import org.session.libsession.avatars.ContactPhoto; -import org.session.libsession.avatars.GeneratedContactPhoto; +import org.session.libsession.avatars.ResourceContactPhoto; import org.session.libsession.messaging.contacts.Contact; import org.session.libsession.utilities.NotificationPrivacyPreference; import org.session.libsession.utilities.TextSecurePreferences; @@ -60,6 +60,8 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil private CharSequence contentTitle; private CharSequence contentText; + private static final Integer ICON_SIZE = 128; + public SingleRecipientNotificationBuilder(@NonNull Context context, @NonNull NotificationPrivacyPreference privacy) { super(context, privacy); @@ -108,7 +110,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil } else { setContentTitle(context.getString(R.string.SingleRecipientNotificationBuilder_signal)); - setLargeIcon(new GeneratedContactPhoto("Unknown", R.drawable.ic_profile_default).asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context))); + setLargeIcon(AvatarPlaceholderGenerator.generate(context, ICON_SIZE, "", "Unknown")); } } @@ -330,7 +332,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil private static Drawable getPlaceholderDrawable(Context context, Recipient recipient) { String publicKey = recipient.getAddress().serialize(); String displayName = recipient.getName(); - return AvatarPlaceholderGenerator.generate(context, 128, publicKey, displayName); + return AvatarPlaceholderGenerator.generate(context, ICON_SIZE, publicKey, displayName); } /** diff --git a/libsession/build.gradle b/libsession/build.gradle index 55146823ec..417b464ca9 100644 --- a/libsession/build.gradle +++ b/libsession/build.gradle @@ -32,7 +32,6 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' implementation "com.github.bumptech.glide:glide:$glideVersion" - implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'com.annimon:stream:1.1.8' implementation 'com.makeramen:roundedimageview:2.1.0' implementation 'com.esotericsoftware:kryo:5.1.1' diff --git a/libsession/src/main/java/org/session/libsession/avatars/FallbackContactPhoto.java b/libsession/src/main/java/org/session/libsession/avatars/FallbackContactPhoto.java index b2095c5980..9be08ea9ff 100644 --- a/libsession/src/main/java/org/session/libsession/avatars/FallbackContactPhoto.java +++ b/libsession/src/main/java/org/session/libsession/avatars/FallbackContactPhoto.java @@ -5,6 +5,6 @@ import android.graphics.drawable.Drawable; public interface FallbackContactPhoto { - public Drawable asDrawable(Context context, int color); public Drawable asDrawable(Context context, int color, boolean inverted); + public Drawable asDrawable(Context context, int color, boolean inverted, Float padding); } diff --git a/libsession/src/main/java/org/session/libsession/avatars/GeneratedContactPhoto.java b/libsession/src/main/java/org/session/libsession/avatars/GeneratedContactPhoto.java deleted file mode 100644 index 0f607d6e33..0000000000 --- a/libsession/src/main/java/org/session/libsession/avatars/GeneratedContactPhoto.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.session.libsession.avatars; - -import android.content.Context; -import android.graphics.Color; -import android.graphics.Typeface; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; -import android.text.TextUtils; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.amulyakhare.textdrawable.TextDrawable; - -import org.session.libsession.R; -import org.session.libsession.utilities.ThemeUtil; -import org.session.libsession.utilities.ViewUtil; - -import java.util.regex.Pattern; - -public class GeneratedContactPhoto implements FallbackContactPhoto { - - private static final Pattern PATTERN = Pattern.compile("[^\\p{L}\\p{Nd}\\p{S}]+"); - private static final Typeface TYPEFACE = Typeface.create("sans-serif-medium", Typeface.NORMAL); - - private final String name; - private final int fallbackResId; - - public GeneratedContactPhoto(@NonNull String name, @DrawableRes int fallbackResId) { - this.name = name; - this.fallbackResId = fallbackResId; - } - - @Override - public Drawable asDrawable(Context context, int color) { - return asDrawable(context, color,false); - } - - @Override - public Drawable asDrawable(Context context, int color, boolean inverted) { - int targetSize = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size); - String character = getAbbreviation(name); - - if (!TextUtils.isEmpty(character)) { - Drawable base = TextDrawable.builder() - .beginConfig() - .width(targetSize) - .height(targetSize) - .useFont(TYPEFACE) - .fontSize(ViewUtil.dpToPx(context, 24)) - .textColor(inverted ? color : Color.WHITE) - .endConfig() - .buildRound(character, inverted ? Color.WHITE : color); - - Drawable gradient = context.getResources().getDrawable(ThemeUtil.isDarkTheme(context) ? R.drawable.avatar_gradient_dark - : R.drawable.avatar_gradient_light); - return new LayerDrawable(new Drawable[] { base, gradient }); - } - - return new ResourceContactPhoto(fallbackResId).asDrawable(context, color, inverted); - } - - private @Nullable String getAbbreviation(String name) { - String[] parts = name.split(" "); - StringBuilder builder = new StringBuilder(); - int count = 0; - - for (int i = 0; i < parts.length && count < 2; i++) { - String cleaned = PATTERN.matcher(parts[i]).replaceFirst(""); - if (!TextUtils.isEmpty(cleaned)) { - builder.appendCodePoint(cleaned.codePointAt(0)); - count++; - } - } - - if (builder.length() == 0) { - return null; - } else { - return builder.toString(); - } - } -} diff --git a/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java b/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java index 2920b4b1ce..f76135e95a 100644 --- a/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java +++ b/libsession/src/main/java/org/session/libsession/avatars/ResourceContactPhoto.java @@ -4,13 +4,13 @@ import android.content.Context; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.LayerDrawable; import android.widget.ImageView; import androidx.annotation.DrawableRes; import androidx.appcompat.content.res.AppCompatResources; -import com.amulyakhare.textdrawable.TextDrawable; import com.makeramen.roundedimageview.RoundedDrawable; import org.session.libsession.R; @@ -25,19 +25,33 @@ public class ResourceContactPhoto implements FallbackContactPhoto { } @Override - public Drawable asDrawable(Context context, int color) { - return asDrawable(context, color, false); + public Drawable asDrawable(Context context, int color, boolean inverted) { + return asDrawable(context, 0, false, 0f); } @Override - public Drawable asDrawable(Context context, int color, boolean inverted) { - Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color); + public Drawable asDrawable(Context context, int color, boolean inverted, Float padding) { + // rounded colored background + GradientDrawable background = new GradientDrawable(); + background.setShape(GradientDrawable.OVAL); + background.setColor(inverted ? Color.WHITE : color); + + // resource image in the foreground RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(AppCompatResources.getDrawable(context, resourceId)); - foreground.setScaleType(ImageView.ScaleType.CENTER_CROP); + if (foreground != null) { + if(padding == 0f){ + foreground.setScaleType(ImageView.ScaleType.CENTER_CROP); + } else { + // apply padding via a transparent border oterhwise things get misaligned + foreground.setScaleType(ImageView.ScaleType.FIT_CENTER); + foreground.setBorderColor(Color.TRANSPARENT); + foreground.setBorderWidth(padding); + } - if (inverted) { - foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); + if (inverted) { + foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); + } } Drawable gradient = AppCompatResources.getDrawable( diff --git a/libsession/src/main/java/org/session/libsession/avatars/TransparentContactPhoto.java b/libsession/src/main/java/org/session/libsession/avatars/TransparentContactPhoto.java index 56f2757e15..74d8e4ddc7 100644 --- a/libsession/src/main/java/org/session/libsession/avatars/TransparentContactPhoto.java +++ b/libsession/src/main/java/org/session/libsession/avatars/TransparentContactPhoto.java @@ -3,6 +3,8 @@ package org.session.libsession.avatars; import android.content.Context; import android.graphics.drawable.Drawable; +import androidx.core.content.ContextCompat; + import com.makeramen.roundedimageview.RoundedDrawable; public class TransparentContactPhoto implements FallbackContactPhoto { @@ -10,13 +12,13 @@ public class TransparentContactPhoto implements FallbackContactPhoto { public TransparentContactPhoto() {} @Override - public Drawable asDrawable(Context context, int color) { - return asDrawable(context, color, false); + public Drawable asDrawable(Context context, int color, boolean inverted) { + return asDrawable(context, color, inverted, 0f); } @Override - public Drawable asDrawable(Context context, int color, boolean inverted) { - return RoundedDrawable.fromDrawable(context.getResources().getDrawable(android.R.color.transparent)); + public Drawable asDrawable(Context context, int color, boolean inverted, Float padding) { + return RoundedDrawable.fromDrawable(ContextCompat.getDrawable(context, android.R.color.transparent)); } } From 26b186452aff9191d9313d95d874053325461c96 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 9 Aug 2024 09:35:03 +1000 Subject: [PATCH 03/12] Replace image cropping library --- app/build.gradle | 2 +- .../securesms/avatar/AvatarSelection.java | 116 -------------- .../securesms/avatar/AvatarSelection.kt | 141 ++++++++++++++++++ .../securesms/preferences/SettingsActivity.kt | 83 +++++++---- 4 files changed, 197 insertions(+), 145 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarSelection.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarSelection.kt diff --git a/app/build.gradle b/app/build.gradle index c99404f898..01f7980845 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -287,7 +287,7 @@ dependencies { implementation 'com.pnikosis:materialish-progress:1.5' implementation 'org.greenrobot:eventbus:3.0.0' implementation 'pl.tajchert:waitingdots:0.1.0' - implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' + implementation 'com.vanniktech:android-image-cropper:4.5.0' implementation 'com.melnykov:floatingactionbutton:1.3.0' implementation 'com.google.zxing:android-integration:3.1.0' implementation "com.google.dagger:hilt-android:$daggerVersion" diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarSelection.java b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarSelection.java deleted file mode 100644 index b0fbd8e22f..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarSelection.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.thoughtcrime.securesms.avatar; - -import android.Manifest; -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Build; -import android.provider.MediaStore; - -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.core.content.ContextCompat; - -import com.theartofdev.edmodo.cropper.CropImage; -import com.theartofdev.edmodo.cropper.CropImageView; - -import org.session.libsignal.utilities.NoExternalStorageException; -import org.session.libsignal.utilities.Log; -import org.session.libsignal.utilities.ExternalStorageUtil; -import org.thoughtcrime.securesms.util.FileProviderUtil; -import org.thoughtcrime.securesms.util.IntentUtils; - -import java.io.File; -import java.io.IOException; -import java.util.LinkedList; -import java.util.List; - -import network.loki.messenger.R; - -import static android.provider.MediaStore.EXTRA_OUTPUT; - -public final class AvatarSelection { - - private static final String TAG = AvatarSelection.class.getSimpleName(); - - public static final int REQUEST_CODE_CROP_IMAGE = CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE; - public static final int REQUEST_CODE_AVATAR = REQUEST_CODE_CROP_IMAGE + 1; - - private AvatarSelection() { - } - - /** - * Returns result on {@link #REQUEST_CODE_CROP_IMAGE} - */ - public static void circularCropImage(Activity activity, Uri inputFile, Uri outputFile, @StringRes int title) { - CropImage.activity(inputFile) - .setGuidelines(CropImageView.Guidelines.ON) - .setAspectRatio(1, 1) - .setCropShape(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? CropImageView.CropShape.RECTANGLE : CropImageView.CropShape.OVAL) - .setOutputUri(outputFile) - .setAllowRotation(true) - .setAllowFlipping(true) - .setBackgroundColor(ContextCompat.getColor(activity, R.color.avatar_background)) - .setActivityTitle(activity.getString(title)) - .start(activity); - } - - public static Uri getResultUri(Intent data) { - return CropImage.getActivityResult(data).getUri(); - } - - /** - * Returns result on {@link #REQUEST_CODE_AVATAR} - * - * @return Temporary capture file if created. - */ - public static File startAvatarSelection(Activity activity, boolean includeClear, boolean attemptToIncludeCamera) { - File captureFile = null; - boolean hasCameraPermission = ContextCompat - .checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED; - if (attemptToIncludeCamera && hasCameraPermission) { - try { - captureFile = File.createTempFile("avatar-capture", ".jpg", ExternalStorageUtil.getImageDir(activity)); - } catch (IOException | NoExternalStorageException e) { - Log.e("Cannot reserve a temporary avatar capture file.", e); - } - } - - Intent chooserIntent = createAvatarSelectionIntent(activity, captureFile, includeClear); - activity.startActivityForResult(chooserIntent, REQUEST_CODE_AVATAR); - return captureFile; - } - - private static Intent createAvatarSelectionIntent(Context context, @Nullable File tempCaptureFile, boolean includeClear) { - List extraIntents = new LinkedList<>(); - Intent galleryIntent = new Intent(Intent.ACTION_PICK); - galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*"); - - if (!IntentUtils.isResolvable(context, galleryIntent)) { - galleryIntent = new Intent(Intent.ACTION_GET_CONTENT); - galleryIntent.setType("image/*"); - } - - if (tempCaptureFile != null) { - Uri uri = FileProviderUtil.getUriFor(context, tempCaptureFile); - Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - cameraIntent.putExtra(EXTRA_OUTPUT, uri); - cameraIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - extraIntents.add(cameraIntent); - } - - if (includeClear) { - extraIntents.add(new Intent("network.loki.securesms.action.CLEAR_PROFILE_PHOTO")); - } - - Intent chooserIntent = Intent.createChooser(galleryIntent, context.getString(R.string.CreateProfileActivity_profile_photo)); - - if (!extraIntents.isEmpty()) { - chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Intent[0])); - } - - return chooserIntent; - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarSelection.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarSelection.kt new file mode 100644 index 0000000000..adf4b0927c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarSelection.kt @@ -0,0 +1,141 @@ +package org.thoughtcrime.securesms.avatar + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.provider.MediaStore +import androidx.activity.result.ActivityResultLauncher +import androidx.core.content.ContextCompat +import com.canhub.cropper.CropImageContractOptions +import com.canhub.cropper.CropImageOptions +import com.canhub.cropper.CropImageView +import network.loki.messenger.R +import org.session.libsession.utilities.getColorFromAttr +import org.session.libsignal.utilities.ExternalStorageUtil.getImageDir +import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.NoExternalStorageException +import org.thoughtcrime.securesms.util.FileProviderUtil +import org.thoughtcrime.securesms.util.IntentUtils +import java.io.File +import java.io.IOException +import java.util.LinkedList + +class AvatarSelection( + private val activity: Activity, + private val onAvatarCropped: ActivityResultLauncher, + private val onPickImage: ActivityResultLauncher +) { + private val TAG: String = AvatarSelection::class.java.simpleName + + private val bgColor by lazy { activity.getColorFromAttr(android.R.attr.colorPrimary) } + private val txtColor by lazy { activity.getColorFromAttr(android.R.attr.textColorPrimary) } + private val imageScrim by lazy { ContextCompat.getColor(activity, R.color.avatar_background) } + private val activityTitle by lazy { activity.getString(R.string.CropImageActivity_profile_avatar) } + + /** + * Returns result on [.REQUEST_CODE_CROP_IMAGE] + */ + fun circularCropImage( + inputFile: Uri?, + outputFile: Uri? + ) { + onAvatarCropped.launch( + CropImageContractOptions( + uri = inputFile, + cropImageOptions = CropImageOptions( + guidelines = CropImageView.Guidelines.ON, + aspectRatioX = 1, + aspectRatioY = 1, + fixAspectRatio = true, + cropShape = CropImageView.CropShape.OVAL, + customOutputUri = outputFile, + allowRotation = true, + allowFlipping = true, + backgroundColor = imageScrim, + toolbarColor = bgColor, + activityBackgroundColor = bgColor, + toolbarTintColor = txtColor, + toolbarBackButtonColor = txtColor, + toolbarTitleColor = txtColor, + activityMenuIconColor = txtColor, + activityMenuTextColor = txtColor, + activityTitle = activityTitle + ) + ) + ) + } + + /** + * Returns result on [.REQUEST_CODE_AVATAR] + * + * @return Temporary capture file if created. + */ + fun startAvatarSelection( + includeClear: Boolean, + attemptToIncludeCamera: Boolean + ): File? { + var captureFile: File? = null + val hasCameraPermission = ContextCompat + .checkSelfPermission( + activity, + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + if (attemptToIncludeCamera && hasCameraPermission) { + try { + captureFile = File.createTempFile("avatar-capture", ".jpg", getImageDir(activity)) + } catch (e: IOException) { + Log.e("Cannot reserve a temporary avatar capture file.", e) + } catch (e: NoExternalStorageException) { + Log.e("Cannot reserve a temporary avatar capture file.", e) + } + } + + val chooserIntent = createAvatarSelectionIntent(activity, captureFile, includeClear) + onPickImage.launch(chooserIntent) + return captureFile + } + + private fun createAvatarSelectionIntent( + context: Context, + tempCaptureFile: File?, + includeClear: Boolean + ): Intent { + val extraIntents: MutableList = LinkedList() + var galleryIntent = Intent(Intent.ACTION_PICK) + galleryIntent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*") + + if (!IntentUtils.isResolvable(context, galleryIntent)) { + galleryIntent = Intent(Intent.ACTION_GET_CONTENT) + galleryIntent.setType("image/*") + } + + if (tempCaptureFile != null) { + val uri = FileProviderUtil.getUriFor(context, tempCaptureFile) + val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri) + cameraIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + extraIntents.add(cameraIntent) + } + + if (includeClear) { + extraIntents.add(Intent("network.loki.securesms.action.CLEAR_PROFILE_PHOTO")) + } + + val chooserIntent = Intent.createChooser( + galleryIntent, + context.getString(R.string.CreateProfileActivity_profile_photo) + ) + + if (!extraIntents.isEmpty()) { + chooserIntent.putExtra( + Intent.EXTRA_INITIAL_INTENTS, + extraIntents.toTypedArray() + ) + } + + return chooserIntent + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index fc98070ca4..ffc2865d78 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -18,6 +18,7 @@ import android.view.View import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -34,6 +35,8 @@ import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.canhub.cropper.CropImage +import com.canhub.cropper.CropImageContract import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose @@ -77,12 +80,12 @@ import org.thoughtcrime.securesms.ui.Cell import org.thoughtcrime.securesms.ui.Divider import org.thoughtcrime.securesms.ui.LargeItemButton import org.thoughtcrime.securesms.ui.LargeItemButtonWithDrawable -import org.thoughtcrime.securesms.ui.theme.LocalDimensions -import org.thoughtcrime.securesms.ui.theme.dangerButtonColors import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton import org.thoughtcrime.securesms.ui.components.PrimaryOutlineCopyButton import org.thoughtcrime.securesms.ui.contentDescription import org.thoughtcrime.securesms.ui.setThemedContent +import org.thoughtcrime.securesms.ui.theme.LocalDimensions +import org.thoughtcrime.securesms.ui.theme.dangerButtonColors import org.thoughtcrime.securesms.util.BitmapDecodingException import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities @@ -109,6 +112,48 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { private val hexEncodedPublicKey: String get() = TextSecurePreferences.getLocalNumber(this)!! + private val onAvatarCropped = registerForActivityResult(CropImageContract()) { result -> + when { + result.isSuccessful -> { + Log.i(TAG, result.getUriFilePath(this).toString()) + + lifecycleScope.launch(Dispatchers.IO) { + try { + val profilePictureToBeUploaded = + BitmapUtil.createScaledBytes( + this@SettingsActivity, + result.getUriFilePath(this@SettingsActivity).toString(), + ProfileMediaConstraints() + ).bitmap + launch(Dispatchers.Main) { + updateProfilePicture(profilePictureToBeUploaded) + } + } catch (e: BitmapDecodingException) { + Log.e(TAG, e) + } + } + } + result is CropImage.CancelledResult -> { + Log.i(TAG, "Cropping image was cancelled by the user") + } + else -> { + Log.e(TAG, "Cropping image failed") + } + } + } + + private val onPickImage = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ){ result -> + if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult + + val outputFile = Uri.fromFile(File(cacheDir, "cropped")) + val inputFile: Uri? = result.data?.data ?: tempFile?.let(Uri::fromFile) + cropImage(inputFile, outputFile) + } + + private val avatarSelection = AvatarSelection(this, onAvatarCropped, onPickImage) + companion object { private const val SCROLL_STATE = "SCROLL_STATE" } @@ -181,31 +226,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { } } - @Deprecated("Deprecated in Java") - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (resultCode != Activity.RESULT_OK) return - when (requestCode) { - AvatarSelection.REQUEST_CODE_AVATAR -> { - val outputFile = Uri.fromFile(File(cacheDir, "cropped")) - val inputFile: Uri? = data?.data ?: tempFile?.let(Uri::fromFile) - AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar) - } - AvatarSelection.REQUEST_CODE_CROP_IMAGE -> { - lifecycleScope.launch(Dispatchers.IO) { - try { - val profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap - launch(Dispatchers.Main) { - updateProfilePicture(profilePictureToBeUploaded) - } - } catch (e: BitmapDecodingException) { - Log.e(TAG, e) - } - } - } - } - } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults) @@ -407,10 +427,17 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { Permissions.with(this) .request(Manifest.permission.CAMERA) .onAnyResult { - tempFile = AvatarSelection.startAvatarSelection(this, false, true) + tempFile = avatarSelection.startAvatarSelection( false, true) } .execute() } + + private fun cropImage(inputFile: Uri?, outputFile: Uri?){ + avatarSelection.circularCropImage( + inputFile = inputFile, + outputFile = outputFile, + ) + } // endregion private inner class DisplayNameEditActionModeCallback: ActionMode.Callback { From 84896f8d234ac0b770d132c45cd7eaf22dba5723 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 9 Aug 2024 14:39:35 +1000 Subject: [PATCH 04/12] Updated to latest web-rtc library It seems setWebRtcBasedAcousticEchoCanceler and setBlacklistDeviceForOpenSLESUsage are no longer available. What this means is unclear and the new library might be handling certain things internally --- app/build.gradle | 2 +- .../securesms/ApplicationContext.java | 32 ------------------- .../securesms/webrtc/PeerConnectionWrapper.kt | 5 ++- 3 files changed, 5 insertions(+), 34 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 01f7980845..9e1900800f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -273,7 +273,7 @@ dependencies { implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1' implementation 'org.conscrypt:conscrypt-android:2.5.2' implementation 'org.signal:aesgcmprovider:0.0.3' - implementation 'org.webrtc:google-webrtc:1.0.32006' + implementation 'io.github.webrtc-sdk:android:125.6422.04' implementation "me.leolin:ShortcutBadger:1.1.16" implementation 'se.emilsjolander:stickylistheaders:2.7.0' implementation 'com.jpardogo.materialtabstrip:library:1.0.9' diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 82699f393a..b23a6741cb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -23,7 +23,6 @@ import android.app.Application; import android.content.Context; import android.content.Intent; import android.os.AsyncTask; -import android.os.Build; import android.os.Handler; import android.os.HandlerThread; @@ -91,8 +90,6 @@ import org.thoughtcrime.securesms.util.dynamiclanguage.LocaleParseHelper; import org.thoughtcrime.securesms.webrtc.CallMessageProcessor; import org.webrtc.PeerConnectionFactory; import org.webrtc.PeerConnectionFactory.InitializationOptions; -import org.webrtc.voiceengine.WebRtcAudioManager; -import org.webrtc.voiceengine.WebRtcAudioUtils; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -100,9 +97,7 @@ import java.io.InputStream; import java.security.Security; import java.util.Arrays; import java.util.Date; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.Timer; import java.util.concurrent.Executors; @@ -394,33 +389,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO private void initializeWebRtc() { try { - Set HARDWARE_AEC_BLACKLIST = new HashSet() {{ - add("Pixel"); - add("Pixel XL"); - add("Moto G5"); - add("Moto G (5S) Plus"); - add("Moto G4"); - add("TA-1053"); - add("Mi A1"); - add("E5823"); // Sony z5 compact - add("Redmi Note 5"); - add("FP2"); // Fairphone FP2 - add("MI 5"); - }}; - - Set OPEN_SL_ES_WHITELIST = new HashSet() {{ - add("Pixel"); - add("Pixel XL"); - }}; - - if (HARDWARE_AEC_BLACKLIST.contains(Build.MODEL)) { - WebRtcAudioUtils.setWebRtcBasedAcousticEchoCanceler(true); - } - - if (!OPEN_SL_ES_WHITELIST.contains(Build.MODEL)) { - WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true); - } - PeerConnectionFactory.initialize(InitializationOptions.builder(this).createInitializationOptions()); } catch (UnsatisfiedLinkError e) { Log.w(TAG, e); diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt index b61edbb6d2..3d5caf1a1b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt @@ -74,7 +74,10 @@ class PeerConnectionWrapper(private val context: Context, newPeerConnection.setAudioPlayout(true) newPeerConnection.setAudioRecording(true) - newPeerConnection.addStream(mediaStream) + // Calls to `addStream` are deprecated & cause errors so we must use `addTracks` when + // using `io.github.webrtc-sdk:android:114.5735.10` and newer. + newPeerConnection.addTrack(mediaStream.audioTracks[0]) + if (mediaStream.videoTracks.isNotEmpty()) newPeerConnection.addTrack(mediaStream.videoTracks[0]) } init { From c3d928ed8cb6e034dbec220b8f621803f2b18050 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 9 Aug 2024 17:03:23 +1000 Subject: [PATCH 05/12] Using PLAN_B for now to make it work with the old code --- .../thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt index 3d5caf1a1b..480ba24ae3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt @@ -64,6 +64,7 @@ class PeerConnectionWrapper(private val context: Context, val configuration = PeerConnection.RTCConfiguration(iceServers).apply { bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE + sdpSemantics = PeerConnection.SdpSemantics.PLAN_B if (relay) { iceTransportsType = PeerConnection.IceTransportsType.RELAY } @@ -74,10 +75,7 @@ class PeerConnectionWrapper(private val context: Context, newPeerConnection.setAudioPlayout(true) newPeerConnection.setAudioRecording(true) - // Calls to `addStream` are deprecated & cause errors so we must use `addTracks` when - // using `io.github.webrtc-sdk:android:114.5735.10` and newer. - newPeerConnection.addTrack(mediaStream.audioTracks[0]) - if (mediaStream.videoTracks.isNotEmpty()) newPeerConnection.addTrack(mediaStream.videoTracks[0]) + newPeerConnection.addStream(mediaStream) } init { From 53c1fd5e9c0af43310762c1cae70a8f1d94f0b73 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 13 Aug 2024 11:26:59 +1000 Subject: [PATCH 06/12] Removing Glide --- .../java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt index b3320a4758..2bded3cccb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt @@ -32,7 +32,6 @@ import org.session.libsession.utilities.truncateIdForDisplay import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.service.WebRtcCallService import org.thoughtcrime.securesms.webrtc.AudioManagerCommand @@ -62,7 +61,6 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { } private val viewModel by viewModels() - private val glide by lazy { GlideApp.with(this) } private lateinit var binding: ActivityWebrtcBinding private var uiJob: Job? = null private var wantsToAnswer = false From 2bf8bc17b50b82349a286ed00612a4a4246b8a3e Mon Sep 17 00:00:00 2001 From: fanchao Date: Tue, 13 Aug 2024 12:09:15 +1000 Subject: [PATCH 07/12] Added stickyheader AAR --- app/build.gradle | 2 +- settings.gradle | 1 + stickyheader/build.gradle | 2 ++ stickyheader/stickyheadergrid-0.9.4.aar | Bin 0 -> 41521 bytes 4 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 stickyheader/build.gradle create mode 100644 stickyheader/stickyheadergrid-0.9.4.aar diff --git a/app/build.gradle b/app/build.gradle index 9e1900800f..a96f4bbc2c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -308,7 +308,7 @@ dependencies { exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection' } implementation 'com.annimon:stream:1.1.8' - implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4' + implementation project(':stickyheader') implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2' implementation 'androidx.sqlite:sqlite-ktx:2.3.1' implementation 'net.zetetic:sqlcipher-android:4.5.4@aar' diff --git a/settings.gradle b/settings.gradle index 7ab26e097c..e2388ee105 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,3 +5,4 @@ include ':liblazysodium' include ':libsession' include ':libsignal' include ':libsession-util' +include ':stickyheader' diff --git a/stickyheader/build.gradle b/stickyheader/build.gradle new file mode 100644 index 0000000000..b7a36ae090 --- /dev/null +++ b/stickyheader/build.gradle @@ -0,0 +1,2 @@ +configurations.maybeCreate("default") +artifacts.add("default", file('stickyheadergrid-0.9.4.aar')) \ No newline at end of file diff --git a/stickyheader/stickyheadergrid-0.9.4.aar b/stickyheader/stickyheadergrid-0.9.4.aar new file mode 100644 index 0000000000000000000000000000000000000000..a704f0985f0c3492ae544b6f36356645966fa420 GIT binary patch literal 41521 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ɐ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 literal 0 HcmV?d00001 From fb937ee292a77cf5fc13aab6babadb1e3c3d3670 Mon Sep 17 00:00:00 2001 From: fanchao Date: Tue, 13 Aug 2024 12:09:36 +1000 Subject: [PATCH 08/12] Remove jcenter from build script --- build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle b/build.gradle index 70d1f63fa4..00eec74b0a 100644 --- a/build.gradle +++ b/build.gradle @@ -54,7 +54,6 @@ allprojects { includeGroupByRegex "org\\.signal.*" } } - jcenter() maven { url "https://jitpack.io" } if (project.hasProperty('huawei')) maven { url 'https://developer.huawei.com/repo/' From d82c5b6a1b040d6f610b78b3e58f18bbcc6109c8 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Mon, 12 Aug 2024 11:01:47 +1000 Subject: [PATCH 09/12] Migrate exoplayer to media3 --- app/build.gradle | 4 +- .../securesms/audio/AudioSlidePlayer.java | 110 ++++++++---------- .../securesms/database/Storage.kt | 2 +- .../mediasend/MediaSendVideoFragment.java | 4 + .../securesms/video/VideoPlayer.java | 90 +++++--------- .../video/exo/AttachmentDataSource.java | 61 ---------- .../exo/AttachmentDataSourceFactory.java | 33 ------ .../securesms/video/exo/PartDataSource.java | 89 -------------- .../layout/media_preview_exoplayer_layout.xml | 2 +- app/src/main/res/layout/video_player.xml | 2 +- app/src/main/res/values/attrs.xml | 16 --- .../loki/messenger/libsession_util/Config.kt | 2 +- libsession/src/main/res/values/attrs.xml | 16 --- 13 files changed, 86 insertions(+), 345 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/video/exo/AttachmentDataSource.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/video/exo/AttachmentDataSourceFactory.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/video/exo/PartDataSource.java diff --git a/app/build.gradle b/app/build.gradle index a96f4bbc2c..3affc44944 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -269,8 +269,8 @@ dependencies { exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' } if (project.hasProperty('huawei')) huaweiImplementation 'com.huawei.hms:push:6.7.0.300' - implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1' - implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1' + implementation 'androidx.media3:media3-exoplayer:1.4.0' + implementation 'androidx.media3:media3-ui:1.4.0' implementation 'org.conscrypt:conscrypt-android:2.5.2' implementation 'org.signal:aesgcmprovider:0.0.3' implementation 'io.github.webrtc-sdk:android:125.6422.04' diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java index ef404bb070..3aa159bc1e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java @@ -6,49 +6,35 @@ import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.media.AudioManager; -import android.net.Uri; -import android.os.Build; import android.os.Handler; import android.os.Message; import android.os.PowerManager; import android.os.PowerManager.WakeLock; +import android.util.Pair; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.util.Pair; -import android.widget.Toast; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultLoadControl; -import com.google.android.exoplayer2.DefaultRenderersFactory; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.LoadControl; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.audio.AudioAttributes; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import androidx.annotation.OptIn; +import androidx.media3.common.AudioAttributes; +import androidx.media3.common.C; +import androidx.media3.common.MediaItem; +import androidx.media3.common.PlaybackException; +import androidx.media3.common.PlaybackParameters; +import androidx.media3.common.Player; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.ExoPlayer; import org.jetbrains.annotations.NotNull; -import org.thoughtcrime.securesms.attachments.AttachmentServer; -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.mms.AudioSlide; import org.session.libsession.utilities.ServiceUtil; - import org.session.libsession.utilities.Util; - +import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.guava.Optional; +import org.thoughtcrime.securesms.attachments.AttachmentServer; +import org.thoughtcrime.securesms.mms.AudioSlide; import java.io.IOException; import java.lang.ref.WeakReference; -import network.loki.messenger.BuildConfig; -import network.loki.messenger.R; - public class AudioSlidePlayer implements SensorEventListener { private static final String TAG = AudioSlidePlayer.class.getSimpleName(); @@ -64,7 +50,7 @@ public class AudioSlidePlayer implements SensorEventListener { private final @Nullable WakeLock wakeLock; private @NonNull WeakReference listener; - private @Nullable SimpleExoPlayer mediaPlayer; + private @Nullable ExoPlayer mediaPlayer; private @Nullable AttachmentServer audioAttachmentServer; private long startTime; @@ -97,40 +83,38 @@ public class AudioSlidePlayer implements SensorEventListener { this.sensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); this.proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); - if (Build.VERSION.SDK_INT >= 21) { - this.wakeLock = ServiceUtil.getPowerManager(context).newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG); - } else { - this.wakeLock = null; - } + this.wakeLock = ServiceUtil.getPowerManager(context).newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG); } public void play(final double progress) throws IOException { play(progress, false); } + @OptIn(markerClass = UnstableApi.class) private void play(final double progress, boolean earpiece) throws IOException { if (this.mediaPlayer != null) { stop(); } - LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).createDefaultLoadControl(); - this.mediaPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), new DefaultTrackSelector(), loadControl); + this.mediaPlayer = new ExoPlayer.Builder(context).build(); this.audioAttachmentServer = new AttachmentServer(context, slide.asAttachment()); this.startTime = System.currentTimeMillis(); audioAttachmentServer.start(); - mediaPlayer.prepare(createMediaSource(audioAttachmentServer.getUri())); - mediaPlayer.setPlayWhenReady(true); + MediaItem mediaItem = MediaItem.fromUri(audioAttachmentServer.getUri()); + mediaPlayer.setMediaItem(mediaItem); + mediaPlayer.setAudioAttributes(new AudioAttributes.Builder() - .setContentType(earpiece ? C.CONTENT_TYPE_SPEECH : C.CONTENT_TYPE_MUSIC) + .setContentType(earpiece ? C.AUDIO_CONTENT_TYPE_SPEECH : C.AUDIO_CONTENT_TYPE_MUSIC) .setUsage(earpiece ? C.USAGE_VOICE_COMMUNICATION : C.USAGE_MEDIA) - .build()); - mediaPlayer.addListener(new Player.EventListener() { + .build(), + !earpiece); + mediaPlayer.addListener(new Player.Listener() { boolean started = false; @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - Log.d(TAG, "onPlayerStateChanged(" + playWhenReady + ", " + playbackState + ")"); + public void onPlaybackStateChanged(int playbackState) { + Log.d(TAG, "onPlaybackStateChanged(" + playbackState + ")"); switch (playbackState) { case Player.STATE_READY: Log.i(TAG, "onPrepared() " + mediaPlayer.getBufferedPercentage() + "% buffered"); @@ -174,9 +158,7 @@ public class AudioSlidePlayer implements SensorEventListener { sensorManager.unregisterListener(AudioSlidePlayer.this); if (wakeLock != null && wakeLock.isHeld()) { - if (Build.VERSION.SDK_INT >= 21) { - wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY); - } + wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY); } } @@ -186,8 +168,9 @@ public class AudioSlidePlayer implements SensorEventListener { } } + @Override - public void onPlayerError(ExoPlaybackException error) { + public void onPlayerError(PlaybackException error) { Log.w(TAG, "MediaPlayer Error: " + error); synchronized (AudioSlidePlayer.this) { @@ -201,9 +184,7 @@ public class AudioSlidePlayer implements SensorEventListener { sensorManager.unregisterListener(AudioSlidePlayer.this); if (wakeLock != null && wakeLock.isHeld()) { - if (Build.VERSION.SDK_INT >= 21) { - wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY); - } + wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY); } } @@ -211,12 +192,9 @@ public class AudioSlidePlayer implements SensorEventListener { progressEventHandler.removeMessages(0); } }); - } - private MediaSource createMediaSource(@NonNull Uri uri) { - return new ExtractorMediaSource.Factory(new DefaultDataSourceFactory(context, BuildConfig.USER_AGENT)) - .setExtractorsFactory(new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)) - .createMediaSource(uri); + mediaPlayer.prepare(); + mediaPlayer.setPlayWhenReady(true); } public synchronized void stop() { @@ -348,14 +326,18 @@ public class AudioSlidePlayer implements SensorEventListener { int streamType; - if (event.values[0] < 5f && event.values[0] != proximitySensor.getMaximumRange()) { - streamType = AudioManager.STREAM_VOICE_CALL; + if ( + proximitySensor != null && + event.values[0] < 5f && + event.values[0] != proximitySensor.getMaximumRange() + ) { + streamType = C.AUDIO_CONTENT_TYPE_SPEECH; } else { - streamType = AudioManager.STREAM_MUSIC; + streamType = C.AUDIO_CONTENT_TYPE_MUSIC; } - if (streamType == AudioManager.STREAM_VOICE_CALL && - mediaPlayer.getAudioStreamType() != streamType && + if (streamType == C.AUDIO_CONTENT_TYPE_SPEECH && + mediaPlayer.getAudioAttributes().contentType != streamType && !audioManager.isWiredHeadsetOn()) { double position = mediaPlayer.getCurrentPosition(); @@ -369,11 +351,11 @@ public class AudioSlidePlayer implements SensorEventListener { } catch (IOException e) { Log.w(TAG, e); } - } else if (streamType == AudioManager.STREAM_MUSIC && - mediaPlayer.getAudioStreamType() != streamType && + } else if (streamType == C.AUDIO_CONTENT_TYPE_MUSIC && + mediaPlayer.getAudioAttributes().contentType != streamType && System.currentTimeMillis() - startTime > 500) { - if (wakeLock != null) wakeLock.release(); + if (wakeLock != null && wakeLock.isHeld()) wakeLock.release(); stop(); notifyOnStop(); } @@ -411,7 +393,7 @@ public class AudioSlidePlayer implements SensorEventListener { sendEmptyMessageDelayed(0, 50); } - private boolean isPlayerActive(@NonNull SimpleExoPlayer player) { + private boolean isPlayerActive(@NonNull ExoPlayer player) { return player.getPlaybackState() == Player.STATE_READY || player.getPlaybackState() == Player.STATE_BUFFERING; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 3115275773..08aad8b6df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -536,7 +536,7 @@ open class Storage( } private fun updateConvoVolatile(convos: ConversationVolatileConfig, messageTimestamp: Long) { - val extracted = convos.all() + val extracted = convos.all().filterNotNull() for (conversation in extracted) { val threadId = when (conversation) { is Conversation.OneToOne -> getThreadIdFor(conversation.accountId, null, null, createThread = false) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendVideoFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendVideoFragment.java index 5e4ee5f3e7..825012c032 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendVideoFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendVideoFragment.java @@ -4,7 +4,10 @@ import android.net.Uri; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.fragment.app.Fragment; +import androidx.media3.common.util.UnstableApi; + import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -16,6 +19,7 @@ import org.thoughtcrime.securesms.video.VideoPlayer; import java.io.IOException; +@OptIn(markerClass = UnstableApi.class) public class MediaSendVideoFragment extends Fragment implements MediaSendPageFragment { private static final String TAG = MediaSendVideoFragment.class.getSimpleName(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/video/VideoPlayer.java b/app/src/main/java/org/thoughtcrime/securesms/video/VideoPlayer.java index 08d3a52a2f..2aa5531263 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/video/VideoPlayer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/video/VideoPlayer.java @@ -18,56 +18,46 @@ package org.thoughtcrime.securesms.video; import android.content.Context; import android.os.Build; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.util.AttributeSet; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.widget.FrameLayout; -import android.widget.MediaController; import android.widget.Toast; import android.widget.VideoView; -import com.google.android.exoplayer2.DefaultLoadControl; -import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.LoadControl; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelector; -import com.google.android.exoplayer2.ui.PlayerControlView; -import com.google.android.exoplayer2.ui.PlayerView; -import com.google.android.exoplayer2.upstream.BandwidthMeter; -import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; -import org.thoughtcrime.securesms.attachments.AttachmentServer; +import androidx.media3.common.MediaItem; +import androidx.media3.common.Player; +import androidx.media3.common.AudioAttributes; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.ui.LegacyPlayerControlView; +import androidx.media3.ui.PlayerView; + + +import org.session.libsession.utilities.ViewUtil; import org.session.libsignal.utilities.Log; +import org.thoughtcrime.securesms.attachments.AttachmentServer; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.VideoSlide; -import org.session.libsession.utilities.ViewUtil; -import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory; import java.io.IOException; import network.loki.messenger.R; +@UnstableApi public class VideoPlayer extends FrameLayout { private static final String TAG = VideoPlayer.class.getSimpleName(); @Nullable private final VideoView videoView; - @Nullable private final PlayerView exoView; + @Nullable private final PlayerView exoView; - @Nullable private SimpleExoPlayer exoPlayer; - @Nullable private PlayerControlView exoControls; + @Nullable private ExoPlayer exoPlayer; + @Nullable private LegacyPlayerControlView exoControls; @Nullable private AttachmentServer attachmentServer; @Nullable private Window window; @@ -84,23 +74,16 @@ public class VideoPlayer extends FrameLayout { inflate(context, R.layout.video_player, this); - if (Build.VERSION.SDK_INT >= 16) { - this.exoView = ViewUtil.findById(this, R.id.video_view); - this.videoView = null; - this.exoControls = new PlayerControlView(getContext()); - this.exoControls.setShowTimeoutMs(-1); - } else { - this.videoView = ViewUtil.findById(this, R.id.video_view); - this.exoView = null; - initializeVideoViewControls(videoView); - } + this.exoView = ViewUtil.findById(this, R.id.video_view); + this.videoView = null; + this.exoControls = new LegacyPlayerControlView(getContext()); + this.exoControls.setShowTimeoutMs(-1); } public void setVideoSource(@NonNull VideoSlide videoSource, boolean autoplay) throws IOException { - if (Build.VERSION.SDK_INT >= 16) setExoViewSource(videoSource, autoplay); - else setVideoViewSource(videoSource, autoplay); + setExoViewSource(videoSource, autoplay); } public void pause() { @@ -141,25 +124,20 @@ public class VideoPlayer extends FrameLayout { private void setExoViewSource(@NonNull VideoSlide videoSource, boolean autoplay) throws IOException { - BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); - TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter); - TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); - LoadControl loadControl = new DefaultLoadControl(); - - exoPlayer = ExoPlayerFactory.newSimpleInstance(getContext(), trackSelector, loadControl); + exoPlayer = new ExoPlayer.Builder(getContext()).build(); exoPlayer.addListener(new ExoPlayerListener(window)); + exoPlayer.setAudioAttributes(AudioAttributes.DEFAULT, true); //noinspection ConstantConditions exoView.setPlayer(exoPlayer); //noinspection ConstantConditions exoControls.setPlayer(exoPlayer); - DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(getContext(), "GenericUserAgent", null); - AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(getContext(), defaultDataSourceFactory, null); - ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); + if(videoSource.getUri() != null){ + MediaItem mediaItem = MediaItem.fromUri(videoSource.getUri()); + exoPlayer.setMediaItem(mediaItem); + } - MediaSource mediaSource = new ExtractorMediaSource(videoSource.getUri(), attachmentDataSourceFactory, extractorsFactory, null, null); - - exoPlayer.prepare(mediaSource); + exoPlayer.prepare(); exoPlayer.setPlayWhenReady(autoplay); } @@ -189,15 +167,7 @@ public class VideoPlayer extends FrameLayout { if (autoplay) this.videoView.start(); } - private void initializeVideoViewControls(@NonNull VideoView videoView) { - MediaController mediaController = new MediaController(getContext()); - mediaController.setAnchorView(videoView); - mediaController.setMediaPlayer(videoView); - - videoView.setMediaController(mediaController); - } - - private static class ExoPlayerListener extends Player.DefaultEventListener { + private static class ExoPlayerListener implements Player.Listener { private final Window window; ExoPlayerListener(Window window) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/video/exo/AttachmentDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/video/exo/AttachmentDataSource.java deleted file mode 100644 index 2989ff35c2..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/video/exo/AttachmentDataSource.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.thoughtcrime.securesms.video.exo; - - -import android.net.Uri; - -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.upstream.DefaultDataSource; -import com.google.android.exoplayer2.upstream.TransferListener; - -import org.thoughtcrime.securesms.mms.PartAuthority; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -public class AttachmentDataSource implements DataSource { - - private final DefaultDataSource defaultDataSource; - private final PartDataSource partDataSource; - - private DataSource dataSource; - - public AttachmentDataSource(DefaultDataSource defaultDataSource, PartDataSource partDataSource) { - this.defaultDataSource = defaultDataSource; - this.partDataSource = partDataSource; - } - - @Override - public void addTransferListener(TransferListener transferListener) { - } - - @Override - public long open(DataSpec dataSpec) throws IOException { - if (PartAuthority.isLocalUri(dataSpec.uri)) dataSource = partDataSource; - else dataSource = defaultDataSource; - - return dataSource.open(dataSpec); - } - - @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { - return dataSource.read(buffer, offset, readLength); - } - - @Override - public Uri getUri() { - return dataSource.getUri(); - } - - @Override - public Map> getResponseHeaders() { - return Collections.emptyMap(); - } - - @Override - public void close() throws IOException { - dataSource.close(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/video/exo/AttachmentDataSourceFactory.java b/app/src/main/java/org/thoughtcrime/securesms/video/exo/AttachmentDataSourceFactory.java deleted file mode 100644 index 99a6e28d9b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/video/exo/AttachmentDataSourceFactory.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.thoughtcrime.securesms.video.exo; - - -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.upstream.TransferListener; - -public class AttachmentDataSourceFactory implements DataSource.Factory { - - private final Context context; - - private final DefaultDataSourceFactory defaultDataSourceFactory; - private final TransferListener listener; - - public AttachmentDataSourceFactory(@NonNull Context context, - @NonNull DefaultDataSourceFactory defaultDataSourceFactory, - @Nullable TransferListener listener) - { - this.context = context; - this.defaultDataSourceFactory = defaultDataSourceFactory; - this.listener = listener; - } - - @Override - public AttachmentDataSource createDataSource() { - return new AttachmentDataSource(defaultDataSourceFactory.createDataSource(), - new PartDataSource(context, listener)); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/video/exo/PartDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/video/exo/PartDataSource.java deleted file mode 100644 index 45e46cf054..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/video/exo/PartDataSource.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.thoughtcrime.securesms.video.exo; - - -import android.content.Context; -import android.net.Uri; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.upstream.TransferListener; - -import org.session.libsession.messaging.sending_receiving.attachments.Attachment; -import org.thoughtcrime.securesms.database.AttachmentDatabase; -import org.thoughtcrime.securesms.dependencies.DatabaseComponent; -import org.thoughtcrime.securesms.mms.PartUriParser; - -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -public class PartDataSource implements DataSource { - - private final @NonNull Context context; - private final @Nullable TransferListener listener; - - private Uri uri; - private InputStream inputSteam; - - PartDataSource(@NonNull Context context, @Nullable TransferListener listener) { - this.context = context.getApplicationContext(); - this.listener = listener; - } - - @Override - public void addTransferListener(TransferListener transferListener) { - } - - @Override - public long open(DataSpec dataSpec) throws IOException { - this.uri = dataSpec.uri; - - AttachmentDatabase attachmentDatabase = DatabaseComponent.get(context).attachmentDatabase(); - PartUriParser partUri = new PartUriParser(uri); - Attachment attachment = attachmentDatabase.getAttachment(partUri.getPartId()); - - if (attachment == null) throw new IOException("Attachment not found"); - - this.inputSteam = attachmentDatabase.getAttachmentStream(partUri.getPartId(), dataSpec.position); - - if (listener != null) { - listener.onTransferStart(this, dataSpec, false); - } - - if (attachment.getSize() - dataSpec.position <= 0) throw new EOFException("No more data"); - - return attachment.getSize() - dataSpec.position; - } - - @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { - int read = inputSteam.read(buffer, offset, readLength); - - if (read > 0 && listener != null) { - listener.onBytesTransferred(this, null, false, read); - } - - return read; - } - - @Override - public Uri getUri() { - return uri; - } - - @Override - public Map> getResponseHeaders() { - return Collections.emptyMap(); - } - - @Override - public void close() throws IOException { - inputSteam.close(); - } -} diff --git a/app/src/main/res/layout/media_preview_exoplayer_layout.xml b/app/src/main/res/layout/media_preview_exoplayer_layout.xml index d6b870c7c5..b904a6a4d1 100644 --- a/app/src/main/res/layout/media_preview_exoplayer_layout.xml +++ b/app/src/main/res/layout/media_preview_exoplayer_layout.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - - - - - - - - - - - - - - - - - diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt index ccdda282fe..eb000017e5 100644 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt @@ -162,7 +162,7 @@ class ConversationVolatileConfig(pointer: Long): ConfigBase(pointer) { external fun allOneToOnes(): List external fun allCommunities(): List external fun allLegacyClosedGroups(): List - external fun all(): List + external fun all(): List } diff --git a/libsession/src/main/res/values/attrs.xml b/libsession/src/main/res/values/attrs.xml index a412c13e76..5588e25ece 100644 --- a/libsession/src/main/res/values/attrs.xml +++ b/libsession/src/main/res/values/attrs.xml @@ -270,22 +270,6 @@ - - - - - - - - - - - - - - - - From bc3f33701e25d8bad77b6b5fe25aa37e748018ba Mon Sep 17 00:00:00 2001 From: fanchao Date: Tue, 13 Aug 2024 14:03:22 +1000 Subject: [PATCH 10/12] Remove unused resources --- app/src/main/res/values/styles.xml | 4 ---- app/src/main/res/values/themes.xml | 1 - 2 files changed, 5 deletions(-) diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 634e45643e..01304ee9c7 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -18,10 +18,6 @@ @dimen/very_large_font_size - -