浏览代码

Version 5.1.3

Threema 2 年之前
父节点
当前提交
74577dee1b
共有 31 个文件被更改,包括 229 次插入164 次删除
  1. 30 22
      app/build.gradle
  2. 10 13
      app/proguard-project.txt
  3. 0 11
      app/src/main/java/ch/threema/app/ThreemaApplication.java
  4. 1 1
      app/src/main/java/ch/threema/app/activities/HomeActivity.java
  5. 2 0
      app/src/main/java/ch/threema/app/activities/MapActivity.java
  6. 21 10
      app/src/main/java/ch/threema/app/activities/MediaViewerActivity.java
  7. 6 2
      app/src/main/java/ch/threema/app/activities/RecipientListBaseActivity.java
  8. 5 1
      app/src/main/java/ch/threema/app/adapters/ComposeMessageAdapter.java
  9. 4 4
      app/src/main/java/ch/threema/app/adapters/MediaGalleryAdapter.kt
  10. 13 2
      app/src/main/java/ch/threema/app/adapters/SendMediaPreviewAdapter.kt
  11. 2 1
      app/src/main/java/ch/threema/app/backuprestore/BackupChatServiceImpl.java
  12. 35 14
      app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java
  13. 2 0
      app/src/main/java/ch/threema/app/locationpicker/LocationPickerActivity.java
  14. 1 5
      app/src/main/java/ch/threema/app/mediaattacher/MediaAttachActivity.java
  15. 4 4
      app/src/main/java/ch/threema/app/services/AvatarCacheServiceImpl.java
  16. 3 4
      app/src/main/java/ch/threema/app/services/MessageServiceImpl.java
  17. 2 0
      app/src/main/java/ch/threema/app/threemasafe/ThreemaSafeServiceImpl.java
  18. 2 2
      app/src/main/java/ch/threema/app/ui/AvatarEditView.java
  19. 10 23
      app/src/main/java/ch/threema/app/utils/BitmapUtil.java
  20. 13 10
      app/src/main/java/ch/threema/app/utils/ConfigUtils.java
  21. 0 3
      app/src/main/java/ch/threema/app/utils/GeoLocationUtil.java
  22. 2 2
      app/src/main/java/ch/threema/app/utils/IconUtil.java
  23. 20 14
      app/src/main/java/ch/threema/app/utils/ShortcutUtil.java
  24. 10 1
      app/src/main/java/ch/threema/app/utils/ThumbnailUtil.java
  25. 10 0
      app/src/main/java/ch/threema/app/webclient/services/instance/message/receiver/FileMessageCreateHandler.java
  26. 2 2
      app/src/main/res/layout/activity_location_picker.xml
  27. 8 9
      app/src/main/res/layout/activity_map.xml
  28. 5 2
      app/src/main/res/menu/activity_media_viewer.xml
  29. 1 1
      app/src/main/res/xml/preference_media.xml
  30. 1 1
      domain/build.gradle
  31. 4 0
      scripts/build-release.sh

+ 30 - 22
app/build.gradle

@@ -17,7 +17,7 @@ if (getGradle().getStartParameter().getTaskRequests().toString().contains("Hms")
 }
 
 // version codes
-def app_version = "5.1.2"
+def app_version = "5.1.3"
 def beta_suffix = "" // with leading dash
 
 /**
@@ -96,7 +96,7 @@ android {
         vectorDrawables.useSupportLibrary = true
         applicationId "ch.threema.app"
         testApplicationId 'ch.threema.app.test'
-        versionCode 912
+        versionCode 919
         versionName "${app_version}${beta_suffix}"
         resValue "string", "app_name", "Threema"
         // package name used for sync adapter - needs to match mime types below
@@ -496,6 +496,9 @@ android {
             multiDexEnabled true
             multiDexKeepProguard file('multidex-keep.pro')
             testCoverageEnabled false
+            ndk {
+                debugSymbolLevel 'FULL'
+            }
 
             if (keystores['debug'] != null) {
                 signingConfig signingConfigs.debug
@@ -510,6 +513,9 @@ android {
             multiDexEnabled true
             multiDexKeepProguard file('multidex-keep.pro')
             proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-project.txt'
+            ndk {
+                debugSymbolLevel 'FULL' // 'SYMBOL_TABLE'
+            }
 
             if (keystores['release'] != null) {
                 productFlavors.store_google.signingConfig signingConfigs.release
@@ -560,6 +566,8 @@ android {
         jniLibs {
             // fix https://stackoverflow.com/questions/42739916/aarch64-linux-android-strip-file-missing
             keepDebugSymbols += ['*/mips/*.so', '*/mips64/*.so', '*/armeabi/*.so']
+            // replacement for extractNativeLibs in AndroidManifest
+            useLegacyPackaging = true
         }
         resources {
             excludes += ['META-INF/DEPENDENCIES.txt', 'META-INF/LICENSE.txt', 'META-INF/NOTICE.txt', 'META-INF/NOTICE', 'META-INF/LICENSE', 'META-INF/DEPENDENCIES', 'META-INF/notice.txt', 'META-INF/license.txt', 'META-INF/dependencies.txt', 'META-INF/LGPL2.1', '**/*.proto']
