Merge branch 'dev' into strings-squashed

This commit is contained in:
Al Lansley 2024-08-15 09:38:10 +10:00
commit eecce08c25
36 changed files with 331 additions and 841 deletions

View File

@ -29,7 +29,7 @@ configurations.all {
} }
def canonicalVersionCode = 379 def canonicalVersionCode = 379
def canonicalVersionName = "1.19.0" def canonicalVersionName = "1.19.1"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,
@ -246,7 +246,7 @@ dependencies {
implementation "androidx.appcompat:appcompat:$appcompatVersion" implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "com.google.android.material:material:$materialVersion" implementation "com.google.android.material:material:$materialVersion"
implementation 'com.google.android:flexbox:2.0.1' implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'androidx.legacy:legacy-support-v13:1.0.0' implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation "androidx.preference:preference-ktx:$preferenceVersion" implementation "androidx.preference:preference-ktx:$preferenceVersion"
@ -273,11 +273,11 @@ dependencies {
if (project.hasProperty('huawei')) huaweiImplementation 'com.huawei.hms:push:6.7.0.300' if (project.hasProperty('huawei')) huaweiImplementation 'com.huawei.hms:push:6.7.0.300'
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1' implementation 'androidx.media3:media3-exoplayer:1.4.0'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1' implementation 'androidx.media3:media3-ui:1.4.0'
implementation 'org.conscrypt:conscrypt-android:2.5.2' implementation 'org.conscrypt:conscrypt-android:2.5.2'
implementation 'org.signal:aesgcmprovider:0.0.3' 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 "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0' implementation 'se.emilsjolander:stickylistheaders:2.7.0'
implementation 'com.jpardogo.materialtabstrip:library:1.0.9' implementation 'com.jpardogo.materialtabstrip:library:1.0.9'
@ -289,17 +289,13 @@ dependencies {
implementation 'com.pnikosis:materialish-progress:1.5' implementation 'com.pnikosis:materialish-progress:1.5'
implementation 'org.greenrobot:eventbus:3.0.0' implementation 'org.greenrobot:eventbus:3.0.0'
implementation 'pl.tajchert:waitingdots:0.1.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.melnykov:floatingactionbutton:1.3.0'
implementation 'com.google.zxing:android-integration:3.1.0' implementation 'com.google.zxing:android-integration:3.1.0'
implementation 'mobi.upod:time-duration-picker:1.1.3'
implementation 'com.google.zxing:core:3.2.1' implementation 'com.google.zxing:core:3.2.1'
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') { implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
exclude group: 'com.android.support', module: 'support-annotations' exclude group: 'com.android.support', module: 'support-annotations'
} }
implementation ('cn.carbswang.android:NumberPickerView:1.0.9') {
exclude group: 'com.android.support', module: 'appcompat-v7'
}
implementation ('com.tomergoldst.android:tooltips:1.0.6') { implementation ('com.tomergoldst.android:tooltips:1.0.6') {
exclude group: 'com.android.support', module: 'appcompat-v7' exclude group: 'com.android.support', module: 'appcompat-v7'
} }
@ -308,7 +304,7 @@ dependencies {
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection' exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
} }
implementation 'com.annimon:stream:1.1.8' 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 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
implementation 'androidx.sqlite:sqlite-ktx:2.3.1' implementation 'androidx.sqlite:sqlite-ktx:2.3.1'
implementation 'net.zetetic:sqlcipher-android:4.5.4@aar' implementation 'net.zetetic:sqlcipher-android:4.5.4@aar'

View File

@ -3,8 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-sdk <uses-sdk tools:overrideLibrary="com.astuetz.pagerslidingtabstrip,pl.tajchert.waitingdots,com.h6ah4i.android.multiselectlistpreferencecompat,android.support.v13,com.davemorrissey.labs.subscaleview,com.tomergoldst.tooltips,com.klinker.android.send_message,com.takisoft.colorpicker,android.support.v14.preference" />
tools:overrideLibrary="com.amulyakhare.textdrawable,com.astuetz.pagerslidingtabstrip,pl.tajchert.waitingdots,com.h6ah4i.android.multiselectlistpreferencecompat,android.support.v13,com.davemorrissey.labs.subscaleview,com.tomergoldst.tooltips,com.klinker.android.send_message,com.takisoft.colorpicker,android.support.v14.preference" />
<permission <permission
android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" android:name="network.loki.messenger.ACCESS_SESSION_SECRETS"

View File

