mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-26 11:51:10 +01:00
Move all files to natural position.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
public class ApnUnavailableException extends Exception {
|
||||
|
||||
public ApnUnavailableException() {
|
||||
}
|
||||
|
||||
public ApnUnavailableException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
public ApnUnavailableException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
|
||||
public ApnUnavailableException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,563 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.provider.ContactsContract;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.components.AudioView;
|
||||
import org.thoughtcrime.securesms.components.DocumentView;
|
||||
import org.thoughtcrime.securesms.components.RemovableEditableMediaView;
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.components.location.SignalMapView;
|
||||
import org.thoughtcrime.securesms.components.location.SignalPlace;
|
||||
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.maps.PlacePickerActivity;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.providers.DeprecatedPersistentBlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
|
||||
public class AttachmentManager {
|
||||
|
||||
private final static String TAG = AttachmentManager.class.getSimpleName();
|
||||
|
||||
private final @NonNull Context context;
|
||||
private final @NonNull Stub<View> attachmentViewStub;
|
||||
private final @NonNull AttachmentListener attachmentListener;
|
||||
|
||||
private RemovableEditableMediaView removableMediaView;
|
||||
private ThumbnailView thumbnail;
|
||||
private AudioView audioView;
|
||||
private DocumentView documentView;
|
||||
private SignalMapView mapView;
|
||||
|
||||
private @NonNull List<Uri> garbage = new LinkedList<>();
|
||||
private @NonNull Optional<Slide> slide = Optional.absent();
|
||||
private @Nullable Uri captureUri;
|
||||
|
||||
public AttachmentManager(@NonNull Activity activity, @NonNull AttachmentListener listener) {
|
||||
this.context = activity;
|
||||
this.attachmentListener = listener;
|
||||
this.attachmentViewStub = ViewUtil.findStubById(activity, R.id.attachment_editor_stub);
|
||||
}
|
||||
|
||||
private void inflateStub() {
|
||||
if (!attachmentViewStub.resolved()) {
|
||||
View root = attachmentViewStub.get();
|
||||
|
||||
this.thumbnail = ViewUtil.findById(root, R.id.attachment_thumbnail);
|
||||
this.audioView = ViewUtil.findById(root, R.id.attachment_audio);
|
||||
this.documentView = ViewUtil.findById(root, R.id.attachment_document);
|
||||
this.mapView = ViewUtil.findById(root, R.id.attachment_location);
|
||||
this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view);
|
||||
|
||||
removableMediaView.setRemoveClickListener(new RemoveButtonListener());
|
||||
thumbnail.setOnClickListener(new ThumbnailClickListener());
|
||||
documentView.getBackground().setColorFilter(ThemeUtil.getThemedColor(context, R.attr.conversation_item_bubble_background), PorterDuff.Mode.MULTIPLY);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void clear(@NonNull GlideRequests glideRequests, boolean animate) {
|
||||
if (attachmentViewStub.resolved()) {
|
||||
|
||||
if (animate) {
|
||||
ViewUtil.fadeOut(attachmentViewStub.get(), 200).addListener(new Listener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
thumbnail.clear(glideRequests);
|
||||
attachmentViewStub.get().setVisibility(View.GONE);
|
||||
attachmentListener.onAttachmentChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
thumbnail.clear(glideRequests);
|
||||
attachmentViewStub.get().setVisibility(View.GONE);
|
||||
attachmentListener.onAttachmentChanged();
|
||||
}
|
||||
|
||||
markGarbage(getSlideUri());
|
||||
slide = Optional.absent();
|
||||
|
||||
audioView.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
cleanup(captureUri);
|
||||
cleanup(getSlideUri());
|
||||
|
||||
captureUri = null;
|
||||
slide = Optional.absent();
|
||||
|
||||
Iterator<Uri> iterator = garbage.listIterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
cleanup(iterator.next());
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanup(final @Nullable Uri uri) {
|
||||
if (uri != null && DeprecatedPersistentBlobProvider.isAuthority(context, uri)) {
|
||||
Log.d(TAG, "cleaning up " + uri);
|
||||
DeprecatedPersistentBlobProvider.getInstance(context).delete(context, uri);
|
||||
} else if (uri != null && BlobProvider.isAuthority(uri)) {
|
||||
BlobProvider.getInstance().delete(context, uri);
|
||||
}
|
||||
}
|
||||
|
||||
private void markGarbage(@Nullable Uri uri) {
|
||||
if (uri != null && (DeprecatedPersistentBlobProvider.isAuthority(context, uri) || BlobProvider.isAuthority(uri))) {
|
||||
Log.d(TAG, "Marking garbage that needs cleaning: " + uri);
|
||||
garbage.add(uri);
|
||||
}
|
||||
}
|
||||
|
||||
private void setSlide(@NonNull Slide slide) {
|
||||
if (getSlideUri() != null) {
|
||||
cleanup(getSlideUri());
|
||||
}
|
||||
|
||||
if (captureUri != null && !captureUri.equals(slide.getUri())) {
|
||||
cleanup(captureUri);
|
||||
captureUri = null;
|
||||
}
|
||||
|
||||
this.slide = Optional.of(slide);
|
||||
}
|
||||
|
||||
public ListenableFuture<Boolean> setLocation(@NonNull final SignalPlace place,
|
||||
@NonNull final MediaConstraints constraints)
|
||||
{
|
||||
inflateStub();
|
||||
|
||||
SettableFuture<Boolean> returnResult = new SettableFuture<>();
|
||||
ListenableFuture<Bitmap> future = mapView.display(place);
|
||||
|
||||
attachmentViewStub.get().setVisibility(View.VISIBLE);
|
||||
removableMediaView.display(mapView, false);
|
||||
|
||||
future.addListener(new AssertedSuccessListener<Bitmap>() {
|
||||
@Override
|
||||
public void onSuccess(@NonNull Bitmap result) {
|
||||
byte[] blob = BitmapUtil.toByteArray(result);
|
||||
Uri uri = BlobProvider.getInstance()
|
||||
.forData(blob)
|
||||
.withMimeType(MediaUtil.IMAGE_JPEG)
|
||||
.createForSingleSessionInMemory();
|
||||
LocationSlide locationSlide = new LocationSlide(context, uri, blob.length, place);
|
||||
|
||||
Util.runOnMain(() -> {
|
||||
setSlide(locationSlide);
|
||||
attachmentListener.onAttachmentChanged();
|
||||
returnResult.set(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return returnResult;
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public ListenableFuture<Boolean> setMedia(@NonNull final GlideRequests glideRequests,
|
||||
@NonNull final Uri uri,
|
||||
@NonNull final MediaType mediaType,
|
||||
@NonNull final MediaConstraints constraints,
|
||||
final int width,
|
||||
final int height)
|
||||
{
|
||||
inflateStub();
|
||||
|
||||
final SettableFuture<Boolean> result = new SettableFuture<>();
|
||||
|
||||
new AsyncTask<Void, Void, Slide>() {
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
thumbnail.clear(glideRequests);
|
||||
thumbnail.showProgressSpinner();
|
||||
attachmentViewStub.get().setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable Slide doInBackground(Void... params) {
|
||||
try {
|
||||
if (PartAuthority.isLocalUri(uri)) {
|
||||
return getManuallyCalculatedSlideInfo(uri, width, height);
|
||||
} else {
|
||||
Slide result = getContentResolverSlideInfo(uri, width, height);
|
||||
|
||||
if (result == null) return getManuallyCalculatedSlideInfo(uri, width, height);
|
||||
else return result;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(@Nullable final Slide slide) {
|
||||
if (slide == null) {
|
||||
attachmentViewStub.get().setVisibility(View.GONE);
|
||||
Toast.makeText(context,
|
||||
R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
result.set(false);
|
||||
} else if (!areConstraintsSatisfied(context, slide, constraints)) {
|
||||
attachmentViewStub.get().setVisibility(View.GONE);
|
||||
Toast.makeText(context,
|
||||
R.string.ConversationActivity_attachment_exceeds_size_limits,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
result.set(false);
|
||||
} else {
|
||||
setSlide(slide);
|
||||
attachmentViewStub.get().setVisibility(View.VISIBLE);
|
||||
|
||||
if (slide.hasAudio()) {
|
||||
audioView.setAudio((AudioSlide) slide, false);
|
||||
removableMediaView.display(audioView, false);
|
||||
result.set(true);
|
||||
} else if (slide.hasDocument()) {
|
||||
documentView.setDocument((DocumentSlide) slide, false);
|
||||
removableMediaView.display(documentView, false);
|
||||
result.set(true);
|
||||
} else {
|
||||
Attachment attachment = slide.asAttachment();
|
||||
result.deferTo(thumbnail.setImageResource(glideRequests, slide, false, true, attachment.getWidth(), attachment.getHeight()));
|
||||
removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE);
|
||||
}
|
||||
|
||||
attachmentListener.onAttachmentChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable Slide getContentResolverSlideInfo(Uri uri, int width, int height) {
|
||||
Cursor cursor = null;
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
cursor = context.getContentResolver().query(uri, null, null, null, null);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
String fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
|
||||
long fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
|
||||
String mimeType = context.getContentResolver().getType(uri);
|
||||
|
||||
if (width == 0 || height == 0) {
|
||||
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, mimeType, uri);
|
||||
width = dimens.first;
|
||||
height = dimens.second;
|
||||
}
|
||||
|
||||
Log.d(TAG, "remote slide with size " + fileSize + " took " + (System.currentTimeMillis() - start) + "ms");
|
||||
return mediaType.createSlide(context, uri, fileName, mimeType, null, fileSize, width, height);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private @NonNull Slide getManuallyCalculatedSlideInfo(Uri uri, int width, int height) throws IOException {
|
||||
long start = System.currentTimeMillis();
|
||||
Long mediaSize = null;
|
||||
String fileName = null;
|
||||
String mimeType = null;
|
||||
|
||||
if (PartAuthority.isLocalUri(uri)) {
|
||||
mediaSize = PartAuthority.getAttachmentSize(context, uri);
|
||||
fileName = PartAuthority.getAttachmentFileName(context, uri);
|
||||
mimeType = PartAuthority.getAttachmentContentType(context, uri);
|
||||
}
|
||||
|
||||
if (mediaSize == null) {
|
||||
mediaSize = MediaUtil.getMediaSize(context, uri);
|
||||
}
|
||||
|
||||
if (mimeType == null) {
|
||||
mimeType = MediaUtil.getMimeType(context, uri);
|
||||
}
|
||||
|
||||
if (width == 0 || height == 0) {
|
||||
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, mimeType, uri);
|
||||
width = dimens.first;
|
||||
height = dimens.second;
|
||||
}
|
||||
|
||||
Log.d(TAG, "local slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms");
|
||||
return mediaType.createSlide(context, uri, fileName, mimeType, null, mediaSize, width, height);
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean isAttachmentPresent() {
|
||||
return attachmentViewStub.resolved() && attachmentViewStub.get().getVisibility() == View.VISIBLE;
|
||||
}
|
||||
|
||||
public @NonNull SlideDeck buildSlideDeck() {
|
||||
SlideDeck deck = new SlideDeck();
|
||||
if (slide.isPresent()) deck.addSlide(slide.get());
|
||||
return deck;
|
||||
}
|
||||
|
||||
public static void selectDocument(Activity activity, int requestCode) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||
.onAllGranted(() -> selectMediaType(activity, "*/*", null, requestCode))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body, @NonNull TransportOption transport) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||
.onAllGranted(() -> selectMediaType(activity, "image/*", new String[] {"image/*", "video/*"}, requestCode))
|
||||
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body, transport), requestCode))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void selectAudio(Activity activity, int requestCode) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||
.onAllGranted(() -> selectMediaType(activity, "audio/*", null, requestCode))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void selectContactInfo(Activity activity, int requestCode) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.WRITE_CONTACTS)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_contacts_permission_in_order_to_attach_contact_information))
|
||||
.onAllGranted(() -> {
|
||||
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
|
||||
activity.startActivityForResult(intent, requestCode);
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void selectLocation(Activity activity, int requestCode) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_location_information_in_order_to_attach_a_location))
|
||||
.onAllGranted(() -> PlacePickerActivity.startActivityForResultAtCurrentLocation(activity, requestCode))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void selectGif(Activity activity, int requestCode, boolean isForMms, @ColorInt int color) {
|
||||
Intent intent = new Intent(activity, GiphyActivity.class);
|
||||
intent.putExtra(GiphyActivity.EXTRA_IS_MMS, isForMms);
|
||||
intent.putExtra(GiphyActivity.EXTRA_COLOR, color);
|
||||
activity.startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
private @Nullable Uri getSlideUri() {
|
||||
return slide.isPresent() ? slide.get().getUri() : null;
|
||||
}
|
||||
|
||||
public @Nullable Uri getCaptureUri() {
|
||||
return captureUri;
|
||||
}
|
||||
|
||||
public void capturePhoto(Activity activity, int requestCode) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied))
|
||||
.onAllGranted(() -> {
|
||||
try {
|
||||
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
|
||||
if (captureUri == null) {
|
||||
captureUri = DeprecatedPersistentBlobProvider.getInstance(context).createForExternal(context, MediaUtil.IMAGE_JPEG);
|
||||
}
|
||||
Log.d(TAG, "captureUri path is " + captureUri.getPath());
|
||||
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, captureUri);
|
||||
activity.startActivityForResult(captureIntent, requestCode);
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
private static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) {
|
||||
final Intent intent = new Intent();
|
||||
intent.setType(type);
|
||||
|
||||
if (extraMimeType != null) {
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, extraMimeType);
|
||||
}
|
||||
|
||||
intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
|
||||
try {
|
||||
activity.startActivityForResult(intent, requestCode);
|
||||
return;
|
||||
} catch (ActivityNotFoundException anfe) {
|
||||
Log.w(TAG, "couldn't complete ACTION_OPEN_DOCUMENT, no activity found. falling back.");
|
||||
}
|
||||
|
||||
intent.setAction(Intent.ACTION_GET_CONTENT);
|
||||
|
||||
try {
|
||||
activity.startActivityForResult(intent, requestCode);
|
||||
} catch (ActivityNotFoundException anfe) {
|
||||
Log.w(TAG, "couldn't complete ACTION_GET_CONTENT intent, no activity found. falling back.");
|
||||
Toast.makeText(activity, R.string.AttachmentManager_cant_open_media_selection, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean areConstraintsSatisfied(final @NonNull Context context,
|
||||
final @Nullable Slide slide,
|
||||
final @NonNull MediaConstraints constraints)
|
||||
{
|
||||
return slide == null ||
|
||||
constraints.isSatisfied(context, slide.asAttachment()) ||
|
||||
constraints.canResize(slide.asAttachment());
|
||||
}
|
||||
|
||||
private void previewImageDraft(final @NonNull Slide slide) {
|
||||
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
|
||||
Intent intent = new Intent(context, MediaPreviewActivity.class);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize());
|
||||
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, slide.getCaption().orNull());
|
||||
intent.setDataAndType(slide.getUri(), slide.getContentType());
|
||||
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
private class ThumbnailClickListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (slide.isPresent()) previewImageDraft(slide.get());
|
||||
}
|
||||
}
|
||||
|
||||
private class RemoveButtonListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
cleanup();
|
||||
clear(GlideApp.with(context.getApplicationContext()), true);
|
||||
}
|
||||
}
|
||||
|
||||
public interface AttachmentListener {
|
||||
void onAttachmentChanged();
|
||||
}
|
||||
|
||||
public enum MediaType {
|
||||
IMAGE, GIF, AUDIO, VIDEO, DOCUMENT, VCARD;
|
||||
|
||||
public @NonNull Slide createSlide(@NonNull Context context,
|
||||
@NonNull Uri uri,
|
||||
@Nullable String fileName,
|
||||
@Nullable String mimeType,
|
||||
@Nullable BlurHash blurHash,
|
||||
long dataSize,
|
||||
int width,
|
||||
int height)
|
||||
{
|
||||
if (mimeType == null) {
|
||||
mimeType = "application/octet-stream";
|
||||
}
|
||||
|
||||
switch (this) {
|
||||
case IMAGE: return new ImageSlide(context, uri, dataSize, width, height, blurHash);
|
||||
case GIF: return new GifSlide(context, uri, dataSize, width, height);
|
||||
case AUDIO: return new AudioSlide(context, uri, dataSize, false);
|
||||
case VIDEO: return new VideoSlide(context, uri, dataSize);
|
||||
case VCARD:
|
||||
case DOCUMENT: return new DocumentSlide(context, uri, mimeType, dataSize, fileName);
|
||||
default: throw new AssertionError("unrecognized enum");
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable MediaType from(final @Nullable String mimeType) {
|
||||
if (TextUtils.isEmpty(mimeType)) return null;
|
||||
if (MediaUtil.isGif(mimeType)) return GIF;
|
||||
if (MediaUtil.isImageType(mimeType)) return IMAGE;
|
||||
if (MediaUtil.isAudioType(mimeType)) return AUDIO;
|
||||
if (MediaUtil.isVideoType(mimeType)) return VIDEO;
|
||||
if (MediaUtil.isVcard(mimeType)) return VCARD;
|
||||
|
||||
return DOCUMENT;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.Priority;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.data.DataFetcher;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
class AttachmentStreamLocalUriFetcher implements DataFetcher<InputStream> {
|
||||
|
||||
private static final String TAG = AttachmentStreamLocalUriFetcher.class.getSimpleName();
|
||||
|
||||
private final File attachment;
|
||||
private final byte[] key;
|
||||
private final Optional<byte[]> digest;
|
||||
private final long plaintextLength;
|
||||
|
||||
private InputStream is;
|
||||
|
||||
AttachmentStreamLocalUriFetcher(File attachment, long plaintextLength, byte[] key, Optional<byte[]> digest) {
|
||||
this.attachment = attachment;
|
||||
this.plaintextLength = plaintextLength;
|
||||
this.digest = digest;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
|
||||
try {
|
||||
if (!digest.isPresent()) throw new InvalidMessageException("No attachment digest!");
|
||||
is = AttachmentCipherInputStream.createForAttachment(attachment, plaintextLength, key, digest.get());
|
||||
callback.onDataReady(is);
|
||||
} catch (IOException | InvalidMessageException e) {
|
||||
callback.onLoadFailed(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
try {
|
||||
if (is != null) is.close();
|
||||
is = null;
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "ioe");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {}
|
||||
|
||||
@Override
|
||||
public @NonNull Class<InputStream> getDataClass() {
|
||||
return InputStream.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull DataSource getDataSource() {
|
||||
return DataSource.LOCAL;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.load.Key;
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.model.ModelLoader;
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||
|
||||
import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
public class AttachmentStreamUriLoader implements ModelLoader<AttachmentModel, InputStream> {
|
||||
|
||||
@Override
|
||||
public @Nullable LoadData<InputStream> buildLoadData(@NonNull AttachmentModel attachmentModel, int width, int height, @NonNull Options options) {
|
||||
return new LoadData<>(attachmentModel, new AttachmentStreamLocalUriFetcher(attachmentModel.attachment, attachmentModel.plaintextLength, attachmentModel.key, attachmentModel.digest));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull AttachmentModel attachmentModel) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static class Factory implements ModelLoaderFactory<AttachmentModel, InputStream> {
|
||||
|
||||
@Override
|
||||
public @NonNull ModelLoader<AttachmentModel, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
|
||||
return new AttachmentStreamUriLoader();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teardown() {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
public static class AttachmentModel implements Key {
|
||||
public @NonNull File attachment;
|
||||
public @NonNull byte[] key;
|
||||
public @NonNull Optional<byte[]> digest;
|
||||
public long plaintextLength;
|
||||
|
||||
public AttachmentModel(@NonNull File attachment, @NonNull byte[] key,
|
||||
long plaintextLength, @NonNull Optional<byte[]> digest)
|
||||
{
|
||||
this.attachment = attachment;
|
||||
this.key = key;
|
||||
this.digest = digest;
|
||||
this.plaintextLength = plaintextLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
|
||||
messageDigest.update(attachment.toString().getBytes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
AttachmentModel that = (AttachmentModel)o;
|
||||
|
||||
return attachment.equals(that.attachment);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return attachment.hashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources.Theme;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.ResUtil;
|
||||
|
||||
|
||||
public class AudioSlide extends Slide {
|
||||
|
||||
public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, 0, 0, false, null, null, null, null, voiceNote, false));
|
||||
}
|
||||
|
||||
public AudioSlide(Context context, Uri uri, long dataSize, String contentType, boolean voiceNote) {
|
||||
super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, null, null, null, null));
|
||||
}
|
||||
|
||||
public AudioSlide(Context context, Attachment attachment) {
|
||||
super(context, attachment);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Uri getThumbnailUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPlaceholder() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasImage() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAudio() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getContentDescription() {
|
||||
return context.getString(R.string.Slide_audio);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @DrawableRes int getPlaceholderRes(Theme theme) {
|
||||
return ResUtil.getDrawableRes(theme, R.attr.conversation_icon_attach_audio);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import com.google.android.mms.pdu_alt.PduHeaders;
|
||||
import com.google.android.mms.pdu_alt.RetrieveConf;
|
||||
import com.google.android.mms.pdu_alt.SendConf;
|
||||
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CompatMmsConnection implements OutgoingMmsConnection, IncomingMmsConnection {
|
||||
private static final String TAG = CompatMmsConnection.class.getSimpleName();
|
||||
|
||||
private Context context;
|
||||
|
||||
public CompatMmsConnection(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public SendConf send(@NonNull byte[] pduBytes, int subscriptionId)
|
||||
throws UndeliverableMessageException
|
||||
{
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP_MR1) {
|
||||
try {
|
||||
Log.i(TAG, "Sending via Lollipop API");
|
||||
return new OutgoingLollipopMmsConnection(context).send(pduBytes, subscriptionId);
|
||||
} catch (UndeliverableMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Falling back to legacy connection...");
|
||||
}
|
||||
|
||||
if (subscriptionId == -1) {
|
||||
Log.i(TAG, "Sending via legacy connection");
|
||||
try {
|
||||
SendConf result = new OutgoingLegacyMmsConnection(context).send(pduBytes, subscriptionId);
|
||||
|
||||
if (result != null && result.getResponseStatus() == PduHeaders.RESPONSE_STATUS_OK) {
|
||||
return result;
|
||||
} else {
|
||||
Log.w(TAG, "Got bad legacy response: " + (result != null ? result.getResponseStatus() : null));
|
||||
}
|
||||
} catch (UndeliverableMessageException | ApnUnavailableException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && VERSION.SDK_INT < VERSION_CODES.LOLLIPOP_MR1) {
|
||||
Log.i(TAG, "Falling back to sending via Lollipop API");
|
||||
return new OutgoingLollipopMmsConnection(context).send(pduBytes, subscriptionId);
|
||||
}
|
||||
|
||||
throw new UndeliverableMessageException("Both lollipop and legacy connections failed...");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public RetrieveConf retrieve(@NonNull String contentLocation,
|
||||
byte[] transactionId,
|
||||
int subscriptionId)
|
||||
throws MmsException, MmsRadioException, ApnUnavailableException, IOException
|
||||
{
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP_MR1) {
|
||||
Log.i(TAG, "Receiving via Lollipop API");
|
||||
try {
|
||||
return new IncomingLollipopMmsConnection(context).retrieve(contentLocation, transactionId, subscriptionId);
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Falling back to receiving via legacy connection");
|
||||
}
|
||||
|
||||
if (VERSION.SDK_INT < 22 || subscriptionId == -1) {
|
||||
Log.i(TAG, "Receiving via legacy API");
|
||||
try {
|
||||
return new IncomingLegacyMmsConnection(context).retrieve(contentLocation, transactionId, subscriptionId);
|
||||
} catch (MmsRadioException | ApnUnavailableException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && VERSION.SDK_INT < VERSION_CODES.LOLLIPOP_MR1) {
|
||||
Log.i(TAG, "Falling back to receiving via Lollipop API");
|
||||
return new IncomingLollipopMmsConnection(context).retrieve(contentLocation, transactionId, subscriptionId);
|
||||
}
|
||||
|
||||
throw new IOException("Both lollipop and fallback APIs failed...");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.provider.ContactsContract;
|
||||
|
||||
import com.bumptech.glide.load.data.StreamLocalUriFetcher;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
|
||||
class ContactPhotoLocalUriFetcher extends StreamLocalUriFetcher {
|
||||
|
||||
private static final String TAG = ContactPhotoLocalUriFetcher.class.getSimpleName();
|
||||
|
||||
ContactPhotoLocalUriFetcher(Context context, Uri uri) {
|
||||
super(context.getContentResolver(), uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream loadResource(Uri uri, ContentResolver contentResolver)
|
||||
throws FileNotFoundException
|
||||
{
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||
return ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, uri, true);
|
||||
} else {
|
||||
return ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.bumptech.glide.load.data.StreamLocalUriFetcher;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher {
|
||||
|
||||
private static final String TAG = DecryptableStreamLocalUriFetcher.class.getSimpleName();
|
||||
|
||||
private Context context;
|
||||
|
||||
DecryptableStreamLocalUriFetcher(Context context, Uri uri) {
|
||||
super(context.getContentResolver(), uri);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream loadResource(Uri uri, ContentResolver contentResolver) throws FileNotFoundException {
|
||||
if (MediaUtil.hasVideoThumbnail(uri)) {
|
||||
Bitmap thumbnail = MediaUtil.getVideoThumbnail(context, uri);
|
||||
|
||||
if (thumbnail != null) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, baos);
|
||||
ByteArrayInputStream thumbnailStream = new ByteArrayInputStream(baos.toByteArray());
|
||||
thumbnail.recycle();
|
||||
return thumbnailStream;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return PartAuthority.getAttachmentThumbnailStream(context, uri);
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
throw new FileNotFoundException("PartAuthority couldn't load Uri resource.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.load.Key;
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.model.ModelLoader;
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
public class DecryptableStreamUriLoader implements ModelLoader<DecryptableUri, InputStream> {
|
||||
|
||||
private final Context context;
|
||||
|
||||
private DecryptableStreamUriLoader(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public LoadData<InputStream> buildLoadData(@NonNull DecryptableUri decryptableUri, int width, int height, @NonNull Options options) {
|
||||
return new LoadData<>(decryptableUri, new DecryptableStreamLocalUriFetcher(context, decryptableUri.uri));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull DecryptableUri decryptableUri) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static class Factory implements ModelLoaderFactory<DecryptableUri, InputStream> {
|
||||
|
||||
private final Context context;
|
||||
|
||||
Factory(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ModelLoader<DecryptableUri, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
|
||||
return new DecryptableStreamUriLoader(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teardown() {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
public static class DecryptableUri implements Key {
|
||||
public @NonNull Uri uri;
|
||||
|
||||
public DecryptableUri(@NonNull Uri uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
|
||||
messageDigest.update(uri.toString().getBytes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
DecryptableUri that = (DecryptableUri)o;
|
||||
|
||||
return uri.equals(that.uri);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return uri.hashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
|
||||
public class DocumentSlide extends Slide {
|
||||
|
||||
public DocumentSlide(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
super(context, attachment);
|
||||
}
|
||||
|
||||
public DocumentSlide(@NonNull Context context, @NonNull Uri uri,
|
||||
@NonNull String contentType, long size,
|
||||
@Nullable String fileName)
|
||||
{
|
||||
super(context, constructAttachmentFromUri(context, uri, contentType, size, 0, 0, true, StorageUtil.getCleanFileName(fileName), null, null, null, false, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasDocument() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
public class GifSlide extends ImageSlide {
|
||||
|
||||
public GifSlide(Context context, Attachment attachment) {
|
||||
super(context, attachment);
|
||||
}
|
||||
|
||||
|
||||
public GifSlide(Context context, Uri uri, long size, int width, int height) {
|
||||
this(context, uri, size, width, height, null);
|
||||
}
|
||||
|
||||
public GifSlide(Context context, Uri uri, long size, int width, int height, @Nullable String caption) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_GIF, size, width, height, true, null, caption, null, null, false, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Uri getThumbnailUri() {
|
||||
return getUri();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources.Theme;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
public class ImageSlide extends Slide {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = ImageSlide.class.getSimpleName();
|
||||
|
||||
public ImageSlide(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
super(context, attachment);
|
||||
}
|
||||
|
||||
public ImageSlide(Context context, Uri uri, long size, int width, int height, @Nullable BlurHash blurHash) {
|
||||
this(context, uri, size, width, height, null, blurHash);
|
||||
}
|
||||
|
||||
public ImageSlide(Context context, Uri uri, long size, int width, int height, @Nullable String caption, @Nullable BlurHash blurHash) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_JPEG, size, width, height, true, null, caption, null, blurHash, false, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @DrawableRes int getPlaceholderRes(Theme theme) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Uri getThumbnailUri() {
|
||||
return getUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasImage() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPlaceholder() {
|
||||
return getPlaceholderBlur() != null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getContentDescription() {
|
||||
return context.getString(R.string.Slide_image);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import com.google.android.mms.InvalidHeaderValueException;
|
||||
import com.google.android.mms.pdu_alt.NotifyRespInd;
|
||||
import com.google.android.mms.pdu_alt.PduComposer;
|
||||
import com.google.android.mms.pdu_alt.PduHeaders;
|
||||
import com.google.android.mms.pdu_alt.PduParser;
|
||||
import com.google.android.mms.pdu_alt.RetrieveConf;
|
||||
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.methods.HttpGetHC4;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class IncomingLegacyMmsConnection extends LegacyMmsConnection implements IncomingMmsConnection {
|
||||
private static final String TAG = IncomingLegacyMmsConnection.class.getSimpleName();
|
||||
|
||||
public IncomingLegacyMmsConnection(Context context) throws ApnUnavailableException {
|
||||
super(context);
|
||||
}
|
||||
|
||||
private HttpUriRequest constructRequest(Apn contentApn, boolean useProxy) throws IOException {
|
||||
HttpGetHC4 request;
|
||||
|
||||
try {
|
||||
request = new HttpGetHC4(contentApn.getMmsc());
|
||||
} catch (IllegalArgumentException e) {
|
||||
// #7339
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
for (Header header : getBaseHeaders()) {
|
||||
request.addHeader(header);
|
||||
}
|
||||
|
||||
if (useProxy) {
|
||||
HttpHost proxy = new HttpHost(contentApn.getProxy(), contentApn.getPort());
|
||||
request.setConfig(RequestConfig.custom().setProxy(proxy).build());
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable RetrieveConf retrieve(@NonNull String contentLocation,
|
||||
byte[] transactionId, int subscriptionId)
|
||||
throws MmsRadioException, ApnUnavailableException, IOException
|
||||
{
|
||||
MmsRadio radio = MmsRadio.getInstance(context);
|
||||
Apn contentApn = new Apn(contentLocation, apn.getProxy(), Integer.toString(apn.getPort()), apn.getUsername(), apn.getPassword());
|
||||
|
||||
if (isDirectConnect()) {
|
||||
Log.i(TAG, "Connecting directly...");
|
||||
try {
|
||||
return retrieve(contentApn, transactionId, false, false);
|
||||
} catch (IOException | ApnUnavailableException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "Changing radio to MMS mode..");
|
||||
radio.connect();
|
||||
|
||||
try {
|
||||
Log.i(TAG, "Downloading in MMS mode with proxy...");
|
||||
|
||||
try {
|
||||
return retrieve(contentApn, transactionId, true, true);
|
||||
} catch (IOException | ApnUnavailableException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Downloading in MMS mode without proxy...");
|
||||
|
||||
return retrieve(contentApn, transactionId, true, false);
|
||||
|
||||
} finally {
|
||||
radio.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
public RetrieveConf retrieve(Apn contentApn, byte[] transactionId, boolean usingMmsRadio, boolean useProxyIfAvailable)
|
||||
throws IOException, ApnUnavailableException
|
||||
{
|
||||
byte[] pdu = null;
|
||||
|
||||
final boolean useProxy = useProxyIfAvailable && contentApn.hasProxy();
|
||||
final String targetHost = useProxy
|
||||
? contentApn.getProxy()
|
||||
: Uri.parse(contentApn.getMmsc()).getHost();
|
||||
if (checkRouteToHost(context, targetHost, usingMmsRadio)) {
|
||||
Log.i(TAG, "got successful route to host " + targetHost);
|
||||
pdu = execute(constructRequest(contentApn, useProxy));
|
||||
}
|
||||
|
||||
if (pdu == null) {
|
||||
throw new IOException("Connection manager could not obtain route to host.");
|
||||
}
|
||||
|
||||
RetrieveConf retrieved = (RetrieveConf)new PduParser(pdu).parse();
|
||||
|
||||
if (retrieved == null) {
|
||||
Log.w(TAG, "Couldn't parse PDU, byte response: " + Arrays.toString(pdu));
|
||||
Log.w(TAG, "Couldn't parse PDU, ASCII: " + new String(pdu));
|
||||
throw new IOException("Bad retrieved PDU");
|
||||
}
|
||||
|
||||
sendRetrievedAcknowledgement(transactionId, usingMmsRadio, useProxy);
|
||||
return retrieved;
|
||||
}
|
||||
|
||||
private void sendRetrievedAcknowledgement(byte[] transactionId,
|
||||
boolean usingRadio,
|
||||
boolean useProxy)
|
||||
throws ApnUnavailableException
|
||||
{
|
||||
try {
|
||||
NotifyRespInd notifyResponse = new NotifyRespInd(PduHeaders.CURRENT_MMS_VERSION,
|
||||
transactionId,
|
||||
PduHeaders.STATUS_RETRIEVED);
|
||||
|
||||
OutgoingLegacyMmsConnection connection = new OutgoingLegacyMmsConnection(context);
|
||||
connection.sendNotificationReceived(new PduComposer(context, notifyResponse).make(), usingRadio, useProxy);
|
||||
} catch (InvalidHeaderValueException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.telephony.SmsManager;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import com.google.android.mms.InvalidHeaderValueException;
|
||||
import com.google.android.mms.pdu_alt.NotifyRespInd;
|
||||
import com.google.android.mms.pdu_alt.PduComposer;
|
||||
import com.google.android.mms.pdu_alt.PduHeaders;
|
||||
import com.google.android.mms.pdu_alt.PduParser;
|
||||
import com.google.android.mms.pdu_alt.RetrieveConf;
|
||||
|
||||
import org.thoughtcrime.securesms.providers.MmsBodyProvider;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public class IncomingLollipopMmsConnection extends LollipopMmsConnection implements IncomingMmsConnection {
|
||||
|
||||
public static final String ACTION = IncomingLollipopMmsConnection.class.getCanonicalName() + "MMS_DOWNLOADED_ACTION";
|
||||
private static final String TAG = IncomingLollipopMmsConnection.class.getSimpleName();
|
||||
|
||||
public IncomingLollipopMmsConnection(Context context) {
|
||||
super(context, ACTION);
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.LOLLIPOP)
|
||||
@Override
|
||||
public synchronized void onResult(Context context, Intent intent) {
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP_MR1) {
|
||||
Log.i(TAG, "HTTP status: " + intent.getIntExtra(SmsManager.EXTRA_MMS_HTTP_STATUS, -1));
|
||||
}
|
||||
Log.i(TAG, "code: " + getResultCode() + ", result string: " + getResultData());
|
||||
}
|
||||
|
||||
@Override
|
||||
@TargetApi(VERSION_CODES.LOLLIPOP)
|
||||
public synchronized @Nullable RetrieveConf retrieve(@NonNull String contentLocation,
|
||||
byte[] transactionId,
|
||||
int subscriptionId) throws MmsException
|
||||
{
|
||||
beginTransaction();
|
||||
|
||||
try {
|
||||
MmsBodyProvider.Pointer pointer = MmsBodyProvider.makeTemporaryPointer(getContext());
|
||||
|
||||
final String transactionIdString = Util.toIsoString(transactionId);
|
||||
Log.i(TAG, String.format(Locale.ENGLISH, "Downloading subscriptionId=%s multimedia from '%s' [transactionId='%s'] to '%s'",
|
||||
subscriptionId,
|
||||
contentLocation,
|
||||
transactionIdString,
|
||||
pointer.getUri()));
|
||||
|
||||
SmsManager smsManager;
|
||||
|
||||
if (VERSION.SDK_INT >= 22 && subscriptionId != -1) {
|
||||
smsManager = SmsManager.getSmsManagerForSubscriptionId(subscriptionId);
|
||||
} else {
|
||||
smsManager = SmsManager.getDefault();
|
||||
}
|
||||
|
||||
final Bundle configOverrides = smsManager.getCarrierConfigValues();
|
||||
|
||||
if (configOverrides.getBoolean(SmsManager.MMS_CONFIG_APPEND_TRANSACTION_ID)) {
|
||||
if (!contentLocation.contains(transactionIdString)) {
|
||||
Log.i(TAG, "Appending transactionId to contentLocation at the direction of CarrierConfigValues. New location: " + contentLocation);
|
||||
contentLocation += transactionIdString;
|
||||
} else {
|
||||
Log.i(TAG, "Skipping 'append transaction id' as contentLocation already contains it");
|
||||
}
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(configOverrides.getString(SmsManager.MMS_CONFIG_USER_AGENT))) {
|
||||
configOverrides.remove(SmsManager.MMS_CONFIG_USER_AGENT);
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(configOverrides.getString(SmsManager.MMS_CONFIG_UA_PROF_URL))) {
|
||||
configOverrides.remove(SmsManager.MMS_CONFIG_UA_PROF_URL);
|
||||
}
|
||||
|
||||
smsManager.downloadMultimediaMessage(getContext(),
|
||||
contentLocation,
|
||||
pointer.getUri(),
|
||||
configOverrides,
|
||||
getPendingIntent());
|
||||
|
||||
waitForResult();
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
Util.copy(pointer.getInputStream(), baos);
|
||||
pointer.close();
|
||||
|
||||
Log.i(TAG, baos.size() + "-byte response: ");// + Hex.dump(baos.toByteArray()));
|
||||
|
||||
RetrieveConf retrieved = (RetrieveConf) new PduParser(baos.toByteArray()).parse();
|
||||
|
||||
if (retrieved == null) return null;
|
||||
|
||||
sendRetrievedAcknowledgement(transactionId, retrieved.getMmsVersion(), subscriptionId);
|
||||
return retrieved;
|
||||
} catch (IOException | TimeoutException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new MmsException(e);
|
||||
} finally {
|
||||
endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendRetrievedAcknowledgement(byte[] transactionId, int mmsVersion, int subscriptionId) {
|
||||
try {
|
||||
NotifyRespInd retrieveResponse = new NotifyRespInd(mmsVersion, transactionId, PduHeaders.STATUS_RETRIEVED);
|
||||
new OutgoingLollipopMmsConnection(getContext()).send(new PduComposer(getContext(), retrieveResponse).make(), subscriptionId);
|
||||
} catch (UndeliverableMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
} catch (InvalidHeaderValueException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class IncomingMediaMessage {
|
||||
|
||||
private final RecipientId from;
|
||||
private final String groupId;
|
||||
private final String body;
|
||||
private final boolean push;
|
||||
private final long sentTimeMillis;
|
||||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final boolean expirationUpdate;
|
||||
private final QuoteModel quote;
|
||||
private final boolean unidentified;
|
||||
private final boolean viewOnce;
|
||||
|
||||
private final List<Attachment> attachments = new LinkedList<>();
|
||||
private final List<Contact> sharedContacts = new LinkedList<>();
|
||||
private final List<LinkPreview> linkPreviews = new LinkedList<>();
|
||||
|
||||
public IncomingMediaMessage(@NonNull RecipientId from,
|
||||
Optional<String> groupId,
|
||||
String body,
|
||||
long sentTimeMillis,
|
||||
List<Attachment> attachments,
|
||||
int subscriptionId,
|
||||
long expiresIn,
|
||||
boolean expirationUpdate,
|
||||
boolean viewOnce,
|
||||
boolean unidentified)
|
||||
{
|
||||
this.from = from;
|
||||
this.groupId = groupId.orNull();
|
||||
this.sentTimeMillis = sentTimeMillis;
|
||||
this.body = body;
|
||||
this.push = false;
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.expiresIn = expiresIn;
|
||||
this.expirationUpdate = expirationUpdate;
|
||||
this.viewOnce = viewOnce;
|
||||
this.quote = null;
|
||||
this.unidentified = unidentified;
|
||||
|
||||
this.attachments.addAll(attachments);
|
||||
}
|
||||
|
||||
public IncomingMediaMessage(@NonNull RecipientId from,
|
||||
long sentTimeMillis,
|
||||
int subscriptionId,
|
||||
long expiresIn,
|
||||
boolean expirationUpdate,
|
||||
boolean viewOnce,
|
||||
boolean unidentified,
|
||||
Optional<String> body,
|
||||
Optional<SignalServiceGroup> group,
|
||||
Optional<List<SignalServiceAttachment>> attachments,
|
||||
Optional<QuoteModel> quote,
|
||||
Optional<List<Contact>> sharedContacts,
|
||||
Optional<List<LinkPreview>> linkPreviews,
|
||||
Optional<Attachment> sticker)
|
||||
{
|
||||
this.push = true;
|
||||
this.from = from;
|
||||
this.sentTimeMillis = sentTimeMillis;
|
||||
this.body = body.orNull();
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.expiresIn = expiresIn;
|
||||
this.expirationUpdate = expirationUpdate;
|
||||
this.viewOnce = viewOnce;
|
||||
this.quote = quote.orNull();
|
||||
this.unidentified = unidentified;
|
||||
|
||||
if (group.isPresent()) this.groupId = GroupUtil.getEncodedId(group.get().getGroupId(), false);
|
||||
else this.groupId = null;
|
||||
|
||||
this.attachments.addAll(PointerAttachment.forPointers(attachments));
|
||||
this.sharedContacts.addAll(sharedContacts.or(Collections.emptyList()));
|
||||
this.linkPreviews.addAll(linkPreviews.or(Collections.emptyList()));
|
||||
|
||||
if (sticker.isPresent()) {
|
||||
this.attachments.add(sticker.get());
|
||||
}
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
return subscriptionId;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public List<Attachment> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getFrom() {
|
||||
return from;
|
||||
}
|
||||
|
||||
public String getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public boolean isPushMessage() {
|
||||
return push;
|
||||
}
|
||||
|
||||
public boolean isExpirationUpdate() {
|
||||
return expirationUpdate;
|
||||
}
|
||||
|
||||
public long getSentTimeMillis() {
|
||||
return sentTimeMillis;
|
||||
}
|
||||
|
||||
public long getExpiresIn() {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public boolean isViewOnce() {
|
||||
return viewOnce;
|
||||
}
|
||||
|
||||
public boolean isGroupMessage() {
|
||||
return groupId != null;
|
||||
}
|
||||
|
||||
public QuoteModel getQuote() {
|
||||
return quote;
|
||||
}
|
||||
|
||||
public List<Contact> getSharedContacts() {
|
||||
return sharedContacts;
|
||||
}
|
||||
|
||||
public List<LinkPreview> getLinkPreviews() {
|
||||
return linkPreviews;
|
||||
}
|
||||
|
||||
public boolean isUnidentified() {
|
||||
return unidentified;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.mms.pdu_alt.RetrieveConf;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public interface IncomingMmsConnection {
|
||||
@Nullable
|
||||
RetrieveConf retrieve(@NonNull String contentLocation, byte[] transactionId, int subscriptionId) throws MmsException, MmsRadioException, ApnUnavailableException, IOException;
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.auth.AuthScope;
|
||||
import org.apache.http.auth.UsernamePasswordCredentials;
|
||||
import org.apache.http.client.CredentialsProvider;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.impl.NoConnectionReuseStrategyHC4;
|
||||
import org.apache.http.impl.client.BasicCredentialsProvider;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.impl.client.LaxRedirectStrategy;
|
||||
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
|
||||
import org.apache.http.message.BasicHeader;
|
||||
import org.thoughtcrime.securesms.database.ApnDatabase;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TelephonyUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URL;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public abstract class LegacyMmsConnection {
|
||||
|
||||
public static final String USER_AGENT = "Android-Mms/2.0";
|
||||
|
||||
private static final String TAG = LegacyMmsConnection.class.getSimpleName();
|
||||
|
||||
protected final Context context;
|
||||
protected final Apn apn;
|
||||
|
||||
protected LegacyMmsConnection(Context context) throws ApnUnavailableException {
|
||||
this.context = context;
|
||||
this.apn = getApn(context);
|
||||
}
|
||||
|
||||
public static Apn getApn(Context context) throws ApnUnavailableException {
|
||||
|
||||
try {
|
||||
Optional<Apn> params = ApnDatabase.getInstance(context)
|
||||
.getMmsConnectionParameters(TelephonyUtil.getMccMnc(context),
|
||||
TelephonyUtil.getApn(context));
|
||||
|
||||
if (!params.isPresent()) {
|
||||
throw new ApnUnavailableException("No parameters available from ApnDefaults.");
|
||||
}
|
||||
|
||||
return params.get();
|
||||
} catch (IOException ioe) {
|
||||
throw new ApnUnavailableException("ApnDatabase threw an IOException", ioe);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isDirectConnect() {
|
||||
// We think Sprint supports direct connection over wifi/data, but not Verizon
|
||||
Set<String> sprintMccMncs = new HashSet<String>() {{
|
||||
add("312530");
|
||||
add("311880");
|
||||
add("311870");
|
||||
add("311490");
|
||||
add("310120");
|
||||
add("316010");
|
||||
add("312190");
|
||||
}};
|
||||
|
||||
return ServiceUtil.getTelephonyManager(context).getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA &&
|
||||
sprintMccMncs.contains(TelephonyUtil.getMccMnc(context));
|
||||
}
|
||||
|
||||
@SuppressWarnings("TryWithIdenticalCatches")
|
||||
protected static boolean checkRouteToHost(Context context, String host, boolean usingMmsRadio)
|
||||
throws IOException
|
||||
{
|
||||
InetAddress inetAddress = InetAddress.getByName(host);
|
||||
if (!usingMmsRadio) {
|
||||
if (inetAddress.isSiteLocalAddress()) {
|
||||
throw new IOException("RFC1918 address in non-MMS radio situation!");
|
||||
}
|
||||
Log.w(TAG, "returning vacuous success since MMS radio is not in use");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (inetAddress == null) {
|
||||
throw new IOException("Unable to lookup host: InetAddress.getByName() returned null.");
|
||||
}
|
||||
|
||||
byte[] ipAddressBytes = inetAddress.getAddress();
|
||||
if (ipAddressBytes == null) {
|
||||
Log.w(TAG, "resolved IP address bytes are null, returning true to attempt a connection anyway.");
|
||||
return true;
|
||||
}
|
||||
|
||||
Log.i(TAG, "Checking route to address: " + host + ", " + inetAddress.getHostAddress());
|
||||
ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
|
||||
try {
|
||||
final Method requestRouteMethod = manager.getClass().getMethod("requestRouteToHostAddress", Integer.TYPE, InetAddress.class);
|
||||
final boolean routeToHostObtained = (Boolean) requestRouteMethod.invoke(manager, MmsRadio.TYPE_MOBILE_MMS, inetAddress);
|
||||
Log.i(TAG, "requestRouteToHostAddress(" + inetAddress + ") -> " + routeToHostObtained);
|
||||
return routeToHostObtained;
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
Log.w(TAG, nsme);
|
||||
} catch (IllegalAccessException iae) {
|
||||
Log.w(TAG, iae);
|
||||
} catch (InvocationTargetException ite) {
|
||||
Log.w(TAG, ite);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static byte[] parseResponse(InputStream is) throws IOException {
|
||||
InputStream in = new BufferedInputStream(is);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
Util.copy(in, baos);
|
||||
|
||||
Log.i(TAG, "Received full server response, " + baos.size() + " bytes");
|
||||
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
protected CloseableHttpClient constructHttpClient() throws IOException {
|
||||
RequestConfig config = RequestConfig.custom()
|
||||
.setConnectTimeout(20 * 1000)
|
||||
.setConnectionRequestTimeout(20 * 1000)
|
||||
.setSocketTimeout(20 * 1000)
|
||||
.setMaxRedirects(20)
|
||||
.build();
|
||||
|
||||
URL mmsc = new URL(apn.getMmsc());
|
||||
CredentialsProvider credsProvider = new BasicCredentialsProvider();
|
||||
|
||||
if (apn.hasAuthentication()) {
|
||||
credsProvider.setCredentials(new AuthScope(mmsc.getHost(), mmsc.getPort() > -1 ? mmsc.getPort() : mmsc.getDefaultPort()),
|
||||
new UsernamePasswordCredentials(apn.getUsername(), apn.getPassword()));
|
||||
}
|
||||
|
||||
return HttpClients.custom()
|
||||
.setConnectionReuseStrategy(new NoConnectionReuseStrategyHC4())
|
||||
.setRedirectStrategy(new LaxRedirectStrategy())
|
||||
.setUserAgent(TextSecurePreferences.getMmsUserAgent(context, USER_AGENT))
|
||||
.setConnectionManager(new BasicHttpClientConnectionManager())
|
||||
.setDefaultRequestConfig(config)
|
||||
.setDefaultCredentialsProvider(credsProvider)
|
||||
.build();
|
||||
}
|
||||
|
||||
protected byte[] execute(HttpUriRequest request) throws IOException {
|
||||
Log.i(TAG, "connecting to " + apn.getMmsc());
|
||||
|
||||
CloseableHttpClient client = null;
|
||||
CloseableHttpResponse response = null;
|
||||
try {
|
||||
client = constructHttpClient();
|
||||
response = client.execute(request);
|
||||
|
||||
Log.i(TAG, "* response code: " + response.getStatusLine());
|
||||
|
||||
if (response.getStatusLine().getStatusCode() == 200) {
|
||||
return parseResponse(response.getEntity().getContent());
|
||||
}
|
||||
} catch (NullPointerException npe) {
|
||||
// TODO determine root cause
|
||||
// see: https://github.com/signalapp/Signal-Android/issues/4379
|
||||
throw new IOException(npe);
|
||||
} finally {
|
||||
if (response != null) response.close();
|
||||
if (client != null) client.close();
|
||||
}
|
||||
|
||||
throw new IOException("unhandled response code");
|
||||
}
|
||||
|
||||
protected List<Header> getBaseHeaders() {
|
||||
final String number = getLine1Number(context);
|
||||
|
||||
return new LinkedList<Header>() {{
|
||||
add(new BasicHeader("Accept", "*/*, application/vnd.wap.mms-message, application/vnd.wap.sic"));
|
||||
add(new BasicHeader("x-wap-profile", "http://www.google.com/oha/rdf/ua-profile-kila.xml"));
|
||||
add(new BasicHeader("Content-Type", "application/vnd.wap.mms-message"));
|
||||
add(new BasicHeader("x-carrier-magic", "http://magic.google.com"));
|
||||
if (!TextUtils.isEmpty(number)) {
|
||||
add(new BasicHeader("x-up-calling-line-id", number));
|
||||
add(new BasicHeader("X-MDN", number));
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
@SuppressLint("HardwareIds")
|
||||
private static String getLine1Number(@NonNull Context context) {
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_SMS) == PackageManager.PERMISSION_GRANTED ||
|
||||
ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_NUMBERS) == PackageManager.PERMISSION_GRANTED ||
|
||||
ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {
|
||||
return TelephonyUtil.getManager(context).getLine1Number();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static class Apn {
|
||||
|
||||
public static Apn EMPTY = new Apn("", "", "", "", "");
|
||||
|
||||
private final String mmsc;
|
||||
private final String proxy;
|
||||
private final String port;
|
||||
private final String username;
|
||||
private final String password;
|
||||
|
||||
public Apn(String mmsc, String proxy, String port, String username, String password) {
|
||||
this.mmsc = mmsc;
|
||||
this.proxy = proxy;
|
||||
this.port = port;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public Apn(Apn customApn, Apn defaultApn,
|
||||
boolean useCustomMmsc,
|
||||
boolean useCustomProxy,
|
||||
boolean useCustomProxyPort,
|
||||
boolean useCustomUsername,
|
||||
boolean useCustomPassword)
|
||||
{
|
||||
this.mmsc = useCustomMmsc ? customApn.mmsc : defaultApn.mmsc;
|
||||
this.proxy = useCustomProxy ? customApn.proxy : defaultApn.proxy;
|
||||
this.port = useCustomProxyPort ? customApn.port : defaultApn.port;
|
||||
this.username = useCustomUsername ? customApn.username : defaultApn.username;
|
||||
this.password = useCustomPassword ? customApn.password : defaultApn.password;
|
||||
}
|
||||
|
||||
public boolean hasProxy() {
|
||||
return !TextUtils.isEmpty(proxy);
|
||||
}
|
||||
|
||||
public String getMmsc() {
|
||||
return mmsc;
|
||||
}
|
||||
|
||||
public String getProxy() {
|
||||
return hasProxy() ? proxy : null;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return TextUtils.isEmpty(port) ? 80 : Integer.parseInt(port);
|
||||
}
|
||||
|
||||
public boolean hasAuthentication() {
|
||||
return !TextUtils.isEmpty(username);
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return Apn.class.getSimpleName() +
|
||||
"{ mmsc: \"" + mmsc + "\"" +
|
||||
", proxy: " + (proxy == null ? "none" : '"' + proxy + '"') +
|
||||
", port: " + (port == null ? "(none)" : port) +
|
||||
", user: " + (username == null ? "none" : '"' + username + '"') +
|
||||
", pass: " + (password == null ? "none" : '"' + password + '"') + " }";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.components.location.SignalPlace;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
public class LocationSlide extends ImageSlide {
|
||||
|
||||
@NonNull
|
||||
private final SignalPlace place;
|
||||
|
||||
public LocationSlide(@NonNull Context context, @NonNull Uri uri, long size, @NonNull SignalPlace place)
|
||||
{
|
||||
super(context, uri, size, 0, 0, null);
|
||||
this.place = place;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Optional<String> getBody() {
|
||||
return Optional.of(place.getDescription());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public SignalPlace getPlace() {
|
||||
return place;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLocation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public abstract class LollipopMmsConnection extends BroadcastReceiver {
|
||||
private static final String TAG = LollipopMmsConnection.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final String action;
|
||||
|
||||
private boolean resultAvailable;
|
||||
|
||||
public abstract void onResult(Context context, Intent intent);
|
||||
|
||||
protected LollipopMmsConnection(Context context, String action) {
|
||||
super();
|
||||
this.context = context;
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onReceive(Context context, Intent intent) {
|
||||
Log.i(TAG, "onReceive()");
|
||||
if (!action.equals(intent.getAction())) {
|
||||
Log.w(TAG, "received broadcast with unexpected action " + intent.getAction());
|
||||
return;
|
||||
}
|
||||
|
||||
onResult(context, intent);
|
||||
|
||||
resultAvailable = true;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
protected void beginTransaction() {
|
||||
getContext().getApplicationContext().registerReceiver(this, new IntentFilter(action));
|
||||
}
|
||||
|
||||
protected void endTransaction() {
|
||||
getContext().getApplicationContext().unregisterReceiver(this);
|
||||
resultAvailable = false;
|
||||
}
|
||||
|
||||
protected void waitForResult() throws TimeoutException {
|
||||
long timeoutExpiration = System.currentTimeMillis() + 60000;
|
||||
while (!resultAvailable) {
|
||||
Util.wait(this, Math.max(1, timeoutExpiration - System.currentTimeMillis()));
|
||||
if (System.currentTimeMillis() >= timeoutExpiration) {
|
||||
throw new TimeoutException("timeout when waiting for MMS");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected PendingIntent getPendingIntent() {
|
||||
return PendingIntent.getBroadcast(getContext(), 1, new Intent(action), PendingIntent.FLAG_ONE_SHOT);
|
||||
}
|
||||
|
||||
protected Context getContext() {
|
||||
return context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.MemoryFileDescriptor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public abstract class MediaConstraints {
|
||||
private static final String TAG = MediaConstraints.class.getSimpleName();
|
||||
|
||||
public static MediaConstraints getPushMediaConstraints() {
|
||||
return new PushMediaConstraints();
|
||||
}
|
||||
|
||||
public static MediaConstraints getMmsMediaConstraints(int subscriptionId) {
|
||||
return new MmsMediaConstraints(subscriptionId);
|
||||
}
|
||||
|
||||
public abstract int getImageMaxWidth(Context context);
|
||||
public abstract int getImageMaxHeight(Context context);
|
||||
public abstract int getImageMaxSize(Context context);
|
||||
|
||||
public abstract int getGifMaxSize(Context context);
|
||||
public abstract int getVideoMaxSize(Context context);
|
||||
|
||||
public int getUncompressedVideoMaxSize(Context context) {
|
||||
return getVideoMaxSize(context);
|
||||
}
|
||||
|
||||
public int getCompressedVideoMaxSize(Context context) {
|
||||
return getVideoMaxSize(context);
|
||||
}
|
||||
|
||||
public abstract int getAudioMaxSize(Context context);
|
||||
public abstract int getDocumentMaxSize(Context context);
|
||||
|
||||
public boolean isSatisfied(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
try {
|
||||
return (MediaUtil.isGif(attachment) && attachment.getSize() <= getGifMaxSize(context) && isWithinBounds(context, attachment.getDataUri())) ||
|
||||
(MediaUtil.isImage(attachment) && attachment.getSize() <= getImageMaxSize(context) && isWithinBounds(context, attachment.getDataUri())) ||
|
||||
(MediaUtil.isAudio(attachment) && attachment.getSize() <= getAudioMaxSize(context)) ||
|
||||
(MediaUtil.isVideo(attachment) && attachment.getSize() <= getVideoMaxSize(context)) ||
|
||||
(MediaUtil.isFile(attachment) && attachment.getSize() <= getDocumentMaxSize(context));
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "Failed to determine if media's constraints are satisfied.", ioe);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isWithinBounds(Context context, Uri uri) throws IOException {
|
||||
try {
|
||||
InputStream is = PartAuthority.getAttachmentStream(context, uri);
|
||||
Pair<Integer, Integer> dimensions = BitmapUtil.getDimensions(is);
|
||||
return dimensions.first > 0 && dimensions.first <= getImageMaxWidth(context) &&
|
||||
dimensions.second > 0 && dimensions.second <= getImageMaxHeight(context);
|
||||
} catch (BitmapDecodingException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canResize(@NonNull Attachment attachment) {
|
||||
return MediaUtil.isImage(attachment) && !MediaUtil.isGif(attachment) ||
|
||||
MediaUtil.isVideo(attachment) && isVideoTranscodeAvailable();
|
||||
}
|
||||
|
||||
public static boolean isVideoTranscodeAvailable() {
|
||||
return Build.VERSION.SDK_INT >= 26 && MemoryFileDescriptor.supported();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
public class MediaNotFoundException extends Exception {
|
||||
|
||||
public MediaNotFoundException() {
|
||||
}
|
||||
|
||||
public MediaNotFoundException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
public MediaNotFoundException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
|
||||
public MediaNotFoundException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public class MediaStream {
|
||||
private final InputStream stream;
|
||||
private final String mimeType;
|
||||
private final int width;
|
||||
private final int height;
|
||||
|
||||
public MediaStream(InputStream stream, String mimeType, int width, int height) {
|
||||
this.stream = stream;
|
||||
this.mimeType = mimeType;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public InputStream getStream() {
|
||||
return stream;
|
||||
}
|
||||
|
||||
public String getMimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
public class MediaTooLargeException extends Exception {
|
||||
|
||||
public MediaTooLargeException() {
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public MediaTooLargeException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public MediaTooLargeException(Throwable throwable) {
|
||||
super(throwable);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public MediaTooLargeException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.android.mms.service_alt.MmsConfig;
|
||||
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
final class MmsConfigManager {
|
||||
|
||||
private static final Map<Integer, MmsConfig> mmsConfigMap = new HashMap<>();
|
||||
|
||||
@WorkerThread
|
||||
synchronized static @NonNull MmsConfig getMmsConfig(Context context, int subscriptionId) {
|
||||
MmsConfig mmsConfig = mmsConfigMap.get(subscriptionId);
|
||||
if (mmsConfig != null) {
|
||||
return mmsConfig;
|
||||
}
|
||||
|
||||
MmsConfig loadedConfig = loadMmsConfig(context, subscriptionId);
|
||||
|
||||
mmsConfigMap.put(subscriptionId, loadedConfig);
|
||||
|
||||
return loadedConfig;
|
||||
}
|
||||
|
||||
private static @NonNull MmsConfig loadMmsConfig(Context context, int subscriptionId) {
|
||||
Optional<SubscriptionInfoCompat> subscriptionInfo = new SubscriptionManagerCompat(context).getActiveSubscriptionInfo(subscriptionId);
|
||||
|
||||
if (subscriptionInfo.isPresent()) {
|
||||
SubscriptionInfoCompat subscriptionInfoCompat = subscriptionInfo.get();
|
||||
Configuration configuration = context.getResources().getConfiguration();
|
||||
configuration.mcc = subscriptionInfoCompat.getMcc();
|
||||
configuration.mnc = subscriptionInfoCompat.getMnc();
|
||||
|
||||
Context subContext = context.createConfigurationContext(configuration);
|
||||
return new MmsConfig(subContext, subscriptionId);
|
||||
}
|
||||
|
||||
return new MmsConfig(context, subscriptionId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2007 Esmertec AG.
|
||||
* Copyright (C) 2007 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
/**
|
||||
* A generic exception that is thrown by the Mms client.
|
||||
*/
|
||||
public class MmsException extends Exception {
|
||||
private static final long serialVersionUID = -7323249827281485390L;
|
||||
|
||||
/**
|
||||
* Creates a new MmsException.
|
||||
*/
|
||||
public MmsException() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new MmsException with the specified detail message.
|
||||
*
|
||||
* @param message the detail message.
|
||||
*/
|
||||
public MmsException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new MmsException with the specified cause.
|
||||
*
|
||||
* @param cause the cause.
|
||||
*/
|
||||
public MmsException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new MmsException with the specified detail message and cause.
|
||||
*
|
||||
* @param message the detail message.
|
||||
* @param cause the cause.
|
||||
*/
|
||||
public MmsException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.android.mms.service_alt.MmsConfig;
|
||||
|
||||
final class MmsMediaConstraints extends MediaConstraints {
|
||||
|
||||
private final int subscriptionId;
|
||||
|
||||
private static final int MIN_IMAGE_DIMEN = 1024;
|
||||
|
||||
MmsMediaConstraints(int subscriptionId) {
|
||||
this.subscriptionId = subscriptionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageMaxWidth(Context context) {
|
||||
return Math.max(MIN_IMAGE_DIMEN, getOverriddenMmsConfig(context).getMaxImageWidth());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageMaxHeight(Context context) {
|
||||
return Math.max(MIN_IMAGE_DIMEN, getOverriddenMmsConfig(context).getMaxImageHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageMaxSize(Context context) {
|
||||
return getMaxMessageSize(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getGifMaxSize(Context context) {
|
||||
return getMaxMessageSize(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVideoMaxSize(Context context) {
|
||||
return getMaxMessageSize(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getUncompressedVideoMaxSize(Context context) {
|
||||
return Math.max(getVideoMaxSize(context), 15 * 1024 * 1024);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAudioMaxSize(Context context) {
|
||||
return getMaxMessageSize(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDocumentMaxSize(Context context) {
|
||||
return getMaxMessageSize(context);
|
||||
}
|
||||
|
||||
private int getMaxMessageSize(Context context) {
|
||||
return getOverriddenMmsConfig(context).getMaxMessageSize();
|
||||
}
|
||||
|
||||
private MmsConfig.Overridden getOverriddenMmsConfig(Context context) {
|
||||
MmsConfig mmsConfig = MmsConfigManager.getMmsConfig(context, subscriptionId);
|
||||
|
||||
return new MmsConfig.Overridden(mmsConfig, null);
|
||||
}
|
||||
}
|
||||
165
app/src/main/java/org/thoughtcrime/securesms/mms/MmsRadio.java
Normal file
165
app/src/main/java/org/thoughtcrime/securesms/mms/MmsRadio.java
Normal file
@@ -0,0 +1,165 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.PowerManager;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class MmsRadio {
|
||||
|
||||
private static final String TAG = MmsRadio.class.getSimpleName();
|
||||
|
||||
private static MmsRadio instance;
|
||||
|
||||
public static synchronized MmsRadio getInstance(Context context) {
|
||||
if (instance == null)
|
||||
instance = new MmsRadio(context.getApplicationContext());
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
private static final String FEATURE_ENABLE_MMS = "enableMMS";
|
||||
private static final int APN_ALREADY_ACTIVE = 0;
|
||||
public static final int TYPE_MOBILE_MMS = 2;
|
||||
|
||||
private final Context context;
|
||||
|
||||
private ConnectivityManager connectivityManager;
|
||||
private ConnectivityListener connectivityListener;
|
||||
private PowerManager.WakeLock wakeLock;
|
||||
private int connectedCounter = 0;
|
||||
|
||||
private MmsRadio(Context context) {
|
||||
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||
this.context = context;
|
||||
this.connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
this.wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "signal:mms");
|
||||
this.wakeLock.setReferenceCounted(true);
|
||||
}
|
||||
|
||||
public synchronized void disconnect() {
|
||||
Log.i(TAG, "MMS Radio Disconnect Called...");
|
||||
wakeLock.release();
|
||||
connectedCounter--;
|
||||
|
||||
Log.i(TAG, "Reference count: " + connectedCounter);
|
||||
|
||||
if (connectedCounter == 0) {
|
||||
Log.i(TAG, "Turning off MMS radio...");
|
||||
try {
|
||||
final Method stopUsingNetworkFeatureMethod = connectivityManager.getClass().getMethod("stopUsingNetworkFeature", Integer.TYPE, String.class);
|
||||
stopUsingNetworkFeatureMethod.invoke(connectivityManager, ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS);
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
Log.w(TAG, nsme);
|
||||
} catch (IllegalAccessException iae) {
|
||||
Log.w(TAG, iae);
|
||||
} catch (InvocationTargetException ite) {
|
||||
Log.w(TAG, ite);
|
||||
}
|
||||
|
||||
if (connectivityListener != null) {
|
||||
Log.i(TAG, "Unregistering receiver...");
|
||||
context.unregisterReceiver(connectivityListener);
|
||||
connectivityListener = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void connect() throws MmsRadioException {
|
||||
int status;
|
||||
|
||||
try {
|
||||
final Method startUsingNetworkFeatureMethod = connectivityManager.getClass().getMethod("startUsingNetworkFeature", Integer.TYPE, String.class);
|
||||
status = (int)startUsingNetworkFeatureMethod.invoke(connectivityManager, ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS);
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
throw new MmsRadioException(nsme);
|
||||
} catch (IllegalAccessException iae) {
|
||||
throw new MmsRadioException(iae);
|
||||
} catch (InvocationTargetException ite) {
|
||||
throw new MmsRadioException(ite);
|
||||
}
|
||||
|
||||
Log.i(TAG, "startUsingNetworkFeature status: " + status);
|
||||
|
||||
if (status == APN_ALREADY_ACTIVE) {
|
||||
wakeLock.acquire();
|
||||
connectedCounter++;
|
||||
return;
|
||||
} else {
|
||||
wakeLock.acquire();
|
||||
connectedCounter++;
|
||||
|
||||
if (connectivityListener == null) {
|
||||
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
|
||||
connectivityListener = new ConnectivityListener();
|
||||
context.registerReceiver(connectivityListener, filter);
|
||||
}
|
||||
|
||||
Util.wait(this, 30000);
|
||||
|
||||
if (!isConnected()) {
|
||||
Log.w(TAG, "Got back from connectivity wait, and not connected...");
|
||||
disconnect();
|
||||
throw new MmsRadioException("Unable to successfully enable MMS radio.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isConnected() {
|
||||
NetworkInfo info = connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS);
|
||||
|
||||
Log.i(TAG, "Connected: " + info);
|
||||
|
||||
if ((info == null) || (info.getType() != TYPE_MOBILE_MMS) || !info.isConnected())
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isConnectivityPossible() {
|
||||
NetworkInfo networkInfo = connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS);
|
||||
|
||||
return networkInfo != null && networkInfo.isAvailable();
|
||||
}
|
||||
|
||||
private boolean isConnectivityFailure() {
|
||||
NetworkInfo networkInfo = connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS);
|
||||
|
||||
return networkInfo == null || networkInfo.getDetailedState() == NetworkInfo.DetailedState.FAILED;
|
||||
}
|
||||
|
||||
private synchronized void issueConnectivityChange() {
|
||||
if (isConnected()) {
|
||||
Log.i(TAG, "Notifying connected...");
|
||||
notifyAll();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isConnected() && (isConnectivityFailure() || !isConnectivityPossible())) {
|
||||
Log.i(TAG, "Notifying not connected...");
|
||||
notifyAll();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private class ConnectivityListener extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.i(TAG, "Got connectivity change...");
|
||||
issueConnectivityChange();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
public class MmsRadioException extends Throwable {
|
||||
public MmsRadioException(String s) {
|
||||
super(s);
|
||||
}
|
||||
|
||||
public MmsRadioException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
public class MmsSendResult {
|
||||
|
||||
private final byte[] messageId;
|
||||
private final int responseStatus;
|
||||
|
||||
public MmsSendResult(byte[] messageId, int responseStatus) {
|
||||
this.messageId = messageId;
|
||||
this.responseStatus = responseStatus;
|
||||
}
|
||||
|
||||
public int getResponseStatus() {
|
||||
return responseStatus;
|
||||
}
|
||||
|
||||
public byte[] getMessageId() {
|
||||
return messageId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
|
||||
public class MmsSlide extends ImageSlide {
|
||||
|
||||
public MmsSlide(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
super(context, attachment);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getContentDescription() {
|
||||
return "MMS";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
|
||||
public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage {
|
||||
|
||||
public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn) {
|
||||
super(recipient, "", new LinkedList<Attachment>(), sentTimeMillis,
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, false, null, Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpirationUpdate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
||||
|
||||
private final GroupContext group;
|
||||
|
||||
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
|
||||
@NonNull String encodedGroupContext,
|
||||
@NonNull List<Attachment> avatar,
|
||||
long sentTimeMillis,
|
||||
long expiresIn,
|
||||
boolean viewOnce,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
throws IOException
|
||||
{
|
||||
super(recipient, encodedGroupContext, avatar, sentTimeMillis,
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, viewOnce, quote, contacts, previews);
|
||||
|
||||
this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext));
|
||||
}
|
||||
|
||||
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
|
||||
@NonNull GroupContext group,
|
||||
@Nullable final Attachment avatar,
|
||||
long sentTimeMillis,
|
||||
long expireIn,
|
||||
boolean viewOnce,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
{
|
||||
super(recipient, Base64.encodeBytes(group.toByteArray()),
|
||||
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
||||
System.currentTimeMillis(),
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, viewOnce, quote, contacts, previews);
|
||||
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGroup() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isGroupUpdate() {
|
||||
return group.getType().getNumber() == GroupContext.Type.UPDATE_VALUE;
|
||||
}
|
||||
|
||||
public boolean isGroupQuit() {
|
||||
return group.getType().getNumber() == GroupContext.Type.QUIT_VALUE;
|
||||
}
|
||||
|
||||
public GroupContext getGroupContext() {
|
||||
return group;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import com.google.android.mms.pdu_alt.PduParser;
|
||||
import com.google.android.mms.pdu_alt.SendConf;
|
||||
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.methods.HttpPostHC4;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.entity.ByteArrayEntityHC4;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class OutgoingLegacyMmsConnection extends LegacyMmsConnection implements OutgoingMmsConnection {
|
||||
private final static String TAG = OutgoingLegacyMmsConnection.class.getSimpleName();
|
||||
|
||||
public OutgoingLegacyMmsConnection(Context context) throws ApnUnavailableException {
|
||||
super(context);
|
||||
}
|
||||
|
||||
private HttpUriRequest constructRequest(byte[] pduBytes, boolean useProxy)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
HttpPostHC4 request = new HttpPostHC4(apn.getMmsc());
|
||||
for (Header header : getBaseHeaders()) {
|
||||
request.addHeader(header);
|
||||
}
|
||||
|
||||
request.setEntity(new ByteArrayEntityHC4(pduBytes));
|
||||
if (useProxy) {
|
||||
HttpHost proxy = new HttpHost(apn.getProxy(), apn.getPort());
|
||||
request.setConfig(RequestConfig.custom().setProxy(proxy).build());
|
||||
}
|
||||
return request;
|
||||
} catch (IllegalArgumentException iae) {
|
||||
throw new IOException(iae);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendNotificationReceived(byte[] pduBytes, boolean usingMmsRadio, boolean useProxyIfAvailable)
|
||||
throws IOException
|
||||
{
|
||||
sendBytes(pduBytes, usingMmsRadio, useProxyIfAvailable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SendConf send(@NonNull byte[] pduBytes, int subscriptionId) throws UndeliverableMessageException {
|
||||
try {
|
||||
MmsRadio radio = MmsRadio.getInstance(context);
|
||||
|
||||
if (isDirectConnect()) {
|
||||
Log.i(TAG, "Sending MMS directly without radio change...");
|
||||
try {
|
||||
return send(pduBytes, false, false);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "Sending MMS with radio change and proxy...");
|
||||
radio.connect();
|
||||
|
||||
try {
|
||||
try {
|
||||
return send(pduBytes, true, true);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Sending MMS with radio change and without proxy...");
|
||||
|
||||
try {
|
||||
return send(pduBytes, true, false);
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
throw new UndeliverableMessageException(ioe);
|
||||
}
|
||||
} finally {
|
||||
radio.disconnect();
|
||||
}
|
||||
|
||||
} catch (MmsRadioException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new UndeliverableMessageException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private SendConf send(byte[] pduBytes, boolean useMmsRadio, boolean useProxyIfAvailable) throws IOException {
|
||||
byte[] response = sendBytes(pduBytes, useMmsRadio, useProxyIfAvailable);
|
||||
return (SendConf) new PduParser(response).parse();
|
||||
}
|
||||
|
||||
private byte[] sendBytes(byte[] pduBytes, boolean useMmsRadio, boolean useProxyIfAvailable) throws IOException {
|
||||
final boolean useProxy = useProxyIfAvailable && apn.hasProxy();
|
||||
final String targetHost = useProxy
|
||||
? apn.getProxy()
|
||||
: Uri.parse(apn.getMmsc()).getHost();
|
||||
|
||||
Log.i(TAG, "Sending MMS of length: " + pduBytes.length
|
||||
+ (useMmsRadio ? ", using mms radio" : "")
|
||||
+ (useProxy ? ", using proxy" : ""));
|
||||
|
||||
try {
|
||||
if (checkRouteToHost(context, targetHost, useMmsRadio)) {
|
||||
Log.i(TAG, "got successful route to host " + targetHost);
|
||||
byte[] response = execute(constructRequest(pduBytes, useProxy));
|
||||
if (response != null) return response;
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
}
|
||||
throw new IOException("Connection manager could not obtain route to host.");
|
||||
}
|
||||
|
||||
|
||||
public static boolean isConnectionPossible(Context context) {
|
||||
try {
|
||||
ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo networkInfo = connectivityManager.getNetworkInfo(MmsRadio.TYPE_MOBILE_MMS);
|
||||
if (networkInfo == null) {
|
||||
Log.w(TAG, "MMS network info was null, unsupported by this device");
|
||||
return false;
|
||||
}
|
||||
|
||||
getApn(context);
|
||||
return true;
|
||||
} catch (ApnUnavailableException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.telephony.SmsManager;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import com.android.mms.service_alt.MmsConfig;
|
||||
import com.google.android.mms.pdu_alt.PduParser;
|
||||
import com.google.android.mms.pdu_alt.SendConf;
|
||||
|
||||
import org.thoughtcrime.securesms.providers.MmsBodyProvider;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public class OutgoingLollipopMmsConnection extends LollipopMmsConnection implements OutgoingMmsConnection {
|
||||
private static final String TAG = OutgoingLollipopMmsConnection.class.getSimpleName();
|
||||
private static final String ACTION = OutgoingLollipopMmsConnection.class.getCanonicalName() + "MMS_SENT_ACTION";
|
||||
|
||||
private byte[] response;
|
||||
|
||||
public OutgoingLollipopMmsConnection(Context context) {
|
||||
super(context, ACTION);
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.LOLLIPOP_MR1)
|
||||
@Override
|
||||
public synchronized void onResult(Context context, Intent intent) {
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP_MR1) {
|
||||
Log.i(TAG, "HTTP status: " + intent.getIntExtra(SmsManager.EXTRA_MMS_HTTP_STATUS, -1));
|
||||
}
|
||||
|
||||
response = intent.getByteArrayExtra(SmsManager.EXTRA_MMS_DATA);
|
||||
}
|
||||
|
||||
@Override
|
||||
@TargetApi(VERSION_CODES.LOLLIPOP)
|
||||
public @Nullable synchronized SendConf send(@NonNull byte[] pduBytes, int subscriptionId)
|
||||
throws UndeliverableMessageException
|
||||
{
|
||||
beginTransaction();
|
||||
try {
|
||||
MmsBodyProvider.Pointer pointer = MmsBodyProvider.makeTemporaryPointer(getContext());
|
||||
Util.copy(new ByteArrayInputStream(pduBytes), pointer.getOutputStream());
|
||||
|
||||
SmsManager smsManager;
|
||||
|
||||
if (VERSION.SDK_INT >= 22 && subscriptionId != -1) {
|
||||
smsManager = SmsManager.getSmsManagerForSubscriptionId(subscriptionId);
|
||||
} else {
|
||||
smsManager = SmsManager.getDefault();
|
||||
}
|
||||
|
||||
Bundle configOverrides = new Bundle();
|
||||
configOverrides.putBoolean(SmsManager.MMS_CONFIG_GROUP_MMS_ENABLED, true);
|
||||
|
||||
MmsConfig mmsConfig = MmsConfigManager.getMmsConfig(getContext(), subscriptionId);
|
||||
|
||||
if (mmsConfig != null) {
|
||||
MmsConfig.Overridden overridden = new MmsConfig.Overridden(mmsConfig, new Bundle());
|
||||
configOverrides.putString(SmsManager.MMS_CONFIG_HTTP_PARAMS, overridden.getHttpParams());
|
||||
configOverrides.putInt(SmsManager.MMS_CONFIG_MAX_MESSAGE_SIZE, overridden.getMaxMessageSize());
|
||||
}
|
||||
|
||||
smsManager.sendMultimediaMessage(getContext(),
|
||||
pointer.getUri(),
|
||||
null,
|
||||
configOverrides,
|
||||
getPendingIntent());
|
||||
|
||||
waitForResult();
|
||||
|
||||
Log.i(TAG, "MMS broadcast received and processed.");
|
||||
pointer.close();
|
||||
|
||||
if (response == null) {
|
||||
throw new UndeliverableMessageException("Null response.");
|
||||
}
|
||||
|
||||
return (SendConf) new PduParser(response).parse();
|
||||
} catch (IOException | TimeoutException e) {
|
||||
throw new UndeliverableMessageException(e);
|
||||
} finally {
|
||||
endTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class OutgoingMediaMessage {
|
||||
|
||||
private final Recipient recipient;
|
||||
protected final String body;
|
||||
protected final List<Attachment> attachments;
|
||||
private final long sentTimeMillis;
|
||||
private final int distributionType;
|
||||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final boolean viewOnce;
|
||||
private final QuoteModel outgoingQuote;
|
||||
|
||||
private final List<NetworkFailure> networkFailures = new LinkedList<>();
|
||||
private final List<IdentityKeyMismatch> identityKeyMismatches = new LinkedList<>();
|
||||
private final List<Contact> contacts = new LinkedList<>();
|
||||
private final List<LinkPreview> linkPreviews = new LinkedList<>();
|
||||
|
||||
public OutgoingMediaMessage(Recipient recipient, String message,
|
||||
List<Attachment> attachments, long sentTimeMillis,
|
||||
int subscriptionId, long expiresIn, boolean viewOnce,
|
||||
int distributionType,
|
||||
@Nullable QuoteModel outgoingQuote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> linkPreviews,
|
||||
@NonNull List<NetworkFailure> networkFailures,
|
||||
@NonNull List<IdentityKeyMismatch> identityKeyMismatches)
|
||||
{
|
||||
this.recipient = recipient;
|
||||
this.body = message;
|
||||
this.sentTimeMillis = sentTimeMillis;
|
||||
this.distributionType = distributionType;
|
||||
this.attachments = attachments;
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.expiresIn = expiresIn;
|
||||
this.viewOnce = viewOnce;
|
||||
this.outgoingQuote = outgoingQuote;
|
||||
|
||||
this.contacts.addAll(contacts);
|
||||
this.linkPreviews.addAll(linkPreviews);
|
||||
this.networkFailures.addAll(networkFailures);
|
||||
this.identityKeyMismatches.addAll(identityKeyMismatches);
|
||||
}
|
||||
|
||||
public OutgoingMediaMessage(Recipient recipient, SlideDeck slideDeck, String message,
|
||||
long sentTimeMillis, int subscriptionId, long expiresIn,
|
||||
boolean viewOnce, int distributionType,
|
||||
@Nullable QuoteModel outgoingQuote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> linkPreviews)
|
||||
{
|
||||
this(recipient,
|
||||
buildMessage(slideDeck, message),
|
||||
slideDeck.asAttachments(),
|
||||
sentTimeMillis, subscriptionId,
|
||||
expiresIn, viewOnce, distributionType, outgoingQuote,
|
||||
contacts, linkPreviews, new LinkedList<>(), new LinkedList<>());
|
||||
}
|
||||
|
||||
public OutgoingMediaMessage(OutgoingMediaMessage that) {
|
||||
this.recipient = that.getRecipient();
|
||||
this.body = that.body;
|
||||
this.distributionType = that.distributionType;
|
||||
this.attachments = that.attachments;
|
||||
this.sentTimeMillis = that.sentTimeMillis;
|
||||
this.subscriptionId = that.subscriptionId;
|
||||
this.expiresIn = that.expiresIn;
|
||||
this.viewOnce = that.viewOnce;
|
||||
this.outgoingQuote = that.outgoingQuote;
|
||||
|
||||
this.identityKeyMismatches.addAll(that.identityKeyMismatches);
|
||||
this.networkFailures.addAll(that.networkFailures);
|
||||
this.contacts.addAll(that.contacts);
|
||||
this.linkPreviews.addAll(that.linkPreviews);
|
||||
}
|
||||
|
||||
public Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public List<Attachment> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public int getDistributionType() {
|
||||
return distributionType;
|
||||
}
|
||||
|
||||
public boolean isSecure() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isGroup() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isExpirationUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public long getSentTimeMillis() {
|
||||
return sentTimeMillis;
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
return subscriptionId;
|
||||
}
|
||||
|
||||
public long getExpiresIn() {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public boolean isViewOnce() {
|
||||
return viewOnce;
|
||||
}
|
||||
|
||||
public @Nullable QuoteModel getOutgoingQuote() {
|
||||
return outgoingQuote;
|
||||
}
|
||||
|
||||
public @NonNull List<Contact> getSharedContacts() {
|
||||
return contacts;
|
||||
}
|
||||
|
||||
public @NonNull List<LinkPreview> getLinkPreviews() {
|
||||
return linkPreviews;
|
||||
}
|
||||
|
||||
public @NonNull List<NetworkFailure> getNetworkFailures() {
|
||||
return networkFailures;
|
||||
}
|
||||
|
||||
public @NonNull List<IdentityKeyMismatch> getIdentityKeyMismatches() {
|
||||
return identityKeyMismatches;
|
||||
}
|
||||
|
||||
private static String buildMessage(SlideDeck slideDeck, String message) {
|
||||
if (!TextUtils.isEmpty(message) && !TextUtils.isEmpty(slideDeck.getBody())) {
|
||||
return slideDeck.getBody() + "\n\n" + message;
|
||||
} else if (!TextUtils.isEmpty(message)) {
|
||||
return message;
|
||||
} else {
|
||||
return slideDeck.getBody();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.mms.pdu_alt.SendConf;
|
||||
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
|
||||
|
||||
public interface OutgoingMmsConnection {
|
||||
@Nullable
|
||||
SendConf send(@NonNull byte[] pduBytes, int subscriptionId) throws UndeliverableMessageException;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
|
||||
|
||||
public OutgoingSecureMediaMessage(Recipient recipient, String body,
|
||||
List<Attachment> attachments,
|
||||
long sentTimeMillis,
|
||||
int distributionType,
|
||||
long expiresIn,
|
||||
boolean viewOnce,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
{
|
||||
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, viewOnce, distributionType, quote, contacts, previews, Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {
|
||||
super(base);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSecure() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.UriMatcher;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.providers.DeprecatedPersistentBlobProvider;
|
||||
import org.thoughtcrime.securesms.providers.PartProvider;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class PartAuthority {
|
||||
|
||||
private static final String PART_URI_STRING = "content://org.thoughtcrime.securesms/part";
|
||||
private static final String THUMB_URI_STRING = "content://org.thoughtcrime.securesms/thumb";
|
||||
private static final String STICKER_URI_STRING = "content://org.thoughtcrime.securesms/sticker";
|
||||
private static final Uri PART_CONTENT_URI = Uri.parse(PART_URI_STRING);
|
||||
private static final Uri THUMB_CONTENT_URI = Uri.parse(THUMB_URI_STRING);
|
||||
private static final Uri STICKER_CONTENT_URI = Uri.parse(STICKER_URI_STRING);
|
||||
|
||||
private static final int PART_ROW = 1;
|
||||
private static final int THUMB_ROW = 2;
|
||||
private static final int PERSISTENT_ROW = 3;
|
||||
private static final int BLOB_ROW = 4;
|
||||
private static final int STICKER_ROW = 5;
|
||||
|
||||
private static final UriMatcher uriMatcher;
|
||||
|
||||
static {
|
||||
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
uriMatcher.addURI("org.thoughtcrime.securesms", "part/*/#", PART_ROW);
|
||||
uriMatcher.addURI("org.thoughtcrime.securesms", "thumb/*/#", THUMB_ROW);
|
||||
uriMatcher.addURI("org.thoughtcrime.securesms", "sticker/#", STICKER_ROW);
|
||||
uriMatcher.addURI(DeprecatedPersistentBlobProvider.AUTHORITY, DeprecatedPersistentBlobProvider.EXPECTED_PATH_OLD, PERSISTENT_ROW);
|
||||
uriMatcher.addURI(DeprecatedPersistentBlobProvider.AUTHORITY, DeprecatedPersistentBlobProvider.EXPECTED_PATH_NEW, PERSISTENT_ROW);
|
||||
uriMatcher.addURI(BlobProvider.AUTHORITY, BlobProvider.PATH, BLOB_ROW);
|
||||
}
|
||||
|
||||
public static InputStream getAttachmentThumbnailStream(@NonNull Context context, @NonNull Uri uri)
|
||||
throws IOException
|
||||
{
|
||||
String contentType = getAttachmentContentType(context, uri);
|
||||
int match = uriMatcher.match(uri);
|
||||
|
||||
if (match == PART_ROW && MediaUtil.isVideoType(contentType)) {
|
||||
return DatabaseFactory.getAttachmentDatabase(context).getThumbnailStream(new PartUriParser(uri).getPartId());
|
||||
}
|
||||
|
||||
return getAttachmentStream(context, uri);
|
||||
}
|
||||
|
||||
public static InputStream getAttachmentStream(@NonNull Context context, @NonNull Uri uri)
|
||||
throws IOException
|
||||
{
|
||||
int match = uriMatcher.match(uri);
|
||||
try {
|
||||
switch (match) {
|
||||
case PART_ROW: return DatabaseFactory.getAttachmentDatabase(context).getAttachmentStream(new PartUriParser(uri).getPartId(), 0);
|
||||
case THUMB_ROW: return DatabaseFactory.getAttachmentDatabase(context).getThumbnailStream(new PartUriParser(uri).getPartId());
|
||||
case STICKER_ROW: return DatabaseFactory.getStickerDatabase(context).getStickerStream(ContentUris.parseId(uri));
|
||||
case PERSISTENT_ROW: return DeprecatedPersistentBlobProvider.getInstance(context).getStream(context, ContentUris.parseId(uri));
|
||||
case BLOB_ROW: return BlobProvider.getInstance().getStream(context, uri);
|
||||
default: return context.getContentResolver().openInputStream(uri);
|
||||
}
|
||||
} catch (SecurityException se) {
|
||||
throw new IOException(se);
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable String getAttachmentFileName(@NonNull Context context, @NonNull Uri uri) {
|
||||
int match = uriMatcher.match(uri);
|
||||
|
||||
switch (match) {
|
||||
case THUMB_ROW:
|
||||
case PART_ROW:
|
||||
Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(new PartUriParser(uri).getPartId());
|
||||
|
||||
if (attachment != null) return attachment.getFileName();
|
||||
else return null;
|
||||
case PERSISTENT_ROW:
|
||||
return DeprecatedPersistentBlobProvider.getFileName(context, uri);
|
||||
case BLOB_ROW:
|
||||
return BlobProvider.getFileName(uri);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable Long getAttachmentSize(@NonNull Context context, @NonNull Uri uri) {
|
||||
int match = uriMatcher.match(uri);
|
||||
|
||||
switch (match) {
|
||||
case THUMB_ROW:
|
||||
case PART_ROW:
|
||||
Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(new PartUriParser(uri).getPartId());
|
||||
|
||||
if (attachment != null) return attachment.getSize();
|
||||
else return null;
|
||||
case PERSISTENT_ROW:
|
||||
return DeprecatedPersistentBlobProvider.getFileSize(context, uri);
|
||||
case BLOB_ROW:
|
||||
return BlobProvider.getFileSize(uri);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable String getAttachmentContentType(@NonNull Context context, @NonNull Uri uri) {
|
||||
int match = uriMatcher.match(uri);
|
||||
|
||||
switch (match) {
|
||||
case THUMB_ROW:
|
||||
case PART_ROW:
|
||||
Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(new PartUriParser(uri).getPartId());
|
||||
|
||||
if (attachment != null) return attachment.getContentType();
|
||||
else return null;
|
||||
case PERSISTENT_ROW:
|
||||
return DeprecatedPersistentBlobProvider.getMimeType(context, uri);
|
||||
case BLOB_ROW:
|
||||
return BlobProvider.getMimeType(uri);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Uri getAttachmentPublicUri(Uri uri) {
|
||||
PartUriParser partUri = new PartUriParser(uri);
|
||||
return PartProvider.getContentUri(partUri.getPartId());
|
||||
}
|
||||
|
||||
public static Uri getAttachmentDataUri(AttachmentId attachmentId) {
|
||||
Uri uri = Uri.withAppendedPath(PART_CONTENT_URI, String.valueOf(attachmentId.getUniqueId()));
|
||||
return ContentUris.withAppendedId(uri, attachmentId.getRowId());
|
||||
}
|
||||
|
||||
public static Uri getAttachmentThumbnailUri(AttachmentId attachmentId) {
|
||||
Uri uri = Uri.withAppendedPath(THUMB_CONTENT_URI, String.valueOf(attachmentId.getUniqueId()));
|
||||
return ContentUris.withAppendedId(uri, attachmentId.getRowId());
|
||||
}
|
||||
|
||||
public static Uri getStickerUri(long id) {
|
||||
return ContentUris.withAppendedId(STICKER_CONTENT_URI, id);
|
||||
}
|
||||
|
||||
public static boolean isLocalUri(final @NonNull Uri uri) {
|
||||
int match = uriMatcher.match(uri);
|
||||
switch (match) {
|
||||
case PART_ROW:
|
||||
case THUMB_ROW:
|
||||
case PERSISTENT_ROW:
|
||||
case BLOB_ROW:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import com.google.android.mms.ContentType;
|
||||
import com.google.android.mms.pdu_alt.CharacterSets;
|
||||
import com.google.android.mms.pdu_alt.PduBody;
|
||||
import com.google.android.mms.pdu_alt.PduPart;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
|
||||
public class PartParser {
|
||||
public static String getMessageText(PduBody body) {
|
||||
String bodyText = null;
|
||||
|
||||
for (int i=0;i<body.getPartsNum();i++) {
|
||||
if (ContentType.isTextType(Util.toIsoString(body.getPart(i).getContentType()))) {
|
||||
String partText;
|
||||
|
||||
try {
|
||||
String characterSet = CharacterSets.getMimeName(body.getPart(i).getCharset());
|
||||
|
||||
if (characterSet.equals(CharacterSets.MIMENAME_ANY_CHARSET))
|
||||
characterSet = CharacterSets.MIMENAME_UTF_8;
|
||||
|
||||
if (body.getPart(i).getData() != null) {
|
||||
partText = new String(body.getPart(i).getData(), characterSet);
|
||||
} else {
|
||||
partText = "";
|
||||
}
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Log.w("PartParser", e);
|
||||
partText = "Unsupported Encoding!";
|
||||
}
|
||||
|
||||
bodyText = (bodyText == null) ? partText : bodyText + " " + partText;
|
||||
}
|
||||
}
|
||||
|
||||
return bodyText;
|
||||
}
|
||||
|
||||
public static PduBody getSupportedMediaParts(PduBody body) {
|
||||
PduBody stripped = new PduBody();
|
||||
|
||||
for (int i=0;i<body.getPartsNum();i++) {
|
||||
if (isDisplayableMedia(body.getPart(i))) {
|
||||
stripped.addPart(body.getPart(i));
|
||||
}
|
||||
}
|
||||
|
||||
return stripped;
|
||||
}
|
||||
|
||||
public static int getSupportedMediaPartCount(PduBody body) {
|
||||
int partCount = 0;
|
||||
|
||||
for (int i=0;i<body.getPartsNum();i++) {
|
||||
if (isDisplayableMedia(body.getPart(i))) {
|
||||
partCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return partCount;
|
||||
}
|
||||
|
||||
public static boolean isImage(PduPart part) {
|
||||
return ContentType.isImageType(Util.toIsoString(part.getContentType()));
|
||||
}
|
||||
|
||||
public static boolean isAudio(PduPart part) {
|
||||
return ContentType.isAudioType(Util.toIsoString(part.getContentType()));
|
||||
}
|
||||
|
||||
public static boolean isVideo(PduPart part) {
|
||||
return ContentType.isVideoType(Util.toIsoString(part.getContentType()));
|
||||
}
|
||||
|
||||
public static boolean isText(PduPart part) {
|
||||
return ContentType.isTextType(Util.toIsoString(part.getContentType()));
|
||||
}
|
||||
|
||||
public static boolean isDisplayableMedia(PduPart part) {
|
||||
return isImage(part) || isAudio(part) || isVideo(part);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
|
||||
public class PartUriParser {
|
||||
|
||||
private final Uri uri;
|
||||
|
||||
public PartUriParser(Uri uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public AttachmentId getPartId() {
|
||||
return new AttachmentId(getId(), getUniqueId());
|
||||
}
|
||||
|
||||
private long getId() {
|
||||
return ContentUris.parseId(uri);
|
||||
}
|
||||
|
||||
private long getUniqueId() {
|
||||
return Long.parseLong(uri.getPathSegments().get(1));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public class PushMediaConstraints extends MediaConstraints {
|
||||
|
||||
private static final int MAX_IMAGE_DIMEN_LOWMEM = 768;
|
||||
private static final int MAX_IMAGE_DIMEN = 4096;
|
||||
private static final int KB = 1024;
|
||||
private static final int MB = 1024 * KB;
|
||||
|
||||
@Override
|
||||
public int getImageMaxWidth(Context context) {
|
||||
return Util.isLowMemory(context) ? MAX_IMAGE_DIMEN_LOWMEM : MAX_IMAGE_DIMEN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageMaxHeight(Context context) {
|
||||
return getImageMaxWidth(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageMaxSize(Context context) {
|
||||
return 6 * MB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getGifMaxSize(Context context) {
|
||||
return 25 * MB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVideoMaxSize(Context context) {
|
||||
return 100 * MB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getUncompressedVideoMaxSize(Context context) {
|
||||
return isVideoTranscodeAvailable() ? 200 * MB
|
||||
: getVideoMaxSize(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCompressedVideoMaxSize(Context context) {
|
||||
return Util.isLowMemory(context) ? 30 * MB
|
||||
: 50 * MB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAudioMaxSize(Context context) {
|
||||
return 100 * MB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDocumentMaxSize(Context context) {
|
||||
return 100 * MB;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
/**
|
||||
* Represents the information required to find the {@link MessageRecord} pointed to by a quote.
|
||||
*/
|
||||
public class QuoteId {
|
||||
|
||||
private static final String TAG = QuoteId.class.getSimpleName();
|
||||
|
||||
private static final String ID = "id";
|
||||
private static final String AUTHOR_DEPRECATED = "author";
|
||||
private static final String AUTHOR = "author_id";
|
||||
|
||||
private final long id;
|
||||
private final RecipientId author;
|
||||
|
||||
public QuoteId(long id, @NonNull RecipientId author) {
|
||||
this.id = id;
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public @NonNull String serialize() {
|
||||
try {
|
||||
JSONObject object = new JSONObject();
|
||||
object.put(ID, id);
|
||||
object.put(AUTHOR, author.serialize());
|
||||
return object.toString();
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Failed to serialize to json", e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable QuoteId deserialize(@NonNull Context context, @NonNull String serialized) {
|
||||
try {
|
||||
JSONObject json = new JSONObject(serialized);
|
||||
RecipientId id = json.has(AUTHOR) ? RecipientId.from(json.getString(AUTHOR))
|
||||
: Recipient.external(context, json.getString(AUTHOR_DEPRECATED)).getId();
|
||||
|
||||
return new QuoteId(json.getLong(ID), id);
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Failed to deserialize from json", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class QuoteModel {
|
||||
|
||||
private final long id;
|
||||
private final RecipientId author;
|
||||
private final String text;
|
||||
private final boolean missing;
|
||||
private final List<Attachment> attachments;
|
||||
|
||||
public QuoteModel(long id, @NonNull RecipientId author, String text, boolean missing, @Nullable List<Attachment> attachments) {
|
||||
this.id = id;
|
||||
this.author = author;
|
||||
this.text = text;
|
||||
this.missing = missing;
|
||||
this.attachments = attachments;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public RecipientId getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public boolean isOriginalMissing() {
|
||||
return missing;
|
||||
}
|
||||
|
||||
public List<Attachment> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.GlideBuilder;
|
||||
import com.bumptech.glide.Registry;
|
||||
import com.bumptech.glide.annotation.GlideModule;
|
||||
import com.bumptech.glide.load.engine.cache.DiskCache;
|
||||
import com.bumptech.glide.load.engine.cache.DiskCacheAdapter;
|
||||
import com.bumptech.glide.load.model.GlideUrl;
|
||||
import com.bumptech.glide.load.model.UnitModelLoader;
|
||||
import com.bumptech.glide.load.resource.bitmap.Downsampler;
|
||||
import com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder;
|
||||
import com.bumptech.glide.load.resource.gif.ByteBufferGifDecoder;
|
||||
import com.bumptech.glide.load.resource.gif.GifDrawable;
|
||||
import com.bumptech.glide.load.resource.gif.StreamGifDecoder;
|
||||
import com.bumptech.glide.module.AppGlideModule;
|
||||
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHashModelLoader;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHashResourceDecoder;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
|
||||
import org.thoughtcrime.securesms.glide.ChunkedImageUrlLoader;
|
||||
import org.thoughtcrime.securesms.glide.ContactPhotoLoader;
|
||||
import org.thoughtcrime.securesms.glide.OkHttpUrlLoader;
|
||||
import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapCacheDecoder;
|
||||
import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapResourceEncoder;
|
||||
import org.thoughtcrime.securesms.glide.cache.EncryptedCacheEncoder;
|
||||
import org.thoughtcrime.securesms.glide.cache.EncryptedGifCacheDecoder;
|
||||
import org.thoughtcrime.securesms.glide.cache.EncryptedGifDrawableResourceEncoder;
|
||||
import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.stickers.StickerRemoteUri;
|
||||
import org.thoughtcrime.securesms.stickers.StickerRemoteUriLoader;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
|
||||
@GlideModule
|
||||
public class SignalGlideModule extends AppGlideModule {
|
||||
|
||||
@Override
|
||||
public boolean isManifestParsingEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyOptions(Context context, GlideBuilder builder) {
|
||||
builder.setLogLevel(Log.ERROR);
|
||||
// builder.setDiskCache(new NoopDiskCacheFactory());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
|
||||
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
|
||||
byte[] secret = attachmentSecret.getModernKey();
|
||||
|
||||
registry.prepend(File.class, File.class, UnitModelLoader.Factory.getInstance());
|
||||
registry.prepend(InputStream.class, new EncryptedCacheEncoder(secret, glide.getArrayPool()));
|
||||
registry.prepend(File.class, Bitmap.class, new EncryptedBitmapCacheDecoder(secret, new StreamBitmapDecoder(new Downsampler(registry.getImageHeaderParsers(), context.getResources().getDisplayMetrics(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool())));
|
||||
registry.prepend(File.class, GifDrawable.class, new EncryptedGifCacheDecoder(secret, new StreamGifDecoder(registry.getImageHeaderParsers(), new ByteBufferGifDecoder(context, registry.getImageHeaderParsers(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool())));
|
||||
|
||||
registry.prepend(BlurHash.class, Bitmap.class, new BlurHashResourceDecoder());
|
||||
|
||||
registry.prepend(Bitmap.class, new EncryptedBitmapResourceEncoder(secret));
|
||||
registry.prepend(GifDrawable.class, new EncryptedGifDrawableResourceEncoder(secret));
|
||||
|
||||
registry.append(ContactPhoto.class, InputStream.class, new ContactPhotoLoader.Factory(context));
|
||||
registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));
|
||||
registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory());
|
||||
registry.append(ChunkedImageUrl.class, InputStream.class, new ChunkedImageUrlLoader.Factory());
|
||||
registry.append(StickerRemoteUri.class, InputStream.class, new StickerRemoteUriLoader.Factory());
|
||||
registry.append(BlurHash.class, BlurHash.class, new BlurHashModelLoader.Factory());
|
||||
registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
|
||||
}
|
||||
|
||||
public static class NoopDiskCacheFactory implements DiskCache.Factory {
|
||||
@Override
|
||||
public DiskCache build() {
|
||||
return new DiskCacheAdapter();
|
||||
}
|
||||
}
|
||||
}
|
||||
225
app/src/main/java/org/thoughtcrime/securesms/mms/Slide.java
Normal file
225
app/src/main/java/org/thoughtcrime/securesms/mms/Slide.java
Normal file
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources.Theme;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public abstract class Slide {
|
||||
|
||||
protected final Attachment attachment;
|
||||
protected final Context context;
|
||||
|
||||
public Slide(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
this.context = context;
|
||||
this.attachment = attachment;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return attachment.getContentType();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Uri getUri() {
|
||||
return attachment.getDataUri();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Uri getThumbnailUri() {
|
||||
return attachment.getThumbnailUri();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Optional<String> getBody() {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Optional<String> getCaption() {
|
||||
return Optional.fromNullable(attachment.getCaption());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Optional<String> getFileName() {
|
||||
return Optional.fromNullable(attachment.getFileName());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getFastPreflightId() {
|
||||
return attachment.getFastPreflightId();
|
||||
}
|
||||
|
||||
public long getFileSize() {
|
||||
return attachment.getSize();
|
||||
}
|
||||
|
||||
public boolean hasImage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasSticker() { return false; }
|
||||
|
||||
public boolean hasVideo() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasAudio() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasDocument() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasLocation() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public @NonNull String getContentDescription() { return ""; }
|
||||
|
||||
public @NonNull Attachment asAttachment() {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
public boolean isInProgress() {
|
||||
return attachment.isInProgress();
|
||||
}
|
||||
|
||||
public boolean isPendingDownload() {
|
||||
return getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_FAILED ||
|
||||
getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_PENDING;
|
||||
}
|
||||
|
||||
public int getTransferState() {
|
||||
return attachment.getTransferState();
|
||||
}
|
||||
|
||||
public @DrawableRes int getPlaceholderRes(Theme theme) {
|
||||
throw new AssertionError("getPlaceholderRes() called for non-drawable slide");
|
||||
}
|
||||
|
||||
public @Nullable BlurHash getPlaceholderBlur() {
|
||||
return attachment.getBlurHash();
|
||||
}
|
||||
|
||||
public boolean hasPlaceholder() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasPlayOverlay() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static Attachment constructAttachmentFromUri(@NonNull Context context,
|
||||
@NonNull Uri uri,
|
||||
@NonNull String defaultMime,
|
||||
long size,
|
||||
int width,
|
||||
int height,
|
||||
boolean hasThumbnail,
|
||||
@Nullable String fileName,
|
||||
@Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator,
|
||||
@Nullable BlurHash blurHash,
|
||||
boolean voiceNote,
|
||||
boolean quote)
|
||||
{
|
||||
String resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri)).or(defaultMime);
|
||||
String fastPreflightId = String.valueOf(new SecureRandom().nextLong());
|
||||
return new UriAttachment(uri,
|
||||
hasThumbnail ? uri : null,
|
||||
resolvedType,
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_STARTED,
|
||||
size,
|
||||
width,
|
||||
height,
|
||||
fileName,
|
||||
fastPreflightId,
|
||||
voiceNote,
|
||||
quote,
|
||||
caption,
|
||||
stickerLocator,
|
||||
blurHash,
|
||||
null);
|
||||
}
|
||||
|
||||
public @NonNull Optional<String> getFileType(@NonNull Context context) {
|
||||
Optional<String> fileName = getFileName();
|
||||
|
||||
if (fileName.isPresent()) {
|
||||
return Optional.of(getFileType(fileName));
|
||||
}
|
||||
|
||||
return Optional.fromNullable(MediaUtil.getExtension(context, getUri()));
|
||||
}
|
||||
|
||||
private static @NonNull String getFileType(Optional<String> fileName) {
|
||||
if (!fileName.isPresent()) return "";
|
||||
|
||||
String[] parts = fileName.get().split("\\.");
|
||||
|
||||
if (parts.length < 2) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String suffix = parts[parts.length - 1];
|
||||
|
||||
if (suffix.length() <= 3) {
|
||||
return suffix;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null) return false;
|
||||
if (!(other instanceof Slide)) return false;
|
||||
|
||||
Slide that = (Slide)other;
|
||||
|
||||
return Util.equals(this.getContentType(), that.getContentType()) &&
|
||||
this.hasAudio() == that.hasAudio() &&
|
||||
this.hasImage() == that.hasImage() &&
|
||||
this.hasVideo() == that.hasVideo() &&
|
||||
this.getTransferState() == that.getTransferState() &&
|
||||
Util.equals(this.getUri(), that.getUri()) &&
|
||||
Util.equals(this.getThumbnailUri(), that.getThumbnailUri());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Util.hashCode(getContentType(), hasAudio(), hasImage(),
|
||||
hasVideo(), getUri(), getThumbnailUri(), getTransferState());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
public interface SlideClickListener {
|
||||
void onClick(View v, Slide slide);
|
||||
}
|
||||
151
app/src/main/java/org/thoughtcrime/securesms/mms/SlideDeck.java
Normal file
151
app/src/main/java/org/thoughtcrime/securesms/mms/SlideDeck.java
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class SlideDeck {
|
||||
|
||||
private final List<Slide> slides = new LinkedList<>();
|
||||
|
||||
public SlideDeck(@NonNull Context context, @NonNull List<? extends Attachment> attachments) {
|
||||
for (Attachment attachment : attachments) {
|
||||
Slide slide = MediaUtil.getSlideForAttachment(context, attachment);
|
||||
if (slide != null) slides.add(slide);
|
||||
}
|
||||
}
|
||||
|
||||
public SlideDeck(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
Slide slide = MediaUtil.getSlideForAttachment(context, attachment);
|
||||
if (slide != null) slides.add(slide);
|
||||
}
|
||||
|
||||
public SlideDeck() {
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
slides.clear();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getBody() {
|
||||
String body = "";
|
||||
|
||||
for (Slide slide : slides) {
|
||||
Optional<String> slideBody = slide.getBody();
|
||||
|
||||
if (slideBody.isPresent()) {
|
||||
body = slideBody.get();
|
||||
}
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<Attachment> asAttachments() {
|
||||
List<Attachment> attachments = new LinkedList<>();
|
||||
|
||||
for (Slide slide : slides) {
|
||||
attachments.add(slide.asAttachment());
|
||||
}
|
||||
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public void addSlide(Slide slide) {
|
||||
slides.add(slide);
|
||||
}
|
||||
|
||||
public List<Slide> getSlides() {
|
||||
return slides;
|
||||
}
|
||||
|
||||
public boolean containsMediaSlide() {
|
||||
for (Slide slide : slides) {
|
||||
if (slide.hasImage() || slide.hasVideo() || slide.hasAudio() || slide.hasDocument() || slide.hasSticker()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public @Nullable Slide getThumbnailSlide() {
|
||||
for (Slide slide : slides) {
|
||||
if (slide.hasImage()) {
|
||||
return slide;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public @NonNull List<Slide> getThumbnailSlides() {
|
||||
return Stream.of(slides).filter(Slide::hasImage).toList();
|
||||
}
|
||||
|
||||
public @Nullable AudioSlide getAudioSlide() {
|
||||
for (Slide slide : slides) {
|
||||
if (slide.hasAudio()) {
|
||||
return (AudioSlide)slide;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public @Nullable DocumentSlide getDocumentSlide() {
|
||||
for (Slide slide: slides) {
|
||||
if (slide.hasDocument()) {
|
||||
return (DocumentSlide)slide;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public @Nullable TextSlide getTextSlide() {
|
||||
for (Slide slide: slides) {
|
||||
if (MediaUtil.isLongTextType(slide.getContentType())) {
|
||||
return (TextSlide)slide;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public @Nullable StickerSlide getStickerSlide() {
|
||||
for (Slide slide: slides) {
|
||||
if (slide.hasSticker()) {
|
||||
return (StickerSlide)slide;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SlidesClickedListener {
|
||||
void onClick(View v, List<Slide> slides);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources.Theme;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
public class StickerSlide extends Slide {
|
||||
|
||||
public static final int WIDTH = 512;
|
||||
public static final int HEIGHT = 512;
|
||||
|
||||
public StickerSlide(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
super(context, attachment);
|
||||
}
|
||||
|
||||
public StickerSlide(Context context, Uri uri, long size, @NonNull StickerLocator stickerLocator) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_WEBP, size, WIDTH, HEIGHT, true, null, null, stickerLocator, null, false, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @DrawableRes int getPlaceholderRes(Theme theme) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Uri getThumbnailUri() {
|
||||
return getUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSticker() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getContentDescription() {
|
||||
return context.getString(R.string.Slide_sticker);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
public class TextSlide extends Slide {
|
||||
|
||||
public TextSlide(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
super(context, attachment);
|
||||
}
|
||||
|
||||
public TextSlide(@NonNull Context context, @NonNull Uri uri, @Nullable String filename, long size) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.LONG_TEXT, size, 0, 0, true, filename, null, null, null, false, false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources.Theme;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.ResUtil;
|
||||
|
||||
public class VideoSlide extends Slide {
|
||||
|
||||
public VideoSlide(Context context, Uri uri, long dataSize) {
|
||||
this(context, uri, dataSize, null);
|
||||
}
|
||||
|
||||
public VideoSlide(Context context, Uri uri, long dataSize, @Nullable String caption) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(uri), null, caption, null, null, false, false));
|
||||
}
|
||||
|
||||
public VideoSlide(Context context, Attachment attachment) {
|
||||
super(context, attachment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPlaceholder() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPlayOverlay() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @DrawableRes int getPlaceholderRes(Theme theme) {
|
||||
return ResUtil.getDrawableRes(theme, R.attr.conversation_icon_attach_video);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasImage() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasVideo() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@NonNull @Override
|
||||
public String getContentDescription() {
|
||||
return context.getString(R.string.Slide_video);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user