@@ -680,7 +688,7 @@ dependencies {
 
     implementation project(':domain')
 
-    implementation 'net.zetetic:sqlcipher-android:4.5.4@aar'
+    implementation 'net.zetetic:sqlcipher-android:4.5.5@aar'
 
     implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
     implementation 'net.sf.opencsv:opencsv:2.3'
@@ -690,15 +698,15 @@ dependencies {
     implementation 'commons-io:commons-io:2.6'
     implementation 'org.apache.commons:commons-text:1.10.0'
     implementation "org.slf4j:slf4j-api:$slf4j_version"
-    implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.25'
+    implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.28'
     implementation 'com.github.CanHub:Android-Image-Cropper:4.3.0'
     implementation 'com.datatheorem.android.trustkit:trustkit:1.1.5'
     implementation 'me.zhanghai.android.fastscroll:library:1.2.0'
     implementation 'com.googlecode.ez-vcard:ez-vcard:0.11.3'
 
     // AndroidX / Jetpack support libraries
-    implementation "androidx.preference:preference-ktx:1.2.0"
-    implementation 'androidx.recyclerview:recyclerview:1.3.0'
+    implementation "androidx.preference:preference-ktx:1.2.1"
+    implementation 'androidx.recyclerview:recyclerview:1.3.1'
     implementation 'androidx.palette:palette-ktx:1.0.0'
     implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
     implementation 'androidx.appcompat:appcompat:1.6.1'
@@ -718,22 +726,22 @@ dependencies {
     implementation 'androidx.media3:media3-ui:1.1.1'
     implementation "androidx.media3:media3-session:1.1.1"
     implementation 'androidx.multidex:multidex:2.0.1'
-    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1"
-    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"
-    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
-    implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1"
-    implementation "androidx.lifecycle:lifecycle-service:2.6.1"
-    implementation "androidx.lifecycle:lifecycle-process:2.6.1"
-    implementation "androidx.lifecycle:lifecycle-common-java8:2.6.1"
+    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2"
+    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2"
+    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
+    implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2"
+    implementation "androidx.lifecycle:lifecycle-service:2.6.2"
+    implementation "androidx.lifecycle:lifecycle-process:2.6.2"
+    implementation "androidx.lifecycle:lifecycle-common-java8:2.6.2"
     implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
     implementation "androidx.paging:paging-runtime-ktx:3.1.1"
     implementation "androidx.sharetarget:sharetarget:1.2.0"
-    implementation 'androidx.room:room-runtime:2.5.1'
-    kapt 'androidx.room:room-compiler:2.5.1'
+    implementation 'androidx.room:room-runtime:2.5.2'
+    kapt 'androidx.room:room-compiler:2.5.2'
 
     implementation 'com.google.android.material:material:1.9.0'
     implementation 'com.google.zxing:core:3.3.3' // zxing 3.4 crashes on API < 24
-    implementation 'com.googlecode.libphonenumber:libphonenumber:8.13.7' // make sure to update this in domain's build.gradle as well
+    implementation 'com.googlecode.libphonenumber:libphonenumber:8.13.19' // make sure to update this in domain's build.gradle as well
 
     // webclient dependencies
     implementation 'org.msgpack:msgpack-core:0.8.24!!'
@@ -748,16 +756,16 @@ dependencies {
     }
 
     implementation 'org.saltyrtc:chunked-dc:1.0.1'
-    implementation 'ch.threema:webrtc-android:110.0.0'
+    implementation 'ch.threema:webrtc-android:114.0.0'
     implementation('org.saltyrtc:saltyrtc-task-webrtc:0.18.1') {
         exclude module: 'saltyrtc-client'
     }
 
     // Glide components
     // Glide 4.15+ does not work on API 21
-    implementation 'com.github.bumptech.glide:glide:4.14.2'
-    kapt 'com.github.bumptech.glide:compiler:4.14.2'
-    annotationProcessor 'com.github.bumptech.glide:compiler:4.14.2'
+    implementation 'com.github.bumptech.glide:glide:4.16.0'
+    kapt 'com.github.bumptech.glide:compiler:4.16.0'
+    annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
 
     // kotlin
     implementation 'androidx.core:core-ktx:1.10.1'
@@ -815,7 +823,7 @@ dependencies {
         'com.google.android.gms:play-services-base:18.1.0': [],
 
         // Firebase push
-        'com.google.firebase:firebase-messaging:23.1.1': [
+        'com.google.firebase:firebase-messaging:23.1.2': [
             [group: 'com.google.firebase', module: 'firebase-core'],
             [group: 'com.google.firebase', module: 'firebase-analytics'],
             [group: 'com.google.firebase', module: 'firebase-measurement-connector'],
@@ -845,7 +853,7 @@ dependencies {
     redImplementation(name: 'libgsaverification-client', ext: 'aar')
 
     // Maplibre (may have transitive dependencies on Google location services)
-    def maplibreDependency = 'org.maplibre.gl:android-sdk:9.6.0'
+    def maplibreDependency = 'org.maplibre.gl:android-sdk:10.2.0'
     noneImplementation maplibreDependency
     store_googleImplementation maplibreDependency
     store_google_workImplementation maplibreDependency

+ 10 - 13
app/proguard-project.txt

@@ -18,16 +18,12 @@
 #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
 #   public *;
 #}
--dontpreverify
-#-flattenpackagehierarchy
--repackageclasses
--optimizationpasses 5
--dontusemixedcaseclassnames
--dontskipnonpubliclibraryclasses
--dontskipnonpubliclibraryclassmembers
--allowaccessmodification
+-dontoptimize
+-dontobfuscate
 -verbose
 
+-keepattributes EnclosingMethod,InnerClasses,Exceptions,*Annotation*,SourceFile,LineNumberTable
+
 -keeppackagenames ch.threema.**
 -keeppackagenames org.saltyrtc.**
 
@@ -122,12 +118,10 @@ public static <fields>;
 # "Warning: org.webrtc.SoftwareVideoDecoderFactory: can't find referenced class org.webrtc.LibvpxVp8Decoder"
 -dontwarn org.webrtc.**
 
--keepattributes EnclosingMethod
--keepattributes InnerClasses
+
 
 # hms requirements
 -ignorewarnings
--keepattributes Exceptions
 -keep class com.huawei.updatesdk.**{*;}
 -keep class com.huawei.hms.**{*;}
 -keep class com.huawei.android.sdk.drm.**{*;}
@@ -142,7 +136,6 @@ public static <fields>;
 
 # remaining options from proguard-android-optimize.txt
 -renamesourcefileattribute SourceFile
--keepattributes *Annotation*,SourceFile,LineNumberTable
 -keep public class com.google.android.vending.licensing.ILicensingService
 
 # keep setters in Views so that animations can still work.
@@ -168,7 +161,6 @@ public static <fields>;
 -dontwarn org.slf4j.impl.StaticMDCBinder
 -dontwarn org.slf4j.impl.StaticMarkerBinder
 -dontwarn org.slf4j.impl.StaticLoggerBinder
--keepattributes *Annotation*
 
 # WebRTC
 -keep class org.webrtc.** { *; }
@@ -233,3 +225,8 @@ public static <fields>;
 }
 
 -keep class java8.util.ImmutableCollections { *; }
+
+# https://stackoverflow.com/questions/73748946/proguard-r8-warnings
+-dontwarn org.conscrypt.**
+-dontwarn org.bouncycastle.**
+-dontwarn org.openjsse.**

+ 0 - 11
app/src/main/java/ch/threema/app/ThreemaApplication.java

@@ -1105,8 +1105,6 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 				}
 			}, "scheduleSync").start();
 
-			initMapLibre();
-
 			// setup locale override
 			ConfigUtils.setLocaleOverride(getAppContext(), serviceManager.getPreferenceService());
 		} catch (MasterKeyLockedException | SQLiteException e) {
@@ -1130,15 +1128,6 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 		);
 	}
 
-	private static void initMapLibre() {
-		if (ConfigUtils.hasNoMapLibreSupport()) {
-			logger.debug("*** MapLibre disabled due to faulty firmware");
-		} else {
-			Mapbox.getInstance(getAppContext());
-			logger.debug("*** MapLibre enabled");
-		}
-	}
-
 	private static long getSchedulePeriodMs(PreferenceStore preferenceStore, int key) {
 		Integer schedulePeriod = preferenceStore.getInt(getAppContext().getString(key));
 		if (schedulePeriod == null || schedulePeriod == 0) {

+ 1 - 1
app/src/main/java/ch/threema/app/activities/HomeActivity.java

@@ -1730,7 +1730,7 @@ public class HomeActivity extends ThreemaAppCompatActivity implements
 
 			if(headerImageView != null) {
 				headerImageView.clearColorFilter();
-				headerImageView.setImageBitmap(BitmapUtil.safeGetBitmapFromUri(this, Uri.fromFile(customAppIcon), ConfigUtils.getUsableWidth(getWindowManager()), false));
+				headerImageView.setImageBitmap(BitmapUtil.safeGetBitmapFromUri(this, Uri.fromFile(customAppIcon), ConfigUtils.getUsableWidth(getWindowManager()), false, false, false));
 			}
 		}
 	}

+ 2 - 0
app/src/main/java/ch/threema/app/activities/MapActivity.java

@@ -127,6 +127,8 @@ public class MapActivity extends ThreemaActivity implements GenericAlertDialog.D
 
 		ConfigUtils.configureSystemBars(this);
 
+		ConfigUtils.getMapLibreInstance();
+
 		setContentView(R.layout.activity_map);
 
 		ConfigUtils.configureTransparentStatusBar(this);

+ 21 - 10
app/src/main/java/ch/threema/app/activities/MediaViewerActivity.java

@@ -131,6 +131,7 @@ public class MediaViewerActivity extends ThreemaToolbarActivity implements
 	private View captionContainer;
 	private TextView caption;
 	private final Handler loadingFragmentHandler = new Handler();
+	private MenuItem saveMenuItem, shareMenuItem, viewMenuItem;
 
 	@Override
 	public void onCreate(Bundle savedInstanceState) {
@@ -300,8 +301,7 @@ public class MediaViewerActivity extends ThreemaToolbarActivity implements
 		this.pager = findViewById(R.id.pager);
 		this.pager.setOnPageChangeListener(new LockableViewPager.OnPageChangeListener() {
 			@Override
-			public void onPageScrolled(int i, float v, int i2) {
-			}
+			public void onPageScrolled(int i, float v, int i2) {}
 
 			@Override
 			public void onPageSelected(int i) {
@@ -309,8 +309,7 @@ public class MediaViewerActivity extends ThreemaToolbarActivity implements
 			}
 
 			@Override
-			public void onPageScrollStateChanged(int i) {
-			}
+			public void onPageScrollStateChanged(int i) {}
 		});
 
 		this.attachAdapter();
@@ -345,6 +344,16 @@ public class MediaViewerActivity extends ThreemaToolbarActivity implements
 		this.captionContainer.setVisibility(TestUtil.empty(captionText) ? View.GONE : View.VISIBLE);
 	}
 
+	private void updateMenus() {
+		boolean visibility = currentMediaFile != null  && !AppRestrictionUtil.isShareMediaDisabled(ThreemaApplication.getAppContext());
+
+		if (saveMenuItem != null) {
+			saveMenuItem.setVisible(visibility);
+			shareMenuItem.setVisible(visibility);
+			viewMenuItem.setVisible(visibility);
+		}
+	}
+
 	private void hideCurrentFragment() {
 		if (this.currentPosition >= 0 && this.currentPosition < this.messageModels.size()) {
 			MediaViewFragment f = this.getFragmentByPosition(this.currentPosition);
@@ -396,11 +405,9 @@ public class MediaViewerActivity extends ThreemaToolbarActivity implements
 			menuBuilder.setOptionalIconsVisible(true);
 		} catch (Exception ignored) {}
 
-		if (AppRestrictionUtil.isShareMediaDisabled(this)) {
-			menu.findItem(R.id.menu_save).setVisible(false);
-			menu.findItem(R.id.menu_share).setVisible(false);
-			menu.findItem(R.id.menu_view).setVisible(false);
-		}
+		saveMenuItem = menu.findItem(R.id.menu_save);
+		shareMenuItem = menu.findItem(R.id.menu_share);
+		viewMenuItem = menu.findItem(R.id.menu_view);
 
 		if (getToolbar().getNavigationIcon() != null) {
 			getToolbar().getNavigationIcon().setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN);
@@ -735,12 +742,16 @@ public class MediaViewerActivity extends ThreemaToolbarActivity implements
 
 					@Override
 					public void decrypted(boolean success) {
-//						Toast.makeText(a.getApplicationContext(), "Decrypted: " + (success ? "success" : "failed"), Toast.LENGTH_LONG).show();
+						if (!success) {
+							a.currentMediaFile = null;
+							a.updateMenus();
+						}
 					}
 
 					@Override
 					public void loaded(File file) {
 						a.currentMediaFile = file;
+						a.updateMenus();
 					}
 
 					@Override

+ 6 - 2
app/src/main/java/ch/threema/app/activities/RecipientListBaseActivity.java

@@ -980,14 +980,18 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 				if (i < originalMessageModels.size() - 1) {
 					forwardSingleMessage(messageReceivers, i+1, intent, keepOriginalCaptions);
 				} else {
-					DialogUtil.dismissDialog(getSupportFragmentManager(), DIALOG_TAG_MULTISEND, true);
+					RuntimeUtil.runOnUiThread(() -> DialogUtil.dismissDialog(getSupportFragmentManager(), DIALOG_TAG_MULTISEND, true));
 					startComposeActivity(intent);
 				}
 			}
 
 			@Override
 			public void error(String message) {
-				RuntimeUtil.runOnUiThread(() -> SingleToast.getInstance().showLongText(getString(R.string.an_error_occurred_during_send)));
+				RuntimeUtil.runOnUiThread(() -> {
+					SingleToast.getInstance().showLongText(getString(R.string.an_error_occurred_during_send));
+					DialogUtil.dismissDialog(getSupportFragmentManager(), DIALOG_TAG_MULTISEND, true);
+				});
+				finish();
 			}
 		});
 	}