@ -23,7 +23,6 @@ import android.app.Application;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
@ -91,8 +90,6 @@ import org.thoughtcrime.securesms.util.dynamiclanguage.LocaleParseHelper;
import org.thoughtcrime.securesms.webrtc.CallMessageProcessor; import org.thoughtcrime.securesms.webrtc.CallMessageProcessor;
import org.webrtc.PeerConnectionFactory; import org.webrtc.PeerConnectionFactory;
import org.webrtc.PeerConnectionFactory.InitializationOptions; import org.webrtc.PeerConnectionFactory.InitializationOptions;
import org.webrtc.voiceengine.WebRtcAudioManager;
import org.webrtc.voiceengine.WebRtcAudioUtils;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
@ -100,9 +97,7 @@ import java.io.InputStream;
import java.security.Security; import java.security.Security;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.Timer; import java.util.Timer;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@ -394,33 +389,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
private void initializeWebRtc() { private void initializeWebRtc() {
try { try {
Set<String> HARDWARE_AEC_BLACKLIST = new HashSet<String>() {{
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<String> OPEN_SL_ES_WHITELIST = new HashSet<String>() {{
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()); PeerConnectionFactory.initialize(InitializationOptions.builder(this).createInitializationOptions());
} catch (UnsatisfiedLinkError e) { } catch (UnsatisfiedLinkError e) {
Log.w(TAG, e); Log.w(TAG, e);

View File

@ -6,8 +6,6 @@ import android.hardware.SensorEvent;
import android.hardware.SensorEventListener; import android.hardware.SensorEventListener;
import android.hardware.SensorManager; import android.hardware.SensorManager;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.Message; import android.os.Message;
import android.os.PowerManager.WakeLock; import android.os.PowerManager.WakeLock;
@ -15,24 +13,17 @@ import android.os.PowerManager;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import androidx.annotation.OptIn;
import com.google.android.exoplayer2.DefaultLoadControl; import androidx.media3.common.AudioAttributes;
import com.google.android.exoplayer2.DefaultRenderersFactory; import androidx.media3.common.C;
import com.google.android.exoplayer2.ExoPlaybackException; import androidx.media3.common.MediaItem;
import com.google.android.exoplayer2.ExoPlayerFactory; import androidx.media3.common.PlaybackException;
import com.google.android.exoplayer2.LoadControl; import androidx.media3.common.PlaybackParameters;
import com.google.android.exoplayer2.PlaybackParameters; import androidx.media3.common.Player;
import com.google.android.exoplayer2.Player; import androidx.media3.common.util.UnstableApi;
import com.google.android.exoplayer2.SimpleExoPlayer; import androidx.media3.exoplayer.ExoPlayer;
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 java.io.IOException; import java.io.IOException;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import network.loki.messenger.BuildConfig;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.session.libsession.utilities.ServiceUtil; import org.session.libsession.utilities.ServiceUtil;
import org.session.libsession.utilities.Util; import org.session.libsession.utilities.Util;
@ -56,7 +47,7 @@ public class AudioSlidePlayer implements SensorEventListener {
private final @Nullable WakeLock wakeLock; private final @Nullable WakeLock wakeLock;
private @NonNull WeakReference<Listener> listener; private @NonNull WeakReference<Listener> listener;
private @Nullable SimpleExoPlayer mediaPlayer; private @Nullable ExoPlayer mediaPlayer;
private @Nullable AttachmentServer audioAttachmentServer; private @Nullable AttachmentServer audioAttachmentServer;
private long startTime; private long startTime;
@ -89,40 +80,38 @@ public class AudioSlidePlayer implements SensorEventListener {
this.sensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); this.sensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
this.proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); 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);
this.wakeLock = ServiceUtil.getPowerManager(context).newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
} else {
this.wakeLock = null;
}
} }
public void play(final double progress) throws IOException { public void play(final double progress) throws IOException {
play(progress, false); play(progress, false);
} }
@OptIn(markerClass = UnstableApi.class)
private void play(final double progress, boolean earpiece) throws IOException { private void play(final double progress, boolean earpiece) throws IOException {
if (this.mediaPlayer != null) { stop(); } 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 = new ExoPlayer.Builder(context).build();
this.mediaPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), new DefaultTrackSelector(), loadControl);
this.audioAttachmentServer = new AttachmentServer(context, slide.asAttachment()); this.audioAttachmentServer = new AttachmentServer(context, slide.asAttachment());
this.startTime = System.currentTimeMillis(); this.startTime = System.currentTimeMillis();
audioAttachmentServer.start(); audioAttachmentServer.start();
mediaPlayer.prepare(createMediaSource(audioAttachmentServer.getUri())); MediaItem mediaItem = MediaItem.fromUri(audioAttachmentServer.getUri());
mediaPlayer.setPlayWhenReady(true); mediaPlayer.setMediaItem(mediaItem);
mediaPlayer.setAudioAttributes(new AudioAttributes.Builder() 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) .setUsage(earpiece ? C.USAGE_VOICE_COMMUNICATION : C.USAGE_MEDIA)
.build()); .build(),
mediaPlayer.addListener(new Player.EventListener() { !earpiece);
mediaPlayer.addListener(new Player.Listener() {
boolean started = false; boolean started = false;
@Override @Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { public void onPlaybackStateChanged(int playbackState) {
Log.d(TAG, "onPlayerStateChanged(" + playWhenReady + ", " + playbackState + ")"); Log.d(TAG, "onPlaybackStateChanged(" + playbackState + ")");
switch (playbackState) { switch (playbackState) {
case Player.STATE_READY: case Player.STATE_READY:
Log.i(TAG, "onPrepared() " + mediaPlayer.getBufferedPercentage() + "% buffered"); Log.i(TAG, "onPrepared() " + mediaPlayer.getBufferedPercentage() + "% buffered");
@ -166,9 +155,7 @@ public class AudioSlidePlayer implements SensorEventListener {
sensorManager.unregisterListener(AudioSlidePlayer.this); sensorManager.unregisterListener(AudioSlidePlayer.this);
if (wakeLock != null && wakeLock.isHeld()) { 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);
}
} }
} }
@ -178,8 +165,9 @@ public class AudioSlidePlayer implements SensorEventListener {
} }
} }
@Override @Override
public void onPlayerError(ExoPlaybackException error) { public void onPlayerError(PlaybackException error) {
Log.w(TAG, "MediaPlayer Error: " + error); Log.w(TAG, "MediaPlayer Error: " + error);
synchronized (AudioSlidePlayer.this) { synchronized (AudioSlidePlayer.this) {
@ -193,9 +181,7 @@ public class AudioSlidePlayer implements SensorEventListener {
sensorManager.unregisterListener(AudioSlidePlayer.this); sensorManager.unregisterListener(AudioSlidePlayer.this);
if (wakeLock != null && wakeLock.isHeld()) { 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);
}
} }
} }
@ -203,12 +189,9 @@ public class AudioSlidePlayer implements SensorEventListener {
progressEventHandler.removeMessages(0); progressEventHandler.removeMessages(0);
} }
}); });
}
private MediaSource createMediaSource(@NonNull Uri uri) { mediaPlayer.prepare();
return new ExtractorMediaSource.Factory(new DefaultDataSourceFactory(context, BuildConfig.USER_AGENT)) mediaPlayer.setPlayWhenReady(true);
.setExtractorsFactory(new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true))
.createMediaSource(uri);
} }
public synchronized void stop() { public synchronized void stop() {
@ -340,14 +323,18 @@ public class AudioSlidePlayer implements SensorEventListener {
int streamType; int streamType;
if (event.values[0] < 5f && event.values[0] != proximitySensor.getMaximumRange()) { if (
streamType = AudioManager.STREAM_VOICE_CALL; proximitySensor != null &&
event.values[0] < 5f &&
event.values[0] != proximitySensor.getMaximumRange()
) {
streamType = C.AUDIO_CONTENT_TYPE_SPEECH;
} else { } else {
streamType = AudioManager.STREAM_MUSIC; streamType = C.AUDIO_CONTENT_TYPE_MUSIC;
} }
if (streamType == AudioManager.STREAM_VOICE_CALL && if (streamType == C.AUDIO_CONTENT_TYPE_SPEECH &&
mediaPlayer.getAudioStreamType() != streamType && mediaPlayer.getAudioAttributes().contentType != streamType &&
!audioManager.isWiredHeadsetOn()) !audioManager.isWiredHeadsetOn())
{ {
double position = mediaPlayer.getCurrentPosition(); double position = mediaPlayer.getCurrentPosition();
@ -361,11 +348,11 @@ public class AudioSlidePlayer implements SensorEventListener {
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, e); Log.w(TAG, e);
} }
} else if (streamType == AudioManager.STREAM_MUSIC && } else if (streamType == C.AUDIO_CONTENT_TYPE_MUSIC &&
mediaPlayer.getAudioStreamType() != streamType && mediaPlayer.getAudioAttributes().contentType != streamType &&
System.currentTimeMillis() - startTime > 500) System.currentTimeMillis() - startTime > 500)
{ {
if (wakeLock != null) wakeLock.release(); if (wakeLock != null && wakeLock.isHeld()) wakeLock.release();
stop(); stop();
notifyOnStop(); notifyOnStop();
} }
@ -403,7 +390,7 @@ public class AudioSlidePlayer implements SensorEventListener {
sendEmptyMessageDelayed(0, 50); 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; return player.getPlaybackState() == Player.STATE_READY || player.getPlaybackState() == Player.STATE_BUFFERING;
} }
} }

