diff --git a/build.gradle b/build.gradle index 6a5325777..75b0190a9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.android.application' version '8.5.2' + id 'com.android.application' version '8.11.1' id 'com.google.gms.google-services' version '4.4.1' } @@ -149,9 +149,9 @@ android { dependencies { implementation 'androidx.sharetarget:sharetarget:1.2.0' - implementation 'androidx.webkit:webkit:1.12.1' + implementation 'androidx.webkit:webkit:1.14.0' implementation 'androidx.multidex:multidex:2.0.1' - implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'androidx.appcompat:appcompat:1.7.1' implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.legacy:legacy-support-v13:1.0.0' implementation ('androidx.preference:preference:1.2.1') { @@ -159,7 +159,7 @@ dependencies { exclude group: 'androidx.lifecycle', module:'lifecycle-viewmodel-ktx' } implementation 'androidx.legacy:legacy-preference-v14:1.0.0' - implementation 'androidx.exifinterface:exifinterface:1.3.7' + implementation 'androidx.exifinterface:exifinterface:1.4.1' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-common-java8:2.6.2' implementation 'androidx.lifecycle:lifecycle-viewmodel:2.6.2' @@ -174,51 +174,47 @@ dependencies { implementation ('com.journeyapps:zxing-android-embedded:4.3.0') { transitive = false } // QR Code scanner implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.1' // used as JSON library implementation 'com.google.code.gson:gson:2.12.1' // used as JSON library. - implementation "me.leolin:ShortcutBadger:1.1.16" // display messagecount on the home screen icon. - implementation 'com.jpardogo.materialtabstrip:library:1.0.9' // used in the emoji selector for the tab selection. implementation 'com.github.Baseflow:PhotoView:2.3.0' // does the zooming on photos / media - implementation 'com.github.penfeizhou.android.animation:awebp:3.0.2' // animated webp support. + implementation 'com.github.penfeizhou.android.animation:awebp:3.0.5' // animated webp support. implementation 'com.caverock:androidsvg-aar:1.4' // SVG support. - implementation 'com.github.bumptech.glide:glide:4.12.0' - annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' + implementation 'com.github.bumptech.glide:glide:4.16.0' + annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0' annotationProcessor 'androidx.annotation:annotation:1.9.1' - implementation 'com.makeramen:roundedimageview:2.1.0' // crops the avatars to circles - implementation 'com.pnikosis:materialish-progress:1.5' // used only in the "Progress Wheel" in Share Activity. + implementation 'com.makeramen:roundedimageview:2.3.0' // crops the avatars to circles implementation 'com.github.amulyakhare:TextDrawable:558677ea31' // number of unread messages, // the one-letter circle for the contacts (when there is not avatar) and a white background. implementation 'com.googlecode.mp4parser:isoparser:1.0.6' // MP4 recoding; upgrading eg. to 1.1.22 breaks recoding, however, i have not investigated further, just reset to 1.0.6 - implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') { // for the zooming on photos / media + implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.10.0') { // for the zooming on photos / media exclude group: 'com.android.support', module: 'support-annotations' } - implementation 'com.annimon:stream:1.1.8' // brings future java streams api to SDK Version < 24 // Replacement for ContentResolver // that protects against the Surreptitious Sharing attack. // implementation 'de.cketti.safecontentresolver:safe-content-resolver-v21:1.0.0' - gplayImplementation('com.google.firebase:firebase-messaging:24.1.0') { // for PUSH notifications + gplayImplementation('com.google.firebase:firebase-messaging:24.1.2') { // for PUSH notifications, don't upgrade: v25.0.0 requires minSdk>=23 exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' } testImplementation 'junit:junit:4.13.2' - testImplementation 'org.assertj:assertj-core:1.7.1' - testImplementation 'org.mockito:mockito-core:1.9.5' - testImplementation 'org.powermock:powermock-api-mockito:1.6.1' - testImplementation 'org.powermock:powermock-module-junit4:1.6.1' - testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1' - testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1' + testImplementation 'org.assertj:assertj-core:3.27.3' + testImplementation 'org.mockito:mockito-core:5.18.0' + testImplementation 'org.powermock:powermock-api-mockito:1.7.4' + testImplementation 'org.powermock:powermock-module-junit4:2.0.9' + testImplementation 'org.powermock:powermock-module-junit4-rule:2.0.9' + testImplementation 'org.powermock:powermock-classloading-xstream:2.0.9' - androidTestImplementation 'androidx.test:runner:1.6.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' - androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.6.1' - androidTestImplementation 'androidx.test:rules:1.6.1' - androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test:runner:1.7.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' + androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.7.0' + androidTestImplementation 'androidx.test:rules:1.7.0' + androidTestImplementation 'androidx.test.ext:junit:1.3.0' androidTestImplementation 'com.android.support:support-annotations:28.0.0' - androidTestImplementation ('org.assertj:assertj-core:1.7.1') { + androidTestImplementation ('org.assertj:assertj-core:3.27.3') { exclude group: 'org.hamcrest', module: 'hamcrest-core' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 43a6e163d..57939822b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationListItem.java b/src/main/java/org/thoughtcrime/securesms/ConversationListItem.java index def17190b..ae32e49ac 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationListItem.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationListItem.java @@ -35,7 +35,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.amulyakhare.textdrawable.TextDrawable; -import com.annimon.stream.Stream; import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; @@ -55,7 +54,6 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; import java.util.Collections; -import java.util.List; import java.util.Set; public class ConversationListItem extends RelativeLayout @@ -318,15 +316,12 @@ public class ConversationListItem extends RelativeLayout String normalizedValue = value.toLowerCase(Util.getLocale()); String normalizedTest = highlight.toLowerCase(Util.getLocale()); - List testTokens; - try (Stream stream = Stream.of(normalizedTest.split(" "))) { - testTokens = stream.filter(s -> !s.trim().isEmpty()).toList(); - } Spannable spanned = new SpannableString(value); int searchStartIndex = 0; - for (String token : testTokens) { + for (String token : normalizedTest.split(" ")) { + if (token.trim().isEmpty()) continue; if (searchStartIndex >= spanned.length()) { break; } diff --git a/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java b/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java index 4c4ac3558..38489ba2e 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java @@ -16,7 +16,6 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.annimon.stream.Stream; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcMsg; import com.b44t.messenger.rpc.RpcException; @@ -194,18 +193,17 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver { } private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) { - List thumbnailSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker() || s.isWebxdcDocument() || s.isVcard()).limit(1).toList(); - List audioSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasAudio()).limit(1).toList(); - List documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList(); + List slides = slideDeck.getSlides(); + Slide slide = slides.isEmpty()? null : slides.get(0); attachmentVideoOverlayView.setVisibility(GONE); - if (!thumbnailSlides.isEmpty() && thumbnailSlides.get(0).getUri() != null) { + if (slide != null && slide.hasQuoteThumbnail()) { thumbnailView.setVisibility(VISIBLE); attachmentContainerView.setVisibility(GONE); dismissView.setBackgroundResource(R.drawable.dismiss_background); - if (thumbnailSlides.get(0).isWebxdcDocument()) { + if (slide.isWebxdcDocument()) { try { JSONObject info = quotedMsg.getWebxdcInfo(); byte[] blob = quotedMsg.getWebxdcBlob(info.getString("icon")); @@ -218,7 +216,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver { Log.e(TAG, "failed to get webxdc icon", e); thumbnailView.setVisibility(GONE); } - } else if (thumbnailSlides.get(0).isVcard()) { + } else if (slide.isVcard()) { try { VcardContact vcardContact = DcHelper.getRpc(getContext()).parseVcard(quotedMsg.getFile()).get(0); Recipient recipient = new Recipient(getContext(), vcardContact); @@ -233,11 +231,11 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver { thumbnailView.setVisibility(GONE); } } else { - Uri thumbnailUri = thumbnailSlides.get(0).getUri(); - if (thumbnailSlides.get(0).hasVideo()) { + Uri thumbnailUri = slide.getUri(); + if (slide.hasVideo()) { attachmentVideoOverlayView.setVisibility(VISIBLE); - MediaUtil.createVideoThumbnailIfNeeded(getContext(), thumbnailSlides.get(0).getUri(), thumbnailSlides.get(0).getThumbnailUri(), null); - thumbnailUri = thumbnailSlides.get(0).getThumbnailUri(); + MediaUtil.createVideoThumbnailIfNeeded(getContext(), slide.getUri(), slide.getThumbnailUri(), null); + thumbnailUri = slide.getThumbnailUri(); } glideRequests.load(new DecryptableUri(thumbnailUri)) .centerCrop() @@ -245,10 +243,10 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver { .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .into(thumbnailView); } - } else if(!audioSlides.isEmpty()) { + } else if(slide != null && slide.hasAudio()) { thumbnailView.setVisibility(GONE); attachmentContainerView.setVisibility(GONE); - } else if (!documentSlides.isEmpty()) { + } else if (slide != null && slide.hasDocument()) { thumbnailView.setVisibility(GONE); attachmentContainerView.setVisibility(VISIBLE); } else { diff --git a/src/main/java/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java b/src/main/java/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java index afaf932c4..b2c6d4f82 100644 --- a/src/main/java/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java +++ b/src/main/java/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java @@ -6,7 +6,6 @@ import android.content.Context; import androidx.annotation.NonNull; import androidx.loader.content.AsyncTaskLoader; -import com.annimon.stream.Stream; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcMsg; @@ -128,28 +127,38 @@ public class BucketedThreadMediaLoader extends AsyncTaskLoader !timeBucket.isEmpty()) - .count() + - OLDER.getSectionCount(); + int count = 0; + for (TimeBucket section : TIME_SECTIONS) { + if (!section.isEmpty()) count++; + } + return count + OLDER.getSectionCount(); } public int getSectionItemCount(int section) { - List activeTimeBuckets = Stream.of(TIME_SECTIONS).filter(timeBucket -> !timeBucket.isEmpty()).toList(); + List activeTimeBuckets = new ArrayList<>(); + for (TimeBucket bucket : TIME_SECTIONS) { + if (!bucket.isEmpty()) activeTimeBuckets.add(bucket); + } if (section < activeTimeBuckets.size()) return activeTimeBuckets.get(section).getItemCount(); else return OLDER.getSectionItemCount(section - activeTimeBuckets.size()); } public DcMsg get(int section, int item) { - List activeTimeBuckets = Stream.of(TIME_SECTIONS).filter(timeBucket -> !timeBucket.isEmpty()).toList(); + List activeTimeBuckets = new ArrayList<>(); + for (TimeBucket bucket : TIME_SECTIONS) { + if (!bucket.isEmpty()) activeTimeBuckets.add(bucket); + } if (section < activeTimeBuckets.size()) return activeTimeBuckets.get(section).getItem(item); else return OLDER.getItem(section - activeTimeBuckets.size(), item); } public String getName(int section) { - List activeTimeBuckets = Stream.of(TIME_SECTIONS).filter(timeBucket -> !timeBucket.isEmpty()).toList(); + List activeTimeBuckets = new ArrayList<>(); + for (TimeBucket bucket : TIME_SECTIONS) { + if (!bucket.isEmpty()) activeTimeBuckets.add(bucket); + } if (section < activeTimeBuckets.size()) return activeTimeBuckets.get(section).getName(); else return OLDER.getName(section - activeTimeBuckets.size()); diff --git a/src/main/java/org/thoughtcrime/securesms/mms/Slide.java b/src/main/java/org/thoughtcrime/securesms/mms/Slide.java index 56fd0d842..4b3c1ea9a 100644 --- a/src/main/java/org/thoughtcrime/securesms/mms/Slide.java +++ b/src/main/java/org/thoughtcrime/securesms/mms/Slide.java @@ -76,6 +76,11 @@ public abstract class Slide { return attachment.getSize(); } + /* Return true if this slide has a thumbnail when being quoted, false otherwise */ + public boolean hasQuoteThumbnail() { + return (hasImage() || hasVideo() || hasSticker() || isWebxdcDocument() || isVcard()) && getUri() != null; + } + public boolean hasImage() { return false; } diff --git a/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java b/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java index 38011590b..42becce2b 100644 --- a/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java +++ b/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java @@ -21,15 +21,13 @@ import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; -import com.annimon.stream.Stream; -import com.annimon.stream.function.Consumer; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.util.LRUCache; import org.thoughtcrime.securesms.util.ServiceUtil; import java.lang.ref.WeakReference; import java.security.SecureRandom; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -70,10 +68,6 @@ public class Permissions { private Runnable anyPermanentlyDeniedListener; private Runnable anyResultListener; - private Consumer> someGrantedListener; - private Consumer> someDeniedListener; - private Consumer> somePermanentlyDeniedListener; - private @DrawableRes int[] rationalDialogHeader; private String rationaleDialogMessage; @@ -148,29 +142,13 @@ public class Permissions { return this; } - public PermissionsBuilder onSomeGranted(Consumer> someGrantedListener) { - this.someGrantedListener = someGrantedListener; - return this; - } - - public PermissionsBuilder onSomeDenied(Consumer> someDeniedListener) { - this.someDeniedListener = someDeniedListener; - return this; - } - - public PermissionsBuilder onSomePermanentlyDenied(Consumer> somePermanentlyDeniedListener) { - this.somePermanentlyDeniedListener = somePermanentlyDeniedListener; - return this; - } - public void execute() { if (alwaysGranted) { allGrantedListener.run(); return; } - PermissionsRequest request = new PermissionsRequest(allGrantedListener, anyDeniedListener, anyPermanentlyDeniedListener, anyResultListener, - someGrantedListener, someDeniedListener, somePermanentlyDeniedListener); + PermissionsRequest request = new PermissionsRequest(allGrantedListener, anyDeniedListener, anyPermanentlyDeniedListener, anyResultListener); if (ifNecesary && (permissionObject.hasAll(requestedPermissions) || !condition)) { executePreGrantedPermissionsRequest(request); @@ -183,7 +161,7 @@ public class Permissions { private void executePreGrantedPermissionsRequest(PermissionsRequest request) { int[] grantResults = new int[requestedPermissions.length]; - for (int i=0;i PackageManager.PERMISSION_DENIED).toArray(); + int[] grantResults = new int[permissions.length]; + Arrays.fill(grantResults, PackageManager.PERMISSION_DENIED); boolean[] showDialog = new boolean[permissions.length]; Arrays.fill(showDialog, true); @@ -236,22 +215,29 @@ public class Permissions { } private static String[] filterNotGranted(@NonNull Context context, String... permissions) { - return Stream.of(permissions) - .filter(permission -> ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) - .toList() - .toArray(new String[0]); + List notGranted = new ArrayList<>(); + for (String permission : permissions) { + if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) { + notGranted.add(permission); + } + } + return notGranted.toArray(new String[0]); } public static boolean hasAny(@NonNull Context context, String... permissions) { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || - Stream.of(permissions).anyMatch(permission -> ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED); - + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true; + for (String permission : permissions) { + if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) return true; + } + return false; } public static boolean hasAll(@NonNull Context context, String... permissions) { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || - Stream.of(permissions).allMatch(permission -> ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED); - + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true; + for (String permission : permissions) { + if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) return false; + } + return true; } public static void onRequestPermissionsResult(Fragment fragment, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { diff --git a/src/main/java/org/thoughtcrime/securesms/permissions/PermissionsRequest.java b/src/main/java/org/thoughtcrime/securesms/permissions/PermissionsRequest.java index 2bc0812fe..1bc24d9aa 100644 --- a/src/main/java/org/thoughtcrime/securesms/permissions/PermissionsRequest.java +++ b/src/main/java/org/thoughtcrime/securesms/permissions/PermissionsRequest.java @@ -5,8 +5,6 @@ import android.content.pm.PackageManager; import androidx.annotation.Nullable; -import com.annimon.stream.function.Consumer; - import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -22,27 +20,16 @@ class PermissionsRequest { private final @Nullable Runnable anyPermanentlyDeniedListener; private final @Nullable Runnable anyResultListener; - private final @Nullable Consumer> someGrantedListener; - private final @Nullable Consumer> someDeniedListener; - private final @Nullable Consumer> somePermanentlyDeniedListener; - PermissionsRequest(@Nullable Runnable allGrantedListener, @Nullable Runnable anyDeniedListener, @Nullable Runnable anyPermanentlyDeniedListener, - @Nullable Runnable anyResultListener, - @Nullable Consumer> someGrantedListener, - @Nullable Consumer> someDeniedListener, - @Nullable Consumer> somePermanentlyDeniedListener) + @Nullable Runnable anyResultListener) { this.allGrantedListener = allGrantedListener; this.anyDeniedListener = anyDeniedListener; this.anyPermanentlyDeniedListener = anyPermanentlyDeniedListener; this.anyResultListener = anyResultListener; - - this.someGrantedListener = someGrantedListener; - this.someDeniedListener = someDeniedListener; - this.somePermanentlyDeniedListener = somePermanentlyDeniedListener; } void onResult(String[] permissions, int[] grantResults, boolean[] shouldShowRationaleDialog) { @@ -56,9 +43,9 @@ class PermissionsRequest { } else { boolean preRequestShouldShowRationaleDialog = PRE_REQUEST_MAPPING.get(permissions[i]); - if ((somePermanentlyDeniedListener != null || anyPermanentlyDeniedListener != null) && - !preRequestShouldShowRationaleDialog && !shouldShowRationaleDialog[i]) - { + if (anyPermanentlyDeniedListener != null + && !preRequestShouldShowRationaleDialog + && !shouldShowRationaleDialog[i]) { permanentlyDenied.add(permissions[i]); } else { denied.add(permissions[i]); @@ -68,18 +55,14 @@ class PermissionsRequest { if (allGrantedListener != null && !granted.isEmpty() && (denied.isEmpty() && permanentlyDenied.isEmpty())) { allGrantedListener.run(); - } else if (someGrantedListener != null && !granted.isEmpty()) { - someGrantedListener.accept(granted); } if (!denied.isEmpty()) { if (anyDeniedListener != null) anyDeniedListener.run(); - if (someDeniedListener != null) someDeniedListener.accept(denied); } if (!permanentlyDenied.isEmpty()) { if (anyPermanentlyDeniedListener != null) anyPermanentlyDeniedListener.run(); - if (somePermanentlyDeniedListener != null) somePermanentlyDeniedListener.accept(permanentlyDenied); } if (anyResultListener != null) { diff --git a/src/main/res/layout/share_activity.xml b/src/main/res/layout/share_activity.xml index 92f2b0b3f..cc676c056 100644 --- a/src/main/res/layout/share_activity.xml +++ b/src/main/res/layout/share_activity.xml @@ -33,10 +33,10 @@ - +