Răsfoiți Sursa

Version 4.5-beta3

Threema 5 ani în urmă
părinte
comite
e8cdc1ee23
28 a modificat fișierele cu 405 adăugiri și 384 ștergeri
  1. 4 4
      app/build.gradle
  2. 1 1
      app/src/main/java/ch/threema/app/ThreemaApplication.java
  3. 38 20
      app/src/main/java/ch/threema/app/activities/RecipientListBaseActivity.java
  4. 9 5
      app/src/main/java/ch/threema/app/activities/SendMediaActivity.java
  5. 0 1
      app/src/main/java/ch/threema/app/activities/ThreemaActivity.java
  6. 1 1
      app/src/main/java/ch/threema/app/emojis/EmojiPicker.java
  7. 4 0
      app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java
  8. 6 2
      app/src/main/java/ch/threema/app/mediaattacher/MediaAttachViewModel.java
  9. 22 9
      app/src/main/java/ch/threema/app/mediaattacher/MediaRepository.java
  10. 53 0
      app/src/main/java/ch/threema/app/mediaattacher/data/FailedMediaItemEntity.java
  11. 44 0
      app/src/main/java/ch/threema/app/mediaattacher/data/FailedMediaItemsDAO.java
  12. 14 2
      app/src/main/java/ch/threema/app/mediaattacher/data/MediaItemsRoomDatabase.java
  13. 55 27
      app/src/main/java/ch/threema/app/mediaattacher/labeling/ImageLabelingWorker.java
  14. 50 0
      app/src/main/java/ch/threema/app/preference/SettingsDeveloperFragment.java
  15. 1 1
      app/src/main/java/ch/threema/app/services/FileServiceImpl.java
  16. 0 9
      app/src/main/java/ch/threema/app/services/MessageService.java
  17. 38 263
      app/src/main/java/ch/threema/app/services/MessageServiceImpl.java
  18. 1 0
      app/src/main/java/ch/threema/app/services/NotificationServiceImpl.java
  19. 3 3
      app/src/main/java/ch/threema/app/utils/FileUtil.java
  20. 1 0
      app/src/main/java/ch/threema/app/utils/MimeUtil.java
  21. 4 3
      app/src/main/java/ch/threema/app/webclient/services/instance/message/receiver/BlobRequestHandler.java
  22. 1 1
      app/src/main/java/ch/threema/client/Utils.java
  23. 27 27
      app/src/main/res/values-de/image_labels_strings.xml
  24. 6 1
      app/src/main/res/values-de/strings.xml
  25. 2 0
      app/src/main/res/values/preferences_strings.xml
  26. 6 4
      app/src/main/res/values/strings.xml
  27. 8 0
      app/src/main/res/xml/preference_developers.xml
  28. 6 0
      app/src/main/res/xml/preference_media.xml

+ 4 - 4
app/build.gradle

@@ -75,8 +75,8 @@ android {
         vectorDrawables.useSupportLibrary = true
         applicationId "ch.threema.app"
         testApplicationId 'ch.threema.app.test'
-        versionCode 656
-        versionName "4.5-beta2"
+        versionCode 657
+        versionName "4.5-beta3"
         resValue "string", "version_name_suffix", ""
         resValue "string", "app_name", "Threema"
         resValue "string", "uri_scheme", "threema"
@@ -141,7 +141,7 @@ android {
         }
         store_threema { }
         store_google_work {
-            versionName "4.5k-beta2"
+            versionName "4.5k-beta3"
             applicationId "ch.threema.app.work"
             testApplicationId 'ch.threema.app.work.test'
             resValue "string", "package_name", applicationId
@@ -178,7 +178,7 @@ android {
             buildConfigField "byte[]", "SERVER_PUBKEY_ALT", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }"
         }
         sandbox_work {
-            versionName "4.5k-beta2"
+            versionName "4.5k-beta3"
             applicationId "ch.threema.app.sandbox.work"
             testApplicationId 'ch.threema.app.sandbox.work.test'
 

+ 1 - 1
app/src/main/java/ch/threema/app/ThreemaApplication.java

@@ -950,7 +950,7 @@ public class ThreemaApplication extends MultiDexApplication implements DefaultLi
 			if (ConfigUtils.isPlayServicesInstalled(getAppContext())) {
 				cleanupOldLabelDatabase();
 
-				if (preferenceStore.getBoolean(getAppContext().getString(R.string.preferences__image_attach_previews))) {
+				if (preferenceStore.getBoolean(getAppContext().getString(R.string.preferences__image_labeling))) {
 					scheduleImageLabelingWork();
 				}
 			}

+ 38 - 20
app/src/main/java/ch/threema/app/activities/RecipientListBaseActivity.java

@@ -29,12 +29,14 @@ import android.content.ClipData;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.database.Cursor;
 import android.location.Location;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.BadParcelableException;
 import android.os.Bundle;
 import android.os.Parcelable;
+import android.provider.DocumentsContract;
 import android.util.SparseArray;
 import android.view.Menu;
 import android.view.MenuItem;
@@ -410,7 +412,7 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 								if (!(textIntent.contains("---") && textIntent.contains("WhatsApp"))) {
 									// whatsapp forwards media as rfc822 with a footer
 									// strip this footer
-									addFileToSend(MimeUtil.MIME_TYPE_TEXT, Uri.fromParts("text", textIntent, null));
+									mediaItems.add(new MediaItem(uri, MimeUtil.MIME_TYPE_TEXT, textIntent));
 								}
 							}
 						}
@@ -427,21 +429,41 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 
 							if (uri != null) {
 								// send text as file
-								addFileToSend("x-text/plain", uri);
+								addMediaItem("x-text/plain", uri);
 								if (textIntent != null) {
 									captionText = textIntent;
 								}
 							} else if (textIntent != null) {
-								addFileToSend(MimeUtil.MIME_TYPE_TEXT, Uri.fromParts("text", textIntent, null));
+								mediaItems.add(new MediaItem(uri, MimeUtil.MIME_TYPE_TEXT, textIntent));
 							}
 						} else {
-							// any other mime type
-							if (isForwardAsFile) {
-								// TODO
-								// force forwarding of jpegs as a file if requested
-								type = "x-threema/file";
+							if (uri != null) {
+								if ("content".equalsIgnoreCase(uri.getScheme())) {
+									// query database for correct mime type as ACTION_SEND may have been called with a generic mime type such as "image/*"
+									String[] proj = {
+										DocumentsContract.Document.COLUMN_MIME_TYPE
+									};
+
+									try (Cursor cursor = getContentResolver().query(uri, proj, null, null, null)) {
+										if (cursor != null && cursor.moveToFirst()) {
+											String mimeType = cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE));
+											if (!TestUtil.empty(mimeType)) {
+												type = mimeType;
+											}
+										}
+									} catch (Exception e) {
+										logger.error("Unable to query content provider", e);
+									}
+								}
+
+								// any other mime type
+								if (isForwardAsFile) {
+									// TODO
+									// force forwarding of jpegs as a file if requested
+									type = "x-threema/file";
+								}
+								addMediaItem(type, uri);
 							}
-							addFileToSend(type, uri);
 						}
 					} else {
 						// try ClipData
@@ -452,9 +474,9 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 								CharSequence text = clipData.getItemAt(i).getText();
 
 								if (uri1 != null) {
-									addFileToSend(type, uri1);
+									addMediaItem(type, uri1);
 								} else if (!TestUtil.empty(text)) {
-									addFileToSend(MimeUtil.MIME_TYPE_TEXT, Uri.fromParts("text", text.toString(), null));
+									mediaItems.add(new MediaItem(uri, MimeUtil.MIME_TYPE_TEXT, text.toString()));
 								}
 							}
 						}
@@ -485,7 +507,7 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 
 					// skip user selection if recipient is already known
 					if (uri != null && "smsto".equals(uri.getScheme())) {
-						addFileToSend(MimeUtil.MIME_TYPE_TEXT, Uri.fromParts("text", intent.getStringExtra("sms_body"), null));
+						mediaItems.add(new MediaItem(uri, MimeUtil.MIME_TYPE_TEXT, intent.getStringExtra("sms_body")));
 
 						final ContactModel contactModel = ContactLookupUtil.phoneNumberToContact(this, contactService, uri.getSchemeSpecificPart());
 
@@ -519,7 +541,7 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 							{
 								String text = dataUri.getQueryParameter("text");
 								if (!TestUtil.empty(text)) {
-									addFileToSend(MimeUtil.MIME_TYPE_TEXT, Uri.fromParts("text", text, null));
+									mediaItems.add(new MediaItem(dataUri, MimeUtil.MIME_TYPE_TEXT, text));
 								}
 
 								String targetIdentity;
@@ -549,7 +571,7 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 								if (mimeType == null) {
 									mimeType = type;
 								}
-								addFileToSend(mimeType, uri);
+								addMediaItem(mimeType, uri);
 							}
 						}
 
@@ -585,11 +607,7 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 		setupUI();
 	}
 