View File

@ -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<Intent> 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.profileDisplayPicture));
if (!extraIntents.isEmpty()) {
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Intent[0]));
}
return chooserIntent;
}
}

View File

@ -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<CropImageContractOptions>,
private val onPickImage: ActivityResultLauncher<Intent>
) {
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<Intent> = 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<Intent>()
)
}
return chooserIntent
}
}

View File

@ -6,13 +6,9 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.graphics.Outline import android.graphics.Outline
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorManager
import android.media.AudioManager import android.media.AudioManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewOutlineProvider import android.view.ViewOutlineProvider
@ -23,8 +19,6 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.squareup.phrase.Phrase import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -34,7 +28,6 @@ import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityWebrtcBinding import network.loki.messenger.databinding.ActivityWebrtcBinding
import org.apache.commons.lang3.time.DurationFormatUtils import org.apache.commons.lang3.time.DurationFormatUtils
import org.session.libsession.avatars.ProfileContactPhoto
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
@ -44,7 +37,6 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.service.WebRtcCallService import org.thoughtcrime.securesms.service.WebRtcCallService
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator
import org.thoughtcrime.securesms.webrtc.AudioManagerCommand import org.thoughtcrime.securesms.webrtc.AudioManagerCommand
import org.thoughtcrime.securesms.webrtc.CallViewModel import org.thoughtcrime.securesms.webrtc.CallViewModel
import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_CONNECTED import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_CONNECTED
@ -56,7 +48,6 @@ import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_RINGING
import org.thoughtcrime.securesms.webrtc.Orientation import org.thoughtcrime.securesms.webrtc.Orientation
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.EARPIECE import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.EARPIECE
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.SPEAKER_PHONE import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.SPEAKER_PHONE
import kotlin.math.asin
@AndroidEntryPoint @AndroidEntryPoint
class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
@ -73,7 +64,6 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
} }
private val viewModel by viewModels<CallViewModel>() private val viewModel by viewModels<CallViewModel>()
private val glide by lazy { Glide.with(this) }
private lateinit var binding: ActivityWebrtcBinding private lateinit var binding: ActivityWebrtcBinding
private var uiJob: Job? = null private var uiJob: Job? = null
private var wantsToAnswer = false private var wantsToAnswer = false