+ 5 - 1
app/src/main/java/ch/threema/app/adapters/ComposeMessageAdapter.java

@@ -480,7 +480,7 @@ public class ComposeMessageAdapter extends ArrayAdapter<AbstractMessageModel> {
 		View itemView = convertView;
 		final ComposeMessageHolder holder;
 		final AbstractMessageModel messageModel = values.get(position);
-		final MessageType messageType = messageModel.getType();
+		MessageType messageType = messageModel.getType();
 
 		@ItemType int itemType = this.getItemType(messageModel);
 		int itemLayout = this.getLayoutByItemType(itemType);
@@ -563,6 +563,10 @@ public class ComposeMessageAdapter extends ArrayAdapter<AbstractMessageModel> {
 		else {
 			final boolean showAvatar = adjustMarginsForMessageGrouping(holder, itemView, itemType, messageModel);
 
+			if (messageType == null) {
+				messageType = MessageType.STATUS;
+			}
+
 			switch (messageType) {
 				case STATUS:
 					decorator = new StatusChatAdapterDecorator(this.context, messageModel, this.decoratorHelper);

+ 4 - 4
app/src/main/java/ch/threema/app/adapters/MediaGalleryAdapter.kt

@@ -152,7 +152,7 @@ class MediaGalleryAdapter(
                         override fun onLoadFailed(
                             e: GlideException?,
                             model: Any?,
-                            target: Target<Bitmap?>?,
+                            target: Target<Bitmap?>,
                             isFirstResource: Boolean
                         ): Boolean {
                             decorateItem(holder, messageModel)
@@ -160,10 +160,10 @@ class MediaGalleryAdapter(
                         }
 
                         override fun onResourceReady(
-                            resource: Bitmap?,
-                            model: Any?,
+                            resource: Bitmap,
+                            model: Any,
                             target: Target<Bitmap?>?,
-                            dataSource: DataSource?,
+                            dataSource: DataSource,
                             isFirstResource: Boolean
                         ): Boolean {
                             holder.textContainerView?.visibility = View.GONE

+ 13 - 2
app/src/main/java/ch/threema/app/adapters/SendMediaPreviewAdapter.kt

@@ -223,13 +223,24 @@ class SendMediaPreviewAdapter(
     private fun loadImage(item: MediaItem, holder: SendMediaItemHolder) {
         Glide.with(context).load(item.uri)
             .addListener(object : RequestListener<Drawable?> {
-                override fun onLoadFailed(e: GlideException?, model: Any, target: Target<Drawable?>, isFirstResource: Boolean): Boolean {
+                override fun onLoadFailed(
+                    e: GlideException?,
+                    model: Any?,
+                    target: Target<Drawable?>,
+                    isFirstResource: Boolean
+                ): Boolean {
                     holder.imageView.setImageDrawable(null)
                     holder.brokenView.visibility = View.VISIBLE
                     return false
                 }
 
-                override fun onResourceReady(resource: Drawable?, model: Any, target: Target<Drawable?>, dataSource: DataSource, isFirstResource: Boolean): Boolean {
+                override fun onResourceReady(
+                    resource: Drawable,
+                    model: Any,
+                    target: Target<Drawable?>?,
+                    dataSource: DataSource,
+                    isFirstResource: Boolean
+                ): Boolean {
                     setQualifierView(item, holder)
                     holder.brokenView.visibility = View.INVISIBLE
                     return false

+ 2 - 1
app/src/main/java/ch/threema/app/backuprestore/BackupChatServiceImpl.java

@@ -127,7 +127,8 @@ public class BackupChatServiceImpl implements BackupChatService {
 						saveMedia = fileDataModel.isDownloaded();
 						filename = TestUtil.empty(fileDataModel.getFileName()) ?
 							FileUtil.getDefaultFilename(fileDataModel.getMimeType()) :
-							m.getApiMessageId() + "-" + fileDataModel.getFileName();
+							(m.getApiMessageId() != null ? m.getApiMessageId() : m.getId()) +
+							"-" + fileDataModel.getFileName();
 						extension = "";
 						break;
 					case LOCATION:

+ 35 - 14
app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java

@@ -354,7 +354,6 @@ public class ComposeMessageFragment extends Fragment implements
 	private MenuItem blockMenuItem = null;
 	private MenuItem deleteDistributionListItem = null;
 	private MenuItem callItem = null;
-	private MenuItem shortCutItem = null;
 	private MenuItem showOpenBallotWindowMenuItem = null;
 	private MenuItem showBallotsMenuItem = null;
 	private MenuItem showAllGroupRequestsMenuItem = null;
@@ -994,8 +993,6 @@ public class ComposeMessageFragment extends Fragment implements
 		}
 		super.onCreate(savedInstanceState);
 
-//		setRetainInstance(true);
-
 		ListenerManager.contactTypingListeners.add(this.contactTypingListener);
 		ListenerManager.messageListeners.add(this.messageListener, true);
 		ListenerManager.groupListeners.add(this.groupListener);
@@ -3004,7 +3001,8 @@ public class ComposeMessageFragment extends Fragment implements
 				selectedMessages.remove(messageModel);
 				convListView.setItemChecked(position, false);
 			} else {
-				if (convListView.getCheckedItemCount() < MAX_SELECTED_ITEMS) {
+				if (convListView.getCheckedItemCount() < MAX_SELECTED_ITEMS &&
+					isItemSelectable(composeMessageAdapter.getItemViewType(position), messageModel)) {
 					// add this to selection
 					selectedMessages.add(messageModel);
 					convListView.setItemChecked(position, true);
@@ -3138,14 +3136,14 @@ public class ComposeMessageFragment extends Fragment implements
 	@UiThread
 	private void onListItemLongClick(@NonNull View view, final int position) {
 		int viewType = composeMessageAdapter.getItemViewType(position);
-		if (viewType == ComposeMessageAdapter.TYPE_FIRST_UNREAD  ||
-			viewType == ComposeMessageAdapter.TYPE_DATE_SEPARATOR) {
-			// Do not allow to select these view types
+		AbstractMessageModel selectedMessage = composeMessageAdapter.getItem(position);
+
+		if (!isItemSelectable(viewType, selectedMessage)) {
 			return;
 		}
 
 		selectedMessages.clear();
-		selectedMessages.add(composeMessageAdapter.getItem(position));
+		selectedMessages.add(selectedMessage);
 
 		if (actionMode != null) {
 			convListView.clearChoices();
@@ -3211,6 +3209,31 @@ public class ComposeMessageFragment extends Fragment implements
 		ackjiPopup.show(view.findViewById(R.id.message_block), selectedMessages.get(0));
 	}
 
+	/**
+	 * Check whether the selected item in the conversation list can be selected
+	 * @param viewType View type of the item
+	 * @param selectedMessage Message Model of the item
+	 * @return true if item is selectable, false otherwise
+	 */
+	private boolean isItemSelectable(int viewType, @Nullable AbstractMessageModel selectedMessage) {
+		if (viewType == ComposeMessageAdapter.TYPE_FIRST_UNREAD  ||
+			viewType == ComposeMessageAdapter.TYPE_DATE_SEPARATOR) {
+			// Do not allow to select these view types
+			return false;
+		}
+
+		if (selectedMessage == null) {
+			return false;
+		}
+
+		if (viewType == ComposeMessageAdapter.TYPE_FILE_VIDEO_SEND && selectedMessage.getState() == MessageState.TRANSCODING) {
+			// transcoding messages cannot be selected
+			return false;
+		}
+
+		return true;
+	}
+
 	private boolean isMuted() {
 		if (messageReceiver != null && mutedChatsListService != null) {
 			String uniqueId = messageReceiver.getUniqueIdString();
@@ -3642,6 +3665,7 @@ public class ComposeMessageFragment extends Fragment implements
 	}
 
 	@Override
+	@SuppressLint("StaticFieldLeak")
 	public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
 		inflater.inflate(R.menu.fragment_compose_message, menu);
 		this.setupToolbar();
@@ -3655,7 +3679,6 @@ public class ComposeMessageFragment extends Fragment implements
 	public void onPrepareOptionsMenu(Menu menu) {
 		this.callItem = menu.findItem(R.id.menu_threema_call);
 		this.deleteDistributionListItem = menu.findItem(R.id.menu_delete_distribution_list);
-		this.shortCutItem = menu.findItem(R.id.menu_shortcut);
 		this.mutedMenuItem = menu.findItem(R.id.menu_muted);
 		this.blockMenuItem = menu.findItem(R.id.menu_block_contact);
 		this.showOpenBallotWindowMenuItem = menu.findItem(R.id.menu_ballot_window_show);
@@ -3677,7 +3700,6 @@ public class ComposeMessageFragment extends Fragment implements
 		if (!TestUtil.required(
 				this.callItem,
 				this.deleteDistributionListItem,
-				this.shortCutItem,
 				this.mutedMenuItem,
 				this.blockMenuItem,
 				this.showOpenBallotWindowMenuItem,
@@ -3689,7 +3711,6 @@ public class ComposeMessageFragment extends Fragment implements
 		}
 
 		this.deleteDistributionListItem.setVisible(this.isDistributionListChat);
-		this.shortCutItem.setVisible(ShortcutManagerCompat.isRequestPinShortcutSupported(getAppContext()));
 		this.mutedMenuItem.setVisible(!this.isDistributionListChat && !(isGroupChat && groupService.isNotesGroup(groupModel)));
 		updateMuteMenu();
 
@@ -4027,7 +4048,7 @@ public class ComposeMessageFragment extends Fragment implements
 				selectorDialog.setTargetFragment(this, 0);
 				selectorDialog.show(getFragmentManager(), DIALOG_TAG_CHOOSE_SHORTCUT_TYPE);
 		} else {
-			ShortcutUtil.createPinnedShortcut(messageReceiver, TYPE_CHAT);
+			RuntimeUtil.runOnWorkerThread(() -> ShortcutUtil.createPinnedShortcut(messageReceiver, TYPE_CHAT));
 		}
 	}
 
@@ -4073,8 +4094,8 @@ public class ComposeMessageFragment extends Fragment implements
 	@Override
 	public void onClick(String tag, int which, Object data) {
 		if (DIALOG_TAG_CHOOSE_SHORTCUT_TYPE.equals(tag)) {
-			int shortcutType = which + 1;
-			ShortcutUtil.createPinnedShortcut(messageReceiver, shortcutType);
+			final int shortcutType = which + 1;
+			RuntimeUtil.runOnWorkerThread(() -> ShortcutUtil.createPinnedShortcut(messageReceiver, shortcutType));
 		}
 	}
 

+ 2 - 0
app/src/main/java/ch/threema/app/locationpicker/LocationPickerActivity.java

@@ -208,6 +208,8 @@ public class LocationPickerActivity extends ThreemaActivity implements
 
 		ConfigUtils.configureSystemBars(this);
 
+		ConfigUtils.getMapLibreInstance();
+
 		setContentView(R.layout.activity_location_picker);
 
 		ConfigUtils.configureTransparentStatusBar(this);

+ 1 - 5
app/src/main/java/ch/threema/app/mediaattacher/MediaAttachActivity.java

@@ -386,11 +386,7 @@ public class MediaAttachActivity extends MediaSelectionBaseActivity implements V
 		super.onClick(v);
 		final int id = v.getId();
 		if (id == R.id.attach_location) {
-			if (!ConfigUtils.hasNoMapLibreSupport()) {
-				launchPlacePicker();
-			} else {
-				Toast.makeText(this, "Feature not available due to firmware error", Toast.LENGTH_LONG).show();
-			}
+			launchPlacePicker();
 		} else if (id == R.id.attach_file) {
 			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || ConfigUtils.requestStoragePermissions(this, null, PERMISSION_REQUEST_ATTACH_FILE)) {
 				attachFile();

+ 4 - 4
app/src/main/java/ch/threema/app/services/AvatarCacheServiceImpl.java

@@ -206,11 +206,11 @@ final public class AvatarCacheServiceImpl implements AvatarCacheService {
 
 	@AnyThread
 	private <M extends ReceiverModel> void loadBitmap(@NonNull AvatarConfig<M> config, @Nullable Drawable placeholder, @NonNull ImageView view) {
-		RequestBuilder<Bitmap> requestBuilder = Glide.with(context).asBitmap().load(config).placeholder(placeholder).transition(BitmapTransitionOptions.withCrossFade(factory)).diskCacheStrategy(DiskCacheStrategy.NONE).signature(new ObjectKey(config.state));
-		if (config.options.disableCache) {
-			requestBuilder = requestBuilder.skipMemoryCache(true);
-		}
 		try {
+			RequestBuilder<Bitmap> requestBuilder = Glide.with(context).asBitmap().load(config).placeholder(placeholder).transition(BitmapTransitionOptions.withCrossFade(factory)).diskCacheStrategy(DiskCacheStrategy.NONE).signature(new ObjectKey(config.state));
+			if (config.options.disableCache) {
+				requestBuilder = requestBuilder.skipMemoryCache(true);
+			}
 			requestBuilder.into(view);
 		} catch (Exception e) {
 			logger.debug("Glide failure", e);

+ 3 - 4
app/src/main/java/ch/threema/app/services/MessageServiceImpl.java

@@ -3998,7 +3998,7 @@ public class MessageServiceImpl implements MessageService {
 				Bitmap bitmap = null;
 				try {
 					boolean hasNoTransparency = MimeUtil.MIME_TYPE_IMAGE_JPG.equals(mediaItem.getMimeType());
-					bitmap = BitmapUtil.safeGetBitmapFromUri(context, mediaItem.getUri(), maxSize, false);
+					bitmap = BitmapUtil.safeGetBitmapFromUri(context, mediaItem.getUri(), maxSize, false, false, false);
 					if (bitmap != null) {
 						bitmap = adjustBitmapOrientation(bitmap, mediaItem, metaData);
 
@@ -4151,8 +4151,7 @@ public class MessageServiceImpl implements MessageService {
 				} else {
 					fileDataModel.setThumbnailMimeType(MimeUtil.MIME_TYPE_IMAGE_PNG);
 				}
-				thumbnailBitmap = BitmapUtil.safeGetBitmapFromUri(context, mediaItem.getUri(), THUMBNAIL_SIZE_PX, false, true);
-
+				thumbnailBitmap = BitmapUtil.safeGetBitmapFromUri(context, mediaItem.getUri(), THUMBNAIL_SIZE_PX, false, true, false);
 				if (thumbnailBitmap != null) {
 					thumbnailBitmap = BitmapUtil.rotateBitmap(BitmapUtil.rotateBitmap(
 						thumbnailBitmap,
@@ -4163,7 +4162,7 @@ public class MessageServiceImpl implements MessageService {
 			case MediaItem.TYPE_IMAGE_CAM:
 				// camera images are always sent as JPGs
 				fileDataModel.setThumbnailMimeType(MimeUtil.MIME_TYPE_IMAGE_JPG);
-				thumbnailBitmap = BitmapUtil.safeGetBitmapFromUri(context, mediaItem.getUri(), THUMBNAIL_SIZE_PX, false, true);
+				thumbnailBitmap = BitmapUtil.safeGetBitmapFromUri(context, mediaItem.getUri(), THUMBNAIL_SIZE_PX, false, true, false);
 				if (thumbnailBitmap != null) {
 					thumbnailBitmap = BitmapUtil.rotateBitmap(BitmapUtil.rotateBitmap(
 						thumbnailBitmap,

+ 2 - 0
app/src/main/java/ch/threema/app/threemasafe/ThreemaSafeServiceImpl.java

@@ -1198,6 +1198,7 @@ public class ThreemaSafeServiceImpl implements ThreemaSafeService {
 			outputStream.close();
 			return compressedBytes;
 		} catch (Exception e) {
+			logger.error("Error compressing", e);
 			return null;
 		}
 	}
@@ -1216,6 +1217,7 @@ public class ThreemaSafeServiceImpl implements ThreemaSafeService {
 			outputStream.close();
 			return uncompressedBytes;
 		} catch (Exception e) {
+			logger.error("Error uncompressing", e);
 			return null;
 		}
 	}

+ 2 - 2
app/src/main/java/ch/threema/app/ui/AvatarEditView.java

@@ -576,7 +576,7 @@ public class AvatarEditView extends FrameLayout implements DefaultLifecycleObser
 				case REQUEST_CODE_CROP:
 					Bitmap bitmap = null;
 					if (avatarData.getCroppedFile() != null && avatarData.getCroppedFile().exists() && avatarData.getCroppedFile().length() > 0) {
-						bitmap = BitmapUtil.safeGetBitmapFromUri(getActivity(), Uri.fromFile(avatarData.getCroppedFile()), CONTACT_AVATAR_HEIGHT_PX);
+						bitmap = BitmapUtil.safeGetBitmapFromUri(getActivity(), Uri.fromFile(avatarData.getCroppedFile()), CONTACT_AVATAR_HEIGHT_PX, true, true, false);
 						if (bitmap != null) {
 							if (listenerRef.get() != null) {
 								listenerRef.get().onAvatarSet(avatarData.getCroppedFile());
@@ -775,7 +775,7 @@ public class AvatarEditView extends FrameLayout implements DefaultLifecycleObser
 	public void setAvatarFile(File avatarFile) {
 		if (avatarFile != null && avatarFile.exists() && avatarFile.length() > 0) {
 			this.avatarData.setCroppedFile(avatarFile);
-			Bitmap bitmap = BitmapUtil.safeGetBitmapFromUri(getActivity(), Uri.fromFile(avatarData.getCroppedFile()), CONTACT_AVATAR_HEIGHT_PX);
+			Bitmap bitmap = BitmapUtil.safeGetBitmapFromUri(getActivity(), Uri.fromFile(avatarData.getCroppedFile()), CONTACT_AVATAR_HEIGHT_PX, true, true, false);
 			if (bitmap != null) {
 				setAvatarBitmap(bitmap);
 			}

+ 10 - 23
app/src/main/java/ch/threema/app/utils/BitmapUtil.java

@@ -220,14 +220,6 @@ public class BitmapUtil {
 		return null;
 	}
 
-	static public Bitmap safeGetBitmapFromUri(Context context, Uri imageUri, int maxSize) {
-		return safeGetBitmapFromUri(context, imageUri, maxSize, true, true);
-	}
-
-	static public Bitmap safeGetBitmapFromUri(Context context, Uri imageUri, int maxSize, boolean replaceTransparency) {
-		return safeGetBitmapFromUri(context, imageUri, maxSize, replaceTransparency, false);
-	}
-
 	/**
 	 * Get a scaled bitmap from a JPG image file pointed at by imageUri keeping its aspect ratio
 	 * The image is scaled so that it fits into a bounding box of maxSize x maxSize unless the scaleToWidth parameter is set.
@@ -237,10 +229,11 @@ public class BitmapUtil {
 	 * @param maxSize max size of the image
 	 * @param replaceTransparency if set to true, transparency in the image will be replaced with Color.WHITE
 	 * @param scaleToWidth if set, the image will be scaled so its width does not exceed maxSize while the height may be larger
+	 * @param applyExifOrientation whether exif rotation or flip settings should be applied, if present
 	 * @return resulting bitmap or null in case of failure
 	 */
 	static public @Nullable
-	Bitmap safeGetBitmapFromUri(Context context, Uri imageUri, int maxSize, boolean replaceTransparency, boolean scaleToWidth) {
+	Bitmap safeGetBitmapFromUri(Context context, Uri imageUri, int maxSize, boolean replaceTransparency, boolean scaleToWidth, boolean applyExifOrientation) {
 		logger.debug("safeGetBitmapFromUri");
 		InputStream measure = null, data = null;
 		BitmapFactory.Options options;
@@ -301,6 +294,14 @@ public class BitmapUtil {
 						logger.debug("Image has alpha channel, replace transparency with white");
 						result = replaceTransparency(result, Color.WHITE);
 					}
+
+					if (applyExifOrientation) {
+						BitmapUtil.ExifOrientation exifOrientation = BitmapUtil.getExifOrientation(context, imageUri);
+						if (result != null && (exifOrientation.getRotation() != 0f || exifOrientation.getFlip() != BitmapUtil.FLIP_NONE)) {
+							return BitmapUtil.rotateBitmap(result, exifOrientation.getRotation(), exifOrientation.getFlip());
+						}
+					}
+
 					return result;
 				} catch (StackOverflowError e) {
 					logger.error("Exception", e);
@@ -553,20 +554,6 @@ public class BitmapUtil {
 		}
 	}
 
-	public static Bitmap cropToSquare(Bitmap bitmap) {
-		int width  = bitmap.getWidth();
-		int height = bitmap.getHeight();
-		int newWidth = (height > width) ? width : height;
-		int newHeight = (height > width)? height - ( height - width) : height;
-		int cropW = (width - height) / 2;
-		int cropH = (height - width) / 2;
-
-		cropW = (cropW < 0)? 0: cropW;
-		cropH = (cropH < 0)? 0: cropH;
-
-		return Bitmap.createBitmap(bitmap, cropW, cropH, newWidth, newHeight);
-	}
-
 	@Nullable
 	public static Bitmap getBitmapFromVectorDrawable(Drawable icon, Integer tintColor) {
 		Bitmap bitmap = null;

+ 13 - 10
app/src/main/java/ch/threema/app/utils/ConfigUtils.java

@@ -85,6 +85,7 @@ import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.StringDef;
 import androidx.annotation.StringRes;
+import androidx.annotation.UiThread;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.appcompat.app.AppCompatDelegate;
 import androidx.appcompat.view.menu.MenuBuilder;
@@ -106,6 +107,7 @@ import com.datatheorem.android.trustkit.TrustKit;
 import com.google.android.material.search.SearchBar;
 import com.google.android.material.snackbar.BaseTransientBottomBar;
 import com.google.android.material.snackbar.Snackbar;
+import com.mapbox.mapboxsdk.Mapbox;
 
 import org.slf4j.Logger;
 
@@ -155,6 +157,7 @@ public class ConfigUtils {
 	private static int emojiStyle = 0;
 	private static Boolean isTablet = null, isBiggerSingleEmojis = null, hasMapLibreSupport = null;
 	private static int preferredThumbnailWidth = -1, preferredAudioMessageWidth = -1, currentDayNightMode;
+	private static Mapbox mapbox = null;
 
 	private static final float[] NEGATIVE_MATRIX = {
 		-1.0f,     0,     0,    0, 255, // red
@@ -299,16 +302,6 @@ public class ConfigUtils {
 				TrustKit.getInstance().getSSLSocketFactory(host));
 	}
 
-	public static boolean hasNoMapLibreSupport() {
-		/* Some broken Samsung devices crash on MapLibre initialization due to a compiler bug, see https://issuetracker.google.com/issues/37013676 */
-		/* Device that do not support OCSP stapling cannot use our maps and POI servers */
-		if (hasMapLibreSupport == null) {
-			hasMapLibreSupport =
-				Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1 && Build.MANUFACTURER.equalsIgnoreCase("marshall");
-		}
-		return hasMapLibreSupport;
-	}
-
 	public static boolean isXiaomiDevice() {
 		return Build.MANUFACTURER.equalsIgnoreCase("Xiaomi");
 	}
@@ -1518,4 +1511,14 @@ public class ConfigUtils {
 			return insets;
 		});
 	}
+
+	@UiThread
+	@NonNull
+	public static Mapbox getMapLibreInstance() {
+		if (mapbox == null) {
+			mapbox = Mapbox.getInstance(ThreemaApplication.getAppContext());
+			logger.info("MapLibre enabled");
+		}
+		return mapbox;
+	}
 }

+ 0 - 3
app/src/main/java/ch/threema/app/utils/GeoLocationUtil.java

@@ -248,9 +248,6 @@ public class GeoLocationUtil {
 	 * @return true if the device supports map libre and the location can be shown, false otherwise
 	 */
 	public static boolean viewLocation(@NonNull Context context, @NonNull LocationDataModel locationData) {
-		if (ConfigUtils.hasNoMapLibreSupport()) {
-			return false;
-		}
 		Intent intent = new Intent(context, MapActivity.class);
 		IntentDataUtil.append(new LatLng(locationData.getLatitude(),
 				locationData.getLongitude()),

+ 2 - 2
app/src/main/java/ch/threema/app/utils/IconUtil.java

@@ -175,7 +175,7 @@ public class IconUtil {
 		if (thumbnailBitmap == null) {
 			// PNGs or GIFs may contain transparency
 			boolean mayContainTransparency = MimeUtil.MIME_TYPE_IMAGE_PNG.equals(mimeType) || MimeUtil.MIME_TYPE_IMAGE_GIF.equals(mimeType);
-			thumbnailBitmap = BitmapUtil.safeGetBitmapFromUri(context, uri, thumbSize, !mayContainTransparency, true);
+			thumbnailBitmap = BitmapUtil.safeGetBitmapFromUri(context, uri, thumbSize, !mayContainTransparency, true, false);
 		}
 
 		if (thumbnailBitmap == null && MimeUtil.isVideoFile(mimeType)) {
@@ -192,7 +192,7 @@ public class IconUtil {
 			}
 		}
 
-		if (thumbnailBitmap != null && !ignoreExifRotate && (exifOrientation.getRotation() != 0f || exifOrientation.getFlip() != 0f)) {
+		if (thumbnailBitmap != null && !ignoreExifRotate && (exifOrientation.getRotation() != 0f || exifOrientation.getFlip() != BitmapUtil.FLIP_NONE)) {
 			return BitmapUtil.rotateBitmap(thumbnailBitmap, exifOrientation.getRotation(), exifOrientation.getFlip());
 		}
 

+ 20 - 14
app/src/main/java/ch/threema/app/utils/ShortcutUtil.java

@@ -22,6 +22,7 @@
 package ch.threema.app.utils;
 
 import static androidx.core.content.pm.ShortcutManagerCompat.FLAG_MATCH_PINNED;
+import static ch.threema.app.ThreemaApplication.getAppContext;
 
 import android.app.PendingIntent;
 import android.content.ComponentName;
@@ -105,24 +106,29 @@ public final class ShortcutUtil {
 
 	@WorkerThread
 	public static void createPinnedShortcut(MessageReceiver<? extends AbstractMessageModel> messageReceiver, int type) {
-		ShortcutInfoCompat shortcutInfoCompat = getPinnedShortcutInfo(messageReceiver, type);
-
-		if (shortcutInfoCompat != null) {
-			if (updatePinnedShortcut(messageReceiver, type)) {
-				Toast.makeText(getContext(), R.string.add_shortcut_exists, Toast. LENGTH_LONG).show();
-			} else {
-				Intent pinnedShortcutCallbackIntent = new Intent(ThreemaApplication.INTENT_ACTION_SHORTCUT_ADDED);
-				PendingIntent callback = PendingIntent.getBroadcast(getContext(), REQUEST_CODE_SHORTCUT_ADDED,
-					pinnedShortcutCallbackIntent, IntentDataUtil.PENDING_INTENT_FLAG_MUTABLE);
-
-				if (ShortcutManagerCompat.requestPinShortcut(getContext(), shortcutInfoCompat, callback.getIntentSender())) {
-					logger.info("Shortcut requested");
+		if (ShortcutManagerCompat.isRequestPinShortcutSupported(getAppContext())) {
+			ShortcutInfoCompat shortcutInfoCompat = getPinnedShortcutInfo(messageReceiver, type);
+
+			if (shortcutInfoCompat != null) {
+				if (updatePinnedShortcut(messageReceiver, type)) {
+					RuntimeUtil.runOnUiThread(() -> Toast.makeText(getContext(), R.string.add_shortcut_exists, Toast.LENGTH_LONG).show());
+					return;
 				} else {
-					Toast.makeText(getContext(), R.string.add_shortcut_error, Toast.LENGTH_SHORT).show();
-					logger.info("Failed to add shortcut");
+					Intent pinnedShortcutCallbackIntent = new Intent(ThreemaApplication.INTENT_ACTION_SHORTCUT_ADDED);
+					PendingIntent callback = PendingIntent.getBroadcast(getContext(), REQUEST_CODE_SHORTCUT_ADDED,
+						pinnedShortcutCallbackIntent, IntentDataUtil.PENDING_INTENT_FLAG_MUTABLE);
+
+					if (ShortcutManagerCompat.requestPinShortcut(getContext(), shortcutInfoCompat, callback.getIntentSender())) {
+						logger.info("Shortcut requested");
+						return;
+					}
 				}
 			}
+			logger.info("Failed to add shortcut");
+		} else {
+			logger.info("Launcher does not support shortcuts");
 		}
+		RuntimeUtil.runOnUiThread(() -> Toast.makeText(getContext(), R.string.add_shortcut_error, Toast.LENGTH_SHORT).show());
 	}
 
 	@WorkerThread

+ 10 - 1
app/src/main/java/ch/threema/app/utils/ThumbnailUtil.java

@@ -40,6 +40,15 @@ public class ThumbnailUtil {
 
 	private ThumbnailUtil() {}
 
+	/**
+	 * Generate a thumbnail for a received file.
+	 * Note that the provided file is supposed to be a temporary file so we cannot use MediaStore or DocumentsContract
+	 * to retrieve a thumbnail. We have to generate our own.
+	 * @param context A Context
+	 * @param mimeType Mime Type of the file
+	 * @param file File to generate a thumbnail for
+	 * @return A byte array containing either a JPG or PNG encoded bitmap
+	 */
 	public static @Nullable byte[] generateThumbnailData(
 		@NonNull Context context,
 		@NonNull String mimeType,
@@ -72,7 +81,7 @@ public class ThumbnailUtil {
 
 		switch (MimeUtil.getMediaTypeFromMimeType(mimeType)) {
 			case MediaItem.TYPE_IMAGE:
-				return BitmapUtil.safeGetBitmapFromUri(context, uri, MessageServiceImpl.THUMBNAIL_SIZE_PX, false, true);
+				return BitmapUtil.safeGetBitmapFromUri(context, uri, MessageServiceImpl.THUMBNAIL_SIZE_PX, false, true, true);
 			case MediaItem.TYPE_GIF:
 				return IconUtil.getThumbnailFromUri(context, uri, MessageServiceImpl.THUMBNAIL_SIZE_PX, mimeType, true);
 			case MediaItem.TYPE_VIDEO:

+ 10 - 0
app/src/main/java/ch/threema/app/webclient/services/instance/message/receiver/FileMessageCreateHandler.java

@@ -40,12 +40,14 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
+import ch.threema.app.ThreemaApplication;
 import ch.threema.app.messagereceiver.MessageReceiver;
 import ch.threema.app.services.FileService;
 import ch.threema.app.services.IdListService;
 import ch.threema.app.services.LifetimeService;
 import ch.threema.app.services.MessageService;
 import ch.threema.app.ui.MediaItem;
+import ch.threema.app.utils.BitmapUtil;
 import ch.threema.app.utils.MimeUtil;
 import ch.threema.app.webclient.Protocol;
 import ch.threema.app.webclient.services.instance.MessageDispatcher;
@@ -130,9 +132,15 @@ public class FileMessageCreateHandler extends MessageCreateHandler {
 		// Create media item
 		final @MediaItem.MediaType int mediaType;
 		final @FileData.RenderingType int renderingType;
+		@BitmapUtil.FlipType int exifFlip = 0;
+		int exifRotation = 0;
+
 		if (!sendAsFile && FileMessageCreateHandler.IMAGE_MIME_TYPES.contains(mimeType)) {
 			mediaType = MediaItem.TYPE_IMAGE;
 			renderingType = FileData.RENDERING_MEDIA;
+			BitmapUtil.ExifOrientation exifOrientation = BitmapUtil.getExifOrientation(ThreemaApplication.getAppContext(), Uri.fromFile(file));
+			exifRotation = (int) exifOrientation.getRotation();
+			exifFlip = exifOrientation.getFlip();
 		} else if (!sendAsFile && FileMessageCreateHandler.AUDIO_MIME_TYPES.contains(mimeType)) {
 			mediaType = MediaItem.TYPE_VOICEMESSAGE;
 			renderingType = FileData.RENDERING_DEFAULT;
@@ -148,6 +156,8 @@ public class FileMessageCreateHandler extends MessageCreateHandler {
 		mediaItem.setCaption(caption);
 		mediaItem.setMimeType(mimeType);
 		mediaItem.setRenderingType(renderingType);
+		mediaItem.setExifFlip(exifFlip);
+		mediaItem.setExifRotation(exifRotation);
 
 		// Send media, get message model
 		final AbstractMessageModel model = messageService.sendMedia(Collections.singletonList(mediaItem), receivers, null);

+ 2 - 2
app/src/main/res/layout/activity_location_picker.xml

@@ -38,8 +38,8 @@
 
 					<com.mapbox.mapboxsdk.maps.MapView
 						android:id="@+id/map"
-						app:mapbox_cameraZoom="16"
-						app:mapbox_uiCompass="false"
+						app:maplibre_cameraZoom="16"
+						app:maplibre_uiCompass="false"
 						android:layout_width="match_parent"
 						android:layout_height="match_parent"/>
 

+ 8 - 9
app/src/main/res/layout/activity_map.xml

@@ -8,7 +8,6 @@
 	xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:app="http://schemas.android.com/apk/res-auto"
 	xmlns:tools="http://schemas.android.com/tools"
-	xmlns:mapbox="http://schemas.android.com/apk/res-auto"
 	android:id="@+id/coordinator"
 	android:layout_width="match_parent"
 	android:layout_height="match_parent"
@@ -19,14 +18,14 @@
 		android:id="@+id/map"
 		android:layout_width="match_parent"
 		android:layout_height="match_parent"
-		mapbox:mapbox_cameraZoom="4"
-		mapbox:mapbox_uiLogo="false"
-		mapbox:mapbox_uiAttribution="false"
-		mapbox:mapbox_uiCompass="true"
-		mapbox:mapbox_uiCompassFadeFacingNorth="false"
-		mapbox:mapbox_uiCompassGravity="top|right"
-		mapbox:mapbox_uiCompassMarginTop="@dimen/map_compass_margin_top"
-		mapbox:mapbox_uiCompassMarginRight="@dimen/map_compass_margin_right"/>
+		app:maplibre_cameraZoom="4"
+		app:maplibre_uiLogo="false"
+		app:maplibre_uiAttribution="false"
+		app:maplibre_uiCompass="true"
+		app:maplibre_uiCompassFadeFacingNorth="false"
+		app:maplibre_uiCompassGravity="top|right"
+		app:maplibre_uiCompassMarginTop="@dimen/map_compass_margin_top"
+		app:maplibre_uiCompassMarginRight="@dimen/map_compass_margin_right"/>
 
 	<RelativeLayout
 		android:id="@+id/map_container"

+ 5 - 2
app/src/main/res/menu/activity_media_viewer.xml

@@ -4,7 +4,7 @@
 
 	<item
 		android:id="@+id/menu_gallery"
-		app:showAsAction="ifRoom"
+		app:showAsAction="always"
 		android:title="@string/media_gallery"
 		android:icon="@drawable/ic_outline_apps"
 		app:iconTint="@android:color/white"
@@ -12,8 +12,9 @@
 
 	<item
 		android:id="@+id/menu_save"
-		app:showAsAction="ifRoom"
+		app:showAsAction="never"
 		android:title="@string/save_message_action"
+		android:visible="false"
 		android:icon="@drawable/ic_save_outline"
 		app:iconTint="@android:color/white"
 		android:orderInCategory="10" />
@@ -22,6 +23,7 @@
 		android:id="@+id/menu_view"
 		app:showAsAction="never"
 		android:title="@string/view_in_gallery"
+		android:visible="false"
 		android:icon="@drawable/ic_collections"
 		app:iconTint="@android:color/white"
 		android:orderInCategory="30" />
@@ -29,6 +31,7 @@
 	<item
 		android:id="@+id/menu_share"
 		app:showAsAction="always"
+		android:visible="false"
 		android:icon="@drawable/ic_share_outline"
 		app:iconTint="@android:color/white"
 		android:title="@string/share_image"

+ 1 - 1
app/src/main/res/xml/preference_media.xml

@@ -20,7 +20,7 @@
 			android:title="@string/prefs_save_media"/>
 
 		<DropDownPreference
-			android:defaultValue="1"
+			android:defaultValue="2"
 			android:entries="@array/list_image_size"
 			android:entryValues="@array/list_image_size_values"
 			android:icon="@drawable/ic_picture_size_outline"

+ 1 - 1
domain/build.gradle

@@ -33,7 +33,7 @@ def getGitVersion = { ->
 
 dependencies {
     api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
-    api 'com.googlecode.libphonenumber:libphonenumber:8.13.7'
+    api 'com.googlecode.libphonenumber:libphonenumber:8.13.19'
     api 'androidx.annotation:annotation:1.6.0'
     api 'net.sourceforge.streamsupport:streamsupport-flow:1.7.4'
 

+ 4 - 0
scripts/build-release.sh

@@ -222,6 +222,10 @@ for variant in "${variant_array[@]}"; do
         log_minor "$(basename "$f")"
         cp "$f" "$targetdir/$variant/mapping/"
     done
+    for f in "$DIR"/../app/build/outputs/native-debug-symbols/"$variant_dir"Release/native-debug-symbols.zip; do
+        log_minor "$(basename "$f")"
+        cp "$f" "$targetdir/$variant/mapping/"
+    done
     for f in "$DIR"/../app/build/outputs/sdk-dependencies/"$variant_dir"Release/sdkDependencies.txt; do
         log_minor "$(basename "$f")"
         cp "$f" "$targetdir/$variant/"