-	private void addFileToSend(String mimeType, Uri uri) {
-		if (uri == null) {
-			return;
-		}
-
+	private void addMediaItem(String mimeType, @NonNull Uri uri) {
 		if ("file".equals(uri.getScheme())) {
 			String path = uri.getPath();
 			File applicationDir = new File(getApplicationInfo().dataDir);
@@ -732,7 +750,7 @@ public class RecipientListBaseActivity extends ThreemaToolbarActivity implements
 
 	private void sendSharedMedia(final MessageReceiver[] messageReceivers, final Intent intent) {
 		if (messageReceivers.length == 1 && mediaItems.size() == 1 && MimeUtil.isTextFile(mediaItems.get(0).getMimeType())) {
-			intent.putExtra(ThreemaApplication.INTENT_DATA_TEXT, mediaItems.get(0).getUri().getSchemeSpecificPart());
+			intent.putExtra(ThreemaApplication.INTENT_DATA_TEXT, mediaItems.get(0).getCaption());
 			startComposeActivity(intent);
 		} else if (messageReceivers.length > 1 || mediaItems.size() > 0) {
 			new Thread(() -> messageService.sendMedia(mediaItems, Arrays.asList(messageReceivers))).start();

+ 9 - 5
app/src/main/java/ch/threema/app/activities/SendMediaActivity.java

@@ -1002,7 +1002,7 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
 							if (videoFile.exists() && videoFile.length() > 0) {
 								final Uri videoUri = Uri.fromFile(videoFile);
 								if (videoUri != null) {
-									final int position = addItem(MediaItem.TYPE_VIDEO_CAM, videoUri, 0, "");
+									final int position = addItemFromCamera(MediaItem.TYPE_VIDEO_CAM, videoUri, 0);
 									showBigImage(position);
 									break;
 								}
@@ -1017,7 +1017,6 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
 									exifRotation = (int) BitmapUtil.rotationForImage(this, cameraUri).getRotation();
 									logger.debug("*** ExifRotation: " + exifRotation);
 								} else {
-									// TODO
 									if (bigImageView != null) {
 										bigImageView.setVisibility(View.GONE);
 									}
@@ -1026,7 +1025,7 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
 									}
 								}
 
-								final int position = addItem(MediaItem.TYPE_IMAGE_CAM, cameraUri, exifRotation, "");
+								final int position = addItemFromCamera(MediaItem.TYPE_IMAGE_CAM, cameraUri, exifRotation);
 								showBigImage(position);
 
 								break;
@@ -1087,7 +1086,7 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
 	}
 
 	@UiThread
-	private int addItem(int type, Uri imageUri, int imageRotation, String imageCaption) {
+	private int addItemFromCamera(int type, Uri imageUri, int imageRotation) {
 		if (mediaItems.size() >= MAX_SELECTABLE_IMAGES) {
 			Snackbar.make((View) gridView.getParent(), String.format(getString(R.string.max_images_reached), MAX_SELECTABLE_IMAGES), Snackbar.LENGTH_LONG).show();
 		}
@@ -1095,7 +1094,12 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
 		MediaItem item = new MediaItem(imageUri, type);
 		item.setRotation(imageRotation);
 		item.setExifRotation(imageRotation);
-		item.setCaption(imageCaption);
+
+		if (type == MediaItem.TYPE_VIDEO_CAM) {
+			item.setMimeType(MimeUtil.MIME_TYPE_VIDEO_MP4);
+		} else {
+			item.setMimeType(MimeUtil.MIME_TYPE_IMAGE_JPG);
+		}
 
 		if (sendMediaGridAdapter != null) {
 			sendMediaGridAdapter.add(item);

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

@@ -45,7 +45,6 @@ public abstract class ThreemaActivity extends ThreemaAppCompatActivity {
 	final static public int ACTIVITY_ID_VERIFY_MOBILE = 20005;
 	final static public int ACTIVITY_ID_CONTACT_DETAIL = 20007;
 	final static public int ACTIVITY_ID_UNLOCK_MASTER_KEY = 20008;
-	final static public int ACTIVITY_ID_PICK_IMAGE = 20009;
 	final static public int ACTIVITY_ID_PICK_CAMERA = 20011;
 	final static public int ACTIVITY_ID_PICK_CAMERA_INTERNAL = 20012;
 	final static public int ACTIVITY_ID_SET_PASSPHRASE = 20013;

+ 1 - 1
app/src/main/java/ch/threema/app/emojis/EmojiPicker.java

@@ -144,7 +144,7 @@ public class EmojiPicker extends LinearLayout {
 		final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height);
 		setLayoutParams(params);
 
-		logger.debug("Show EmojiPicker. Height = " + height);
+		logger.info("Show EmojiPicker. Height = " + height);
 
 		setVisibility(VISIBLE);
 

+ 4 - 0
app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java

@@ -924,7 +924,9 @@ public class ComposeMessageFragment extends Fragment implements
 			this.emojiButton.setOnClickListener(new View.OnClickListener() {
 				@Override
 				public void onClick(View v) {
+					logger.info("Emoji button clicked");
 					if (activity.isSoftKeyboardOpen()) {
+						logger.info("Show emoji picker after keyboard close");
 						activity.runOnSoftKeyboardClose(new Runnable() {
 							@Override
 							public void run() {
@@ -943,6 +945,7 @@ public class ComposeMessageFragment extends Fragment implements
 					} else {
 						if (emojiPicker != null) {
 							if (emojiPicker.isShown()) {
+								logger.info("EmojPicker currently shown. Closing.");
 								if (ConfigUtils.isLandscape(activity) &&
 									!ConfigUtils.isTabletLayout() &&
 									preferenceService.isFullscreenIme()) {
@@ -954,6 +957,7 @@ public class ComposeMessageFragment extends Fragment implements
 									}
 								}
 							} else {
+								logger.info("Show emoji picker immediately");
 								emojiPicker.show(activity.loadStoredSoftKeyboardHeight());
 							}
 						}

+ 6 - 2
app/src/main/java/ch/threema/app/mediaattacher/MediaAttachViewModel.java

@@ -57,6 +57,7 @@ import ch.threema.app.R;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.app.collections.Functional;
 import ch.threema.app.collections.IPredicateNonNull;
+import ch.threema.app.mediaattacher.data.FailedMediaItemsDAO;
 import ch.threema.app.mediaattacher.data.ImageLabelListConverter;
 import ch.threema.app.mediaattacher.data.MediaItemsRoomDatabase;
 import ch.threema.app.mediaattacher.data.PersistentMediaItemsDAO;
@@ -120,7 +121,7 @@ public class MediaAttachViewModel extends AndroidViewModel {
 				// Check whether search can be shown
 				if (ConfigUtils.isPlayServicesInstalled(this.application) &&
 					sharedPreferences != null &&
-					sharedPreferences.getBoolean(application.getString(R.string.preferences__image_attach_previews), false)) {
+					sharedPreferences.getBoolean(application.getString(R.string.preferences__image_labeling), false)) {
 					this.checkLabelingComplete();
 				}
 			});
@@ -161,8 +162,10 @@ public class MediaAttachViewModel extends AndroidViewModel {
 		new Thread(() -> {
 			// Open database
 			final PersistentMediaItemsDAO mediaItemsDAO;
+			final FailedMediaItemsDAO failedMediaItemsDAO;
 			try {
 				mediaItemsDAO = MediaItemsRoomDatabase.getDatabase(application).mediaItemsDAO();
+				failedMediaItemsDAO = MediaItemsRoomDatabase.getDatabase(application).failedMediaItemsDAO();
 			} catch (MasterKeyLockedException e) {
 				logger.error("Could not access database", e);
 				return;
@@ -173,11 +176,12 @@ public class MediaAttachViewModel extends AndroidViewModel {
 
 			// Get label count from database
 			final int labeledMediaCount = mediaItemsDAO.getRowCount();
+			final int failedMediaCount = failedMediaItemsDAO.getRowCount();
 
 			// Get the media count (by this time, it should be ready because
 			// this method is called after `initialLoadDone` fires)
 			final List<MediaAttachItem> allMediaValue = Objects.requireNonNull(this.allMedia.getValue());
-			final int totalMediaSize = Functional.filter(allMediaValue, (IPredicateNonNull<MediaAttachItem>) ImageLabelingWorker::mediaCanBeLabeled).size();
+			final int totalMediaSize = Functional.filter(allMediaValue, (IPredicateNonNull<MediaAttachItem>) ImageLabelingWorker::mediaCanBeLabeled).size() - failedMediaCount;
 
 			final float labeledRatio = (float) labeledMediaCount / (float) totalMediaSize;
 			if (labeledRatio > 0.9) {

+ 22 - 9
app/src/main/java/ch/threema/app/mediaattacher/MediaRepository.java

@@ -64,15 +64,29 @@ public class MediaRepository {
 		final String[] imageProjection = this.getImageProjection();
 		final String[] videoProjection = this.getVideoProjection();
 
-		final Cursor imageCursor = appContext.getContentResolver()
-			.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, imageProjection, null, null, MediaStore.Images.Media.DATE_MODIFIED + " DESC");
-		final Cursor videoCursor = appContext.getContentResolver()
-			.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, videoProjection, null, null, MediaStore.Video.Media.DATE_MODIFIED + " DESC");
-
 		final List<MediaAttachItem> mediaList = new ArrayList<>();
 
-		addToMediaResults(imageCursor, mediaList,  false);
-		addToMediaResults(videoCursor, mediaList, true);
+		// Process images
+		try (Cursor imageCursor = appContext.getContentResolver().query(
+			MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+			imageProjection,
+			null,
+			null,
+			MediaStore.Images.Media.DATE_MODIFIED + " DESC"
+		)) {
+			addToMediaResults(imageCursor, mediaList, false);
+		}
+
+		// Process videos
+		try (Cursor videoCursor = appContext.getContentResolver().query(
+			MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+			videoProjection,
+			null,
+			null,
+			MediaStore.Video.Media.DATE_MODIFIED + " DESC"
+		)) {
+			addToMediaResults(videoCursor, mediaList, true);
+		}
 
 		// Sort media list from most recent descending
 		Collections.sort(mediaList, (o1, o2) -> Double.compare(o2.getDateModified(), o1.getDateModified()));
@@ -112,7 +126,7 @@ public class MediaRepository {
 
 	/**
 	 * Consume the cursor and add the entries to the provided media list.
-	 * After this method was called, the cursor is closed and should not be re-used.
+	 * The cursor will not be closed, make sure to run this method inside a try-with-resources block!
 	 */
 	@SuppressLint("NewApi")
 	@WorkerThread
@@ -156,7 +170,6 @@ public class MediaRepository {
 					mediaList.add(item);
 //				}
 			}
-			cursor.close();
 		}
 	}
 }

+ 53 - 0
app/src/main/java/ch/threema/app/mediaattacher/data/FailedMediaItemEntity.java

@@ -0,0 +1,53 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2020 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.mediaattacher.data;
+
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+@Entity(tableName = "failed_media_items")
+public class FailedMediaItemEntity {
+	@PrimaryKey
+	public int id;
+	private long timestamp;
+
+	public FailedMediaItemEntity(int id, long timestamp) {
+		this.id = id;
+		this.timestamp = timestamp;
+	}
+
+	public int getId() {
+		return id;
+	}
+
+	public void setId(int id) {
+		this.id = id;
+	}
+
+	public long getTimestamp() {
+		return timestamp;
+	}
+
+	public void setTimestamp(long timestamp) {
+		this.timestamp = timestamp;
+	}
+}

+ 44 - 0
app/src/main/java/ch/threema/app/mediaattacher/data/FailedMediaItemsDAO.java

@@ -0,0 +1,44 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2020 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.mediaattacher.data;
+
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+
+@Dao
+public interface FailedMediaItemsDAO {
+
+	@Insert(onConflict = OnConflictStrategy.REPLACE)
+	void insert(FailedMediaItemEntity mediaItem);
+
+	@Delete
+	void delete(FailedMediaItemEntity mediaItem);
+
+	@Query("SELECT * FROM failed_media_items WHERE id = :id LIMIT 1")
+	FailedMediaItemEntity get(int id);
+
+	@Query("SELECT COUNT(*) FROM failed_media_items")
+	int getRowCount();
+}

+ 14 - 2
app/src/main/java/ch/threema/app/mediaattacher/data/MediaItemsRoomDatabase.java

@@ -34,12 +34,14 @@ import androidx.room.Database;
 import androidx.room.Room;
 import androidx.room.RoomDatabase;
 import androidx.room.TypeConverters;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
 import ch.threema.app.ThreemaApplication;
 import ch.threema.localcrypto.MasterKeyLockedException;
 
 @Database(
-	entities = {PersistentMediaItem.class},
-	version = 1,
+	entities = {PersistentMediaItem.class, FailedMediaItemEntity.class},
+	version = 2,
 	exportSchema = false
 )
 @TypeConverters({ImageLabelListConverter.class})
@@ -49,9 +51,18 @@ public abstract class MediaItemsRoomDatabase extends RoomDatabase {
 	public static final String DATABASE_NAME = "media_items.db";
 
 	public abstract PersistentMediaItemsDAO mediaItemsDAO();
+	public abstract FailedMediaItemsDAO failedMediaItemsDAO();
 
 	private static volatile MediaItemsRoomDatabase db;
 
+	static final Migration MIGRATION_1_2 = new Migration(1, 2) {
+		@Override
+		public void migrate(SupportSQLiteDatabase database) {
+			database.execSQL("CREATE TABLE `failed_media_items` (`id` INTEGER NOT NULL, "
+				+ "`timestamp` INTEGER NOT NULL, PRIMARY KEY(`id`))");
+		}
+	};
+
 	public static MediaItemsRoomDatabase getDatabase(final Context context) throws MasterKeyLockedException, SQLiteException {
 		if (db == null) {
 			synchronized (MediaItemsRoomDatabase.class) {
@@ -65,6 +76,7 @@ public abstract class MediaItemsRoomDatabase extends RoomDatabase {
 					}
 					db = Room
 						.databaseBuilder(context.getApplicationContext(), MediaItemsRoomDatabase.class, DATABASE_NAME)
+						.addMigrations(MIGRATION_1_2)
 						.openHelperFactory(factory)
 						.build();
 				}

+ 55 - 27
app/src/main/java/ch/threema/app/mediaattacher/labeling/ImageLabelingWorker.java

@@ -25,6 +25,7 @@ import android.Manifest;
 import android.app.Notification;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.net.Uri;
 import android.os.SystemClock;
 
 import com.google.android.gms.tasks.Task;
@@ -40,6 +41,7 @@ import net.sqlcipher.database.SQLiteException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -58,12 +60,15 @@ import ch.threema.app.ThreemaApplication;
 import ch.threema.app.managers.ServiceManager;
 import ch.threema.app.mediaattacher.MediaAttachItem;
 import ch.threema.app.mediaattacher.MediaRepository;
+import ch.threema.app.mediaattacher.data.FailedMediaItemEntity;
+import ch.threema.app.mediaattacher.data.FailedMediaItemsDAO;
 import ch.threema.app.mediaattacher.data.MediaItemsRoomDatabase;
 import ch.threema.app.mediaattacher.data.PersistentMediaItem;
 import ch.threema.app.mediaattacher.data.PersistentMediaItemsDAO;
 import ch.threema.app.services.NotificationService;
 import ch.threema.app.ui.MediaItem;
 import ch.threema.app.utils.RandomUtil;
+import ch.threema.client.Utils;
 import ch.threema.localcrypto.MasterKeyLockedException;
 import ch.threema.logging.ThreemaLogger;
 
@@ -77,14 +82,15 @@ public class ImageLabelingWorker extends Worker {
 	// Non-static logger (so that a prefix can be set)
 	private final Logger logger = LoggerFactory.getLogger(ImageLabelingWorker.class);
 
-	// Threema services
-	private final NotificationService notificationService;
+	// Context
+	private final Context appContext;
 
 	// Media on device
 	private final MediaRepository repository;
 
 	// Database
 	private final PersistentMediaItemsDAO mediaItemsDAO;
+	private final FailedMediaItemsDAO failedMediaDAO;
 
 	// Image labeling
 	private final ImageLabeler labeler;
@@ -116,22 +122,13 @@ public class ImageLabelingWorker extends Worker {
 			((ThreemaLogger)logger).setPrefix("id=" + RandomUtil.generateInsecureRandomAsciiString(6));
 		}
 
-		final ServiceManager serviceManager = ThreemaApplication.getServiceManager();
-		if (serviceManager == null) {
-			logger.error("Could not get service manager");
-			onStopped();
-			throw new IllegalStateException("Could not get service manager");
-		}
-		this.notificationService = serviceManager.getNotificationService();
-		if (this.notificationService == null) {
-			logger.error("Could not get notification service");
-			onStopped();
-			throw new IllegalStateException("Could not get notification service");
-		}
+		this.appContext = getApplicationContext();
 
 		// Get database reference
 		try {
-			this.mediaItemsDAO = MediaItemsRoomDatabase.getDatabase(ThreemaApplication.getAppContext()).mediaItemsDAO();
+			this.mediaItemsDAO = MediaItemsRoomDatabase.getDatabase(this.appContext).mediaItemsDAO();
+			this.failedMediaDAO = MediaItemsRoomDatabase.getDatabase(this.appContext).failedMediaItemsDAO();
+
 		} catch (MasterKeyLockedException e) {
 			logger.error("Could not get media items database, master key locked", e);
 			onStopped();
@@ -143,7 +140,7 @@ public class ImageLabelingWorker extends Worker {
 		}
 
 		// Initialize media repository
-		this.repository = new MediaRepository(appContext);
+		this.repository = new MediaRepository(this.appContext);
 
 		// Create labeler
 		final ImageLabelerOptions options = new ImageLabelerOptions.Builder()
@@ -189,9 +186,9 @@ public class ImageLabelingWorker extends Worker {
 	@NonNull
 	@WorkerThread
 	public Result doWork() {
-		if (!(ContextCompat.checkSelfPermission(ThreemaApplication.getAppContext(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)) {
+		if (!(ContextCompat.checkSelfPermission(this.appContext, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)) {
 			// we do not currently have permission to read storage - get out of here
-			logger.info("Unable to label images. Permission denied.");
+			logger.warn("Unable to label images. Permission denied.");
 			this.cancelled = false;
 			this.onFinish();
 
@@ -199,14 +196,31 @@ public class ImageLabelingWorker extends Worker {
 			return Result.success();
 		}
 
+		final ServiceManager serviceManager = ThreemaApplication.getServiceManager();
+		if (serviceManager == null) {
+			logger.error("Could not get service manager");
+			this.cancelled = false;
+			this.onFinish();
+			// This might be due to the masterkey, maybe it'll be unlocked later
+			return Result.retry();
+		}
+		final NotificationService notificationService = serviceManager.getNotificationService();
+		if (notificationService == null) {
+			logger.error("Could not get notification service");
+			this.cancelled = false;
+			this.onFinish();
+			return Result.failure();
+		}
+
 		// Synchronize on a global static lock, because a cancelled task may still
 		// be running when a replacing task starts.
+		logger.info("Waiting for lock");
 		synchronized (globalLock) {
-			logger.info("Starting...");
+			logger.info("Starting");
 			long startTime = SystemClock.elapsedRealtime();
 
 			// Make this a foreground service with a progress notification
-			final Notification notification = this.notificationService.createImageLabelingProgressNotification().build();
+			final Notification notification = notificationService.createImageLabelingProgressNotification().build();
 			if (notification == null) {
 				logger.error("Could not create notification");
 				return Result.failure();
@@ -220,11 +234,12 @@ public class ImageLabelingWorker extends Worker {
 			final List<MediaAttachItem> allMediaCache = repository.getMediaFromMediaStore();
 			this.mediaCount = allMediaCache.size();
 			logger.info("Found {} media items", this.mediaCount);
-			this.notificationService.updateImageLabelingProgressNotification(0, this.mediaCount);
+			notificationService.updateImageLabelingProgressNotification(0, this.mediaCount);
 
 			// Label images without labels
 			int imageCounter = 0;
 			int unlabeledCounter = 0;
+			int skippedCounter = 0;
 			for (MediaAttachItem mediaItem : allMediaCache) {
 				// Check whether we were stopped
 				if (this.isStopped()) {
@@ -233,13 +248,18 @@ public class ImageLabelingWorker extends Worker {
 				}
 
 				// Update notification
-				this.notificationService.updateImageLabelingProgressNotification(this.progress, this.mediaCount);
+				notificationService.updateImageLabelingProgressNotification(this.progress, this.mediaCount);
 
 				// Update progress
 				this.progress++;
 
 				// We're only interested in image media
 				if (mediaCanBeLabeled(mediaItem)) {
+					if (failedMediaDAO.get(mediaItem.getId()) != null) {
+						logger.info("Item " + mediaItem.getId() + " failed to load previously. Skipping");
+						skippedCounter++;
+						continue;
+					}
 					imageCounter++;
 				} else {
 					continue;
@@ -253,9 +273,17 @@ public class ImageLabelingWorker extends Worker {
 					// Load image from filesystem
 					InputImage image;
 					try {
-						image = InputImage.fromFilePath(ThreemaApplication.getAppContext(), mediaItem.getUri());
+						final Uri uri = mediaItem.getUri();
+						// TODO(ANDR-1318): Make this logging less verbose!
+						final String hashedFilename = Utils.byteArrayToSha256HexString(mediaItem.getDisplayName().getBytes(StandardCharsets.UTF_8));
+						logger.info("Loading image {}/{} ({})", this.progress, this.mediaCount, hashedFilename.substring(0, 8));
+						image = InputImage.fromFilePath(this.appContext, uri);
+						logger.info("Loaded image {}/{} ({})", this.progress, this.mediaCount, hashedFilename.substring(0, 8));
 					} catch (Exception e) {
 						logger.warn("Exception, could not generate input image from file path: {}", e.getMessage());
+						logger.info("Unable to load Item " + mediaItem.getId() + ". Adding to list of failed items");
+
+						failedMediaDAO.insert(new FailedMediaItemEntity(mediaItem.getId(), System.currentTimeMillis()));
 						if (e.getCause() != null) {
 							logger.warn("  Caused by: {}", e.getCause().getMessage());
 						}
@@ -298,18 +326,18 @@ public class ImageLabelingWorker extends Worker {
 			}
 
 			// Update notification
-			this.notificationService.updateImageLabelingProgressNotification(this.progress, this.mediaCount);
+			notificationService.updateImageLabelingProgressNotification(this.progress, this.mediaCount);
 
 			final long secondsElapsedLabeling = (SystemClock.elapsedRealtime() - startTime) / 1000;
 			if (this.isStopped()) {
 				logger.info("Aborting now after {}s, because work was cancelled", secondsElapsedLabeling);
 				return Result.failure();
 			} else {
-				logger.info("Processed {} unlabeled images among {} total images", unlabeledCounter, imageCounter);
+				logger.info("Processed {} unlabeled images among {} total and {} skipped images", unlabeledCounter, imageCounter, skippedCounter);
 				logger.info("Labeling work done after {}s, starting cleanup", secondsElapsedLabeling);
 			}
 
-			// Delete labels from database that belong to nonexisting media items
+			// Delete labels from database that belong to nonexistent media items
 			List<PersistentMediaItem> currentlyStoredLabels = mediaItemsDAO.getAllItemsByAscIdOrder();
 			if (currentlyStoredLabels != null) {
 				Collections.sort(allMediaCache, (o1, o2) -> Double.compare(o1.getId(), o2.getId()));
@@ -324,7 +352,7 @@ public class ImageLabelingWorker extends Worker {
 						indexStoredMediaItemsList++;
 					} else if (storedItemIDCurrent < retrievedItemIDCurrent) {
 						// No match, discard entry in labels database
-						logger.info("deleting media labels for id {}", currentlyStoredLabels.get(indexStoredLabelsList).getId());
+						logger.info("Deleting media labels for id {}", currentlyStoredLabels.get(indexStoredLabelsList).getId());
 						mediaItemsDAO.deleteMediaItemById(currentlyStoredLabels.get(indexStoredLabelsList).getId());
 						indexStoredLabelsList++;
 					} else {

+ 50 - 0
app/src/main/java/ch/threema/app/preference/SettingsDeveloperFragment.java

@@ -31,6 +31,7 @@ import android.widget.Toast;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.File;
 import java.util.Date;
 
 import androidx.annotation.Nullable;
@@ -42,6 +43,7 @@ import ch.threema.app.ThreemaApplication;
 import ch.threema.app.exceptions.EntryAlreadyExistsException;
 import ch.threema.app.exceptions.InvalidEntryException;
 import ch.threema.app.managers.ServiceManager;
+import ch.threema.app.mediaattacher.data.MediaItemsRoomDatabase;
 import ch.threema.app.messagereceiver.ContactMessageReceiver;
 import ch.threema.app.services.ContactService;
 import ch.threema.app.services.MessageService;
@@ -56,6 +58,8 @@ import ch.threema.storage.DatabaseServiceNew;
 import ch.threema.storage.models.ContactModel;
 import ch.threema.storage.models.data.status.VoipStatusDataModel;
 
+import static ch.threema.app.ThreemaApplication.getAppContext;
+
 public class SettingsDeveloperFragment extends ThreemaPreferenceFragment {
 	private static final Logger logger = LoggerFactory.getLogger(SettingsDeveloperFragment.class);
 
@@ -89,6 +93,10 @@ public class SettingsDeveloperFragment extends ThreemaPreferenceFragment {
 			+ TEST_IDENTITY_2 + " and add some test quotes.");
 		generateRecursiveQuote.setOnPreferenceClickListener(this::generateTestQuotes);
 
+		// Delete labels database
+		final Preference deleteLabelsDb = findPreference(getResources().getString(R.string.preferences__labels_delete));
+		deleteLabelsDb.setOnPreferenceClickListener(this::deleteMediaLabelsDatabase);
+
 		// Remove developer menu
 		final Preference removeMenuPreference = findPreference(getResources().getString(R.string.preferences__remove_menu));
 		removeMenuPreference.setSummary("Hide the developer menu from the settings.");
@@ -248,6 +256,48 @@ public class SettingsDeveloperFragment extends ThreemaPreferenceFragment {
 		return true;
 	}
 
+	@UiThread
+	@SuppressLint("StaticFieldLeak")
+	private boolean deleteMediaLabelsDatabase(Preference preference) {
+		new AsyncTask<Void, Void, Exception>() {
+			@Override
+			protected Exception doInBackground(Void... voids) {
+				try {
+					final String[] files = new String[] {
+						MediaItemsRoomDatabase.DATABASE_NAME,
+						MediaItemsRoomDatabase.DATABASE_NAME + "-shm",
+						MediaItemsRoomDatabase.DATABASE_NAME + "-wal",
+					};
+					for (String filename : files) {
+						final File databasePath = getAppContext().getDatabasePath(filename);
+						if (databasePath.exists() && databasePath.isFile()) {
+							logger.info("Removing file {}", filename);
+							if (!databasePath.delete()) {
+								logger.warn("Could not remove file {}", filename);
+							}
+						} else {
+							logger.debug("File {} not found", filename);
+						}
+					}
+				} catch (Exception e) {
+					logger.error("Exception while deleting media labels database");
+					return e;
+				}
+				return null;
+			}
+
+			@Override
+			protected void onPostExecute(Exception e) {
+				if (e == null) {
+					showOk("Database deleted");
+				} else {
+					showError(e);
+				}
+			}
+		}.execute();
+		return true;
+	}
+
 	@UiThread
 	@SuppressLint("StaticFieldLeak")
 	private boolean hideDeveloperMenu(Preference preference) {

+ 1 - 1
app/src/main/java/ch/threema/app/services/FileServiceImpl.java

@@ -120,7 +120,7 @@ public class FileServiceImpl implements FileService {
 
 	private final static String JPEG_EXTENSION = ".jpg";
 	private final static String MPEG_EXTENSION = ".mp4";
-	private final static String VOICEMESSAGE_EXTENSION = ".m4a";
+	public final static String VOICEMESSAGE_EXTENSION = ".aac";
 	private final static String THUMBNAIL_EXTENSION = "_T";
 	private final static String WALLPAPER_FILENAME = "/wallpaper" + JPEG_EXTENSION;
 

+ 0 - 9
app/src/main/java/ch/threema/app/services/MessageService.java

@@ -22,12 +22,10 @@
 package ch.threema.app.services;
 
 import android.content.Context;
-import android.graphics.Bitmap;
 import android.location.Location;
 import android.net.Uri;
 
 import java.io.IOException;
-import java.io.InputStream;
 import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.Date;
@@ -54,7 +52,6 @@ import ch.threema.storage.models.MessageType;
 import ch.threema.storage.models.ServerMessageModel;
 import ch.threema.storage.models.ballot.BallotModel;
 import ch.threema.storage.models.data.MessageContentsType;
-import ch.threema.storage.models.data.media.FileDataModel;
 import ch.threema.storage.models.data.status.VoipStatusDataModel;
 
 /**
@@ -126,12 +123,6 @@ public interface MessageService {
 
 	String getCorrelationId();
 
-	AbstractMessageModel sendFile(final InputStream fileStream,
-	                                     Bitmap thumbnail,
-	                                     FileDataModel fileDataModel,
-	                                     String correlationId,
-	                                     MessageReceiver receiver,
-	                                     CompletionHandler completionHandler) throws Exception;
 	@WorkerThread
 	AbstractMessageModel sendMedia(@NonNull List<MediaItem> mediaItems, @NonNull List<MessageReceiver> messageReceivers);
 	@WorkerThread

+ 38 - 263
app/src/main/java/ch/threema/app/services/MessageServiceImpl.java

@@ -50,7 +50,6 @@ import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -624,41 +623,6 @@ public class MessageServiceImpl implements MessageService {
 		}
 	}
 
-	@Override
-	@Deprecated
-	public AbstractMessageModel sendFile(
-		final @NonNull InputStream fileStream,
-	    @Nullable Bitmap thumbnail,
-	    @NonNull FileDataModel fileDataModel,
-	    String correlationId,
-	    @NonNull MessageReceiver receiver,
-	    @Nullable CompletionHandler completionHandler
-	) throws Exception {
-		if (fileStream != null && fileStream.available() > 0) {
-			int fileStreamLength = fileStream.available();
-
-			if (ConfigUtils.checkAvailableMemory(fileStreamLength + NaCl.BOXOVERHEAD)) {
-				try {
-					byte[] fileData = new byte[fileStreamLength + NaCl.BOXOVERHEAD];
-					IOUtils.readFully(fileStream, fileData, NaCl.BOXOVERHEAD, fileStreamLength);
-					return this.sendFileMessage(fileData,
-						fileDataModel,
-						thumbnail,
-						fileStreamLength,
-						correlationId,
-						receiver,
-						completionHandler);
-				} catch (OutOfMemoryError e) {
-					throw new ThreemaException(context.getString(R.string.error_out_of_memory));
-				}
-			} else {
-				throw new ThreemaException(context.getString(R.string.error_out_of_memory));
-			}
-		} else {
-			throw new FileNotFoundException(context.getString(R.string.cannot_open_file));
-		}
-	}
-
 	private void resendFileMessage(final AbstractMessageModel messageModel,
 								  final MessageReceiver receiver,
 								  final CompletionHandler completionHandler) throws Exception {
@@ -809,213 +773,6 @@ public class MessageServiceImpl implements MessageService {
 		});
 	}
 
-	private AbstractMessageModel sendFileMessage(final byte[] fileData,
-	                                             final FileDataModel fileDataModel,
-	                                             final Bitmap thumbnail,
-	                                             final long fileSize,
-	                                             final String correlationId,
-	                                             final MessageReceiver receiver,
-	                                             final CompletionHandler completionHandler) throws Exception {
-		final String tag = "sendFileMessage";
-		logger.info(tag + ": start");
-
-		if (fileData != null) {
-			// For now, we default to PNG thumbnails for everything but JPGs
-			// TODO use PNG only for images that do not contain transparency
-			final String thumbnailMimeType = MimeUtil.MIME_TYPE_IMAGE_JPG.equals(fileDataModel.getMimeType()) ?
-				MimeUtil.MIME_TYPE_IMAGE_JPG :
-				MimeUtil.MIME_TYPE_IMAGE_PNG;
-
-			final AbstractMessageModel messageModel = receiver.createLocalModel(
-				MessageType.FILE,
-				MimeUtil.getContentTypeFromMimeType(fileDataModel.getMimeType()),
-				new Date()
-			);
-			this.cache(messageModel);
-
-			messageModel.setOutbox(true);
-			messageModel.setState(MessageState.PENDING);
-
-			fileDataModel.isDownloaded(true);
-			fileDataModel.setThumbnailMimeType(thumbnailMimeType);
-
-			messageModel.setFileData(fileDataModel);
-			messageModel.setCorrelationId(correlationId);
-
-			//set as saved!
-			messageModel.setSaved(true);
-
-			receiver.saveLocalModel(messageModel);
-			this.fireOnCreatedMessage(messageModel);
-
-			//enqueue processing and uploading stuff...
-			this.messageSendingService.addToQueue(new MessageSendingService.MessageSendingProcess() {
-				public byte[] blobIdThumbnail;
-				public byte[] blobId;
-				public MessageReceiver.EncryptResult thumbnailEncryptResult;
-				public MessageReceiver.EncryptResult encryptResult;
-				public boolean success = false;
-				public byte[] thumbnailData;
-				@Override
-				public MessageReceiver getReceiver() {
-					return receiver;
-				}
-
-				@Override
-				public AbstractMessageModel getMessageModel() {
-					return messageModel;
-				}
-
-				@Override
-				public boolean send() throws Exception {
-					SendMachine sendMachine = getSendMachine(messageModel);
-
-					sendMachine.reset()
-							.next(new SendMachineProcess() {
-								@Override
-								public void run() throws Exception {
-									thumbnailData = null;
-
-									if (thumbnail != null) {
-										// write thumbnail first
-										if (MimeUtil.MIME_TYPE_IMAGE_JPG.equals(fileDataModel.getThumbnailMimeType())) {
-											thumbnailData = BitmapUtil.bitmapToJpegByteArray(thumbnail);
-										} else {
-											thumbnailData = BitmapUtil.bitmapToPngByteArray(thumbnail);
-										}
-										fileService.writeConversationMediaThumbnail(messageModel, thumbnailData);
-									}
-									fileService.writeConversationMedia(messageModel, fileData, NaCl.BOXOVERHEAD, (int)fileSize);
-								}
-							})
-							.next(new SendMachineProcess() {
-								@Override
-								public void run() throws Exception {
-									if (getReceiver().sendMediaData()) {
-										// note that encryptFileData will overwrite contents of fileData
-										encryptResult = getReceiver().encryptFileData(fileData);
-
-										if (encryptResult.getData() == null || encryptResult.getSize() == 0) {
-											throw new Exception("File data encrypt failed");
-										}
-									}
-								}
-							})
-							.next(new SendMachineProcess() {
-								@Override
-								public void run() throws Exception {
-									//do not upload if sendMediaData Disabled (Distribution Lists)
-									if (getReceiver().sendMediaData()) {
-										BlobUploader blobUploader = initUploader(getMessageModel(), encryptResult.getData());
-										blobUploader.setProgressListener(new ProgressListener() {
-												@Override
-												public void updateProgress(int progress) {
-													updateMessageLoadingProgress(messageModel, progress);
-												}
-
-											@Override
-											public void onFinished(boolean success) {
-												setMessageLoadingFinished(messageModel, success);
-											}
-										});
-										blobId = blobUploader.upload();
-										logger.debug("blobId = " + Utils.byteArrayToHexString(blobId));
-									}
-								}
-							})
-							.next(new SendMachineProcess() {
-								@Override
-								public void run() throws Exception {
-									//do not upload if sendMediaData Disabled (Distribution Lists)
-									if (getReceiver().sendMediaData()) {
-										if (thumbnailData != null) {
-											thumbnailEncryptResult = getReceiver().encryptFileThumbnailData(thumbnailData, encryptResult.getKey());
-
-											if (thumbnailEncryptResult.getData() != null) {
-												BlobUploader blobUploader = initUploader(getMessageModel(), thumbnailEncryptResult.getData());
-												blobUploader.setProgressListener(new ProgressListener() {
-													@Override
-													public void updateProgress(int progress) {
-														updateMessageLoadingProgress(messageModel, progress);
-													}
-
-													@Override
-													public void onFinished(boolean success) {
-														setMessageLoadingFinished(messageModel, success);
-													}
-												});
-												blobIdThumbnail = blobUploader.upload();
-												logger.debug("blobIdThumbnail = " + Utils.byteArrayToHexString(blobIdThumbnail));
-
-												fireOnModifiedMessage(messageModel);
-											}
-											else {
-												throw new Exception("Thumbnail encrypt failed");
-											}
-										}
-									}
-								}
-							})
-							.next(new SendMachineProcess() {
-								@Override
-								public void run() throws Exception {
-									if (getReceiver().createBoxedFileMessage(
-											blobIdThumbnail,
-											blobId,
-											encryptResult,
-											messageModel
-									)) {
-										updateMessageState(messageModel,
-											getReceiver().sendMediaData() && getReceiver().offerRetry() ?
-												MessageState.SENDING :
-												MessageState.SENT, null);
-
-										//save updated model
-										save(messageModel);
-									} else {
-										throw new Exception("Failed to create box");
-									}
-								}
-							})
-							.next(new SendMachineProcess() {
-								@Override
-								public void run() throws Exception {
-									messageModel.setSaved(true);
-									// Verify current saved state
-									updateMessageState(messageModel,
-										getReceiver().sendMediaData() && getReceiver().offerRetry() ?
-											MessageState.SENDING :
-											MessageState.SENT, null);
-
-									if (!getReceiver().sendMediaData()) {
-										// update status for message that stay local
-										fireOnModifiedMessage(messageModel);
-									}
-
-									if (completionHandler != null) {
-										completionHandler.sendComplete(messageModel);
-									}
-
-									success = true;
-								}
-							});
-
-					if(this.success) {
-						removeSendMachine(sendMachine);
-					}
-					return this.success;
-				}
-			});
-
-			if (completionHandler != null)
-				completionHandler.sendQueued(messageModel);
-
-			return messageModel;
-		}
-
-		return null;
-	}
-
 	@Override
 	public AbstractMessageModel sendBallotMessage(BallotModel ballotModel) throws MessageTooLongException {
 		//create a new ballot model
@@ -3651,7 +3408,30 @@ public class MessageServiceImpl implements MessageService {
 		// resolve receivers to account for distribution lists
 		final MessageReceiver[] resolvedReceivers = MessageUtil.addDistributionListReceivers(messageReceivers.toArray(new MessageReceiver[0]));
 
+		logger.info("sendMedia: Sending " + mediaItems.size() + " items to " + resolvedReceivers.length + " receivers");
+
 		for (MediaItem mediaItem : mediaItems) {
+			if (MimeUtil.isTextFile(mediaItem.getMimeType())) {
+				String text = mediaItem.getCaption();
+				if (!TestUtil.empty(text)) {
+					for (MessageReceiver messageReceiver : resolvedReceivers) {
+						try {
+							successfulMessageModel = sendText(text, messageReceiver);
+							if (successfulMessageModel != null) {
+								logger.info("Text successfuly sent");
+							} else {
+								logger.info("Text send failed");
+							}
+						} catch (Exception e) {
+							logger.error("Could not send text message", e);
+						}
+					}
+				} else {
+					logger.info("Text is empty");
+				}
+				continue;
+			}
+
 			final Map<MessageReceiver, AbstractMessageModel> messageModels = new HashMap<>();
 
 			final FileDataModel fileDataModel = createFileDataModel(context, mediaItem);
@@ -3661,15 +3441,16 @@ public class MessageServiceImpl implements MessageService {
 			}
 
 			if (!createMessagesAndSetPending(mediaItem, resolvedReceivers, messageModels, fileDataModel)) {
-				logger.info("Unable to create messages ");
+				logger.info("Unable to create messages");
 				continue;
 			}
 
 			final byte[] thumbnailData = generateThumbnailData(mediaItem, fileDataModel);
-
-			writeThumbnails(messageModels, resolvedReceivers, thumbnailData);
-
-			updateFileDataModel(mediaItem, fileDataModel);
+			if (thumbnailData != null) {
+				writeThumbnails(messageModels, resolvedReceivers, thumbnailData);
+			} else {
+				logger.info("Unable to write thumbnails");
+			}
 
 			if (!allChatsArePrivate(resolvedReceivers)) {
 				saveToGallery(mediaItem);
@@ -3681,11 +3462,14 @@ public class MessageServiceImpl implements MessageService {
 					successfulMessageModel = messageModels.get(resolvedReceivers[0]);
 				}
 			} else {
+				logger.info("Error encrypting and sending");
 				markAsTerminallyFailed(resolvedReceivers, messageModels);
 			}
 		}
 
 		if (successfulMessageModel != null) {
+			logger.info("sendMedia: Send successful.");
+
 			sendProfilePicture(resolvedReceivers);
 
 			if (sendCompletionHandler != null) {
@@ -3693,11 +3477,13 @@ public class MessageServiceImpl implements MessageService {
 			}
 		} else {
 			final String errorString = context.getString(R.string.an_error_occurred_during_send);
+			logger.info("sendMedia: " + errorString);
 			RuntimeUtil.runOnUiThread(() -> Toast.makeText(context, errorString, Toast.LENGTH_LONG).show());
 			if (sendCompletionHandler != null) {
 				sendCompletionHandler.onError(errorString);
 			}
 		}
+
 		return successfulMessageModel;
 	}
 
@@ -3720,13 +3506,6 @@ public class MessageServiceImpl implements MessageService {
 		}
 	}
 
-	/**
-	 * Update the FileDataModel with data from the MediaItem such as file name and rendering type
-	 */
-	private void updateFileDataModel(@NonNull MediaItem mediaItem, @NonNull FileDataModel fileDataModel) {
-
-	}
-
 	/**
 	 * Generate content data for this MediaItem
 	 * @param mediaItem
@@ -3931,14 +3710,9 @@ public class MessageServiceImpl implements MessageService {
 	                       @NonNull Map<MessageReceiver, AbstractMessageModel> messageModels,
 	                       @NonNull FileDataModel fileDataModel,
 	                       @Nullable byte[] thumbnailData,
-	                       @Nullable byte[] contentData) {
-		if (contentData == null) {
-			logger.debug("No content to send");
-			return false;
-		}
-
-		final MessageReceiver.EncryptResult thumbnailEncryptResult[] = new MessageReceiver.EncryptResult[1];
-		final MessageReceiver.EncryptResult contentEncryptResult[] = new MessageReceiver.EncryptResult[1];
+	                       @NonNull byte[] contentData) {
+		final MessageReceiver.EncryptResult[] thumbnailEncryptResult = new MessageReceiver.EncryptResult[1];
+		final MessageReceiver.EncryptResult[] contentEncryptResult = new MessageReceiver.EncryptResult[1];
 
 		thumbnailEncryptResult[0] = null;
 		contentEncryptResult[0] = null;
@@ -3968,6 +3742,7 @@ public class MessageServiceImpl implements MessageService {
 			AbstractMessageModel messageModel = messageModels.get(messageReceiver);
 			if (messageModel == null) {
 				// no messagemodel has been created for this receiver - skip
+				logger.info("Mo MessageCodel could be created for this receiver - skip");
 				continue;
 			}
 

+ 1 - 0
app/src/main/java/ch/threema/app/services/NotificationServiceImpl.java

@@ -1902,6 +1902,7 @@ public class NotificationServiceImpl implements NotificationService {
 		final NotificationCompat.Builder builder = this.createImageLabelingProgressNotification();
 		if (builder != null) {
 			builder.setProgress(maxProgress, currentProgress, false);
+			builder.setContentText(currentProgress + "/" + maxProgress);
 			this.notificationManager.notify(ThreemaApplication.IMAGE_LABELING_NOTIFICATION_ID, builder.build());
 		}
 	}

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

@@ -64,6 +64,7 @@ import ch.threema.app.ThreemaApplication;
 import ch.threema.app.camera.CameraActivity;
 import ch.threema.app.filepicker.FilePickerActivity;
 import ch.threema.app.services.FileService;
+import ch.threema.app.services.FileServiceImpl;
 import ch.threema.storage.models.AbstractMessageModel;
 import ch.threema.storage.models.data.media.FileDataModel;
 
@@ -546,9 +547,8 @@ public class FileUtil {
 			mimeType = MimeUtil.MIME_TYPE_DEFAULT;
 		}
 
-		return getMediaFilenamePrefix() +
-			"." +
-			MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+		String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+		return getMediaFilenamePrefix() + "." + extension;
 	}
 
 	public static String sanitizeFileName(String filename) {

+ 1 - 0
app/src/main/java/ch/threema/app/utils/MimeUtil.java

@@ -53,6 +53,7 @@ public class MimeUtil {
 	public static final String MIME_TYPE_IMAGE_HEIC = "image/heic";
 	public static final String MIME_TYPE_IMAGE_TIFF = "image/tiff";
 	public static final String MIME_TYPE_VIDEO_MPEG = "video/mpeg";
+	public static final String MIME_TYPE_VIDEO_MP4 = "video/mp4";
 	public static final String MIME_TYPE_VIDEO_AVC = "video/avc";
 	public static final String MIME_TYPE_AUDIO_AAC = "audio/aac";
 	public static final String MIME_TYPE_AUDIO_MIDI = "audio/midi";

+ 4 - 3
app/src/main/java/ch/threema/app/webclient/services/instance/message/receiver/BlobRequestHandler.java

@@ -43,6 +43,7 @@ import ch.threema.app.services.MessageService;
 import ch.threema.app.services.messageplayer.MessagePlayer;
 import ch.threema.app.services.messageplayer.WebClientMessagePlayer;
 import ch.threema.app.utils.FileUtil;
+import ch.threema.app.utils.MimeUtil;
 import ch.threema.app.utils.executor.HandlerExecutor;
 import ch.threema.app.voicemessage.VoiceRecorderActivity;
 import ch.threema.app.webclient.Protocol;
@@ -174,7 +175,7 @@ public class BlobRequestHandler extends MessageReceiver {
 					//noinspection EnumSwitchStatementWhichMissesCases
 					switch (messageModel.getType()) {
 						case VOICEMESSAGE:
-							mime = "audio/aac";
+							mime = MimeUtil.MIME_TYPE_AUDIO_AAC;
 							name = filename + VoiceRecorderActivity.VOICEMESSAGE_FILE_EXTENSION;
 							break;
 						case FILE:
@@ -183,11 +184,11 @@ public class BlobRequestHandler extends MessageReceiver {
 							name = Message.fixFileName(ownFileName == null ? filename : ownFileName, mime);
 							break;
 						case VIDEO:
-							mime = "video/mp4";
+							mime = MimeUtil.MIME_TYPE_VIDEO_MP4;
 							name = filename + ".mp4";
 							break;
 						case IMAGE:
-							mime = "image/jpeg";
+							mime = MimeUtil.MIME_TYPE_IMAGE_JPG;
 							name = filename + ".jpg";
 							break;
 						default:

+ 1 - 1
app/src/main/java/ch/threema/client/Utils.java

@@ -49,7 +49,7 @@ public class Utils {
 		return data;
 	}
 
-	public static String byteArrayToHexString(byte[] bytes) {
+	public static @Nullable String byteArrayToHexString(byte[] bytes) {
 		if(bytes != null) {
 			final char[] hexArray = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
 			char[] hexChars = new char[bytes.length * 2];

+ 27 - 27
app/src/main/res/values-de/image_labels_strings.xml

@@ -3,7 +3,7 @@
 	<string name="label_0">Mannschaft</string>
 	<string name="label_1">Lagerfeuer</string>
 	<string name="label_2">Comics</string>
-	<string name="label_3">Himalaja</string>
+	<string name="label_3">Himalajaner</string>
 	<string name="label_4">Eisberg</string>
 	<string name="label_5">Bento</string>
 	<string name="label_7">Spülbecken</string>
@@ -22,7 +22,7 @@
 	<string name="label_21">Rafting</string>
 	<string name="label_22">Park</string>
 	<string name="label_24">Fabrik</string>
-	<string name="label_25">Graduierung</string>
+	<string name="label_25">Abschlussfeier</string>
 	<string name="label_26">Porzellan</string>
 	<string name="label_27">Zweig</string>
 	<string name="label_28">Blütenblatt</string>
@@ -93,22 +93,22 @@
 	<string name="label_95">Schiffbruch</string>
 	<string name="label_96">Pier</string>
 	<string name="label_97">Gemeinschaft</string>
-	<string name="label_98">Höhlenforschung</string>
+	<string name="label_98">Höhlenwandern</string>
 	<string name="label_99">Höhle</string>
 	<string name="label_100">Krawatte</string>
-	<string name="label_101">Kabinettgewerbe</string>
+	<string name="label_101">Möbel</string>
 	<string name="label_102">Unterwasser</string>
 	<string name="label_103">Clown</string>
 	<string name="label_104">Nachtclub</string>
 	<string name="label_105">Radfahren</string>
 	<string name="label_106">Komet</string>
 	<string name="label_107">Mörtelbrett</string>
-	<string name="label_108">Verfolgen Sie</string>
+	<string name="label_108">Spur</string>
 	<string name="label_109">Weihnachten</string>
 	<string name="label_110">Kirche</string>
 	<string name="label_111">Uhr</string>
 	<string name="label_112">Kumpel</string>
-	<string name="label_113">Rinder</string>
+	<string name="label_113">Vieh</string>
 	<string name="label_114">Dschungel</string>
 	<string name="label_115">Schreibtisch</string>
 	<string name="label_116">Curling</string>
@@ -119,7 +119,7 @@
 	<string name="label_121">Bildschirmfoto</string>
 	<string name="label_122">Besatzung</string>
 	<string name="label_123">Skyline</string>
-	<string name="label_125">Gefülltes Spielzeug</string>
+	<string name="label_125">Stofftier</string>
 	<string name="label_126">Keks</string>
 	<string name="label_127">Kachel</string>
 	<string name="label_128">Chanukka</string>
@@ -135,11 +135,11 @@
 	<string name="label_139">Tasche</string>
 	<string name="label_140">Neon</string>
 	<string name="label_141">Eiszapfen</string>
-	<string name="label_142">Pastelltöne</string>
+	<string name="label_142">Pasteles</string>
 	<string name="label_143">Kette</string>
 	<string name="label_144">Tanz</string>
 	<string name="label_145">Düne</string>
-	<string name="label_146">Der Weihnachtsmann</string>
+	<string name="label_146">Weihnachtsmann</string>
 	<string name="label_147">Danksagung</string>
 	<string name="label_148">Smoking</string>
 	<string name="label_149">Mund</string>
@@ -155,23 +155,23 @@
 	<string name="label_159">Kappe</string>
 	<string name="label_160">Weisswandtafel</string>
 	<string name="label_161">Hut</string>
-	<string name="label_162">Gelato</string>
+	<string name="label_162">Speiseeis</string>
 	<string name="label_163">Kavalier</string>
-	<string name="label_164">Beanie</string>
-	<string name="label_165">Jersey</string>
+	<string name="label_164">Mütze</string>
+	<string name="label_165">Trikot</string>
 	<string name="label_166">Schal</string>
 	<string name="label_167">Urlaub</string>
-	<string name="label_168">Stellplatz</string>
+	<string name="label_168">Spielfeld</string>
 	<string name="label_169">Tafel</string>
-	<string name="label_170">Deejay</string>
+	<string name="label_170">DJ</string>
 	<string name="label_171">Denkmal</string>
 	<string name="label_172">Stossfänger</string>
 	<string name="label_173">Longboard</string>
-	<string name="label_174">Wasservögel</string>
+	<string name="label_174">Wasservogel</string>
 	<string name="label_175">Fleisch</string>
 	<string name="label_176">Netz</string>
 	<string name="label_177">Vereisung</string>
-	<string name="label_178">Dalmatinisch</string>
+	<string name="label_178">Dalmatiner</string>
 	<string name="label_179">Schnellboot</string>
 	<string name="label_180">Stamm</string>
 	<string name="label_181">Kaffee</string>
@@ -197,15 +197,15 @@
 	<string name="label_203">Turnen</string>
 	<string name="label_204">Ohr</string>
 	<string name="label_205">Flora</string>
-	<string name="label_206">Shell</string>
+	<string name="label_206">Muschel</string>
 	<string name="label_207">Grosseltern</string>
 	<string name="label_208">Ruinen</string>
 	<string name="label_209">Wimpern</string>
 	<string name="label_210">Etagenbett</string>
-	<string name="label_211">Bilanz</string>
+	<string name="label_211">Waage</string>
 	<string name="label_212">Rucksackreisen</string>
 	<string name="label_213">Pferd</string>
-	<string name="label_214">Glitzern</string>
+	<string name="label_214">Glitzer</string>
 	<string name="label_215">Untertasse</string>
 	<string name="label_216">Haare</string>
 	<string name="label_217">Miniatur</string>
@@ -243,7 +243,7 @@
 	<string name="label_249">Leggings</string>
 	<string name="label_250">Pool</string>
 	<string name="label_251">Musikinstrument</string>
-	<string name="label_252">Musikalisch</string>
+	<string name="label_252">Musical</string>
 	<string name="label_253">Metall</string>
 	<string name="label_254">Mond</string>
 	<string name="label_255">Blazer</string>
@@ -251,7 +251,7 @@
 	<string name="label_257">Mobiltelefon</string>
 	<string name="label_258">Miliz</string>
 	<string name="label_259">Tischdecke</string>
-	<string name="label_260">Partei</string>
+	<string name="label_260">Party</string>
 	<string name="label_261">Nebel</string>
 	<string name="label_262">Nachrichten</string>
 	<string name="label_263">Zeitung</string>
@@ -278,7 +278,7 @@
 	<string name="label_285">Rennen</string>
 	<string name="label_286">Rudern</string>
 	<string name="label_287">Strasse</string>
-	<string name="label_288">Laufende</string>
+	<string name="label_288">Laufen</string>
 	<string name="label_289">Raum</string>
 	<string name="label_290">Dach</string>
 	<string name="label_291">Stern</string>
@@ -286,12 +286,12 @@
 	<string name="label_293">Schuh</string>
 	<string name="label_294">Rohrleitungen</string>
 	<string name="label_295">Weltraum</string>
-	<string name="label_296">Schlafen</string>
+	<string name="label_296">Schlaf</string>
 	<string name="label_297">Haut</string>
 	<string name="label_298">Schwimmen</string>
 	<string name="label_299">Schule</string>
 	<string name="label_300">Sushi</string>
-	<string name="label_301">Loveseat</string>
+	<string name="label_301">Sofa</string>
 	<string name="label_302">Supermann</string>
 	<string name="label_303">Cool</string>
 	<string name="label_304">Skifahren</string>
@@ -301,7 +301,7 @@
 	<string name="label_308">Wolkenkratzer</string>
 	<string name="label_309">Vulkan</string>
 	<string name="label_310">Fernsehen</string>
-	<string name="label_311">Rein</string>
+	<string name="label_311">Zügel</string>
 	<string name="label_312">Tätowierung</string>
 	<string name="label_313">Zug</string>
 	<string name="label_314">Handlauf</string>
@@ -342,7 +342,7 @@
 	<string name="label_352">Theke</string>
 	<string name="label_353">Strand</string>
 	<string name="label_354">Regenbogen</string>
-	<string name="label_355">Zweigstelle</string>
+	<string name="label_355">Ast</string>
 	<string name="label_356">Schnurrbart</string>
 	<string name="label_357">Garten</string>
 	<string name="label_358">Kittel</string>
@@ -398,7 +398,7 @@
 	<string name="label_410">Regenschirm</string>
 	<string name="label_411">Asphalt</string>
 	<string name="label_412">Segelboot</string>
-	<string name="label_413">Dachshund</string>
+	<string name="label_413">Dackel</string>
 	<string name="label_414">Muster</string>
 	<string name="label_415">Abendessen</string>
 	<string name="label_416">Schleier</string>

+ 6 - 1
app/src/main/res/values-de/strings.xml

@@ -1258,7 +1258,7 @@ sicheren Ort gesichert oder ausgedruckt haben.</string>
 	<string name="an_error_occurred_during_send">Beim Versand einer oder mehrerer Nachrichten ist ein Fehler aufgetreten.</string>
 	<string name="state_processing">wird verarbeitet</string>
 	<string name="passphrase_locked">Die Passphrase ist gesperrt</string>
-	<string name="image_labeling_new">Neu: Bilderkennung</string>
+	<string name="image_labeling_new">Neu: Bildersuche</string>
 	<string name="tooltip_image_labeling">Um ein bestimmtes visuelles Medium in Ihrer Galerie zu finden, geben Sie einfach einen Begriff ein, der den Inhalt beschreibt oder wählen Sie ihn aus der Liste der gefundenen Beschreibungen aus.</string>
 	<string name="error_unable_loading_media_thumb">Vorschau kann nicht geladen werden</string>
 	<string name="select">Auswählen</string>
@@ -1269,5 +1269,10 @@ sicheren Ort gesichert oder ausgedruckt haben.</string>
 	<string name="show_text">Text anzeigen</string>
 	<string name="only_images_or_videos">Nur Bilder und Videos können ausgewählt werden</string>
 	<string name="media_gallery_gifs">GIFs</string>
+	<string name="notification_channel_image_labeling">Fortschritt der Bilder-Indexierung</string>
+	<string name="notification_channel_image_labeling_desc">Bilder in der Galerie werden im Hintergrund indexiert</string>
+	<string name="notification_image_labeling_desc">Bilder-Indexierung</string>
 	<string name="no_media_found_global">Auf diesem Gerät wurden keine Medien gefunden</string>
+	<string name="prefs_sum_image_labeling">Stichwort-Suche für die öffentlichen Bilder in Ihrer Galerie ermöglichen</string>
+	<string name="prefs_image_labeling">Bildersuche</string>
 </resources>

+ 2 - 0
app/src/main/res/values/preferences_strings.xml

@@ -170,6 +170,8 @@
 	<string name="preferences__camera_permission_request_shown" translatable="false">pref_camera_permission_request_shown</string>
 	<string name="preferences__disable_smart_replies" translatable="false">pref_disable_smart_replies</string>
 	<string name="preferences__poi_host" translatable="false">pref_poi_host</string>
+	<string name="preferences__labels_delete" translatable="false">pref_labels_delete</string>
 	<string name="preferences__last_syncadapter_run" translatable="false">pref_last_syncadapter_run</string>
 	<string name="preferences__image_labeling_tooltip_shown" translatable="false">pref_image_labeling_tooltip_shown</string>
+	<string name="preferences__image_labeling" translatable="false">pref_key_image_labeling</string>
 </resources>

+ 6 - 4
app/src/main/res/values/strings.xml

@@ -1172,7 +1172,7 @@
 	<string name="an_error_occurred_during_send">An error occurred sending one or more messages.</string>
 	<string name="state_processing">processing</string>
 	<string name="passphrase_locked">Passphrase is locked</string>
-	<string name="image_labeling_new">New: Image recognition</string>
+	<string name="image_labeling_new">New: Image search</string>
 	<string name="tooltip_image_labeling">To find any particular visual media in your gallery, simply enter a term that describes the content and select it from the list of identified labels.</string>
 	<string name="selected_media">Your selection</string>
 	<string name="attach_gif">Gif</string>
@@ -1198,8 +1198,10 @@
 	<string name="show_text">Show text</string>
 	<string name="only_images_or_videos">Only images or videos can be selected</string>
 	<string name="media_gallery_gifs">GIFs</string>
-	<string name="notification_channel_image_labeling">Image labeling progress</string>
-	<string name="notification_channel_image_labeling_desc">Threema does media gallery image classification in the background</string>
-	<string name="notification_image_labeling_desc">Media gallery image classification in progress…</string>
+	<string name="notification_channel_image_labeling">Image indexing progress</string>
+	<string name="notification_channel_image_labeling_desc">Performing indexing of public gallery images in the background</string>
+	<string name="notification_image_labeling_desc">Media gallery indexing</string>
 	<string name="no_media_found_global">No media found on this device</string>
+	<string name="prefs_sum_image_labeling">Enable searching public images in your gallery by keywords</string>
+	<string name="prefs_image_labeling">Image search</string>
 </resources>

+ 8 - 0
app/src/main/res/xml/preference_developers.xml

@@ -25,6 +25,14 @@
 			android:title="POI Host Override"
 			android:summary="Will default to 'poi.threema.ch' if not set."/>
 	</PreferenceCategory>
+	<PreferenceCategory
+		android:key="pref_key_labels"
+		android:title="Image Labels">
+		<Preference
+			android:key="@string/preferences__labels_delete"
+			android:title="Delete image label database"
+			android:summary="This will delete the database with the media image labels. You may need to restart the app afterwards."/>
+	</PreferenceCategory>
 	<PreferenceCategory
 		android:key="pref_key_various"
 		android:title="Various">

+ 6 - 0
app/src/main/res/xml/preference_media.xml

@@ -33,6 +33,12 @@
 			android:summary="@string/prefs_sum_save_media"
 			android:title="@string/prefs_save_media"/>
 
+		<CheckBoxPreference
+			android:defaultValue="true"
+			android:key="@string/preferences__image_labeling"
+			android:summary="@string/prefs_sum_image_labeling"
+			android:title="@string/prefs_image_labeling"/>
+
 	</PreferenceCategory>
 
 	<PreferenceCategory