View File

@ -1,198 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.provider.ContactsContract;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewOutlineProvider;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.session.libsession.avatars.ContactColors;
import org.session.libsession.avatars.ContactPhoto;
import org.session.libsession.avatars.ResourceContactPhoto;
import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.ThemeUtil;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.recipients.RecipientExporter;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestManager;
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator;
import java.util.Objects;
import network.loki.messenger.R;
public class AvatarImageView extends AppCompatImageView {
private static final String TAG = AvatarImageView.class.getSimpleName();
private static final Paint LIGHT_THEME_OUTLINE_PAINT = new Paint();
private static final Paint DARK_THEME_OUTLINE_PAINT = new Paint();
static {
LIGHT_THEME_OUTLINE_PAINT.setColor(Color.argb((int) (255 * 0.2), 0, 0, 0));
LIGHT_THEME_OUTLINE_PAINT.setStyle(Paint.Style.STROKE);
LIGHT_THEME_OUTLINE_PAINT.setStrokeWidth(1f);
LIGHT_THEME_OUTLINE_PAINT.setAntiAlias(true);
DARK_THEME_OUTLINE_PAINT.setColor(Color.argb((int) (255 * 0.2), 255, 255, 255));
DARK_THEME_OUTLINE_PAINT.setStyle(Paint.Style.STROKE);
DARK_THEME_OUTLINE_PAINT.setStrokeWidth(1f);
DARK_THEME_OUTLINE_PAINT.setAntiAlias(true);
}
private boolean inverted;
private Paint outlinePaint;
private OnClickListener listener;
private @Nullable RecipientContactPhoto recipientContactPhoto;
private @NonNull Drawable unknownRecipientDrawable;
public AvatarImageView(Context context) {
super(context);
initialize(context, null);
}
public AvatarImageView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs);
}
private void initialize(@NonNull Context context, @Nullable AttributeSet attrs) {
setScaleType(ScaleType.CENTER_CROP);
if (attrs != null) {
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AvatarImageView, 0, 0);
inverted = typedArray.getBoolean(0, false);
typedArray.recycle();
}
outlinePaint = ThemeUtil.isDarkTheme(getContext()) ? DARK_THEME_OUTLINE_PAINT : LIGHT_THEME_OUTLINE_PAINT;
setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setOval(0, 0, view.getWidth(), view.getHeight());
}
});
setClipToOutline(true);
unknownRecipientDrawable = new ResourceContactPhoto(R.drawable.ic_profile_default).asDrawable(getContext(), ContactColors.UNKNOWN_COLOR.toConversationColor(getContext()), inverted);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float width = getWidth() - getPaddingRight() - getPaddingLeft();
float height = getHeight() - getPaddingBottom() - getPaddingTop();
float cx = width / 2f;
float cy = height / 2f;
float radius = Math.min(cx, cy) - (outlinePaint.getStrokeWidth() / 2f);
canvas.translate(getPaddingLeft(), getPaddingTop());
canvas.drawCircle(cx, cy, radius, outlinePaint);
}
@Override
public void setOnClickListener(OnClickListener listener) {
this.listener = listener;
super.setOnClickListener(listener);
}
public void update(String hexEncodedPublicKey) {
Address address = Address.fromSerialized(hexEncodedPublicKey);
Recipient recipient = Recipient.from(getContext(), address, false);
updateAvatar(recipient);
}
private void updateAvatar(Recipient recipient) {
setAvatar(Glide.with(getContext()), recipient, false);
}
public void setAvatar(@NonNull RequestManager requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) {
if (recipient != null) {
if (recipient.isLocalNumber()) {
setImageDrawable(new ResourceContactPhoto(R.drawable.ic_note_to_self).asDrawable(getContext(), recipient.getColor().toAvatarColor(getContext()), inverted));
} else {
RecipientContactPhoto photo = new RecipientContactPhoto(recipient);
if (!photo.equals(recipientContactPhoto)) {
requestManager.clear(this);
recipientContactPhoto = photo;
Drawable photoPlaceholderDrawable = AvatarPlaceholderGenerator.generate(
getContext(), 128, recipient.getAddress().serialize(), recipient.getName());
if (photo.contactPhoto != null) {
requestManager.load(photo.contactPhoto)
.fallback(photoPlaceholderDrawable)
.error(photoPlaceholderDrawable)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.into(this);
} else {
requestManager.load(photoPlaceholderDrawable)
.circleCrop()
.into(this);
// setImageDrawable(photoPlaceholderDrawable);
}
}
}
} else {
recipientContactPhoto = null;
requestManager.clear(this);
setImageDrawable(unknownRecipientDrawable);
super.setOnClickListener(listener);
}
}
public void clear(@NonNull RequestManager glideRequests) {
glideRequests.clear(this);
}
private void setAvatarClickHandler(final Recipient recipient, boolean quickContactEnabled) {
if (!recipient.isGroupRecipient() && quickContactEnabled) {
super.setOnClickListener(v -> {
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);
}
}
}

View File

@ -38,10 +38,13 @@ class ProfilePictureView @JvmOverloads constructor(
var additionalDisplayName: String? = null var additionalDisplayName: String? = null
private val profilePicturesCache = mutableMapOf<View, Recipient>() private val profilePicturesCache = mutableMapOf<View, Recipient>()
private val resourcePadding by lazy {
context.resources.getDimensionPixelSize(R.dimen.normal_padding).toFloat()
}
private val unknownRecipientDrawable by lazy { ResourceContactPhoto(R.drawable.ic_profile_default) 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) 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) { constructor(context: Context, sender: Recipient): this(context) {
update(sender) update(sender)

View File

@ -868,9 +868,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun onDestroy() { override fun onDestroy() {
viewModel.saveDraft(binding.inputBar.text.trim()) if(::binding.isInitialized) {
cancelVoiceMessage() viewModel.saveDraft(binding.inputBar.text.trim())
tearDownRecipientObserver() cancelVoiceMessage()
tearDownRecipientObserver()
}
super.onDestroy() super.onDestroy()
} }
// endregion // endregion

View File

@ -536,7 +536,7 @@ open class Storage(
} }
private fun updateConvoVolatile(convos: ConversationVolatileConfig, messageTimestamp: Long) { private fun updateConvoVolatile(convos: ConversationVolatileConfig, messageTimestamp: Long) {
val extracted = convos.all() val extracted = convos.all().filterNotNull()
for (conversation in extracted) { for (conversation in extracted) {
val threadId = when (conversation) { val threadId = when (conversation) {
is Conversation.OneToOne -> getThreadIdFor(conversation.accountId, null, null, createThread = false) is Conversation.OneToOne -> getThreadIdFor(conversation.accountId, null, null, createThread = false)

View File

@ -4,7 +4,10 @@ import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.media3.common.util.UnstableApi;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -16,6 +19,7 @@ import org.thoughtcrime.securesms.video.VideoPlayer;
import java.io.IOException; import java.io.IOException;
@OptIn(markerClass = UnstableApi.class)
public class MediaSendVideoFragment extends Fragment implements MediaSendPageFragment { public class MediaSendVideoFragment extends Fragment implements MediaSendPageFragment {
private static final String TAG = MediaSendVideoFragment.class.getSimpleName(); private static final String TAG = MediaSendVideoFragment.class.getSimpleName();

View File

@ -28,7 +28,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.session.libsession.avatars.ContactColors; import org.session.libsession.avatars.ContactColors;
import org.session.libsession.avatars.ContactPhoto; 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.messaging.contacts.Contact;
import org.session.libsession.utilities.NotificationPrivacyPreference; import org.session.libsession.utilities.NotificationPrivacyPreference;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
@ -60,6 +60,8 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
private CharSequence contentTitle; private CharSequence contentTitle;
private CharSequence contentText; private CharSequence contentText;
private static final Integer ICON_SIZE = 128;
public SingleRecipientNotificationBuilder(@NonNull Context context, @NonNull NotificationPrivacyPreference privacy) public SingleRecipientNotificationBuilder(@NonNull Context context, @NonNull NotificationPrivacyPreference privacy)
{ {
super(context, privacy); super(context, privacy);
@ -108,7 +110,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
} else { } else {
setContentTitle(context.getString(R.string.app_name)); setContentTitle(context.getString(R.string.app_name));
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) { private static Drawable getPlaceholderDrawable(Context context, Recipient recipient) {
String publicKey = recipient.getAddress().serialize(); String publicKey = recipient.getAddress().serialize();
String displayName = recipient.getName(); String displayName = recipient.getName();
return AvatarPlaceholderGenerator.generate(context, 128, publicKey, displayName); return AvatarPlaceholderGenerator.generate(context, ICON_SIZE, publicKey, displayName);
} }
/** /**

View File

@ -18,6 +18,7 @@ import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -34,6 +35,8 @@ import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.canhub.cropper.CropImage
import com.canhub.cropper.CropImageContract
import com.squareup.phrase.Phrase import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.io.File import java.io.File
@ -111,6 +114,48 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
private val hexEncodedPublicKey: String get() = TextSecurePreferences.getLocalNumber(this)!! 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 { companion object {
private const val SCROLL_STATE = "SCROLL_STATE" private const val SCROLL_STATE = "SCROLL_STATE"
} }
@ -186,32 +231,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.photo)
}
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<out String>, grantResults: IntArray) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults) Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
@ -417,10 +436,17 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
Permissions.with(this) Permissions.with(this)
.request(Manifest.permission.CAMERA) .request(Manifest.permission.CAMERA)
.onAnyResult { .onAnyResult {
tempFile = AvatarSelection.startAvatarSelection(this, false, true) tempFile = avatarSelection.startAvatarSelection( false, true)
} }
.execute() .execute()
} }
private fun cropImage(inputFile: Uri?, outputFile: Uri?){
avatarSelection.circularCropImage(
inputFile = inputFile,
outputFile = outputFile,
)
}
// endregion // endregion
private inner class DisplayNameEditActionModeCallback: ActionMode.Callback { private inner class DisplayNameEditActionModeCallback: ActionMode.Callback {

View File

@ -18,56 +18,46 @@ package org.thoughtcrime.securesms.video;
import android.content.Context; import android.content.Context;
import android.os.Build; import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
import android.view.Window; import android.view.Window;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.MediaController;
import android.widget.Toast; import android.widget.Toast;
import android.widget.VideoView; import android.widget.VideoView;
import com.google.android.exoplayer2.DefaultLoadControl; import androidx.annotation.NonNull;
import com.google.android.exoplayer2.ExoPlayerFactory; import androidx.annotation.Nullable;
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 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.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.attachments.AttachmentServer;
import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.mms.VideoSlide;
import org.session.libsession.utilities.ViewUtil;
import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
import java.io.IOException; import java.io.IOException;
import network.loki.messenger.R; import network.loki.messenger.R;
@UnstableApi
public class VideoPlayer extends FrameLayout { public class VideoPlayer extends FrameLayout {
private static final String TAG = VideoPlayer.class.getSimpleName(); private static final String TAG = VideoPlayer.class.getSimpleName();
@Nullable private final VideoView videoView; @Nullable private final VideoView videoView;
@Nullable private final PlayerView exoView; @Nullable private final PlayerView exoView;
@Nullable private SimpleExoPlayer exoPlayer; @Nullable private ExoPlayer exoPlayer;
@Nullable private PlayerControlView exoControls; @Nullable private LegacyPlayerControlView exoControls;
@Nullable private AttachmentServer attachmentServer; @Nullable private AttachmentServer attachmentServer;
@Nullable private Window window; @Nullable private Window window;
@ -84,23 +74,16 @@ public class VideoPlayer extends FrameLayout {
inflate(context, R.layout.video_player, this); inflate(context, R.layout.video_player, this);
if (Build.VERSION.SDK_INT >= 16) { this.exoView = ViewUtil.findById(this, R.id.video_view);
this.exoView = ViewUtil.findById(this, R.id.video_view); this.videoView = null;
this.videoView = null; this.exoControls = new LegacyPlayerControlView(getContext());
this.exoControls = new PlayerControlView(getContext()); this.exoControls.setShowTimeoutMs(-1);
this.exoControls.setShowTimeoutMs(-1);
} else {
this.videoView = ViewUtil.findById(this, R.id.video_view);
this.exoView = null;
initializeVideoViewControls(videoView);
}
} }
public void setVideoSource(@NonNull VideoSlide videoSource, boolean autoplay) public void setVideoSource(@NonNull VideoSlide videoSource, boolean autoplay)
throws IOException throws IOException
{ {
if (Build.VERSION.SDK_INT >= 16) setExoViewSource(videoSource, autoplay); setExoViewSource(videoSource, autoplay);
else setVideoViewSource(videoSource, autoplay);
} }
public void pause() { public void pause() {
@ -141,25 +124,20 @@ public class VideoPlayer extends FrameLayout {
private void setExoViewSource(@NonNull VideoSlide videoSource, boolean autoplay) private void setExoViewSource(@NonNull VideoSlide videoSource, boolean autoplay)
throws IOException throws IOException
{ {
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); exoPlayer = new ExoPlayer.Builder(getContext()).build();
TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter);
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
LoadControl loadControl = new DefaultLoadControl();
exoPlayer = ExoPlayerFactory.newSimpleInstance(getContext(), trackSelector, loadControl);
exoPlayer.addListener(new ExoPlayerListener(window)); exoPlayer.addListener(new ExoPlayerListener(window));
exoPlayer.setAudioAttributes(AudioAttributes.DEFAULT, true);
//noinspection ConstantConditions //noinspection ConstantConditions
exoView.setPlayer(exoPlayer); exoView.setPlayer(exoPlayer);
//noinspection ConstantConditions //noinspection ConstantConditions
exoControls.setPlayer(exoPlayer); exoControls.setPlayer(exoPlayer);
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(getContext(), "GenericUserAgent", null); if(videoSource.getUri() != null){
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(getContext(), defaultDataSourceFactory, null); MediaItem mediaItem = MediaItem.fromUri(videoSource.getUri());
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); exoPlayer.setMediaItem(mediaItem);
}
MediaSource mediaSource = new ExtractorMediaSource(videoSource.getUri(), attachmentDataSourceFactory, extractorsFactory, null, null); exoPlayer.prepare();
exoPlayer.prepare(mediaSource);
exoPlayer.setPlayWhenReady(autoplay); exoPlayer.setPlayWhenReady(autoplay);
} }
@ -189,15 +167,7 @@ public class VideoPlayer extends FrameLayout {
if (autoplay) this.videoView.start(); if (autoplay) this.videoView.start();
} }
private void initializeVideoViewControls(@NonNull VideoView videoView) { private static class ExoPlayerListener implements Player.Listener {
MediaController mediaController = new MediaController(getContext());
mediaController.setAnchorView(videoView);
mediaController.setMediaPlayer(videoView);
videoView.setMediaController(mediaController);
}
private static class ExoPlayerListener extends Player.DefaultEventListener {
private final Window window; private final Window window;
ExoPlayerListener(Window window) { ExoPlayerListener(Window window) {

View File

@ -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<String, List<String>> getResponseHeaders() {
return Collections.emptyMap();
}
@Override
public void close() throws IOException {
dataSource.close();
}
}

View File

@ -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));
}
}

View File

@ -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<String, List<String>> getResponseHeaders() {
return Collections.emptyMap();
}
@Override
public void close() throws IOException {
inputSteam.close();
}
}

View File

@ -63,6 +63,7 @@ class PeerConnectionWrapper(private val context: Context,
val configuration = PeerConnection.RTCConfiguration(iceServers).apply { val configuration = PeerConnection.RTCConfiguration(iceServers).apply {
bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE
rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE
sdpSemantics = PeerConnection.SdpSemantics.PLAN_B
if (relay) { if (relay) {
iceTransportsType = PeerConnection.IceTransportsType.RELAY iceTransportsType = PeerConnection.IceTransportsType.RELAY
} }

View File

@ -4,7 +4,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.exoplayer2.ui.AspectRatioFrameLayout <androidx.media3.ui.AspectRatioFrameLayout
android:id="@+id/exo_content_frame" android:id="@+id/exo_content_frame"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -6,7 +6,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.exoplayer2.ui.PlayerView <androidx.media3.ui.PlayerView
android:id="@+id/video_view" android:id="@+id/video_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -338,22 +338,6 @@
<attr name="labeledEditText_textLayout" format="reference" /> <attr name="labeledEditText_textLayout" format="reference" />
</declare-styleable> </declare-styleable>
<declare-styleable name="WaveformSeekBar">
<attr name="progress" format="float"/>
<attr name="bar_width" format="dimension"/>
<attr name="bar_gap" format="dimension"/>
<attr name="bar_min_height" format="dimension"/>
<attr name="bar_corner_radius" format="dimension"/>
<attr name="bar_background_color" format="color"/>
<attr name="bar_progress_color" format="color"/>
<!-- Corresponds to WaveformSeekBar.WaveGravity enum. -->
<attr name="bar_gravity" format="enum">
<enum name="top" value="1" />
<enum name="center" value="2" />
<enum name="bottom" value="3" />
</attr>
</declare-styleable>
<declare-styleable name="KeyboardPageSearchView"> <declare-styleable name="KeyboardPageSearchView">
<attr name="show_always" format="boolean" /> <attr name="show_always" format="boolean" />
<attr name="search_bar_tint" format="color|reference" /> <attr name="search_bar_tint" format="color|reference" />

View File

@ -18,10 +18,6 @@
<item name="android:textSize">@dimen/very_large_font_size</item> <item name="android:textSize">@dimen/very_large_font_size</item>
</style> </style>
<style name="Widget.Session.SearchView" parent="@style/Widget.AppCompat.SearchView">
<item name="closeIcon">@drawable/ic_clear</item>
</style>
<style name="ThemeOverlay.Session.AlertDialog" parent="ThemeOverlay.AppCompat.Dialog.Alert"> <style name="ThemeOverlay.Session.AlertDialog" parent="ThemeOverlay.AppCompat.Dialog.Alert">
<item name="android:windowBackground">@drawable/default_dialog_background</item> <item name="android:windowBackground">@drawable/default_dialog_background</item>
<item name="android:colorBackground">?backgroundSecondary</item> <item name="android:colorBackground">?backgroundSecondary</item>

View File

@ -29,7 +29,6 @@
<item name="android:colorControlNormal">?android:textColorPrimary</item> <item name="android:colorControlNormal">?android:textColorPrimary</item>
<item name="conversation_menu_border_color">?colorDividerBackground</item> <item name="conversation_menu_border_color">?colorDividerBackground</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.Fix.Settings</item> <item name="preferenceTheme">@style/PreferenceThemeOverlay.Fix.Settings</item>
<item name="searchViewStyle">@style/Widget.Session.SearchView</item>
<item name="shapeAppearanceSmallComponent">@style/ShapeAppearance.MaterialComponents.SmallComponent</item> <item name="shapeAppearanceSmallComponent">@style/ShapeAppearance.MaterialComponents.SmallComponent</item>
<item name="elementBorderColor">?android:textColorSecondary</item> <item name="elementBorderColor">?android:textColorSecondary</item>
<item name="colorOnSurface">?android:textColorPrimary</item> <item name="colorOnSurface">?android:textColorPrimary</item>

View File

@ -56,7 +56,6 @@ allprojects {
includeGroupByRegex "org\\.signal.*" includeGroupByRegex "org\\.signal.*"
} }
} }
jcenter()
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }
if (project.hasProperty('huawei')) maven { if (project.hasProperty('huawei')) maven {
url 'https://developer.huawei.com/repo/' url 'https://developer.huawei.com/repo/'

View File

@ -551,6 +551,7 @@ class InstrumentedTests {
is Conversation.OneToOne -> seen.add("1-to-1: ${convo.accountId}") is Conversation.OneToOne -> seen.add("1-to-1: ${convo.accountId}")
is Conversation.Community -> seen.add("og: ${convo.baseCommunityInfo.baseUrl}/r/${convo.baseCommunityInfo.room}") is Conversation.Community -> seen.add("og: ${convo.baseCommunityInfo.baseUrl}/r/${convo.baseCommunityInfo.room}")
is Conversation.LegacyGroup -> seen.add("cl: ${convo.groupId}") is Conversation.LegacyGroup -> seen.add("cl: ${convo.groupId}")
null -> TODO()
} }
} }

View File

@ -162,7 +162,7 @@ class ConversationVolatileConfig(pointer: Long): ConfigBase(pointer) {
external fun allOneToOnes(): List<Conversation.OneToOne> external fun allOneToOnes(): List<Conversation.OneToOne>
external fun allCommunities(): List<Conversation.Community> external fun allCommunities(): List<Conversation.Community>
external fun allLegacyClosedGroups(): List<Conversation.LegacyGroup> external fun allLegacyClosedGroups(): List<Conversation.LegacyGroup>
external fun all(): List<Conversation> external fun all(): List<Conversation?>
} }

View File

@ -54,7 +54,6 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
implementation "com.github.bumptech.glide:glide:$glideVersion" 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.annimon:stream:1.1.8'
implementation 'com.makeramen:roundedimageview:2.1.0' implementation 'com.makeramen:roundedimageview:2.1.0'
implementation 'com.esotericsoftware:kryo:5.1.1' implementation 'com.esotericsoftware:kryo:5.1.1'

View File

@ -5,6 +5,6 @@ import android.graphics.drawable.Drawable;
public interface FallbackContactPhoto { 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);
public Drawable asDrawable(Context context, int color, boolean inverted, Float padding);
} }

View File

@ -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();
}
}
}

View File

@ -4,13 +4,13 @@ import android.content.Context;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.LayerDrawable;
import android.widget.ImageView; import android.widget.ImageView;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.content.res.AppCompatResources;
import com.amulyakhare.textdrawable.TextDrawable;
import com.makeramen.roundedimageview.RoundedDrawable; import com.makeramen.roundedimageview.RoundedDrawable;
import org.session.libsession.R; import org.session.libsession.R;
@ -25,19 +25,33 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
} }
@Override @Override
public Drawable asDrawable(Context context, int color) { public Drawable asDrawable(Context context, int color, boolean inverted) {
return asDrawable(context, color, false); return asDrawable(context, 0, false, 0f);
} }
@Override @Override
public Drawable asDrawable(Context context, int color, boolean inverted) { public Drawable asDrawable(Context context, int color, boolean inverted, Float padding) {
Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color); // 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)); 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) { if (inverted) {
foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
}
} }
Drawable gradient = AppCompatResources.getDrawable( Drawable gradient = AppCompatResources.getDrawable(

View File

@ -3,6 +3,8 @@ package org.session.libsession.avatars;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import androidx.core.content.ContextCompat;
import com.makeramen.roundedimageview.RoundedDrawable; import com.makeramen.roundedimageview.RoundedDrawable;
public class TransparentContactPhoto implements FallbackContactPhoto { public class TransparentContactPhoto implements FallbackContactPhoto {
@ -10,13 +12,13 @@ public class TransparentContactPhoto implements FallbackContactPhoto {
public TransparentContactPhoto() {} public TransparentContactPhoto() {}
@Override @Override
public Drawable asDrawable(Context context, int color) { public Drawable asDrawable(Context context, int color, boolean inverted) {
return asDrawable(context, color, false); return asDrawable(context, color, inverted, 0f);
} }
@Override @Override
public Drawable asDrawable(Context context, int color, boolean inverted) { public Drawable asDrawable(Context context, int color, boolean inverted, Float padding) {
return RoundedDrawable.fromDrawable(context.getResources().getDrawable(android.R.color.transparent)); return RoundedDrawable.fromDrawable(ContextCompat.getDrawable(context, android.R.color.transparent));
} }
} }

View File

@ -266,22 +266,6 @@
<attr name="labeledEditText_textLayout" format="reference" /> <attr name="labeledEditText_textLayout" format="reference" />
</declare-styleable> </declare-styleable>
<declare-styleable name="WaveformSeekBar">
<attr name="progress" format="float"/>
<attr name="bar_width" format="dimension"/>
<attr name="bar_gap" format="dimension"/>
<attr name="bar_min_height" format="dimension"/>
<attr name="bar_corner_radius" format="dimension"/>
<attr name="bar_background_color" format="color"/>
<attr name="bar_progress_color" format="color"/>
<!-- Corresponds to WaveformSeekBar.WaveGravity enum. -->
<attr name="bar_gravity" format="enum">
<enum name="top" value="1" />
<enum name="center" value="2" />
<enum name="bottom" value="3" />
</attr>
</declare-styleable>
<declare-styleable name="KeyboardPageSearchView"> <declare-styleable name="KeyboardPageSearchView">
<attr name="show_always" format="boolean" /> <attr name="show_always" format="boolean" />
<attr name="search_bar_tint" format="color|reference" /> <attr name="search_bar_tint" format="color|reference" />

View File

@ -6,3 +6,4 @@ include ':libsession'
include ':libsignal' include ':libsignal'
include ':libsession-util' include ':libsession-util'
include ':content-descriptions' // ONLY AccessibilityID strings (non-translated) used to identify UI elements in automated testing include ':content-descriptions' // ONLY AccessibilityID strings (non-translated) used to identify UI elements in automated testing
include ':stickyheader'

View File

@ -0,0 +1,2 @@
configurations.maybeCreate("default")
artifacts.add("default", file('stickyheadergrid-0.9.4.aar'))

Binary file not shown.