Copione merged onto master
continuous-integration/drone/push Build is passing Details

master
blallo 2020-02-15 00:00:29 +00:00
commit dd487c7329
234 changed files with 6606 additions and 2599 deletions

View File

@ -13,5 +13,7 @@ jobs:
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
- name: Build with Gradle
run: ./gradlew qa

View File

@ -1,74 +0,0 @@
Building Signal
===============
Basics
------
Signal uses [Gradle](http://gradle.org) to build the project and to maintain
dependencies. However, you needn't install it yourself; the
"gradle wrapper" `gradlew`, mentioned below, will do that for you.
Building Signal
---------------
The following steps should help you (re)build Signal from the command line.
1. Checkout the Signal-Android project source with the command:
git clone https://github.com/signalapp/Signal-Android.git
2. Make sure you have the [Android SDK](https://developer.android.com/sdk/index.html) installed.
3. Ensure that the following packages are installed from the Android SDK manager:
* Android SDK Build Tools (see buildToolsVersion in build.gradle)
* SDK Platform (All API levels)
* Android Support Repository
* Google Repository
4. Create a local.properties file at the root of your source checkout and add an sdk.dir entry to it. For example:
sdk.dir=/Application/android-sdk-macosx
5. Using Java 8
6. Execute Gradle:
./gradlew build
Visual assets
----------------------
Source assets tend to be large binary blobs, which are best stored outside of git repositories. Some source files are SVGs that can be auto-colored and sized using a tool like [android-res-utils](https://github.com/sebkur/android-res-utils).
Sample command for generating our audio placeholder image:
```bash
pngs_from_svg.py ic_audio.svg /path/to/Signal/res/ 150 --color #000 --opacity 0.54 --suffix _light
pngs_from_svg.py ic_audio.svg /path/to/Signal/res/ 150 --color #fff --opacity 1.00 --suffix _light
```
Setting up a development environment
------------------------------------
[Android Studio](https://developer.android.com/sdk/installing/studio.html) is the recommended development environment.
1. Install Android Studio.
2. Open Android Studio. On a new installation, the Quickstart panel will appear. If you have open projects, close them using "File > Close Project" to see the Quickstart panel.
3. From the Quickstart panel, choose "Configure" then "SDK Manager".
4. In the SDK Tools tab of the SDK Manager, make sure that the "Android Support Repository" is installed, and that the latest "Android SDK build-tools" are installed. Click "OK" to return to the Quickstart panel.
5. From the Quickstart panel, choose "Checkout from Version Control" then "git".
6. Paste the URL for the Signal-Android project when prompted (https://github.com/signalapp/Signal-Android.git).
7. Android studio should detect the presence of a project file and ask you whether to open it. Click "yes".
9. Default config options should be good enough.
9. Project initialisation and build should proceed.
Contributing code
-----------------
Code contributions should be sent via github as pull requests, from feature branches [as explained here](https://help.github.com/articles/using-pull-requests).
Mailing list
------------
Development discussion happens on the whispersystems mailing list.
[To join](https://lists.riseup.net/www/info/whispersystems)
Send emails to whispersystems@lists.riseup.net

View File

@ -27,7 +27,6 @@ Interested in helping to translate Signal? Contribute here:
https://www.transifex.com/projects/p/signal-android/
## Contributing Code
Instructions on how to setup your development environment and build Signal can be found in [BUILDING.md](https://github.com/signalapp/Signal-Android/blob/master/BUILDING.md).
If you're new to the Signal codebase, we recommend going through our issues and picking out a simple bug to fix (check the "easy" label in our issues) in order to get yourself familiar. Also please have a look at the [CONTRIBUTING.md](https://github.com/signalapp/Signal-Android/blob/master/CONTRIBUTING.md), that might answer some of your questions.

View File

@ -119,7 +119,7 @@ dependencies {
implementation 'org.signal:argon2:13.1@aar'
implementation 'org.signal:ringrtc-android:0.3.3'
implementation 'org.signal:ringrtc-android:1.0.1'
implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
@ -188,8 +188,8 @@ dependencyVerification {
configuration = '(play|website)(Debug|Release)RuntimeClasspath'
}
def canonicalVersionCode = 605
def canonicalVersionName = "4.55.8"
def canonicalVersionCode = 607
def canonicalVersionName = "4.56.1"
def postFixSize = 10
def abiPostFix = ['universal' : 0,
@ -233,6 +233,7 @@ android {
buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
buildConfigField "String", "KEY_BACKUP_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\""
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"\""
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
@ -305,6 +306,7 @@ android {
buildConfigField "String", "MRENCLAVE", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\""
buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"a1e9c1d3f352b5c4f0fc7a421b98119e60e5ff703c28fbea85c66bfa7306deab\""
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"\""
}
release {
minifyEnabled true

View File

@ -148,7 +148,7 @@
<activity android:name=".preferences.MmsPreferencesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ShareActivity"
<activity android:name=".sharing.ShareActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:excludeFromRecents="true"
android:launchMode="singleTask"
@ -156,7 +156,6 @@
android:noHistory="true"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT"/>
@ -169,8 +168,15 @@
<data android:mimeType="*/*"/>
</intent-filter>
<meta-data
android:name="android.service.chooser.chooser_target_service"
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
<meta-data
android:name="android.service.chooser.chooser_target_service"
android:value=".service.DirectShareService" />
</activity>

View File

@ -34,7 +34,7 @@ import com.google.android.gms.security.ProviderInstaller;
import org.conscrypt.Conscrypt;
import org.signal.aesgcmprovider.AesGcmProvider;
import org.signal.ringrtc.CallConnectionFactory;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.components.TypingStatusRepository;
import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -254,6 +254,8 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
SignalStore.registrationValues().onNewInstall();
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
}
Log.i(TAG, "Setting first install version to " + BuildConfig.CANONICAL_VERSION_CODE);
@ -334,9 +336,9 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true);
}
CallConnectionFactory.initialize(this, new RingRtcLogger());
CallManager.initialize(this, new RingRtcLogger());
} catch (UnsatisfiedLinkError e) {
Log.w(TAG, e);
throw new AssertionError("Unable to load ringrtc library", e);
}
}

View File

@ -64,6 +64,7 @@ import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;

View File

@ -78,8 +78,10 @@ import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
@ -235,6 +237,13 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
this.avatar.setBackgroundColor(recipient.getColor().toActionBarColor(this));
this.toolbarLayout.setTitle(recipient.toShortString(this));
this.toolbarLayout.setContentScrimColor(recipient.getColor().toActionBarColor(this));
if (recipient.getUuid().isPresent()) {
toolbarLayout.setOnLongClickListener(v -> {
Util.copyToClipboard(this, recipient.getUuid().get().toString());
ServiceUtil.getVibrator(this).vibrate(200);
return true;
});
}
}
@Override

View File

@ -43,6 +43,7 @@ import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -173,8 +174,8 @@ public class WebRtcCallActivity extends Activity {
private void handleSetMuteVideo(boolean muted) {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_SET_MUTE_VIDEO);
intent.putExtra(WebRtcCallService.EXTRA_MUTE, muted);
intent.setAction(WebRtcCallService.ACTION_SET_ENABLE_VIDEO);
intent.putExtra(WebRtcCallService.EXTRA_ENABLE, !muted);
startService(intent);
}
@ -198,7 +199,7 @@ public class WebRtcCallActivity extends Activity {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_answering), event.getLocalRenderer());
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_ANSWER_CALL);
intent.setAction(WebRtcCallService.ACTION_ACCEPT_CALL);
startService(intent);
})
.onAnyDenied(this::handleDenyCall)
@ -249,7 +250,7 @@ public class WebRtcCallActivity extends Activity {
private void handleCallBusy(@NonNull WebRtcViewModel event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_busy), event.getLocalRenderer());
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
delayedFinish(BUSY_SIGNAL_DELAY_FINISH);
}
@ -260,11 +261,13 @@ public class WebRtcCallActivity extends Activity {
private void handleRecipientUnavailable(@NonNull WebRtcViewModel event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_recipient_unavailable), event.getLocalRenderer());
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
delayedFinish();
}
private void handleServerFailure(@NonNull WebRtcViewModel event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_network_failed), event.getLocalRenderer());
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
delayedFinish();
}
@ -304,8 +307,8 @@ public class WebRtcCallActivity extends Activity {
}
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_RECIPIENT, recipient.getId());
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL);
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(recipient.getId()));
startService(intent);
}
});
@ -411,4 +414,4 @@ public class WebRtcCallActivity extends Activity {
}
}
}
}

View File

@ -23,7 +23,6 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
import org.thoughtcrime.securesms.database.RecipientDatabase;
@ -37,19 +36,16 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
import org.thoughtcrime.securesms.sms.IncomingJoinedMessage;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.IOException;
import java.util.Calendar;
@ -343,25 +339,8 @@ class DirectoryHelperV1 {
}
private static boolean isUuidRegistered(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient);
SignalServiceMessagePipe authPipe = IncomingMessageObserver.getPipe();
SignalServiceMessagePipe unidentifiedPipe = IncomingMessageObserver.getUnidentifiedPipe();
SignalServiceMessagePipe pipe = unidentifiedPipe != null && unidentifiedAccess.isPresent() ? unidentifiedPipe : authPipe;
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient);
if (pipe != null) {
try {
pipe.getProfile(address, unidentifiedAccess.get().getTargetUnidentifiedAccess());
return true;
} catch (NotFoundException e) {
return false;
} catch (IOException e) {
Log.w(TAG, "Websocket request failed. Falling back to REST.");
}
}
try {
ApplicationDependencies.getSignalServiceMessageReceiver().retrieveProfile(address, unidentifiedAccess.get().getTargetUnidentifiedAccess());
ProfileUtil.retrieveProfile(context, recipient, SignalServiceProfile.RequestType.PROFILE);
return true;
} catch (NotFoundException e) {
return false;

View File

@ -18,6 +18,7 @@ import org.whispersystems.signalservice.api.storage.SignalContactRecord;
import org.whispersystems.signalservice.api.storage.SignalContactRecord.IdentityState;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.util.OptionalUtil;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@ -362,7 +363,7 @@ public final class StorageSyncHelper {
private final SignalContactRecord oldContact;
private final SignalContactRecord newContact;
public ContactUpdate(@NonNull SignalContactRecord oldContact, @NonNull SignalContactRecord newContact) {
ContactUpdate(@NonNull SignalContactRecord oldContact, @NonNull SignalContactRecord newContact) {
this.oldContact = oldContact;
this.newContact = newContact;
}
@ -377,6 +378,10 @@ public final class StorageSyncHelper {
return newContact;
}
public boolean profileKeyChanged() {
return !OptionalUtil.byteArrayEquals(oldContact.getProfileKey(), newContact.getProfileKey());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@ -494,7 +499,7 @@ public final class StorageSyncHelper {
private final WriteOperationResult writeResult;
private final Map<RecipientId, byte[]> storageKeyUpdates;
public LocalWriteResult(WriteOperationResult writeResult, Map<RecipientId, byte[]> storageKeyUpdates) {
private LocalWriteResult(WriteOperationResult writeResult, Map<RecipientId, byte[]> storageKeyUpdates) {
this.writeResult = writeResult;
this.storageKeyUpdates = storageKeyUpdates;
}
@ -510,17 +515,17 @@ public final class StorageSyncHelper {
private static final class ContactRecordMergeResult {
final Set<SignalContactRecord> localInserts;
final Set<ContactUpdate> localUpdates;
final Set<ContactUpdate> localUpdates;
final Set<SignalContactRecord> remoteInserts;
final Set<ContactUpdate> remoteUpdates;
final Set<ContactUpdate> remoteUpdates;
ContactRecordMergeResult(@NonNull Set<SignalContactRecord> localInserts,
@NonNull Set<ContactUpdate> localUpdates,
@NonNull Set<SignalContactRecord> remoteInserts,
@NonNull Set<ContactUpdate> remoteUpdates)
{
this.localInserts = localInserts;
this.localUpdates = localUpdates;
this.localInserts = localInserts;
this.localUpdates = localUpdates;
this.remoteInserts = remoteInserts;
this.remoteUpdates = remoteUpdates;
}

View File

@ -609,7 +609,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
for (Media mediaItem : result.getNonUploadedMedia()) {
if (MediaUtil.isVideoType(mediaItem.getMimeType())) {
slideDeck.addSlide(new VideoSlide(this, mediaItem.getUri(), 0, mediaItem.getCaption().orNull()));
slideDeck.addSlide(new VideoSlide(this, mediaItem.getUri(), 0, mediaItem.getCaption().orNull(), mediaItem.getTransformProperties().orNull()));
} else if (MediaUtil.isGif(mediaItem.getMimeType())) {
slideDeck.addSlide(new GifSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull()));
} else if (MediaUtil.isImageType(mediaItem.getMimeType())) {
@ -1322,11 +1322,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
final StickerLocator stickerLocator = getIntent().getParcelableExtra(STICKER_EXTRA);
if (stickerLocator != null && draftMedia != null) {
Log.d(TAG, "Handling shared sticker.");
sendSticker(stickerLocator, draftMedia, 0, true);
return new SettableFuture<>(false);
}
if (!Util.isEmpty(mediaList)) {
Log.d(TAG, "Handling shared Media.");
Intent sendIntent = MediaSendActivity.buildEditorIntent(this, mediaList, recipient.get(), draftText, sendButton.getSelectedTransport());
startActivityForResult(sendIntent, MEDIA_SENDER);
return new SettableFuture<>(false);
@ -1339,6 +1341,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
if (draftMedia != null && draftMediaType != null) {
Log.d(TAG, "Handling shared Data.");
return setMedia(draftMedia, draftMediaType);
}
@ -1904,7 +1907,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
openContactShareEditor(uri);
return new SettableFuture<>(false);
} else if (MediaType.IMAGE.equals(mediaType) || MediaType.GIF.equals(mediaType) || MediaType.VIDEO.equals(mediaType)) {
Media media = new Media(uri, MediaUtil.getMimeType(this, uri), 0, width, height, 0, 0, Optional.absent(), Optional.absent());
Media media = new Media(uri, MediaUtil.getMimeType(this, uri), 0, width, height, 0, 0, Optional.absent(), Optional.absent(), Optional.absent());
startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
return new SettableFuture<>(false);
} else {
@ -2617,7 +2620,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private void sendSticker(@NonNull StickerLocator stickerLocator, @NonNull Uri uri, long size, boolean clearCompose) {
if (sendButton.getSelectedTransport().isSms()) {
Media media = new Media(uri, MediaUtil.IMAGE_WEBP, System.currentTimeMillis(), StickerSlide.WIDTH, StickerSlide.HEIGHT, size, 0, Optional.absent(), Optional.absent());
Media media = new Media(uri, MediaUtil.IMAGE_WEBP, System.currentTimeMillis(), StickerSlide.WIDTH, StickerSlide.HEIGHT, size, 0, Optional.absent(), Optional.absent(), Optional.absent());
Intent intent = MediaSendActivity.buildEditorIntent(this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport());
startActivityForResult(intent, MEDIA_SENDER);
return;
@ -2633,7 +2636,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
slideDeck.addSlide(stickerSlide);
sendMediaMessage(transport.isSms(), "", slideDeck, null, Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, clearCompose);
}
private void silentlySetComposeText(String text) {

View File

@ -63,7 +63,7 @@ import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.MessageDetailsActivity;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.ShareActivity;
import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.components.ConversationTypingView;
import org.thoughtcrime.securesms.components.TooltipPopup;
@ -605,7 +605,8 @@ public class ConversationFragment extends Fragment
attachment.getSize(),
0,
Optional.absent(),
Optional.fromNullable(attachment.getCaption())));
Optional.fromNullable(attachment.getCaption()),
Optional.absent()));
}
};

View File

@ -1,38 +1,79 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.util.Locale;
public class ProfileKeyUtil {
public final class ProfileKeyUtil {
public static synchronized boolean hasProfileKey(@NonNull Context context) {
return TextSecurePreferences.getProfileKey(context) != null;
private static final String TAG = Log.tag(ProfileKeyUtil.class);
private ProfileKeyUtil() {
}
public static synchronized @NonNull byte[] getProfileKey(@NonNull Context context) {
/** @deprecated Use strongly typed {@link org.signal.zkgroup.profiles.ProfileKey}
* from {@link #getSelfProfileKey()}
* or {@code getSelfProfileKey().serialize()} if you need the bytes. */
@Deprecated
public static @NonNull byte[] getProfileKey(@NonNull Context context) {
byte[] profileKey = Recipient.self().getProfileKey();
if (profileKey == null) {
throw new AssertionError();
}
return profileKey;
}
public static synchronized @NonNull ProfileKey getSelfProfileKey() {
try {
String encodedProfileKey = TextSecurePreferences.getProfileKey(context);
if (encodedProfileKey == null) {
encodedProfileKey = Util.getSecret(32);
TextSecurePreferences.setProfileKey(context, encodedProfileKey);
}
return Base64.decode(encodedProfileKey);
} catch (IOException e) {
return new ProfileKey(Recipient.self().getProfileKey());
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
public static synchronized @NonNull byte[] rotateProfileKey(@NonNull Context context) {
TextSecurePreferences.setProfileKey(context, null);
return getProfileKey(context);
public static @Nullable ProfileKey profileKeyOrNull(@Nullable byte[] profileKey) {
if (profileKey != null) {
try {
return new ProfileKey(profileKey);
} catch (InvalidInputException e) {
Log.w(TAG, String.format(Locale.US, "Seen non-null profile key of wrong length %d", profileKey.length), e);
}
}
return null;
}
public static @NonNull ProfileKey profileKeyOrThrow(@NonNull byte[] profileKey) {
try {
return new ProfileKey(profileKey);
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
public static @NonNull Optional<ProfileKey> profileKeyOptional(@Nullable byte[] profileKey) {
return Optional.fromNullable(profileKeyOrNull(profileKey));
}
public static @NonNull Optional<ProfileKey> profileKeyOptionalOrThrow(@NonNull byte[] profileKey) {
return Optional.of(profileKeyOrThrow(profileKey));
}
public static @NonNull ProfileKey createNew() {
try {
return new ProfileKey(Util.getSecretBytes(32));
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
}

View File

@ -8,6 +8,7 @@ import androidx.annotation.WorkerThread;
import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
@ -42,7 +43,7 @@ public class UnidentifiedAccessUtil {
{
try {
byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context);
byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
byte[] ourUnidentifiedAccessCertificate = recipient.resolve().isUuidSupported() && Recipient.self().isUuidSupported()
? TextSecurePreferences.getUnidentifiedAccessCertificate(context)
: TextSecurePreferences.getUnidentifiedAccessCertificateLegacy(context);
@ -75,7 +76,7 @@ public class UnidentifiedAccessUtil {
public static Optional<UnidentifiedAccessPair> getAccessForSync(@NonNull Context context) {
try {
byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context);
byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
byte[] ourUnidentifiedAccessCertificate = Recipient.self().isUuidSupported() ? TextSecurePreferences.getUnidentifiedAccessCertificate(context)
: TextSecurePreferences.getUnidentifiedAccessCertificateLegacy(context);
@ -97,12 +98,8 @@ public class UnidentifiedAccessUtil {
}
}
public static @NonNull byte[] getSelfUnidentifiedAccessKey(@NonNull Context context) {
return UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getProfileKey(context));
}
private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) {
byte[] theirProfileKey = recipient.resolve().getProfileKey();
ProfileKey theirProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.resolve().getProfileKey());
switch (recipient.resolve().getUnidentifiedAccessMode()) {
case UNKNOWN:

View File

@ -33,6 +33,7 @@ import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import com.bumptech.glide.Glide;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import net.sqlcipher.DatabaseUtils;
@ -166,9 +167,12 @@ public class AttachmentDatabase extends Database {
"CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");",
"CREATE INDEX IF NOT EXISTS pending_push_index ON " + TABLE_NAME + " (" + TRANSFER_STATE + ");",
"CREATE INDEX IF NOT EXISTS part_sticker_pack_id_index ON " + TABLE_NAME + " (" + STICKER_PACK_ID + ");",
"CREATE INDEX IF NOT EXISTS part_data_hash_index ON " + TABLE_NAME + " (" + DATA_HASH + ");"
"CREATE INDEX IF NOT EXISTS part_data_hash_index ON " + TABLE_NAME + " (" + DATA_HASH + ");",
"CREATE INDEX IF NOT EXISTS part_data_index ON " + TABLE_NAME + " (" + DATA + ");"
};
private static final long STANDARD_THUMB_TIME = 1000;
private final ExecutorService thumbnailExecutor = Util.newSingleThreadedLifoExecutor();
private final AttachmentSecret attachmentSecret;
@ -198,7 +202,7 @@ public class AttachmentDatabase extends Database {
}
try {
InputStream generatedStream = thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId)).get();
InputStream generatedStream = thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId, STANDARD_THUMB_TIME)).get();
if (generatedStream == null) throw new FileNotFoundException("No thumbnail stream available: " + attachmentId);
else return generatedStream;
@ -525,7 +529,7 @@ public class AttachmentDatabase extends Database {
notifyConversationListListeners();
}
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId));
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId, STANDARD_THUMB_TIME));
}
private static @Nullable String getBlurHashStringOrNull(@Nullable BlurHash blurHash) {
@ -671,9 +675,14 @@ public class AttachmentDatabase extends Database {
return insertedAttachments;
}
/**
* @param onlyModifyThisAttachment If false and more than one attachment shares this file, they will all up updated.
* If true, then guarantees not to affect other attachments.
*/
public void updateAttachmentData(@NonNull DatabaseAttachment databaseAttachment,
@NonNull MediaStream mediaStream)
throws MmsException
@NonNull MediaStream mediaStream,
boolean onlyModifyThisAttachment)
throws MmsException, IOException
{
SQLiteDatabase database = databaseHelper.getWritableDatabase();
DataInfo oldDataInfo = getAttachmentDataFileInfo(databaseAttachment.getAttachmentId(), DATA);
@ -682,7 +691,16 @@ public class AttachmentDatabase extends Database {
throw new MmsException("No attachment data found!");
}
DataInfo dataInfo = setAttachmentData(oldDataInfo.file,
File destination = oldDataInfo.file;
if (onlyModifyThisAttachment) {
if (fileReferencedByMoreThanOneAttachment(destination)) {
Log.i(TAG, "Creating a new file as this one is used by more than one attachment");
destination = newFile();
}
}
DataInfo dataInfo = setAttachmentData(destination,
mediaStream.getStream(),
false,
databaseAttachment.getAttachmentId());
@ -700,19 +718,37 @@ public class AttachmentDatabase extends Database {
Log.i(TAG, "[updateAttachmentData] Updated " + updateCount + " rows.");
}
/**
* Returns true if the file referenced by two or more attachments.
* Returns false if the file is referenced by zero or one attachments.
*/
private boolean fileReferencedByMoreThanOneAttachment(@NonNull File file) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
String selection = DATA + " = ?";
String[] args = new String[]{file.getAbsolutePath()};
try (Cursor cursor = database.query(TABLE_NAME, null, selection, args, null, null, null, "2")) {
return cursor != null && cursor.moveToFirst() && cursor.moveToNext();
}
}
public void markAttachmentAsTransformed(@NonNull AttachmentId attachmentId) {
updateAttachmentTransformProperties(attachmentId, TransformProperties.forSkipTransform());
}
public void updateAttachmentTransformProperties(@NonNull AttachmentId attachmentId, @NonNull TransformProperties transformProperties) {
DataInfo dataInfo = getAttachmentDataFileInfo(attachmentId, DATA);
if (dataInfo == null) {
Log.w(TAG, "[markAttachmentAsTransformed] No data info found!");
Log.w(TAG, "[updateAttachmentTransformProperties] No data info found!");
return;
}
ContentValues contentValues = new ContentValues();
contentValues.put(TRANSFORM_PROPERTIES, TransformProperties.forSkipTransform().serialize());
contentValues.put(TRANSFORM_PROPERTIES, transformProperties.serialize());
int updateCount = updateAttachmentAndMatchingHashes(databaseHelper.getWritableDatabase(), attachmentId, dataInfo.hash, contentValues);
Log.i(TAG, "[markAttachmentAsTransformed] Updated " + updateCount + " rows.");
Log.i(TAG, "[updateAttachmentTransformProperties] Updated " + updateCount + " rows.");
}
public @NonNull File getOrCreateTransferFile(@NonNull AttachmentId attachmentId) throws IOException {
@ -925,14 +961,18 @@ public class AttachmentDatabase extends Database {
throws MmsException
{
try {
File partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE);
File dataFile = File.createTempFile("part", ".mms", partsDirectory);
File dataFile = newFile();
return setAttachmentData(dataFile, in, isThumbnail, attachmentId);
} catch (IOException e) {
throw new MmsException(e);
}
}
private File newFile() throws IOException {
File partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE);
return File.createTempFile("part", ".mms", partsDirectory);
}
private @NonNull DataInfo setAttachmentData(@NonNull File destination,
@NonNull InputStream in,
boolean isThumbnail,
@ -1098,9 +1138,10 @@ public class AttachmentDatabase extends Database {
{
Log.d(TAG, "Inserting attachment for mms id: " + mmsId);
SQLiteDatabase database = databaseHelper.getWritableDatabase();
DataInfo dataInfo = null;
long uniqueId = System.currentTimeMillis();
SQLiteDatabase database = databaseHelper.getWritableDatabase();
DataInfo dataInfo = null;
long uniqueId = System.currentTimeMillis();
long thumbnailTimeUs;
if (attachment.getDataUri() != null) {
dataInfo = setAttachmentData(attachment.getDataUri(), false, null);
@ -1135,8 +1176,15 @@ public class AttachmentDatabase extends Database {
contentValues.put(HEIGHT, template.getHeight());
contentValues.put(QUOTE, quote);
contentValues.put(CAPTION, attachment.getCaption());
contentValues.put(BLUR_HASH, getBlurHashStringOrNull(attachment.getBlurHash()));
contentValues.put(TRANSFORM_PROPERTIES, template.getTransformProperties().serialize());
if (attachment.getTransformProperties().isVideoEdited()) {
contentValues.putNull(BLUR_HASH);
contentValues.put(TRANSFORM_PROPERTIES, attachment.getTransformProperties().serialize());
thumbnailTimeUs = Math.max(STANDARD_THUMB_TIME, attachment.getTransformProperties().videoTrimStartTimeUs);
} else {
contentValues.put(BLUR_HASH, getBlurHashStringOrNull(attachment.getBlurHash()));
contentValues.put(TRANSFORM_PROPERTIES, template.getTransformProperties().serialize());
thumbnailTimeUs = STANDARD_THUMB_TIME;
}
if (attachment.isSticker()) {
contentValues.put(STICKER_PACK_ID, attachment.getSticker().getPackId());
@ -1148,7 +1196,11 @@ public class AttachmentDatabase extends Database {
contentValues.put(DATA, dataInfo.file.getAbsolutePath());
contentValues.put(SIZE, dataInfo.length);
contentValues.put(DATA_RANDOM, dataInfo.random);
contentValues.put(DATA_HASH, dataInfo.hash);
if (attachment.getTransformProperties().isVideoEdited()) {
contentValues.putNull(DATA_HASH);
} else {
contentValues.put(DATA_HASH, dataInfo.hash);
}
}
boolean notifyPacks = attachment.isSticker() && !hasStickerAttachments();
@ -1170,8 +1222,8 @@ public class AttachmentDatabase extends Database {
}
if (!hasThumbnail && dataInfo != null) {
if (MediaUtil.hasVideoThumbnail(attachment.getDataUri())) {
Bitmap bitmap = MediaUtil.getVideoThumbnail(context, attachment.getDataUri());
if (MediaUtil.hasVideoThumbnail(attachment.getDataUri()) && thumbnailTimeUs == STANDARD_THUMB_TIME) {
Bitmap bitmap = MediaUtil.getVideoThumbnail(context, attachment.getDataUri(), thumbnailTimeUs);
if (bitmap != null) {
try (ThumbnailData thumbnailData = new ThumbnailData(bitmap)) {
@ -1179,11 +1231,11 @@ public class AttachmentDatabase extends Database {
}
} else {
Log.w(TAG, "Retrieving video thumbnail failed, submitting thumbnail generation job...");
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId));
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId, thumbnailTimeUs));
}
} else {
Log.i(TAG, "Submitting thumbnail generation job...");
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId));
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId, thumbnailTimeUs));
}
}
@ -1241,9 +1293,11 @@ public class AttachmentDatabase extends Database {
class ThumbnailFetchCallable implements Callable<InputStream> {
private final AttachmentId attachmentId;
private final long timeUs;
ThumbnailFetchCallable(AttachmentId attachmentId) {
ThumbnailFetchCallable(AttachmentId attachmentId, long timeUs) {
this.attachmentId = attachmentId;
this.timeUs = timeUs;
}
@Override
@ -1263,7 +1317,7 @@ public class AttachmentDatabase extends Database {
if (MediaUtil.isVideoType(attachment.getContentType())) {
try (ThumbnailData data = generateVideoThumbnail(attachmentId)) {
try (ThumbnailData data = generateVideoThumbnail(attachmentId, timeUs)) {
if (data != null) {
updateAttachmentThumbnail(attachmentId, data.toDataStream(), data.getAspectRatio());
@ -1276,7 +1330,7 @@ public class AttachmentDatabase extends Database {
return null;
}
private ThumbnailData generateVideoThumbnail(AttachmentId attachmentId) throws IOException {
private ThumbnailData generateVideoThumbnail(AttachmentId attachmentId, long timeUs) throws IOException {
if (Build.VERSION.SDK_INT < 23) {
Log.w(TAG, "Video thumbnails not supported...");
return null;
@ -1288,7 +1342,7 @@ public class AttachmentDatabase extends Database {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
MediaMetadataRetrieverUtil.setDataSource(retriever, dataSource);
Bitmap bitmap = retriever.getFrameAtTime(1000);
Bitmap bitmap = retriever.getFrameAtTime(timeUs);
Log.i(TAG, "Generated video thumbnail...");
return bitmap != null ? new ThumbnailData(bitmap) : null;
@ -1325,23 +1379,54 @@ public class AttachmentDatabase extends Database {
public static final class TransformProperties {
@JsonProperty private final boolean skipTransform;
@JsonProperty private final boolean videoTrim;
@JsonProperty private final long videoTrimStartTimeUs;
@JsonProperty private final long videoTrimEndTimeUs;
public TransformProperties(@JsonProperty("skipTransform") boolean skipTransform) {
this.skipTransform = skipTransform;
@JsonCreator
public TransformProperties(@JsonProperty("skipTransform") boolean skipTransform,
@JsonProperty("videoTrim") boolean videoTrim,
@JsonProperty("videoTrimStartTimeUs") long videoTrimStartTimeUs,
@JsonProperty("videoTrimEndTimeUs") long videoTrimEndTimeUs)
{
this.skipTransform = skipTransform;
this.videoTrim = videoTrim;
this.videoTrimStartTimeUs = videoTrimStartTimeUs;
this.videoTrimEndTimeUs = videoTrimEndTimeUs;
}
public static @NonNull TransformProperties empty() {
return new TransformProperties(false);
return new TransformProperties(false, false, 0, 0);
}
public static @NonNull TransformProperties forSkipTransform() {
return new TransformProperties(true);
return new TransformProperties(true, false, 0, 0);
}
public static @NonNull TransformProperties forVideoTrim(long videoTrimStartTimeUs, long videoTrimEndTimeUs) {
return new TransformProperties(false, true, videoTrimStartTimeUs, videoTrimEndTimeUs);
}
public boolean shouldSkipTransform() {
return skipTransform;
}
public boolean isVideoEdited() {
return isVideoTrim();
}
public boolean isVideoTrim() {
return videoTrim;
}
public long getVideoTrimStartTimeUs() {
return videoTrimStartTimeUs;
}
public long getVideoTrimEndTimeUs() {
return videoTrimEndTimeUs;
}
@NonNull String serialize() {
return JsonUtil.toJson(this);
}

View File

@ -248,8 +248,8 @@ public class MediaDatabase extends Database {
}
public enum Sorting {
Newest (AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " DESC, " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + " DESC"),
Oldest (AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " ASC, " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + " DESC"),
Newest (AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " DESC, " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + " DESC, " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " DESC"),
Oldest (AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " ASC, " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + " DESC, " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " ASC"),
Largest(AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + " DESC, " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + " DESC");
private final String postFix;

View File

@ -14,6 +14,8 @@ import com.google.android.gms.common.util.ArrayUtils;
import net.sqlcipher.database.SQLiteDatabase;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.sync.StorageSyncHelper;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
@ -31,11 +33,10 @@ import org.thoughtcrime.securesms.util.SqlUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.Closeable;
import java.io.IOException;
@ -80,6 +81,7 @@ public class RecipientDatabase extends Database {
private static final String SYSTEM_CONTACT_URI = "system_contact_uri";
private static final String SYSTEM_INFO_PENDING = "system_info_pending";
private static final String PROFILE_KEY = "profile_key";
private static final String PROFILE_KEY_CREDENTIAL = "profile_key_credential";
private static final String SIGNAL_PROFILE_AVATAR = "signal_profile_avatar";
private static final String PROFILE_SHARING = "profile_sharing";
private static final String UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode";
@ -100,7 +102,8 @@ public class RecipientDatabase extends Database {
private static final String[] RECIPIENT_PROJECTION = new String[] {
UUID, USERNAME, PHONE, EMAIL, GROUP_ID,
BLOCKED, MESSAGE_RINGTONE, CALL_RINGTONE, MESSAGE_VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, MESSAGE_EXPIRATION_TIME, REGISTERED,
PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI,
PROFILE_KEY, PROFILE_KEY_CREDENTIAL,
SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI,
PROFILE_GIVEN_NAME, PROFILE_FAMILY_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
UNIDENTIFIED_ACCESS_MODE,
FORCE_SMS_SELECTION, UUID_SUPPORTED, STORAGE_SERVICE_KEY, DIRTY
@ -242,6 +245,7 @@ public class RecipientDatabase extends Database {
SYSTEM_CONTACT_URI + " TEXT DEFAULT NULL, " +
SYSTEM_INFO_PENDING + " INTEGER DEFAULT 0, " +
PROFILE_KEY + " TEXT DEFAULT NULL, " +
PROFILE_KEY_CREDENTIAL + " TEXT DEFAULT NULL, " +
PROFILE_GIVEN_NAME + " TEXT DEFAULT NULL, " +
PROFILE_FAMILY_NAME + " TEXT DEFAULT NULL, " +
PROFILE_JOINED_NAME + " TEXT DEFAULT NULL, " +
@ -428,6 +432,10 @@ public class RecipientDatabase extends Database {
RecipientId recipientId = getByStorageKeyOrThrow(update.getNewContact().getKey());
if (update.profileKeyChanged()) {
clearProfileKeyCredential(recipientId);
}
try {
Optional<IdentityRecord> oldIdentityRecord = identityDatabase.getIdentity(recipientId);
IdentityKey identityKey = update.getNewContact().getIdentityKey().isPresent() ? new IdentityKey(update.getNewContact().getIdentityKey().get(), 0) : null;
@ -562,42 +570,44 @@ public class RecipientDatabase extends Database {
}
@NonNull RecipientSettings getRecipientSettings(@NonNull Cursor cursor) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
UUID uuid = UuidUtil.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(UUID)));
String username = cursor.getString(cursor.getColumnIndexOrThrow(USERNAME));
String e164 = cursor.getString(cursor.getColumnIndexOrThrow(PHONE));
String email = cursor.getString(cursor.getColumnIndexOrThrow(EMAIL));
String groupId = cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID));
boolean blocked = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCKED)) == 1;
String messageRingtone = cursor.getString(cursor.getColumnIndexOrThrow(MESSAGE_RINGTONE));
String callRingtone = cursor.getString(cursor.getColumnIndexOrThrow(CALL_RINGTONE));
int messageVibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(MESSAGE_VIBRATE));
int callVibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(CALL_VIBRATE));
long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL));
String serializedColor = cursor.getString(cursor.getColumnIndexOrThrow(COLOR));
int insightsBannerTier = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_INVITE_REMINDER));
int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID));
int expireMessages = cursor.getInt(cursor.getColumnIndexOrThrow(MESSAGE_EXPIRATION_TIME));
int registeredState = cursor.getInt(cursor.getColumnIndexOrThrow(REGISTERED));
String profileKeyString = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_KEY));
String systemDisplayName = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_DISPLAY_NAME));
String systemContactPhoto = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHOTO_URI));
String systemPhoneLabel = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHONE_LABEL));
String systemContactUri = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_CONTACT_URI));
String profileGivenName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_GIVEN_NAME));
String profileFamilyName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_FAMILY_NAME));
String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_AVATAR));
boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1;
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE));
boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1;
boolean uuidSupported = cursor.getInt(cursor.getColumnIndexOrThrow(UUID_SUPPORTED)) == 1;
String storageKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(STORAGE_SERVICE_KEY));
String identityKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
int identityStatusRaw = cursor.getInt(cursor.getColumnIndexOrThrow(IDENTITY_STATUS));
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
UUID uuid = UuidUtil.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(UUID)));
String username = cursor.getString(cursor.getColumnIndexOrThrow(USERNAME));
String e164 = cursor.getString(cursor.getColumnIndexOrThrow(PHONE));
String email = cursor.getString(cursor.getColumnIndexOrThrow(EMAIL));
String groupId = cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID));
boolean blocked = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCKED)) == 1;
String messageRingtone = cursor.getString(cursor.getColumnIndexOrThrow(MESSAGE_RINGTONE));
String callRingtone = cursor.getString(cursor.getColumnIndexOrThrow(CALL_RINGTONE));
int messageVibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(MESSAGE_VIBRATE));
int callVibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(CALL_VIBRATE));
long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL));
String serializedColor = cursor.getString(cursor.getColumnIndexOrThrow(COLOR));
int insightsBannerTier = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_INVITE_REMINDER));
int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID));
int expireMessages = cursor.getInt(cursor.getColumnIndexOrThrow(MESSAGE_EXPIRATION_TIME));
int registeredState = cursor.getInt(cursor.getColumnIndexOrThrow(REGISTERED));
String profileKeyString = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_KEY));
String profileKeyCredentialString = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_KEY_CREDENTIAL));
String systemDisplayName = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_DISPLAY_NAME));
String systemContactPhoto = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHOTO_URI));
String systemPhoneLabel = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHONE_LABEL));
String systemContactUri = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_CONTACT_URI));
String profileGivenName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_GIVEN_NAME));
String profileFamilyName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_FAMILY_NAME));
String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_AVATAR));
boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1;
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE));
boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1;
boolean uuidSupported = cursor.getInt(cursor.getColumnIndexOrThrow(UUID_SUPPORTED)) == 1;
String storageKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(STORAGE_SERVICE_KEY));
String identityKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
int identityStatusRaw = cursor.getInt(cursor.getColumnIndexOrThrow(IDENTITY_STATUS));
MaterialColor color;
byte[] profileKey = null;
byte[] profileKey = null;
byte[] profileKeyCredential = null;
try {
color = serializedColor == null ? null : MaterialColor.fromSerialized(serializedColor);
@ -613,6 +623,15 @@ public class RecipientDatabase extends Database {
Log.w(TAG, e);
profileKey = null;
}
if (profileKeyCredentialString != null) {
try {
profileKeyCredential = Base64.decode(profileKeyCredentialString);
} catch (IOException e) {
Log.w(TAG, e);
profileKeyCredential = null;
}
}
}
byte[] storageKey = null;
@ -637,7 +656,8 @@ public class RecipientDatabase extends Database {
Util.uri(messageRingtone), Util.uri(callRingtone),
color, defaultSubscriptionId, expireMessages,
RegisteredState.fromId(registeredState),
profileKey, systemDisplayName, systemContactPhoto,
profileKey, profileKeyCredential,
systemDisplayName, systemContactPhoto,
systemPhoneLabel, systemContactUri,
ProfileName.fromParts(profileGivenName, profileFamilyName), signalProfileAvatar, profileSharing,
notificationChannel, UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
@ -776,9 +796,61 @@ public class RecipientDatabase extends Database {
Recipient.live(id).refresh();
}
public void setProfileKey(@NonNull RecipientId id, @Nullable byte[] profileKey) {
/**
* Updates the profile key.
* <p>
* If it changes, it clears out the profile key credential and resets the unidentified access mode.
* @return true iff changed.
*/
public boolean setProfileKey(@NonNull RecipientId id, @NonNull ProfileKey profileKey) {
String selection = ID + " = ?";
String[] args = new String[]{id.serialize()};
ContentValues valuesToCompare = new ContentValues(1);
ContentValues valuesToSet = new ContentValues(3);
String encodedProfileKey = Base64.encodeBytes(profileKey.serialize());
valuesToCompare.put(PROFILE_KEY, encodedProfileKey);
valuesToSet.put(PROFILE_KEY, encodedProfileKey);
valuesToSet.putNull(PROFILE_KEY_CREDENTIAL);
valuesToSet.put(UNIDENTIFIED_ACCESS_MODE, UnidentifiedAccessMode.UNKNOWN.getMode());
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, valuesToCompare);
if (update(updateQuery, valuesToSet)) {
markDirty(id, DirtyState.UPDATE);
Recipient.live(id).refresh();
return true;
} else {
return false;
}
}
/**
* Updates the profile key credential as long as the profile key matches.
*/
public void setProfileKeyCredential(@NonNull RecipientId id,
@NonNull ProfileKey profileKey,
@NonNull ProfileKeyCredential profileKeyCredential)
{
String selection = ID + " = ? AND " + PROFILE_KEY + " = ?";
String[] args = new String[]{id.serialize(), Base64.encodeBytes(profileKey.serialize())};
ContentValues values = new ContentValues(1);
values.put(PROFILE_KEY_CREDENTIAL, Base64.encodeBytes(profileKeyCredential.serialize()));
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values);
if (update(updateQuery, values)) {
// TODO [greyson] If we sync this in future, mark dirty
//markDirty(id, DirtyState.UPDATE);
Recipient.live(id).refresh();
}
}
private void clearProfileKeyCredential(@NonNull RecipientId id) {
ContentValues values = new ContentValues(1);
values.put(PROFILE_KEY, profileKey == null ? null : Base64.encodeBytes(profileKey));
values.putNull(PROFILE_KEY_CREDENTIAL);
if (update(id, values)) {
markDirty(id, DirtyState.UPDATE);
Recipient.live(id).refresh();
@ -1224,14 +1296,23 @@ public class RecipientDatabase extends Database {
* Will update the database with the content values you specified. It will make an intelligent
* query such that this will only return true if a row was *actually* updated.
*/
private boolean update(@NonNull RecipientId id, ContentValues contentValues) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
String selection = ID + " = ?";
String[] args = new String[]{id.serialize()};
private boolean update(@NonNull RecipientId id, @NonNull ContentValues contentValues) {
String selection = ID + " = ?";
String[] args = new String[]{id.serialize()};
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, contentValues);
Pair<String, String[]> result = SqlUtil.buildTrueUpdateQuery(selection, args, contentValues);
return update(updateQuery, contentValues);
}
return database.update(TABLE_NAME, contentValues, result.first(), result.second()) > 0;
/**
* Will update the database with the {@param contentValues} you specified.
* <p>
* This will only return true if a row was *actually* updated with respect to the where clause of the {@param updateQuery}.
*/
private boolean update(@NonNull SqlUtil.UpdateQuery updateQuery, @NonNull ContentValues contentValues) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
return database.update(TABLE_NAME, contentValues, updateQuery.getWhere(), updateQuery.getWhereArgs()) > 0;
}
private @NonNull Optional<RecipientId> getByColumn(@NonNull String column, String value) {
@ -1374,6 +1455,7 @@ public class RecipientDatabase extends Database {
private final int expireMessages;
private final RegisteredState registered;
private final byte[] profileKey;
private final byte[] profileKeyCredential;
private final String systemDisplayName;
private final String systemContactPhoto;
private final String systemPhoneLabel;
@ -1406,6 +1488,7 @@ public class RecipientDatabase extends Database {
int expireMessages,
@NonNull RegisteredState registered,
@Nullable byte[] profileKey,
@Nullable byte[] profileKeyCredential,
@Nullable String systemDisplayName,
@Nullable String systemContactPhoto,
@Nullable String systemPhoneLabel,
@ -1439,6 +1522,7 @@ public class RecipientDatabase extends Database {
this.expireMessages = expireMessages;
this.registered = registered;
this.profileKey = profileKey;
this.profileKeyCredential = profileKeyCredential;
this.systemDisplayName = systemDisplayName;
this.systemContactPhoto = systemContactPhoto;
this.systemPhoneLabel = systemPhoneLabel;
@ -1528,6 +1612,10 @@ public class RecipientDatabase extends Database {
return profileKey;
}
public @Nullable byte[] getProfileKeyCredential() {
return profileKeyCredential;
}
public @Nullable String getSystemDisplayName() {
return systemDisplayName;
}

View File

@ -9,6 +9,7 @@ import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import androidx.annotation.NonNull;
@ -47,10 +48,12 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import java.io.File;
import java.util.List;
@ -105,8 +108,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int STICKER_PACK_ORDER = 44;
private static final int MEGAPHONES = 45;
private static final int MEGAPHONE_FIRST_APPEARANCE = 46;
private static final int PROFILE_KEY_TO_DB = 47;
private static final int PROFILE_KEY_CREDENTIALS = 48;
private static final int ATTACHMENT_FILE_INDEX = 49;
private static final int DATABASE_VERSION = 46;
private static final int DATABASE_VERSION = 49;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@ -724,6 +730,29 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE megaphone ADD COLUMN first_visible INTEGER DEFAULT 0");
}
if (oldVersion < PROFILE_KEY_TO_DB) {
String localNumber = TextSecurePreferences.getLocalNumber(context);
if (!TextUtils.isEmpty(localNumber)) {
String encodedProfileKey = PreferenceManager.getDefaultSharedPreferences(context).getString("pref_profile_key", null);
byte[] profileKey = encodedProfileKey != null ? Base64.decodeOrThrow(encodedProfileKey) : Util.getSecretBytes(32);
ContentValues values = new ContentValues(1);
values.put("profile_key", Base64.encodeBytes(profileKey));
if (db.update("recipient", values, "phone = ?", new String[]{localNumber}) == 0) {
throw new AssertionError("No rows updated!");
}
}
}
if (oldVersion < PROFILE_KEY_CREDENTIALS) {
db.execSQL("ALTER TABLE recipient ADD COLUMN profile_key_credential TEXT DEFAULT NULL");
}
if (oldVersion < ATTACHMENT_FILE_INDEX) {
db.execSQL("CREATE INDEX IF NOT EXISTS part_data_index ON part (_data)");
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@ -4,8 +4,6 @@ import android.content.Context;
import android.media.MediaDataSource;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import org.greenrobot.eventbus.EventBus;
import org.thoughtcrime.securesms.R;
@ -29,7 +27,6 @@ import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
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 org.thoughtcrime.securesms.util.MemoryFileDescriptor.MemoryFileException;
import org.thoughtcrime.securesms.video.InMemoryTranscoder;
import org.thoughtcrime.securesms.video.VideoSizeException;
@ -143,17 +140,20 @@ public final class AttachmentCompressionJob extends BaseJob {
throws UndeliverableMessageException
{
try {
if (MediaUtil.isVideo(attachment) && MediaConstraints.isVideoTranscodeAvailable()) {
if (MediaUtil.isVideo(attachment)) {
transcodeVideoIfNeededToDatabase(context, attachmentDatabase, attachment, constraints, EventBus.getDefault(), this::isCanceled);
if (!constraints.isSatisfied(context, attachment)) {
throw new UndeliverableMessageException("Size constraints could not be met on video!");
}
} else if (constraints.isSatisfied(context, attachment)) {
if (MediaUtil.isJpeg(attachment)) {
MediaStream stripped = getResizedMedia(context, attachment, constraints);
attachmentDatabase.updateAttachmentData(attachment, stripped);
attachmentDatabase.updateAttachmentData(attachment, stripped, false);
attachmentDatabase.markAttachmentAsTransformed(attachmentId);
}
} else if (constraints.canResize(attachment)) {
MediaStream resized = getResizedMedia(context, attachment, constraints);
attachmentDatabase.updateAttachmentData(attachment, resized);
attachmentDatabase.updateAttachmentData(attachment, resized, false);
attachmentDatabase.markAttachmentAsTransformed(attachmentId);
} else {
throw new UndeliverableMessageException("Size constraints could not be met!");
@ -163,7 +163,6 @@ public final class AttachmentCompressionJob extends BaseJob {
}
}
@RequiresApi(26)
private static void transcodeVideoIfNeededToDatabase(@NonNull Context context,
@NonNull AttachmentDatabase attachmentDatabase,
@NonNull DatabaseAttachment attachment,
@ -172,6 +171,17 @@ public final class AttachmentCompressionJob extends BaseJob {
@NonNull InMemoryTranscoder.CancelationSignal cancelationSignal)
throws UndeliverableMessageException
{
AttachmentDatabase.TransformProperties transformProperties = attachment.getTransformProperties();
boolean allowSkipOnFailure = false;
if (!MediaConstraints.isVideoTranscodeAvailable()) {
if (transformProperties.isVideoEdited()) {
throw new UndeliverableMessageException("Video edited, but transcode is not available");
}
return;
}
try (NotificationController notification = GenericForegroundService.startForegroundTask(context, context.getString(R.string.AttachmentUploadJob_compressing_video_start))) {
notification.setIndeterminateProgress();
@ -182,10 +192,14 @@ public final class AttachmentCompressionJob extends BaseJob {
throw new UndeliverableMessageException("Cannot get media data source for attachment.");
}
try (InMemoryTranscoder transcoder = new InMemoryTranscoder(context, dataSource, constraints.getCompressedVideoMaxSize(context))) {
allowSkipOnFailure = !transformProperties.isVideoEdited();
InMemoryTranscoder.Options options = null;
if (transformProperties.isVideoTrim()) {
options = new InMemoryTranscoder.Options(transformProperties.getVideoTrimStartTimeUs(), transformProperties.getVideoTrimEndTimeUs());
}
try (InMemoryTranscoder transcoder = new InMemoryTranscoder(context, dataSource, options, constraints.getCompressedVideoMaxSize(context))) {
if (transcoder.isTranscodeRequired()) {
MediaStream mediaStream = transcoder.transcode(percent -> {
notification.setProgress(100, percent);
eventBus.postSticky(new PartProgressEvent(attachment,
@ -194,7 +208,7 @@ public final class AttachmentCompressionJob extends BaseJob {
percent));
}, cancelationSignal);
attachmentDatabase.updateAttachmentData(attachment, mediaStream);
attachmentDatabase.updateAttachmentData(attachment, mediaStream, transformProperties.isVideoEdited());
attachmentDatabase.markAttachmentAsTransformed(attachment.getAttachmentId());
}
}
@ -203,7 +217,11 @@ public final class AttachmentCompressionJob extends BaseJob {
if (attachment.getSize() > constraints.getVideoMaxSize(context)) {
throw new UndeliverableMessageException("Duration not found, attachment too large to skip transcode", e);
} else {
Log.w(TAG, "Problem with video source, but video small enough to skip transcode", e);
if (allowSkipOnFailure) {
Log.w(TAG, "Problem with video source, but video small enough to skip transcode", e);
} else {
throw new UndeliverableMessageException("Failed to transcode and cannot skip due to editing", e);
}
}
} catch (IOException | MmsException | VideoSizeException e) {
throw new UndeliverableMessageException("Failed to transcode", e);

View File

@ -0,0 +1,96 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.logging.Log;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* Only marks an attachment as uploaded.
*/
public final class AttachmentMarkUploadedJob extends BaseJob {
public static final String KEY = "AttachmentMarkUploadedJob";
@SuppressWarnings("unused")
private static final String TAG = Log.tag(AttachmentMarkUploadedJob.class);
private static final String KEY_ROW_ID = "row_id";
private static final String KEY_UNIQUE_ID = "unique_id";
private static final String KEY_MESSAGE_ID = "message_id";
private final AttachmentId attachmentId;
private final long messageId;
public AttachmentMarkUploadedJob(long messageId, @NonNull AttachmentId attachmentId) {
this(new Parameters.Builder()
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.build(),
messageId,
attachmentId);
}
private AttachmentMarkUploadedJob(@NonNull Parameters parameters, long messageId, @NonNull AttachmentId attachmentId) {
super(parameters);
this.attachmentId = attachmentId;
this.messageId = messageId;
}
@Override
public @NonNull Data serialize() {
return new Data.Builder().putLong(KEY_ROW_ID, attachmentId.getRowId())
.putLong(KEY_UNIQUE_ID, attachmentId.getUniqueId())
.putLong(KEY_MESSAGE_ID, messageId)
.build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onRun() throws Exception {
AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
DatabaseAttachment databaseAttachment = database.getAttachment(attachmentId);
if (databaseAttachment == null) {
throw new InvalidAttachmentException("Cannot find the specified attachment.");
}
database.markAttachmentUploaded(messageId, databaseAttachment);
}
@Override
public void onFailure() {
}
@Override
protected boolean onShouldRetry(@NonNull Exception exception) {
return exception instanceof IOException;
}
private class InvalidAttachmentException extends Exception {
InvalidAttachmentException(String message) {
super(message);
}
}
public static final class Factory implements Job.Factory<AttachmentMarkUploadedJob> {
@Override
public @NonNull AttachmentMarkUploadedJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new AttachmentMarkUploadedJob(parameters,
data.getLong(KEY_MESSAGE_ID),
new AttachmentId(data.getLong(KEY_ROW_ID), data.getLong(KEY_UNIQUE_ID)));
}
}
}

View File

@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
import org.thoughtcrime.securesms.migrations.MigrationCompleteJob;
import org.thoughtcrime.securesms.migrations.RecipientSearchMigrationJob;
import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob;
import org.thoughtcrime.securesms.migrations.StickerAdditionMigrationJob;
import org.thoughtcrime.securesms.migrations.StickerLaunchMigrationJob;
import org.thoughtcrime.securesms.migrations.UuidMigrationJob;
@ -41,6 +42,7 @@ public final class JobManagerFactories {
put(AttachmentCopyJob.KEY, new AttachmentCopyJob.Factory());
put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory());
put(AttachmentUploadJob.KEY, new AttachmentUploadJob.Factory());
put(AttachmentMarkUploadedJob.KEY, new AttachmentMarkUploadedJob.Factory());
put(AttachmentCompressionJob.KEY, new AttachmentCompressionJob.Factory());
put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory());
put(CleanPreKeysJob.KEY, new CleanPreKeysJob.Factory());
@ -100,16 +102,17 @@ public final class JobManagerFactories {
put(ProfileUploadJob.KEY, new ProfileUploadJob.Factory());
// Migrations
put(Argon2TestMigrationJob.KEY, new Argon2TestMigrationJob.Factory());
put(AvatarMigrationJob.KEY, new AvatarMigrationJob.Factory());
put(CachedAttachmentsMigrationJob.KEY, new CachedAttachmentsMigrationJob.Factory());
put(DatabaseMigrationJob.KEY, new DatabaseMigrationJob.Factory());
put(LegacyMigrationJob.KEY, new LegacyMigrationJob.Factory());
put(MigrationCompleteJob.KEY, new MigrationCompleteJob.Factory());
put(RecipientSearchMigrationJob.KEY, new RecipientSearchMigrationJob.Factory());
put(StickerLaunchMigrationJob.KEY, new StickerLaunchMigrationJob.Factory());
put(UuidMigrationJob.KEY, new UuidMigrationJob.Factory());
put(RegistrationPinV2MigrationJob.KEY, new RegistrationPinV2MigrationJob.Factory());
put(Argon2TestMigrationJob.KEY, new Argon2TestMigrationJob.Factory());
put(StickerLaunchMigrationJob.KEY, new StickerLaunchMigrationJob.Factory());
put(StickerAdditionMigrationJob.KEY, new StickerAdditionMigrationJob.Factory());
put(UuidMigrationJob.KEY, new UuidMigrationJob.Factory());
// Dead jobs
put("PushContentReceiveJob", new FailingJob.Factory());

View File

@ -4,15 +4,16 @@ import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
@ -42,8 +43,6 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -137,7 +136,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
getSystemAvatar(recipient.getContactUri()),
Optional.fromNullable(recipient.getColor().serialize()),
verifiedMessage,
Optional.fromNullable(recipient.getProfileKey()),
ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey()),
recipient.isBlocked(),
recipient.getExpireMessages() > 0 ? Optional.of(recipient.getExpireMessages())
: Optional.absent(),
@ -184,7 +183,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
Optional<VerifiedMessage> verified = getVerifiedMessage(recipient, identity);
Optional<String> name = Optional.fromNullable(recipient.getName(context));
Optional<String> color = Optional.of(recipient.getColor().serialize());
Optional<byte[]> profileKey = Optional.fromNullable(recipient.getProfileKey());
Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey());
boolean blocked = recipient.isBlocked();
Optional<Integer> expireTimer = recipient.getExpireMessages() > 0 ? Optional.of(recipient.getExpireMessages()) : Optional.absent();
Optional<Integer> inboxPosition = Optional.fromNullable(inboxPositions.get(recipient.getId()));
@ -201,14 +200,17 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
archived.contains(recipient.getId())));
}
if (ProfileKeyUtil.hasProfileKey(context)) {
Recipient self = Recipient.self();
Recipient self = Recipient.self();
byte[] profileKey = self.getProfileKey();
if (profileKey != null) {
out.write(new DeviceContact(RecipientUtil.toSignalServiceAddress(context, self),
Optional.absent(),
Optional.absent(),
Optional.of(self.getColor().serialize()),
Optional.absent(),
Optional.of(ProfileKeyUtil.getProfileKey(context)),
ProfileKeyUtil.profileKeyOptionalOrThrow(self.getProfileKey()),
false,
self.getExpireMessages() > 0 ? Optional.of(self.getExpireMessages()) : Optional.absent(),
Optional.fromNullable(inboxPositions.get(self.getId())),

View File

@ -3,14 +3,14 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -66,7 +66,7 @@ public class MultiDeviceProfileKeyUpdateJob extends BaseJob {
return;
}
Optional<byte[]> profileKey = Optional.of(ProfileKeyUtil.getProfileKey(context));
Optional<ProfileKey> profileKey = Optional.of(ProfileKeyUtil.getSelfProfileKey());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DeviceContactsOutputStream out = new DeviceContactsOutputStream(baos);

View File

@ -4,6 +4,7 @@ import android.content.Context;
import androidx.annotation.NonNull;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
@ -11,16 +12,11 @@ import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.util.StreamDetails;
import java.io.ByteArrayInputStream;
public final class ProfileUploadJob extends BaseJob {
public static final String KEY = "ProfileUploadJob";
@ -47,8 +43,17 @@ public final class ProfileUploadJob extends BaseJob {
@Override
protected void onRun() throws Exception {
uploadProfileName();
uploadAvatar();
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
ProfileName profileName = TextSecurePreferences.getProfileName(context);
try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) {
if (FeatureFlags.VERSIONED_PROFILES) {
accountManager.setVersionedProfile(profileKey, profileName.serialize(), avatar);
} else {
accountManager.setProfileName(profileKey, profileName.serialize());
accountManager.setProfileAvatar(profileKey, avatar);
}
}
}
@Override
@ -70,33 +75,6 @@ public final class ProfileUploadJob extends BaseJob {
public void onFailure() {
}
private void uploadProfileName() throws Exception {
ProfileName profileName = TextSecurePreferences.getProfileName(context);
accountManager.setProfileName(ProfileKeyUtil.getProfileKey(context), profileName.serialize());
}
private void uploadAvatar() throws Exception {
final RecipientId selfId = Recipient.self().getId();
final byte[] avatar;
if (AvatarHelper.getAvatarFile(context, selfId).exists() && AvatarHelper.getAvatarFile(context, selfId).length() > 0) {
avatar = Util.readFully(AvatarHelper.getInputStreamFor(context, Recipient.self().getId()));
} else {
avatar = null;
}
final StreamDetails avatarDetails;
if (avatar == null || avatar.length == 0) {
avatarDetails = null;
} else {
avatarDetails = new StreamDetails(new ByteArrayInputStream(avatar),
MediaUtil.IMAGE_JPEG,
avatar.length);
}
accountManager.setProfileAvatar(ProfileKeyUtil.getProfileKey(context), avatarDetails);
}
public static class Factory implements Job.Factory {
@NonNull

View File

@ -12,6 +12,7 @@ import androidx.annotation.Nullable;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
@ -20,6 +21,7 @@ import org.thoughtcrime.securesms.attachments.TombstoneAttachment;
import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
@ -61,6 +63,8 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.ringrtc.IceCandidateParcel;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
@ -102,7 +106,6 @@ import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
@ -271,8 +274,8 @@ public final class PushProcessMessageJob extends BaseJob {
handleUnknownGroupMessage(content, message.getGroupInfo().get());
}
if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
handleProfileKey(content, message);
if (message.getProfileKey().isPresent()) {
handleProfileKey(content, message.getProfileKey().get());
}
if (content.isNeedsReceipt()) {
@ -367,18 +370,21 @@ public final class PushProcessMessageJob extends BaseJob {
@NonNull OfferMessage message,
@NonNull Optional<Long> smsMessageId)
{
Log.w(TAG, "handleCallOfferMessage...");
Log.i(TAG, "handleCallOfferMessage...");
if (smsMessageId.isPresent()) {
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
database.markAsMissedCall(smsMessageId.get());
} else {
Intent intent = new Intent(context, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_INCOMING_CALL);
intent.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId());
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_RECIPIENT, Recipient.externalPush(context, content.getSender()).getId());
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_DESCRIPTION, message.getDescription());
intent.putExtra(WebRtcCallService.EXTRA_TIMESTAMP, content.getTimestamp());
Intent intent = new Intent(context, WebRtcCallService.class);
RemotePeer remotePeer = new RemotePeer(Recipient.externalPush(context, content.getSender()).getId());
intent.setAction(WebRtcCallService.ACTION_RECEIVE_OFFER)
.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer)
.putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice())
.putExtra(WebRtcCallService.EXTRA_OFFER_DESCRIPTION, message.getDescription())
.putExtra(WebRtcCallService.EXTRA_TIMESTAMP, content.getTimestamp());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) context.startForegroundService(intent);
else context.startService(intent);
@ -389,11 +395,14 @@ public final class PushProcessMessageJob extends BaseJob {
@NonNull AnswerMessage message)
{
Log.i(TAG, "handleCallAnswerMessage...");
Intent intent = new Intent(context, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_RESPONSE_MESSAGE);
intent.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId());
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_RECIPIENT, Recipient.externalPush(context, content.getSender()).getId());
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_DESCRIPTION, message.getDescription());
Intent intent = new Intent(context, WebRtcCallService.class);
RemotePeer remotePeer = new RemotePeer(Recipient.externalPush(context, content.getSender()).getId());
intent.setAction(WebRtcCallService.ACTION_RECEIVE_ANSWER)
.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer)
.putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice())
.putExtra(WebRtcCallService.EXTRA_ANSWER_DESCRIPTION, message.getDescription());
context.startService(intent);
}
@ -401,18 +410,25 @@ public final class PushProcessMessageJob extends BaseJob {
private void handleCallIceUpdateMessage(@NonNull SignalServiceContent content,
@NonNull List<IceUpdateMessage> messages)
{
Log.w(TAG, "handleCallIceUpdateMessage... " + messages.size());
for (IceUpdateMessage message : messages) {
Intent intent = new Intent(context, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_ICE_MESSAGE);
intent.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId());
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_RECIPIENT, Recipient.externalPush(context, content.getSender()).getId());
intent.putExtra(WebRtcCallService.EXTRA_ICE_SDP, message.getSdp());
intent.putExtra(WebRtcCallService.EXTRA_ICE_SDP_MID, message.getSdpMid());
intent.putExtra(WebRtcCallService.EXTRA_ICE_SDP_LINE_INDEX, message.getSdpMLineIndex());
Log.i(TAG, "handleCallIceUpdateMessage... " + messages.size());
context.startService(intent);
ArrayList<IceCandidateParcel> iceCandidates = new ArrayList(messages.size());
long callId = -1;
for (IceUpdateMessage iceMessage : messages) {
iceCandidates.add(new IceCandidateParcel(iceMessage));
callId = iceMessage.getId();
}
Intent intent = new Intent(context, WebRtcCallService.class);
RemotePeer remotePeer = new RemotePeer(Recipient.externalPush(context, content.getSender()).getId());
intent.setAction(WebRtcCallService.ACTION_RECEIVE_ICE_CANDIDATES)
.putExtra(WebRtcCallService.EXTRA_CALL_ID, callId)
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer)
.putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice())
.putParcelableArrayListExtra(WebRtcCallService.EXTRA_ICE_CANDIDATES, iceCandidates);
context.startService(intent);
}
private void handleCallHangupMessage(@NonNull SignalServiceContent content,
@ -423,10 +439,13 @@ public final class PushProcessMessageJob extends BaseJob {
if (smsMessageId.isPresent()) {
DatabaseFactory.getSmsDatabase(context).markAsMissedCall(smsMessageId.get());
} else {
Intent intent = new Intent(context, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_REMOTE_HANGUP);
intent.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId());
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_RECIPIENT, Recipient.externalPush(context, content.getSender()).getId());
Intent intent = new Intent(context, WebRtcCallService.class);
RemotePeer remotePeer = new RemotePeer(Recipient.externalPush(context, content.getSender()).getId());
intent.setAction(WebRtcCallService.ACTION_RECEIVE_HANGUP)
.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer)
.putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice());
context.startService(intent);
}
@ -435,10 +454,15 @@ public final class PushProcessMessageJob extends BaseJob {
private void handleCallBusyMessage(@NonNull SignalServiceContent content,
@NonNull BusyMessage message)
{
Intent intent = new Intent(context, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_REMOTE_BUSY);
intent.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId());
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_RECIPIENT, Recipient.externalPush(context, content.getSender()).getId());
Log.i(TAG, "handleCallBusyMessage");
Intent intent = new Intent(context, WebRtcCallService.class);
RemotePeer remotePeer = new RemotePeer(Recipient.externalPush(context, content.getSender()).getId());
intent.setAction(WebRtcCallService.ACTION_RECEIVE_BUSY)
.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer)
.putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice());
context.startService(intent);
}
@ -1175,15 +1199,18 @@ public final class PushProcessMessageJob extends BaseJob {
}
private void handleProfileKey(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message)
@NonNull byte[] messageProfileKeyBytes)
{
RecipientDatabase database = DatabaseFactory.getRecipientDatabase(context);
Recipient recipient = Recipient.externalPush(context, content.getSender());
RecipientDatabase database = DatabaseFactory.getRecipientDatabase(context);
Recipient recipient = Recipient.externalPush(context, content.getSender());
ProfileKey messageProfileKey = ProfileKeyUtil.profileKeyOrNull(messageProfileKeyBytes);
if (recipient.getProfileKey() == null || !MessageDigest.isEqual(recipient.getProfileKey(), message.getProfileKey().get())) {
database.setProfileKey(recipient.getId(), message.getProfileKey().get());
database.setUnidentifiedAccessMode(recipient.getId(), RecipientDatabase.UnidentifiedAccessMode.UNKNOWN);
ApplicationDependencies.getJobManager().add(new RetrieveProfileJob(recipient));
if (messageProfileKey != null) {
if (database.setProfileKey(recipient.getId(), messageProfileKey)) {
ApplicationDependencies.getJobManager().add(new RetrieveProfileJob(recipient));
}
} else {
Log.w(TAG, "Ignored invalid profile key seen in message");
}
}

View File

@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
import java.io.IOException;
@ -47,7 +48,7 @@ public class RefreshAttributesJob extends BaseJob {
public void onRun() throws IOException {
int registrationId = TextSecurePreferences.getLocalRegistrationId(context);
boolean fetchesMessages = TextSecurePreferences.isFcmDisabled(context);
byte[] unidentifiedAccessKey = UnidentifiedAccessUtil.getSelfUnidentifiedAccessKey(context);
byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
String pin = null;
String registrationLockToken = null;

View File

@ -3,8 +3,11 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
@ -12,9 +15,12 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
@ -58,11 +64,31 @@ public class RefreshOwnProfileJob extends BaseJob {
@Override
protected void onRun() throws Exception {
SignalServiceProfile profile = ProfileUtil.retrieveProfile(context, Recipient.self());
Recipient self = Recipient.self();
ProfileAndCredential profileAndCredential = ProfileUtil.retrieveProfile(context, self, getRequestType(self));
SignalServiceProfile profile = profileAndCredential.getProfile();
setProfileName(profile.getName());
setProfileAvatar(profile.getAvatar());
setProfileCapabilities(profile.getCapabilities());
Optional<ProfileKeyCredential> profileKeyCredential = profileAndCredential.getProfileKeyCredential();
if (profileKeyCredential.isPresent()) {
setProfileKeyCredential(self, ProfileKeyUtil.getSelfProfileKey(), profileKeyCredential.get());
}
}
private void setProfileKeyCredential(@NonNull Recipient recipient,
@NonNull ProfileKey recipientProfileKey,
@NonNull ProfileKeyCredential credential)
{
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
recipientDatabase.setProfileKeyCredential(recipient.getId(), recipientProfileKey, credential);
}
private static SignalServiceProfile.RequestType getRequestType(@NonNull Recipient recipient) {
return FeatureFlags.VERSIONED_PROFILES && !recipient.hasProfileKeyCredential()
? SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL
: SignalServiceProfile.RequestType.PROFILE;
}
@Override
@ -75,7 +101,7 @@ public class RefreshOwnProfileJob extends BaseJob {
private void setProfileName(@Nullable String encryptedName) {
try {
byte[] profileKey = ProfileKeyUtil.getProfileKey(context);
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
String plaintextName = ProfileUtil.decryptName(profileKey, encryptedName);
ProfileName profileName = ProfileName.fromSerialized(plaintextName);
@ -86,7 +112,7 @@ public class RefreshOwnProfileJob extends BaseJob {
}
}
private void setProfileAvatar(@Nullable String avatar) {
private static void setProfileAvatar(@Nullable String avatar) {
ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(Recipient.self(), avatar));
}

View File

@ -1,9 +1,12 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@ -37,8 +40,8 @@ public class RetrieveProfileAvatarJob extends BaseJob {
private static final String KEY_PROFILE_AVATAR = "profile_avatar";
private static final String KEY_RECIPIENT = "recipient";
private String profileAvatar;
private Recipient recipient;
private final String profileAvatar;
private final Recipient recipient;
public RetrieveProfileAvatarJob(Recipient recipient, String profileAvatar) {
this(new Job.Parameters.Builder()
@ -73,7 +76,7 @@ public class RetrieveProfileAvatarJob extends BaseJob {
@Override
public void onRun() throws IOException {
RecipientDatabase database = DatabaseFactory.getRecipientDatabase(context);
byte[] profileKey = recipient.resolve().getProfileKey();
ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.resolve().getProfileKey());
if (profileKey == null) {
Log.w(TAG, "Recipient profile key is gone!");

View File

@ -1,11 +1,14 @@
package org.thoughtcrime.securesms.jobs;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
@ -24,8 +27,10 @@ import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import java.io.IOException;
@ -92,9 +97,11 @@ public class RetrieveProfileJob extends BaseJob {
}
private void handlePhoneNumberRecipient(Recipient recipient) throws IOException {
SignalServiceProfile profile = ProfileUtil.retrieveProfile(context, recipient);
ProfileAndCredential profileAndCredential = ProfileUtil.retrieveProfile(context, recipient, getRequestType(recipient));
SignalServiceProfile profile = profileAndCredential.getProfile();
ProfileKey recipientProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
if (recipient.getProfileKey() == null) {
if (recipientProfileKey == null) {
Log.i(TAG, "No profile key available for " + recipient.getId());
} else {
Log.i(TAG, "Profile key available for " + recipient.getId());
@ -106,6 +113,27 @@ public class RetrieveProfileJob extends BaseJob {
setProfileCapabilities(recipient, profile.getCapabilities());
setIdentityKey(recipient, profile.getIdentityKey());
setUnidentifiedAccessMode(recipient, profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess());
if (recipientProfileKey != null) {
Optional<ProfileKeyCredential> profileKeyCredential = profileAndCredential.getProfileKeyCredential();
if (profileKeyCredential.isPresent()) {
setProfileKeyCredential(recipient, recipientProfileKey, profileKeyCredential.get());
}
}
}
private void setProfileKeyCredential(@NonNull Recipient recipient,
@NonNull ProfileKey recipientProfileKey,
@NonNull ProfileKeyCredential credential)
{
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
recipientDatabase.setProfileKeyCredential(recipient.getId(), recipientProfileKey, credential);
}
private static SignalServiceProfile.RequestType getRequestType(@NonNull Recipient recipient) {
return FeatureFlags.VERSIONED_PROFILES && !recipient.hasProfileKeyCredential()
? SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL
: SignalServiceProfile.RequestType.PROFILE;
}
private void handleGroupRecipient(Recipient group) throws IOException {
@ -141,7 +169,7 @@ public class RetrieveProfileJob extends BaseJob {
private void setUnidentifiedAccessMode(Recipient recipient, String unidentifiedAccessVerifier, boolean unrestrictedUnidentifiedAccess) {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
byte[] profileKey = recipient.getProfileKey();
ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
if (unrestrictedUnidentifiedAccess && unidentifiedAccessVerifier != null) {
if (recipient.getUnidentifiedAccessMode() != UnidentifiedAccessMode.UNRESTRICTED) {
@ -175,7 +203,7 @@ public class RetrieveProfileJob extends BaseJob {
private void setProfileName(Recipient recipient, String profileName) {
try {
byte[] profileKey = recipient.getProfileKey();
ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
if (profileKey == null) return;
String plaintextProfileName = ProfileUtil.decryptName(profileKey, profileName);

View File

@ -1,24 +1,23 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.util.StreamDetails;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class RotateProfileKeyJob extends BaseJob {
public static String KEY = "RotateProfileKeyJob";
@ -48,11 +47,22 @@ public class RotateProfileKeyJob extends BaseJob {
@Override
public void onRun() throws Exception {
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
byte[] profileKey = ProfileKeyUtil.rotateProfileKey(context);
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
ProfileKey profileKey = ProfileKeyUtil.createNew();
Recipient self = Recipient.self();
accountManager.setProfileName(profileKey, TextSecurePreferences.getProfileName(context).serialize());
accountManager.setProfileAvatar(profileKey, getProfileAvatar());
recipientDatabase.setProfileKey(self.getId(), profileKey);
try (StreamDetails avatarStream = AvatarHelper.getSelfProfileAvatarStream(context)) {
if (FeatureFlags.VERSIONED_PROFILES) {
accountManager.setVersionedProfile(profileKey,
TextSecurePreferences.getProfileName(context).serialize(),
avatarStream);
} else {
accountManager.setProfileName(profileKey, TextSecurePreferences.getProfileName(context).serialize());
accountManager.setProfileAvatar(profileKey, avatarStream);
}
}
ApplicationDependencies.getJobManager().add(new RefreshAttributesJob());
}
@ -67,19 +77,6 @@ public class RotateProfileKeyJob extends BaseJob {
return exception instanceof PushNetworkException;
}
private @Nullable StreamDetails getProfileAvatar() {
try {
File avatarFile = AvatarHelper.getAvatarFile(context, Recipient.self().getId());
if (avatarFile.exists()) {
return new StreamDetails(new FileInputStream(avatarFile), "image/jpeg", avatarFile.length());
}
} catch (IOException e) {
return null;
}
return null;
}
public static final class Factory implements Job.Factory<RotateProfileKeyJob> {
@Override
public @NonNull RotateProfileKeyJob create(@NonNull Parameters parameters, @NonNull Data data) {

View File

@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.stickers.BlessedPacks;
import org.thoughtcrime.securesms.util.Hex;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
@ -101,7 +102,7 @@ public class StickerPackDownloadJob extends BaseJob {
@Override
protected void onRun() throws IOException, InvalidMessageException {
if (isReferencePack && !DatabaseFactory.getAttachmentDatabase(context).containsStickerPackId(packId)) {
if (isReferencePack && !DatabaseFactory.getAttachmentDatabase(context).containsStickerPackId(packId) && !BlessedPacks.contains(packId)) {
Log.w(TAG, "There are no attachments with the requested packId present for this reference pack. Skipping.");
return;
}

View File

@ -154,7 +154,7 @@ public final class RegistrationLockDialog {
if (s == null) return;
String pin = s.toString();
if (TextUtils.isEmpty(pin)) return;
if (pin.length() < KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH) return;
if (pin.length() < KbsConstants.LEGACY_MINIMUM_PIN_LENGTH) return;
if (PinHashing.verifyLocalPinHash(localPinHash, pin)) {
dialog.dismiss();
@ -186,9 +186,9 @@ public final class RegistrationLockDialog {
String pinValue = pin.getText().toString().replace(" ", "");
String repeatValue = repeat.getText().toString().replace(" ", "");
if (pinValue.length() < KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH) {
if (pinValue.length() < KbsConstants.LEGACY_MINIMUM_PIN_LENGTH) {
Toast.makeText(context,
context.getString(R.string.RegistrationLockDialog_the_registration_lock_pin_must_be_at_least_d_digits, KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH),
context.getString(R.string.RegistrationLockDialog_the_registration_lock_pin_must_be_at_least_d_digits, KbsConstants.LEGACY_MINIMUM_PIN_LENGTH),
Toast.LENGTH_LONG).show();
return;
}
@ -209,7 +209,7 @@ public final class RegistrationLockDialog {
@Override
protected Boolean doInBackground(Void... voids) {
try {
Log.i(TAG, "Setting pin on KBS");
Log.i(TAG, "Setting pin on KBS - dialog");
KbsValues kbsValues = SignalStore.kbsValues();
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
@ -217,21 +217,15 @@ public final class RegistrationLockDialog {
KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
HashedPin hashedPin = PinHashing.hashPin(pinValue, pinChangeSession);
RegistrationLockData kbsData = pinChangeSession.setPin(hashedPin, masterKey);
RegistrationLockData restoredData = keyBackupService.newRestoreSession(kbsData.getTokenResponse())
.restorePin(hashedPin);
if (!restoredData.getMasterKey().equals(masterKey)) {
throw new AssertionError("Failed to set the pin correctly");
} else {
Log.i(TAG, "Set and retrieved pin on KBS successfully");
}
kbsValues.setRegistrationLockMasterKey(restoredData, PinHashing.localPinHash(pinValue));
kbsValues.setRegistrationLockMasterKey(kbsData, PinHashing.localPinHash(pinValue));
TextSecurePreferences.clearOldRegistrationLockPin(context);
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL);
Log.i(TAG, "Pin set on KBS");
return true;
} catch (IOException | UnauthenticatedResponseException | KeyBackupServicePinException | KeyBackupSystemNoDataException e) {
} catch (IOException | UnauthenticatedResponseException e) {
Log.w(TAG, e);
return false;
}

View File

@ -115,7 +115,7 @@ public final class SignalPinReminderDialog {
pinEditText.addTextChangedListener(new SimpleTextWatcher() {
@Override
public void onTextChanged(String text) {
if (text.length() >= KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH) {
if (text.length() >= KbsConstants.minimumPossiblePinLength()) {
submit.setEnabled(true);
} else {
submit.setEnabled(false);
@ -192,7 +192,7 @@ public final class SignalPinReminderDialog {
if (pin == null) return;
if (TextUtils.isEmpty(pin)) return;
if (pin.length() < KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH) return;
if (pin.length() < KbsConstants.minimumPossiblePinLength()) return;
if (PinHashing.verifyLocalPinHash(localPinHash, pin)) {
callback.onPinCorrect();

View File

@ -46,7 +46,7 @@ abstract class BaseKbsPinFragment<ViewModel extends BaseKbsPinViewModel> extends
viewModel = initializeViewModel();
viewModel.getUserEntry().observe(getViewLifecycleOwner(), kbsPin -> {
boolean isEntryValid = kbsPin.length() >= KbsConstants.MINIMUM_NEW_PIN_LENGTH;
boolean isEntryValid = kbsPin.length() >= KbsConstants.MINIMUM_PIN_LENGTH;
confirm.setEnabled(isEntryValid);
confirm.setAlpha(isEntryValid ? 1f : 0.5f);

View File

@ -43,24 +43,17 @@ final class ConfirmKbsPinRepository {
KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
HashedPin hashedPin = PinHashing.hashPin(pinValue, pinChangeSession);
RegistrationLockData kbsData = pinChangeSession.setPin(hashedPin, masterKey);
RegistrationLockData restoredData = keyBackupService.newRestoreSession(kbsData.getTokenResponse())
.restorePin(hashedPin);
if (!restoredData.getMasterKey().equals(masterKey)) {
throw new AssertionError("Failed to set the pin correctly");
} else {
Log.i(TAG, "Set and retrieved pin on KBS successfully");
}
kbsValues.setRegistrationLockMasterKey(restoredData, PinHashing.localPinHash(pinValue));
kbsValues.setRegistrationLockMasterKey(kbsData, PinHashing.localPinHash(pinValue));
TextSecurePreferences.clearOldRegistrationLockPin(context);
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL);
SignalStore.kbsValues().setKeyboardType(keyboard);
ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.PINS_FOR_ALL);
Log.i(TAG, "Pin set on KBS");
return PinSetResult.SUCCESS;
} catch (IOException | UnauthenticatedResponseException | KeyBackupServicePinException | KeyBackupSystemNoDataException e) {
} catch (IOException | UnauthenticatedResponseException e) {
Log.w(TAG, e);
return PinSetResult.FAILURE;
}

View File

@ -77,6 +77,6 @@ public class CreateKbsPinFragment extends BaseKbsPinFragment<CreateKbsPinViewMod
}
private String getPinLengthRestrictionText(@PluralsRes int plurals) {
return requireContext().getResources().getQuantityString(plurals, KbsConstants.MINIMUM_NEW_PIN_LENGTH, KbsConstants.MINIMUM_NEW_PIN_LENGTH);
return requireContext().getResources().getQuantityString(plurals, KbsConstants.MINIMUM_PIN_LENGTH, KbsConstants.MINIMUM_PIN_LENGTH);
}
}

View File

@ -1,13 +1,15 @@
package org.thoughtcrime.securesms.lock.v2;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
public final class KbsConstants {
static final int MINIMUM_NEW_PIN_LENGTH = 6;
public static final int MINIMUM_PIN_LENGTH = 6;
public static final int LEGACY_MINIMUM_PIN_LENGTH = 4;
/** Migrated pins from V1 might be 4 */
public static final int MINIMUM_POSSIBLE_PIN_LENGTH = 4;
private KbsConstants() { }
private KbsConstants() {
public static int minimumPossiblePinLength() {
return SignalStore.kbsValues().hasMigratedToPinsForAll() ? MINIMUM_PIN_LENGTH : LEGACY_MINIMUM_PIN_LENGTH;
}
}

View File

@ -633,7 +633,8 @@ public class SubmitLogFragment extends Fragment {
Map<String, Boolean> disk = FeatureFlags.getDiskValues();
Map<String, Boolean> forced = FeatureFlags.getForcedValues();
int remoteLength = Stream.of(memory.keySet()).map(String::length).max(Integer::compareTo).orElse(0);
int forcedLength = Stream.of(disk.keySet()).map(String::length).max(Integer::compareTo).orElse(0);
int diskLength = Stream.of(disk.keySet()).map(String::length).max(Integer::compareTo).orElse(0);
int forcedLength = Stream.of(forced.keySet()).map(String::length).max(Integer::compareTo).orElse(0);
out.append("-- Memory\n");
for (Map.Entry<String, Boolean> entry : memory.entrySet()) {
@ -643,7 +644,7 @@ public class SubmitLogFragment extends Fragment {
out.append("-- Disk\n");
for (Map.Entry<String, Boolean> entry : disk.entrySet()) {
out.append(Util.rightPad(entry.getKey(), forcedLength)).append(": ").append(entry.getValue()).append("\n");
out.append(Util.rightPad(entry.getKey(), diskLength)).append(": ").append(entry.getValue()).append("\n");
}
out.append("\n");

View File

@ -1,13 +1,14 @@
package org.thoughtcrime.securesms.mediapreview;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
import org.thoughtcrime.securesms.mediasend.Media;
@ -112,7 +113,8 @@ public class MediaPreviewViewModel extends ViewModel {
mediaRecord.getAttachment().getSize(),
0,
Optional.absent(),
Optional.fromNullable(mediaRecord.getAttachment().getCaption()));
Optional.fromNullable(mediaRecord.getAttachment().getCaption()),
Optional.absent());
}
public LiveData<PreviewData> getPreviewData() {

View File

@ -0,0 +1,56 @@
package org.thoughtcrime.securesms.mediasend;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public final class ImageEditorModelRenderMediaTransform implements MediaTransform {
private static final String TAG = Log.tag(ImageEditorModelRenderMediaTransform.class);
private final EditorModel modelToRender;
ImageEditorModelRenderMediaTransform(@NonNull EditorModel modelToRender) {
this.modelToRender = modelToRender;
}
@WorkerThread
@Override
public @NonNull Media transform(@NonNull Context context, @NonNull Media media) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Bitmap bitmap = modelToRender.render(context);
try {
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
Uri uri = BlobProvider.getInstance()
.forData(outputStream.toByteArray())
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleSessionOnDisk(context);
return new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), 0, media.getBucketId(), media.getCaption(), Optional.absent());
} catch (IOException e) {
Log.w(TAG, "Failed to render image. Using base image.");
return media;
} finally {
bitmap.recycle();
try {
outputStream.close();
} catch (IOException e) {
Log.w(TAG, e);
}
}
}
}

View File

@ -3,9 +3,14 @@ package org.thoughtcrime.securesms.mediasend;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import java.io.IOException;
/**
* Represents a piece of media that the user has on their device.
@ -22,8 +27,9 @@ public class Media implements Parcelable {
private final long size;
private final long duration;
private Optional<String> bucketId;
private Optional<String> caption;
private Optional<String> bucketId;
private Optional<String> caption;
private Optional<AttachmentDatabase.TransformProperties> transformProperties;
public Media(@NonNull Uri uri,
@NonNull String mimeType,
@ -33,17 +39,19 @@ public class Media implements Parcelable {
long size,
long duration,
Optional<String> bucketId,
Optional<String> caption)
Optional<String> caption,
Optional<AttachmentDatabase.TransformProperties> transformProperties)
{
this.uri = uri;
this.mimeType = mimeType;
this.date = date;
this.width = width;
this.height = height;
this.size = size;
this.duration = duration;
this.bucketId = bucketId;
this.caption = caption;
this.uri = uri;
this.mimeType = mimeType;
this.date = date;
this.width = width;
this.height = height;
this.size = size;
this.duration = duration;
this.bucketId = bucketId;
this.caption = caption;
this.transformProperties = transformProperties;
}
protected Media(Parcel in) {
@ -56,6 +64,12 @@ public class Media implements Parcelable {
duration = in.readLong();
bucketId = Optional.fromNullable(in.readString());
caption = Optional.fromNullable(in.readString());
try {
String json = in.readString();
transformProperties = json == null ? Optional.absent() : Optional.fromNullable(JsonUtil.fromJson(json, AttachmentDatabase.TransformProperties.class));
} catch (IOException e) {
throw new AssertionError(e);
}
}
public Uri getUri() {
@ -98,6 +112,10 @@ public class Media implements Parcelable {
this.caption = Optional.fromNullable(caption);
}
public Optional<AttachmentDatabase.TransformProperties> getTransformProperties() {
return transformProperties;
}
@Override
public int describeContents() {
return 0;
@ -114,6 +132,7 @@ public class Media implements Parcelable {
dest.writeLong(duration);
dest.writeString(bucketId.orNull());
dest.writeString(caption.orNull());
dest.writeString(transformProperties.transform(JsonUtil::toJson).orNull());
}
public static final Creator<Media> CREATOR = new Creator<Media>() {

View File

@ -4,31 +4,28 @@ import android.Manifest;
import android.annotation.TargetApi;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Video;
import android.provider.OpenableColumns;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import android.util.Pair;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
@ -77,12 +74,12 @@ public class MediaRepository {
SignalExecutors.BOUNDED.execute(() -> callback.onComplete(getMostRecentItem(context)));
}
void renderMedia(@NonNull Context context,
@NonNull List<Media> currentMedia,
@NonNull Map<Media, EditorModel> modelsToRender,
@NonNull Callback<LinkedHashMap<Media, Media>> callback)
static void transformMedia(@NonNull Context context,
@NonNull List<Media> currentMedia,
@NonNull Map<Media, MediaTransform> modelsToTransform,
@NonNull Callback<LinkedHashMap<Media, Media>> callback)
{
SignalExecutors.BOUNDED.execute(() -> callback.onComplete(renderMedia(context, currentMedia, modelsToRender)));
SignalExecutors.BOUNDED.execute(() -> callback.onComplete(transformMedia(context, currentMedia, modelsToTransform)));
}
@WorkerThread
@ -220,7 +217,7 @@ public class MediaRepository {
long size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE));
long duration = !isImage ? cursor.getInt(cursor.getColumnIndexOrThrow(Video.Media.DURATION)) : 0;
media.add(new Media(uri, mimetype, date, width, height, size, duration, Optional.of(bucketId), Optional.absent()));
media.add(new Media(uri, mimetype, date, width, height, size, duration, Optional.of(bucketId), Optional.absent(), Optional.absent()));
}
}
@ -249,35 +246,16 @@ public class MediaRepository {
}
@WorkerThread
private LinkedHashMap<Media, Media> renderMedia(@NonNull Context context,
@NonNull List<Media> currentMedia,
@NonNull Map<Media, EditorModel> modelsToRender)
private static LinkedHashMap<Media, Media> transformMedia(@NonNull Context context,
@NonNull List<Media> currentMedia,
@NonNull Map<Media, MediaTransform> modelsToTransform)
{
LinkedHashMap<Media, Media> updatedMedia = new LinkedHashMap<>(currentMedia.size());
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
for (Media media : currentMedia) {
EditorModel modelToRender = modelsToRender.get(media);
if (modelToRender != null) {
Bitmap bitmap = modelToRender.render(context);
try {
outputStream.reset();
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
Uri uri = BlobProvider.getInstance()
.forData(outputStream.toByteArray())
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleSessionOnDisk(context);
Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), 0, media.getBucketId(), media.getCaption());
updatedMedia.put(media, updated);
} catch (IOException e) {
Log.w(TAG, "Failed to render image. Using base image.");
updatedMedia.put(media, media);
} finally {
bitmap.recycle();
}
MediaTransform transformer = modelsToTransform.get(media);
if (transformer != null) {
updatedMedia.put(media, transformer.transform(context, media));
} else {
updatedMedia.put(media, media);
}
@ -333,7 +311,7 @@ public class MediaRepository {
height = dimens.second;
}
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.getBucketId(), media.getCaption());
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.getBucketId(), media.getCaption(), Optional.absent());
}
private Media getContentResolverPopulatedMedia(@NonNull Context context, @NonNull Media media) throws IOException {
@ -359,7 +337,7 @@ public class MediaRepository {
height = dimens.second;
}
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.getBucketId(), media.getCaption());
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.getBucketId(), media.getCaption(), Optional.absent());
}
private static class FolderResult {

View File

@ -87,6 +87,7 @@ import java.util.Map;
public class MediaSendActivity extends PassphraseRequiredActionBarActivity implements MediaPickerFolderFragment.Controller,
MediaPickerItemFragment.Controller,
ImageEditorFragment.Controller,
MediaSendVideoFragment.Controller,
CameraFragment.Controller,
CameraContactSelectionFragment.Controller,
ViewTreeObserver.OnGlobalLayoutListener,
@ -141,11 +142,11 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
/**
* Get an intent to launch the media send flow starting with the picker.
*/
public static Intent buildGalleryIntent(@NonNull Context context, @NonNull Recipient recipient, @NonNull String body, @NonNull TransportOption transport) {
public static Intent buildGalleryIntent(@NonNull Context context, @NonNull Recipient recipient, @Nullable String body, @NonNull TransportOption transport) {
Intent intent = new Intent(context, MediaSendActivity.class);
intent.putExtra(KEY_RECIPIENT, recipient.getId());
intent.putExtra(KEY_TRANSPORT, transport);
intent.putExtra(KEY_BODY, body);
intent.putExtra(KEY_BODY, body == null ? "" : body);
return intent;
}
@ -346,6 +347,11 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
navigateToMediaSend(Locale.getDefault());
}
@Override
public void onVideoBeginEdit(@NonNull Uri uri) {
viewModel.onVideoBeginEdit(uri);
}
@Override
public void onTouchEventsNeeded(boolean needed) {
MediaSendFragment fragment = (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
@ -414,6 +420,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
length,
0,
Optional.of(Media.ALL_MEDIA_BUCKET_ID),
Optional.absent(),
Optional.absent()
);
} catch (IOException e) {
@ -512,7 +519,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
MediaSendFragment fragment = getMediaSendFragment();
if (fragment != null) {
viewModel.onSendClicked(buildModelsToRender(fragment), recipients).observe(this, result -> {
viewModel.onSendClicked(buildModelsToTransform(fragment), recipients).observe(this, result -> {
finish();
});
} else {
@ -533,13 +540,13 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
sendButton.setEnabled(false);
viewModel.onSendClicked(buildModelsToRender(fragment), Collections.emptyList()).observe(this, this::setActivityResultAndFinish);
viewModel.onSendClicked(buildModelsToTransform(fragment), Collections.emptyList()).observe(this, this::setActivityResultAndFinish);
}
private Map<Media, EditorModel> buildModelsToRender(@NonNull MediaSendFragment fragment) {
private static Map<Media, MediaTransform> buildModelsToTransform(@NonNull MediaSendFragment fragment) {
List<Media> mediaList = fragment.getAllMedia();
Map<Uri, Object> savedState = fragment.getSavedState();
Map<Media, EditorModel> modelsToRender = new HashMap<>();
Map<Media, MediaTransform> modelsToRender = new HashMap<>();
for (Media media : mediaList) {
Object state = savedState.get(media.getUri());
@ -547,7 +554,14 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
if (state instanceof ImageEditorFragment.Data) {
EditorModel model = ((ImageEditorFragment.Data) state).readModel();
if (model != null && model.isChanged()) {
modelsToRender.put(media, model);
modelsToRender.put(media, new ImageEditorModelRenderMediaTransform(model));
}
}
if (state instanceof MediaSendVideoFragment.Data) {
MediaSendVideoFragment.Data data = (MediaSendVideoFragment.Data) state;
if (data.durationEdited) {
modelsToRender.put(media, new VideoTrimTransform(data));
}
}
}

View File

@ -0,0 +1,6 @@
package org.thoughtcrime.securesms.mediasend;
public class MediaSendConstants {
public static final int MAX_PUSH = 32;
public static final int MAX_SMS = 1;
}

View File

@ -1,28 +1,45 @@
package org.thoughtcrime.securesms.mediasend;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.fragment.app.Fragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.scribbles.VideoEditorHud;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.Throttler;
import org.thoughtcrime.securesms.video.VideoPlayer;
import java.io.IOException;
public class MediaSendVideoFragment extends Fragment implements MediaSendPageFragment {
public class MediaSendVideoFragment extends Fragment implements VideoEditorHud.EventListener,
MediaSendPageFragment {
private static final String TAG = MediaSendVideoFragment.class.getSimpleName();
private static final String TAG = Log.tag(MediaSendVideoFragment.class);
private static final String KEY_URI = "uri";
private Uri uri;
private final Throttler videoScanThrottle = new Throttler(150);
private final Handler handler = new Handler();
private Controller controller;
private Data data = new Data();
private Uri uri;
private VideoPlayer player;
private VideoEditorHud hud;
private Runnable updatePosition;
public static MediaSendVideoFragment newInstance(@NonNull Uri uri) {
Bundle args = new Bundle();
@ -34,6 +51,15 @@ public class MediaSendVideoFragment extends Fragment implements MediaSendPageFra
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!(getActivity() instanceof Controller)) {
throw new IllegalStateException("Parent activity must implement Controller interface.");
}
controller = (Controller) getActivity();
}
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.mediasend_video_fragment, container, false);
@ -43,19 +69,50 @@ public class MediaSendVideoFragment extends Fragment implements MediaSendPageFra
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
uri = getArguments().getParcelable(KEY_URI);
player = view.findViewById(R.id.video_player);
uri = requireArguments().getParcelable(KEY_URI);
VideoSlide slide = new VideoSlide(requireContext(), uri, 0);
((VideoPlayer) view).setWindow(requireActivity().getWindow());
((VideoPlayer) view).setVideoSource(slide, true);
player.setWindow(requireActivity().getWindow());
player.setVideoSource(slide, true);
if (FeatureFlags.videoTrimming() && MediaConstraints.isVideoTranscodeAvailable()) {
hud = view.findViewById(R.id.video_editor_hud);
hud.setEventListener(this);
updateHud(data);
if (data.durationEdited) {
player.clip(data.startTimeUs, data.endTimeUs, true);
}
try {
hud.setVideoSource(slide);
hud.setVisibility(View.VISIBLE);
startPositionUpdates();
} catch (IOException e) {
Log.w(TAG, e);
}
player.setPlayerCallback(new VideoPlayer.PlayerCallback() {
@Override
public void onPlaying() {
hud.playing();
}
@Override
public void onStopped() {
hud.stopped();
}
});
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (getView() != null) {
((VideoPlayer) getView()).cleanup();
if (player != null) {
player.cleanup();
}
}
@ -63,6 +120,32 @@ public class MediaSendVideoFragment extends Fragment implements MediaSendPageFra
public void onPause() {
super.onPause();
notifyHidden();
stopPositionUpdates();
}
@Override
public void onResume() {
super.onResume();
startPositionUpdates();
}
private void startPositionUpdates() {
if (hud != null && Build.VERSION.SDK_INT >= 23) {
stopPositionUpdates();
updatePosition = new Runnable() {
@Override
public void run() {
hud.setPosition(player.getPlaybackPositionUs());
handler.postDelayed(this, 100);
}
};
handler.post(updatePosition);
}
}
private void stopPositionUpdates() {
handler.removeCallbacks(updatePosition);
}
@Override
@ -84,22 +167,106 @@ public class MediaSendVideoFragment extends Fragment implements MediaSendPageFra
@Override
public @Nullable View getPlaybackControls() {
VideoPlayer player = (VideoPlayer) getView();
if (hud != null && hud.getVisibility() == View.VISIBLE) return null;
return player != null ? player.getControlView() : null;
}
@Override
public @Nullable Object saveState() {
return null;
return data;
}
@Override
public void restoreState(@NonNull Object state) { }
public void restoreState(@NonNull Object state) {
if (state instanceof Data) {
data = (Data) state;
if (Build.VERSION.SDK_INT >= 23) {
updateHud(data);
}
} else {
Log.w(TAG, "Received a bad saved state. Received class: " + state.getClass().getName());
}
}
@RequiresApi(api = 23)
private void updateHud(Data data) {
if (hud != null && data.totalDurationUs > 0 && data.durationEdited) {
hud.setDurationRange(data.totalDurationUs, data.startTimeUs, data.endTimeUs);
}
}
@Override
public void notifyHidden() {
if (getView() != null) {
((VideoPlayer) getView()).pause();
if (player != null) {
player.pause();
}
}
@Override
public void onEditVideoDuration(long totalDurationUs, long startTimeUs, long endTimeUs, boolean fromEdited, boolean editingComplete) {
controller.onTouchEventsNeeded(!editingComplete);
boolean wasEdited = data.durationEdited;
boolean durationEdited = startTimeUs > 0 || endTimeUs < totalDurationUs;
data.durationEdited = durationEdited;
data.totalDurationUs = totalDurationUs;
data.startTimeUs = startTimeUs;
data.endTimeUs = endTimeUs;
if (editingComplete) {
videoScanThrottle.clear();
}
videoScanThrottle.publish(() -> {
player.pause();
if (!editingComplete) {
player.removeClip(false);
}
player.setPlaybackPosition(fromEdited || editingComplete ? startTimeUs / 1000 : endTimeUs / 1000);
if (editingComplete) {
if (durationEdited) {
player.clip(startTimeUs, endTimeUs, true);
} else {
player.removeClip(true);
}
}
});
if (!wasEdited && durationEdited) {
controller.onVideoBeginEdit(uri);
}
}
@Override
public void onPlay() {
player.playFromStart();
}
@Override
public void onSeek(long position, boolean dragComplete) {
if (dragComplete) {
videoScanThrottle.clear();
}
videoScanThrottle.publish(() -> {
player.pause();
player.setPlaybackPosition(position);
});
}
static class Data {
boolean durationEdited;
long totalDurationUs;
long startTimeUs;
long endTimeUs;
}
public interface Controller {
void onTouchEventsNeeded(boolean needed);
void onVideoBeginEdit(@NonNull Uri uri);
}
}

View File

@ -1,24 +1,22 @@
package org.thoughtcrime.securesms.mediasend;
import android.app.Application;
import android.content.Context;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import android.text.TextUtils;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
@ -34,7 +32,6 @@ import org.thoughtcrime.securesms.util.MessageUtil;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.libsignal.util.guava.Preconditions;
@ -54,9 +51,6 @@ class MediaSendViewModel extends ViewModel {
private static final String TAG = MediaSendViewModel.class.getSimpleName();
private static final int MAX_PUSH = 32;
private static final int MAX_SMS = 1;
private final Application application;
private final MediaRepository repository;
private final MediaUploadRepository uploadRepository;
@ -125,11 +119,11 @@ class MediaSendViewModel extends ViewModel {
if (transport.isSms()) {
isSms = true;
maxSelection = MAX_SMS;
maxSelection = MediaSendConstants.MAX_SMS;
mediaConstraints = MediaConstraints.getMmsMediaConstraints(transport.getSimSubscriptionId().or(-1));
} else {
isSms = false;
maxSelection = MAX_PUSH;
maxSelection = MediaSendConstants.MAX_PUSH;
mediaConstraints = MediaConstraints.getPushMediaConstraints();
}
@ -154,7 +148,9 @@ class MediaSendViewModel extends ViewModel {
if (filteredMedia.size() != newMedia.size()) {
error.setValue(Error.ITEM_TOO_LARGE);
} else if (filteredMedia.size() > maxSelection) {
}
if (filteredMedia.size() > maxSelection) {
filteredMedia = filteredMedia.subList(0, maxSelection);
error.setValue(Error.TOO_MANY_ITEMS);
}
@ -303,7 +299,7 @@ class MediaSendViewModel extends ViewModel {
captionVisible = false;
List<Media> uncaptioned = Stream.of(getSelectedMediaOrDefault())
.map(m -> new Media(m.getUri(), m.getMimeType(), m.getDate(), m.getWidth(), m.getHeight(), m.getSize(), m.getDuration(), m.getBucketId(), Optional.absent()))
.map(m -> new Media(m.getUri(), m.getMimeType(), m.getDate(), m.getWidth(), m.getHeight(), m.getSize(), m.getDuration(), m.getBucketId(), Optional.absent(), Optional.absent()))
.toList();
selectedMedia.setValue(uncaptioned);
@ -405,6 +401,10 @@ class MediaSendViewModel extends ViewModel {
hudState.setValue(buildHudState());
}
void onVideoBeginEdit(@NonNull Uri uri) {
cancelUpload(new Media(uri, "", 0, 0, 0, 0, 0, Optional.absent(), Optional.absent(), Optional.absent()));
}
void onMediaCaptured(@NonNull Media media) {
lastCameraCapture = Optional.of(media);
@ -449,7 +449,7 @@ class MediaSendViewModel extends ViewModel {
savedDrawState.putAll(state);
}
@NonNull LiveData<MediaSendActivityResult> onSendClicked(Map<Media, EditorModel> modelsToRender, @NonNull List<Recipient> recipients) {
@NonNull LiveData<MediaSendActivityResult> onSendClicked(Map<Media, MediaTransform> modelsToTransform, @NonNull List<Recipient> recipients) {
if (isSms && recipients.size() > 0) {
throw new IllegalStateException("Provided recipients to send to, but this is SMS!");
}
@ -463,9 +463,13 @@ class MediaSendViewModel extends ViewModel {
Util.runOnMainDelayed(dialogRunnable, 250);
repository.renderMedia(application, initialMedia, modelsToRender, (oldToNew) -> {
MediaRepository.transformMedia(application, initialMedia, modelsToTransform, (oldToNew) -> {
List<Media> updatedMedia = new ArrayList<>(oldToNew.values());
for (Media media : updatedMedia){
Log.w(TAG, media.getUri().toString() + " : " + media.getTransformProperties().transform(t->"" + t.isVideoTrim()).or("null"));
}
if (isSms || MessageSender.isLocalSelfSend(application, recipient, isSms)) {
Log.i(TAG, "SMS or local self-send. Skipping pre-upload.");
result.postValue(MediaSendActivityResult.forTraditionalSend(updatedMedia, trimmedBody, transport, isViewOnce()));
@ -477,7 +481,7 @@ class MediaSendViewModel extends ViewModel {
if (splitMessage.getTextSlide().isPresent()) {
Slide slide = splitMessage.getTextSlide().get();
uploadRepository.startUpload(new Media(Objects.requireNonNull(slide.getUri()), slide.getContentType(), System.currentTimeMillis(), 0, 0, slide.getFileSize(), 0, Optional.absent(), Optional.absent()), recipient);
uploadRepository.startUpload(new Media(Objects.requireNonNull(slide.getUri()), slide.getContentType(), System.currentTimeMillis(), 0, 0, slide.getFileSize(), 0, Optional.absent(), Optional.absent(), Optional.absent()), recipient);
}
uploadRepository.applyMediaUpdates(oldToNew, recipient);

View File

@ -0,0 +1,12 @@
package org.thoughtcrime.securesms.mediasend;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
public interface MediaTransform {
@WorkerThread
@NonNull Media transform(@NonNull Context context, @NonNull Media media);
}

View File

@ -78,7 +78,9 @@ class MediaUploadRepository {
void applyMediaUpdates(@NonNull Map<Media, Media> oldToNew, @Nullable Recipient recipient) {
executor.execute(() -> {
for (Map.Entry<Media, Media> entry : oldToNew.entrySet()) {
if (!entry.getKey().equals(entry.getValue()) || !uploadResults.containsKey(entry.getValue())) {
boolean same = entry.getKey().equals(entry.getValue()) && (!entry.getValue().getTransformProperties().isPresent() || !entry.getValue().getTransformProperties().get().isVideoEdited());
if (!same || !uploadResults.containsKey(entry.getValue())) {
cancelUploadInternal(entry.getKey());
uploadMediaInternal(entry.getValue(), recipient);
}
@ -187,9 +189,9 @@ class MediaUploadRepository {
}
}
private static @NonNull Attachment asAttachment(@NonNull Context context, @NonNull Media media) {
public static @NonNull Attachment asAttachment(@NonNull Context context, @NonNull Media media) {
if (MediaUtil.isVideoType(media.getMimeType())) {
return new VideoSlide(context, media.getUri(), 0, media.getCaption().orNull()).asAttachment();
return new VideoSlide(context, media.getUri(), 0, media.getCaption().orNull(), media.getTransformProperties().orNull()).asAttachment();
} else if (MediaUtil.isGif(media.getMimeType())) {
return new GifSlide(context, media.getUri(), 0, media.getWidth(), media.getHeight(), media.getCaption().orNull()).asAttachment();
} else if (MediaUtil.isImageType(media.getMimeType())) {

View File

@ -0,0 +1,33 @@
package org.thoughtcrime.securesms.mediasend;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.whispersystems.libsignal.util.guava.Optional;
public final class VideoTrimTransform implements MediaTransform {
private final MediaSendVideoFragment.Data data;
VideoTrimTransform(@NonNull MediaSendVideoFragment.Data data) {
this.data = data;
}
@WorkerThread
@Override
public @NonNull Media transform(@NonNull Context context, @NonNull Media media) {
return new Media(media.getUri(),
media.getMimeType(),
media.getDate(),
media.getWidth(),
media.getHeight(),
media.getSize(),
media.getDuration(),
media.getBucketId(),
media.getCaption(),
Optional.of(new AttachmentDatabase.TransformProperties(false, data.durationEdited, data.startTimeUs, data.endTimeUs)));
}
}

View File

@ -19,12 +19,16 @@ import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.lock.v2.KbsMigrationActivity;
import org.thoughtcrime.securesms.lock.v2.PinUtil;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* Creating a new megaphone:
@ -43,6 +47,10 @@ public final class Megaphones {
private static final String TAG = Log.tag(Megaphones.class);
private static final MegaphoneSchedule ALWAYS = new ForeverSchedule(true);
private static final MegaphoneSchedule NEVER = new ForeverSchedule(false);
private static final MegaphoneSchedule EVERY_TWO_DAYS = new RecurringSchedule(TimeUnit.DAYS.toMillis(2));
private Megaphones() {}
static @Nullable Megaphone getNextMegaphone(@NonNull Context context, @NonNull Map<Event, MegaphoneRecord> records) {
@ -80,8 +88,9 @@ public final class Megaphones {
*/
private static Map<Event, MegaphoneSchedule> buildDisplayOrder() {
return new LinkedHashMap<Event, MegaphoneSchedule>() {{
put(Event.REACTIONS, new ForeverSchedule(true));
put(Event.REACTIONS, ALWAYS);
put(Event.PINS_FOR_ALL, new PinsForAllSchedule());
put(Event.PROFILE_NAMES_FOR_ALL, FeatureFlags.profileNamesMegaphoneEnabled() ? EVERY_TWO_DAYS : NEVER);
put(Event.PIN_REMINDER, new SignalPinReminderSchedule());
}};
}
@ -94,6 +103,8 @@ public final class Megaphones {
return buildPinsForAllMegaphone(record);
case PIN_REMINDER:
return buildPinReminderMegaphone(context);
case PROFILE_NAMES_FOR_ALL:
return buildProfileNamesMegaphone(context);
default:
throw new IllegalArgumentException("Event not handled!");
}
@ -121,8 +132,6 @@ public final class Megaphones {
.setMandatory(true)
.setImage(R.drawable.kbs_pin_megaphone);
long daysRemaining = PinsForAllSchedule.getDaysRemaining(record.getFirstVisible(), System.currentTimeMillis());
if (PinUtil.userHasPin(ApplicationDependencies.getApplication())) {
return buildPinsForAllMegaphoneForUserWithPin(builder.enableSnooze(null));
} else {
@ -185,10 +194,35 @@ public final class Megaphones {
.build();
}
private static @NonNull Megaphone buildProfileNamesMegaphone(@NonNull Context context) {
Megaphone.Builder builder = new Megaphone.Builder(Event.PROFILE_NAMES_FOR_ALL, Megaphone.Style.BASIC)
.enableSnooze(null)
.setImage(R.drawable.profile_megaphone);
if (TextSecurePreferences.getProfileName(ApplicationDependencies.getApplication()) == ProfileName.EMPTY) {
return builder.setTitle(R.string.ProfileNamesMegaphone__add_a_profile_name)
.setBody(R.string.ProfileNamesMegaphone__this_will_be_displayed_when_you_start)
.setActionButton(R.string.ProfileNamesMegaphone__add_profile_name, (megaphone, listener) -> {
listener.onMegaphoneSnooze(Event.PROFILE_NAMES_FOR_ALL);
listener.onMegaphoneNavigationRequested(new Intent(context, EditProfileActivity.class));
})
.build();
} else {
return builder.setTitle(R.string.ProfileNamesMegaphone__confirm_your_profile_name)
.setBody(R.string.ProfileNamesMegaphone__your_profile_can_now_include)
.setActionButton(R.string.ProfileNamesMegaphone__confirm_name, (megaphone, listener) -> {
listener.onMegaphoneCompleted(Event.PROFILE_NAMES_FOR_ALL);
listener.onMegaphoneNavigationRequested(new Intent(context, EditProfileActivity.class));
})
.build();
}
}
public enum Event {
REACTIONS("reactions"),
PINS_FOR_ALL("pins_for_all"),
PIN_REMINDER("pin_reminder");
PIN_REMINDER("pin_reminder"),
PROFILE_NAMES_FOR_ALL("profile_names");
private final String key;

View File

@ -12,6 +12,7 @@ import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.Argon2TestJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.stickers.BlessedPacks;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VersionTracker;
@ -39,7 +40,7 @@ public class ApplicationMigrations {
private static final int LEGACY_CANONICAL_VERSION = 455;
public static final int CURRENT_VERSION = 9;
public static final int CURRENT_VERSION = 10;
private static final class Version {
static final int LEGACY = 1;
@ -51,6 +52,7 @@ public class ApplicationMigrations {
static final int CACHED_ATTACHMENTS = 7;
static final int STICKERS_LAUNCH = 8;
static final int TEST_ARGON2 = 9;
static final int SWOON_STICKERS = 10;
}
/**
@ -199,6 +201,10 @@ public class ApplicationMigrations {
jobs.put(Version.TEST_ARGON2, new Argon2TestMigrationJob());
}
if (lastSeenVersion < Version.SWOON_STICKERS) {
jobs.put(Version.SWOON_STICKERS, new StickerAdditionMigrationJob(BlessedPacks.SWOON_HANDS, BlessedPacks.SWOON_FACES));
}
return jobs;
}

View File

@ -78,16 +78,8 @@ public final class RegistrationPinV2MigrationJob extends BaseJob {
KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
HashedPin hashedPin = PinHashing.hashPin(pinValue, pinChangeSession);
RegistrationLockData kbsData = pinChangeSession.setPin(hashedPin, masterKey);
RegistrationLockData restoredData = keyBackupService.newRestoreSession(kbsData.getTokenResponse())
.restorePin(hashedPin);
if (!restoredData.getMasterKey().equals(masterKey)) {
throw new RuntimeException("Failed to migrate the pin correctly");
} else {
Log.i(TAG, "Set and retrieved pin on KBS successfully");
}
kbsValues.setRegistrationLockMasterKey(restoredData, PinHashing.localPinHash(pinValue));
kbsValues.setRegistrationLockMasterKey(kbsData, PinHashing.localPinHash(pinValue));
TextSecurePreferences.clearOldRegistrationLockPin(context);
Log.i(TAG, "Pin migrated to Key Backup Service");

View File

@ -0,0 +1,82 @@
package org.thoughtcrime.securesms.migrations;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.stickers.BlessedPacks;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Migration job for installing new blessed packs as references. This means that the packs will
* show up in the list as available blessed packs, but they *won't* be auto-installed.
*/
public class StickerAdditionMigrationJob extends MigrationJob {
public static final String KEY = "StickerInstallMigrationJob";
private static String TAG = Log.tag(StickerAdditionMigrationJob.class);
private static final String KEY_PACKS = "packs";
private final List<BlessedPacks.Pack> packs;
StickerAdditionMigrationJob(@NonNull BlessedPacks.Pack... packs) {
this(new Parameters.Builder().build(), Arrays.asList(packs));
}
private StickerAdditionMigrationJob(@NonNull Parameters parameters, @NonNull List<BlessedPacks.Pack> packs) {
super(parameters);
this.packs = packs;
}
@Override
public boolean isUiBlocking() {
return false;
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public @NonNull Data serialize() {
String[] packsRaw = Stream.of(packs).map(BlessedPacks.Pack::toJson).toArray(String[]::new);
return new Data.Builder().putStringArray(KEY_PACKS, packsRaw).build();
}
@Override
public void performMigration() {
JobManager jobManager = ApplicationDependencies.getJobManager();
for (BlessedPacks.Pack pack : packs) {
Log.i(TAG, "Installing reference for blessed pack: " + pack.getPackId());
jobManager.add(StickerPackDownloadJob.forReference(pack.getPackId(), pack.getPackKey()));
}
}
@Override
boolean shouldRetry(@NonNull Exception e) {
return false;
}
public static class Factory implements Job.Factory<StickerAdditionMigrationJob> {
@Override
public @NonNull StickerAdditionMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
String[] raw = data.getStringArray(KEY_PACKS);
List<BlessedPacks.Pack> packs = Stream.of(raw).map(BlessedPacks.Pack::fromJson).toList();
return new StickerAdditionMigrationJob(parameters, packs);
}
}
}

View File

@ -30,7 +30,7 @@ class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher {
@Override
protected InputStream loadResource(Uri uri, ContentResolver contentResolver) throws FileNotFoundException {
if (MediaUtil.hasVideoThumbnail(uri)) {
Bitmap thumbnail = MediaUtil.getVideoThumbnail(context, uri);
Bitmap thumbnail = MediaUtil.getVideoThumbnail(context, uri, 1000);
if (thumbnail != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();

View File

@ -157,6 +157,24 @@ public abstract class Slide {
@Nullable BlurHash blurHash,
boolean voiceNote,
boolean quote)
{
return constructAttachmentFromUri(context, uri, defaultMime, size, width, height, hasThumbnail, fileName, caption, stickerLocator, blurHash, voiceNote, quote, null);
}
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,
@Nullable AttachmentDatabase.TransformProperties transformProperties)
{
String resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri)).or(defaultMime);
String fastPreflightId = String.valueOf(new SecureRandom().nextLong());
@ -174,7 +192,7 @@ public abstract class Slide {
caption,
stickerLocator,
blurHash,
null);
transformProperties);
}
public @NonNull Optional<String> getFileType(@NonNull Context context) {

View File

@ -19,24 +19,25 @@ 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.database.AttachmentDatabase;
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);
this(context, uri, dataSize, null, 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, Uri uri, long dataSize, @Nullable String caption, @Nullable AttachmentDatabase.TransformProperties transformProperties) {
super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(uri), null, caption, null, null, false, false, transformProperties));
}
public VideoSlide(Context context, Attachment attachment) {

View File

@ -7,6 +7,7 @@ import androidx.preference.ListPreference;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.Arrays;

View File

@ -2,15 +2,21 @@ package org.thoughtcrime.securesms.profiles;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.whispersystems.signalservice.api.util.StreamDetails;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@ -57,4 +63,24 @@ public class AvatarHelper {
out.close();
}
}
public static @NonNull StreamDetails avatarStream(@NonNull byte[] data) {
return new StreamDetails(new ByteArrayInputStream(data), MediaUtil.IMAGE_JPEG, data.length);
}
public static @Nullable StreamDetails getSelfProfileAvatarStream(@NonNull Context context) {
File avatarFile = getAvatarFile(context, Recipient.self().getId());
if (avatarFile.exists() && avatarFile.length() > 0) {
try {
FileInputStream stream = new FileInputStream(avatarFile);
return new StreamDetails(stream, MediaUtil.IMAGE_JPEG, avatarFile.length());
} catch (FileNotFoundException e) {
throw new AssertionError(e);
}
} else {
return null;
}
}
}

View File

@ -19,9 +19,9 @@ public class EditProfileActivity extends BaseActionBarActivity implements EditPr
public static final String EXCLUDE_SYSTEM = "exclude_system";
public static final String DISPLAY_USERNAME = "display_username";
public static final String NEXT_BUTTON_TEXT = "next_button_text";
public static final String SHOW_TOOLBAR = "show_back_arrow";
public static final String SHOW_TOOLBAR = "show_back_arrow";
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
@Override
public void onCreate(Bundle bundle) {
@ -32,8 +32,10 @@ public class EditProfileActivity extends BaseActionBarActivity implements EditPr
setContentView(R.layout.profile_create_activity);
if (bundle == null) {
NavGraph graph = Navigation.findNavController(this, R.id.nav_host_fragment).getGraph();
Navigation.findNavController(this, R.id.nav_host_fragment).setGraph(graph, getIntent().getExtras());
Bundle extras = getIntent().getExtras();
NavGraph graph = Navigation.findNavController(this, R.id.nav_host_fragment).getGraph();
Navigation.findNavController(this, R.id.nav_host_fragment).setGraph(graph, extras != null ? extras : new Bundle());
}
}

View File

@ -35,9 +35,11 @@ import com.dd.CircularProgressButton;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.avatar.AvatarSelection;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.PinUtil;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.megaphone.Megaphones;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints;
@ -328,6 +330,8 @@ public class EditProfileFragment extends Fragment {
SignalStore.registrationValues().setRegistrationComplete();
}
ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.PROFILE_NAMES_FOR_ALL);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) handleFinishedLollipop();
else handleFinishedLegacy();
} else {

View File

@ -20,17 +20,14 @@ import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.profiles.SystemProfileUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.IOException;
import java.security.SecureRandom;
@ -132,7 +129,7 @@ class EditProfileRepository {
@WorkerThread
private @NonNull Optional<String> getUsernameInternal() {
try {
SignalServiceProfile profile = retrieveOwnProfile();
SignalServiceProfile profile = ProfileUtil.retrieveProfile(context, Recipient.self(), SignalServiceProfile.RequestType.PROFILE).getProfile();
TextSecurePreferences.setLocalUsername(context, profile.getUsername());
DatabaseFactory.getRecipientDatabase(context).setUsername(Recipient.self().getId(), profile.getUsername());
} catch (IOException e) {
@ -141,22 +138,6 @@ class EditProfileRepository {
return Optional.fromNullable(TextSecurePreferences.getLocalUsername(context));
}
private SignalServiceProfile retrieveOwnProfile() throws IOException {
SignalServiceAddress address = new SignalServiceAddress(TextSecurePreferences.getLocalUuid(context), TextSecurePreferences.getLocalNumber(context));
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver();
SignalServiceMessagePipe pipe = IncomingMessageObserver.getPipe();
if (pipe != null) {
try {
return pipe.getProfile(address, Optional.absent());
} catch (IOException e) {
Log.w(TAG, e);
}
}
return receiver.retrieveProfile(address, Optional.absent());
}
public enum UploadResult {
SUCCESS,
ERROR_FILE_IO

View File

@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.net.UserAgentInterceptor;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
@ -16,6 +17,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -144,7 +146,13 @@ public class SignalServiceNetworkAccess {
final SignalStorageUrl qatarGoogleStorage = new SignalStorageUrl("https://www.google.com.qa/storage", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC);
final List<Interceptor> interceptors = Collections.singletonList(new UserAgentInterceptor());
final byte[] zkGroupServerPublicParams;
try {
zkGroupServerPublicParams = Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS);
} catch (IOException e) {
throw new AssertionError(e);
}
this.censorshipConfiguration = new HashMap<String, SignalServiceConfiguration>() {{
put(COUNTRY_CODE_EGYPT, new SignalServiceConfiguration(new SignalServiceUrl[] {egyptGoogleService, baseGoogleService, baseAndroidService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService},
@ -152,21 +160,24 @@ public class SignalServiceNetworkAccess {
new SignalContactDiscoveryUrl[] {egyptGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery},
new SignalKeyBackupServiceUrl[] {egyptGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs},
new SignalStorageUrl[] {egyptGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
interceptors));
interceptors,
zkGroupServerPublicParams));
put(COUNTRY_CODE_UAE, new SignalServiceConfiguration(new SignalServiceUrl[] {uaeGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService},
new SignalCdnUrl[] {uaeGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn},
new SignalContactDiscoveryUrl[] {uaeGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery},
new SignalKeyBackupServiceUrl[] {uaeGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs},
new SignalStorageUrl[] {uaeGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
interceptors));
interceptors,
zkGroupServerPublicParams));
put(COUNTRY_CODE_OMAN, new SignalServiceConfiguration(new SignalServiceUrl[] {omanGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService},
new SignalCdnUrl[] {omanGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn},
new SignalContactDiscoveryUrl[] {omanGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery},
new SignalKeyBackupServiceUrl[] {omanGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs},
new SignalStorageUrl[] {omanGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
interceptors));
interceptors,
zkGroupServerPublicParams));
put(COUNTRY_CODE_QATAR, new SignalServiceConfiguration(new SignalServiceUrl[] {qatarGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService},
@ -174,7 +185,8 @@ public class SignalServiceNetworkAccess {
new SignalContactDiscoveryUrl[] {qatarGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery},
new SignalKeyBackupServiceUrl[] {qatarGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs},
new SignalStorageUrl[] {qatarGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
interceptors));
interceptors,
zkGroupServerPublicParams));
}};
this.uncensoredConfiguration = new SignalServiceConfiguration(new SignalServiceUrl[] {new SignalServiceUrl(BuildConfig.SIGNAL_URL, new SignalServiceTrustStore(context))},
@ -182,7 +194,8 @@ public class SignalServiceNetworkAccess {
new SignalContactDiscoveryUrl[] {new SignalContactDiscoveryUrl(BuildConfig.SIGNAL_CONTACT_DISCOVERY_URL, new SignalServiceTrustStore(context))},
new SignalKeyBackupServiceUrl[] { new SignalKeyBackupServiceUrl(BuildConfig.SIGNAL_KEY_BACKUP_URL, new SignalServiceTrustStore(context)) },
new SignalStorageUrl[] {new SignalStorageUrl(BuildConfig.STORAGE_URL, new SignalServiceTrustStore(context))},
interceptors);
interceptors,
zkGroupServerPublicParams);
this.censoredCountries = this.censorshipConfiguration.keySet().toArray(new String[0]);
}

View File

@ -78,6 +78,7 @@ public class Recipient {
private final int expireMessages;
private final RegisteredState registered;
private final byte[] profileKey;
private final byte[] profileKeyCredential;
private final String name;
private final Uri systemContactPhoto;
private final String customLabel;
@ -297,6 +298,7 @@ public class Recipient {
this.expireMessages = 0;
this.registered = RegisteredState.UNKNOWN;
this.profileKey = null;
this.profileKeyCredential = null;
this.name = null;
this.systemContactPhoto = null;
this.customLabel = null;
@ -336,6 +338,7 @@ public class Recipient {
this.expireMessages = details.expireMessages;
this.registered = details.registered;
this.profileKey = details.profileKey;
this.profileKeyCredential = details.profileKeyCredential;
this.name = details.name;
this.systemContactPhoto = details.systemContactPhoto;
this.customLabel = details.customLabel;
@ -666,6 +669,14 @@ public class Recipient {
return profileKey;
}
public @Nullable byte[] getProfileKeyCredential() {
return profileKeyCredential;
}
public boolean hasProfileKeyCredential() {
return profileKeyCredential != null;
}
public @Nullable byte[] getStorageServiceKey() {
return storageKey;
}

View File

@ -7,9 +7,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier;
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
@ -48,6 +47,7 @@ public class RecipientDetails {
final Optional<Integer> defaultSubscriptionId;
final RegisteredState registered;
final byte[] profileKey;
final byte[] profileKeyCredential;
final String profileAvatar;
final boolean profileSharing;
final boolean systemContact;
@ -90,7 +90,8 @@ public class RecipientDetails {
this.profileName = isLocalNumber ? TextSecurePreferences.getProfileName(context) : settings.getProfileName();
this.defaultSubscriptionId = settings.getDefaultSubscriptionId();
this.registered = settings.getRegistered();
this.profileKey = isLocalNumber ? ProfileKeyUtil.getProfileKey(context) : settings.getProfileKey();
this.profileKey = settings.getProfileKey();
this.profileKeyCredential = settings.getProfileKeyCredential();
this.profileAvatar = settings.getProfileAvatar();
this.profileSharing = settings.isProfileSharing();
this.systemContact = systemContact;
@ -135,6 +136,7 @@ public class RecipientDetails {
this.defaultSubscriptionId = Optional.absent();
this.registered = RegisteredState.UNKNOWN;
this.profileKey = null;
this.profileKeyCredential = null;
this.profileAvatar = null;
this.profileSharing = false;
this.systemContact = true;

View File

@ -6,10 +6,11 @@ import android.os.AsyncTask;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
@ -22,8 +23,8 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.PinHashing;
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob;
import org.thoughtcrime.securesms.push.AccountManagerFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
@ -38,6 +39,7 @@ import org.whispersystems.signalservice.api.KeyBackupServicePinException;
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
import org.whispersystems.signalservice.api.RegistrationLockData;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.kbs.HashedPin;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
@ -190,10 +192,17 @@ public final class CodeVerificationRequest {
@Nullable String fcmToken)
throws IOException, KeyBackupSystemWrongPinException, KeyBackupSystemNoDataException
{
boolean isV2KbsPin = kbsTokenResponse != null;
int registrationId = KeyHelper.generateRegistrationId(false);
byte[] unidentifiedAccessKey = UnidentifiedAccessUtil.getSelfUnidentifiedAccessKey(context);
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
boolean isV2KbsPin = kbsTokenResponse != null;
int registrationId = KeyHelper.generateRegistrationId(false);
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
ProfileKey profileKey = findExistingProfileKey(context, credentials.getE164number());
if (profileKey == null) {
profileKey = ProfileKeyUtil.createNew();
Log.i(TAG, "No profile key found, created a new one");
}
byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(profileKey);
TextSecurePreferences.setLocalRegistrationId(context, registrationId);
SessionUtil.archiveAllSessions(context);
@ -227,6 +236,7 @@ public final class CodeVerificationRequest {
TextSecurePreferences.setLocalNumber(context, credentials.getE164number());
TextSecurePreferences.setLocalUuid(context, uuid);
recipientDatabase.setProfileKey(selfId, profileKey);
ApplicationDependencies.getRecipientCache().clearSelf();
TextSecurePreferences.setFcmToken(context, fcmToken);
@ -260,6 +270,17 @@ public final class CodeVerificationRequest {
}
}
private static @Nullable ProfileKey findExistingProfileKey(@NonNull Context context, @NonNull String e164number) {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
Optional<RecipientId> recipient = recipientDatabase.getByE164(e164number);
if (recipient.isPresent()) {
return ProfileKeyUtil.profileKeyOrNull(Recipient.resolved(recipient.get()).getProfileKey());
}
return null;
}
private static void repostPinToResetTries(@NonNull Context context, @Nullable String pin, @NonNull RegistrationLockData kbsData) {
if (pin == null) return;

View File

@ -1,400 +0,0 @@
package org.thoughtcrime.securesms.ringrtc;
import android.annotation.TargetApi;
import android.content.Context;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import java.io.IOException;
import java.util.List;
import java.util.LinkedList;
import org.signal.ringrtc.CallConnection;
import org.signal.ringrtc.CallConnectionFactory;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.SignalMessageRecipient;
import org.thoughtcrime.securesms.logging.Log;
import org.webrtc.AudioSource;
import org.webrtc.AudioTrack;
import org.webrtc.Camera1Enumerator;
import org.webrtc.Camera2Capturer;
import org.webrtc.Camera2Enumerator;
import org.webrtc.CameraEnumerator;
import org.webrtc.CameraVideoCapturer;
import org.webrtc.EglBase;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.SurfaceTextureHelper;
import org.webrtc.VideoSink;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.BACK;
import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.FRONT;
import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.NONE;
import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.PENDING;
public class CallConnectionWrapper {
private static final String TAG = Log.tag(CallConnectionWrapper.class);
@NonNull private final CallConnection callConnection;
@NonNull private final AudioTrack audioTrack;
@NonNull private final AudioSource audioSource;
@NonNull private final Camera camera;
@Nullable private final VideoSource videoSource;
@Nullable private final VideoTrack videoTrack;
public CallConnectionWrapper(@NonNull Context context,
@NonNull CallConnectionFactory factory,
@NonNull CallConnection.Observer observer,
@NonNull VideoSink localRenderer,
@NonNull CameraEventListener cameraEventListener,
@NonNull EglBase eglBase,
boolean hideIp,
long callId,
boolean outBound,
@NonNull SignalMessageRecipient recipient,
@NonNull SignalServiceAccountManager accountManager)
throws UnregisteredUserException, IOException, CallException
{
CallConnection.Configuration configuration = new CallConnection.Configuration(callId,
outBound,
recipient,
accountManager,
hideIp);
this.callConnection = factory.createCallConnection(configuration, observer);
this.callConnection.setAudioPlayout(false);
this.callConnection.setAudioRecording(false);
MediaStream mediaStream = factory.createLocalMediaStream("ARDAMS");
MediaConstraints audioConstraints = new MediaConstraints();
audioConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
this.audioSource = factory.createAudioSource(audioConstraints);
this.audioTrack = factory.createAudioTrack("ARDAMSa0", audioSource);
this.audioTrack.setEnabled(false);
mediaStream.addTrack(audioTrack);
this.camera = new Camera(context, cameraEventListener);
if (camera.capturer != null) {
this.videoSource = factory.createVideoSource(false);
this.videoTrack = factory.createVideoTrack("ARDAMSv0", videoSource);
camera.capturer.initialize(SurfaceTextureHelper.create("WebRTC-SurfaceTextureHelper", eglBase.getEglBaseContext()), context, videoSource.getCapturerObserver());
this.videoTrack.addSink(localRenderer);
this.videoTrack.setEnabled(false);
mediaStream.addTrack(videoTrack);
} else {
this.videoSource = null;
this.videoTrack = null;
}
this.callConnection.addStream(mediaStream);
}
public boolean addIceCandidate(IceCandidate candidate) {
return callConnection.addIceCandidate(candidate);
}
public void sendOffer() throws CallException {
callConnection.sendOffer();
}
public boolean validateResponse(SignalMessageRecipient recipient, Long inCallId)
throws CallException
{
return callConnection.validateResponse(recipient, inCallId);
}
public void handleOfferAnswer(String sessionDescription) throws CallException {
callConnection.handleOfferAnswer(sessionDescription);
}
public void acceptOffer(String offer) throws CallException {
callConnection.acceptOffer(offer);
}
public void hangUp() throws CallException {
callConnection.hangUp();
}
public void answerCall() throws CallException {
callConnection.answerCall();
}
public void setVideoEnabled(boolean enabled) throws CallException {
if (videoTrack != null) {
videoTrack.setEnabled(enabled);
}
camera.setEnabled(enabled);
callConnection.sendVideoStatus(enabled);
}
public void flipCamera() {
camera.flip();
}
public CameraState getCameraState() {
return new CameraState(camera.getActiveDirection(), camera.getCount());
}
public void setCommunicationMode() {
callConnection.setAudioPlayout(true);
callConnection.setAudioRecording(true);
}
public void setAudioEnabled(boolean enabled) {
audioTrack.setEnabled(enabled);
}
public void dispose() {
camera.dispose();
if (videoSource != null) {
videoSource.dispose();
}
audioSource.dispose();
callConnection.dispose();
}
private static class Camera implements CameraVideoCapturer.CameraSwitchHandler {
@Nullable
private final CameraVideoCapturer capturer;
private final CameraEventListener cameraEventListener;
private final int cameraCount;
private CameraState.Direction activeDirection;
private boolean enabled;
Camera(@NonNull Context context, @NonNull CameraEventListener cameraEventListener)
{
this.cameraEventListener = cameraEventListener;
CameraEnumerator enumerator = getCameraEnumerator(context);
cameraCount = enumerator.getDeviceNames().length;
CameraVideoCapturer capturerCandidate = createVideoCapturer(enumerator, FRONT);
if (capturerCandidate != null) {
activeDirection = FRONT;
} else {
capturerCandidate = createVideoCapturer(enumerator, BACK);
if (capturerCandidate != null) {
activeDirection = BACK;
} else {
activeDirection = NONE;
}
}
capturer = capturerCandidate;
}
void flip() {
if (capturer == null || cameraCount < 2) {
throw new AssertionError("Tried to flip the camera, but we only have " + cameraCount +
" of them.");
}
activeDirection = PENDING;
capturer.switchCamera(this);
}
void setEnabled(boolean enabled) {
this.enabled = enabled;
if (capturer == null) {
return;
}
try {
if (enabled) {
capturer.startCapture(1280, 720, 30);
} else {
capturer.stopCapture();
}
} catch (InterruptedException e) {
Log.w(TAG, "Got interrupted while trying to stop video capture", e);
}
}
void dispose() {
if (capturer != null) {
capturer.dispose();
}
}
int getCount() {
return cameraCount;
}
@NonNull CameraState.Direction getActiveDirection() {
return enabled ? activeDirection : NONE;
}
@Nullable CameraVideoCapturer getCapturer() {
return capturer;
}
private @Nullable CameraVideoCapturer createVideoCapturer(@NonNull CameraEnumerator enumerator,
@NonNull CameraState.Direction direction)
{
String[] deviceNames = enumerator.getDeviceNames();
for (String deviceName : deviceNames) {
if ((direction == FRONT && enumerator.isFrontFacing(deviceName)) ||
(direction == BACK && enumerator.isBackFacing(deviceName)))
{
return enumerator.createCapturer(deviceName, null);
}
}
return null;
}
private @NonNull CameraEnumerator getCameraEnumerator(@NonNull Context context) {
boolean camera2EnumeratorIsSupported = false;
try {
camera2EnumeratorIsSupported = Camera2Enumerator.isSupported(context);
} catch (final Throwable throwable) {
Log.w(TAG, "Camera2Enumator.isSupport() threw.", throwable);
}
Log.i(TAG, "Camera2 enumerator supported: " + camera2EnumeratorIsSupported);
return camera2EnumeratorIsSupported ? new FilteredCamera2Enumerator(context)
: new Camera1Enumerator(true);
}
@Override
public void onCameraSwitchDone(boolean isFrontFacing) {
activeDirection = isFrontFacing ? FRONT : BACK;
cameraEventListener.onCameraSwitchCompleted(new CameraState(getActiveDirection(), getCount()));
}
@Override
public void onCameraSwitchError(String errorMessage) {
Log.e(TAG, "onCameraSwitchError: " + errorMessage);
cameraEventListener.onCameraSwitchCompleted(new CameraState(getActiveDirection(), getCount()));
}
}
public interface CameraEventListener {
void onCameraSwitchCompleted(@NonNull CameraState newCameraState);
}
@TargetApi(21)
private static class FilteredCamera2Enumerator extends Camera2Enumerator {
@NonNull private final Context context;
@Nullable private final CameraManager cameraManager;
@Nullable private String[] deviceNames;
FilteredCamera2Enumerator(@NonNull Context context) {
super(context);
this.context = context;
this.cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
this.deviceNames = null;
}
private boolean isMonochrome(String deviceName, CameraManager cameraManager) {
try {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(deviceName);
int[] capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
if (capabilities != null) {
for (int cap : capabilities) {
if (cap == CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_MONOCHROME) {
return true;
}
}
}
} catch (CameraAccessException e) {
return false;
}
return false;
}
private boolean isLensFacing(String deviceName, CameraManager cameraManager, Integer facing) {
try {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(deviceName);
Integer lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING);
return facing.equals(lensFacing);
} catch (CameraAccessException e) {
return false;
}
}
@Override
public @NonNull String[] getDeviceNames() {
if (deviceNames != null) {
return deviceNames;
}
try {
List<String> cameraList = new LinkedList<>();
if (cameraManager != null) {
// While skipping cameras that are monochrome, gather cameras
// until we have at most 1 front facing camera and 1 back
// facing camera.
List<String> devices = Stream.of(cameraManager.getCameraIdList())
.filterNot(id -> isMonochrome(id, cameraManager))
.toList();
String frontCamera = Stream.of(devices)
.filter(id -> isLensFacing(id, cameraManager, CameraMetadata.LENS_FACING_FRONT))
.findFirst()
.orElse(null);
if (frontCamera != null) {
cameraList.add(frontCamera);
}
String backCamera = Stream.of(devices)
.filter(id -> isLensFacing(id, cameraManager, CameraMetadata.LENS_FACING_BACK))
.findFirst()
.orElse(null);
if (backCamera != null) {
cameraList.add(backCamera);
}
}
this.deviceNames = cameraList.toArray(new String[0]);
} catch (CameraAccessException e) {
Log.e(TAG, "Camera access exception: " + e);
this.deviceNames = new String[] {};
}
return deviceNames;
}
@Override
public @NonNull CameraVideoCapturer createCapturer(@Nullable String deviceName,
@Nullable CameraVideoCapturer.CameraEventsHandler eventsHandler) {
return new Camera2Capturer(context, deviceName, eventsHandler, new FilteredCamera2Enumerator(context));
}
}
}

View File

@ -0,0 +1,34 @@
package org.thoughtcrime.securesms.ringrtc;
/**
*
* Enumeration of call state
*
*/
public enum CallState {
/** Idle, setting up objects */
IDLE,
/** Dialing. Outgoing call is signaling the remote peer */
DIALING,
/** Answering. Incoming call is responding to remote peer */
ANSWERING,
/** Remote ringing. Outgoing call, ICE negotiation is complete */
REMOTE_RINGING,
/** Local ringing. Incoming call, ICE negotiation is complete */
LOCAL_RINGING,
/** Connected. Incoming/Outgoing call, the call is connected */
CONNECTED,
/** Terminated. Incoming/Outgoing call, the call is terminated */
TERMINATED,
/** Busy. Outgoing call received a busy notification */
RECEIVED_BUSY;
}

View File

@ -0,0 +1,280 @@
package org.thoughtcrime.securesms.ringrtc;
import android.annotation.TargetApi;
import android.content.Context;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import java.io.IOException;
import java.util.List;
import java.util.LinkedList;
import org.signal.ringrtc.CameraControl;
import org.thoughtcrime.securesms.logging.Log;
import org.webrtc.Camera1Enumerator;
import org.webrtc.Camera2Capturer;
import org.webrtc.Camera2Enumerator;
import org.webrtc.CameraEnumerator;
import org.webrtc.CameraVideoCapturer;
import org.webrtc.CapturerObserver;
import org.webrtc.EglBase;
import org.webrtc.SurfaceTextureHelper;
import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.BACK;
import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.FRONT;
import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.NONE;
import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.PENDING;
/**
* Encapsulate the camera functionality needed for video calling.
*/
public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHandler {
private static final String TAG = Log.tag(Camera.class);
@NonNull private final Context context;
@Nullable private final CameraVideoCapturer capturer;
@NonNull private final CameraEventListener cameraEventListener;
@NonNull private final EglBase eglBase;
private final int cameraCount;
@NonNull private CameraState.Direction activeDirection;
private boolean enabled;
public Camera(@NonNull Context context,
@NonNull CameraEventListener cameraEventListener,
@NonNull EglBase eglBase)
{
this.context = context;
this.cameraEventListener = cameraEventListener;
this.eglBase = eglBase;
CameraEnumerator enumerator = getCameraEnumerator(context);
cameraCount = enumerator.getDeviceNames().length;
CameraVideoCapturer capturerCandidate = createVideoCapturer(enumerator, FRONT);
if (capturerCandidate != null) {
activeDirection = FRONT;
} else {
capturerCandidate = createVideoCapturer(enumerator, BACK);
if (capturerCandidate != null) {
activeDirection = BACK;
} else {
activeDirection = NONE;
}
}
capturer = capturerCandidate;
}
@Override
public void initCapturer(@NonNull CapturerObserver observer) {
if (capturer != null) {
capturer.initialize(SurfaceTextureHelper.create("WebRTC-SurfaceTextureHelper", eglBase.getEglBaseContext()),
context,
observer);
}
}
@Override
public boolean hasCapturer() {
return capturer != null;
}
@Override
public void flip() {
if (capturer == null || cameraCount < 2) {
throw new AssertionError("Tried to flip the camera, but we only have " + cameraCount + " of them.");
}
activeDirection = PENDING;
capturer.switchCamera(this);
}
@Override
public void setEnabled(boolean enabled) {
Log.i(TAG, "setEnabled(): " + enabled);
this.enabled = enabled;
if (capturer == null) {
return;
}
try {
if (enabled) {
Log.i(TAG, "setEnabled(): starting capture");
capturer.startCapture(1280, 720, 30);
} else {
Log.i(TAG, "setEnabled(): stopping capture");
capturer.stopCapture();
}
} catch (InterruptedException e) {
Log.w(TAG, "Got interrupted while trying to stop video capture", e);
}
}
public void dispose() {
if (capturer != null) {
capturer.dispose();
}
}
int getCount() {
return cameraCount;
}
@NonNull CameraState.Direction getActiveDirection() {
return enabled ? activeDirection : NONE;
}
@NonNull public CameraState getCameraState() {
return new CameraState(getActiveDirection(), getCount());
}
@Nullable CameraVideoCapturer getCapturer() {
return capturer;
}
private @Nullable CameraVideoCapturer createVideoCapturer(@NonNull CameraEnumerator enumerator,
@NonNull CameraState.Direction direction)
{
String[] deviceNames = enumerator.getDeviceNames();
for (String deviceName : deviceNames) {
if ((direction == FRONT && enumerator.isFrontFacing(deviceName)) ||
(direction == BACK && enumerator.isBackFacing(deviceName)))
{
return enumerator.createCapturer(deviceName, null);
}
}
return null;
}
private @NonNull CameraEnumerator getCameraEnumerator(@NonNull Context context) {
boolean camera2EnumeratorIsSupported = false;
try {
camera2EnumeratorIsSupported = Camera2Enumerator.isSupported(context);
} catch (final Throwable throwable) {
Log.w(TAG, "Camera2Enumator.isSupport() threw.", throwable);
}
Log.i(TAG, "Camera2 enumerator supported: " + camera2EnumeratorIsSupported);
return camera2EnumeratorIsSupported ? new FilteredCamera2Enumerator(context)
: new Camera1Enumerator(true);
}
@Override
public void onCameraSwitchDone(boolean isFrontFacing) {
activeDirection = isFrontFacing ? FRONT : BACK;
cameraEventListener.onCameraSwitchCompleted(new CameraState(getActiveDirection(), getCount()));
}
@Override
public void onCameraSwitchError(String errorMessage) {
Log.e(TAG, "onCameraSwitchError: " + errorMessage);
cameraEventListener.onCameraSwitchCompleted(new CameraState(getActiveDirection(), getCount()));
}
@TargetApi(21)
private static class FilteredCamera2Enumerator extends Camera2Enumerator {
private static final String TAG = Log.tag(Camera2Enumerator.class);
@NonNull private final Context context;
@Nullable private final CameraManager cameraManager;
@Nullable private String[] deviceNames;
FilteredCamera2Enumerator(@NonNull Context context) {
super(context);
this.context = context;
this.cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
this.deviceNames = null;
}
private static boolean isMonochrome(@NonNull String deviceName, @NonNull CameraManager cameraManager) {
try {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(deviceName);
int[] capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
if (capabilities != null) {
for (int capability : capabilities) {
if (capability == CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_MONOCHROME) {
return true;
}
}
}
} catch (CameraAccessException e) {
return false;
}
return false;
}
private static boolean isLensFacing(@NonNull String deviceName, @NonNull CameraManager cameraManager, @NonNull Integer facing) {
try {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(deviceName);
Integer lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING);
return facing.equals(lensFacing);
} catch (CameraAccessException e) {
return false;
}
}
@Override
public @NonNull String[] getDeviceNames() {
if (deviceNames != null) {
return deviceNames;
}
try {
List<String> cameraList = new LinkedList<>();
if (cameraManager != null) {
List<String> devices = Stream.of(cameraManager.getCameraIdList())
.filterNot(id -> isMonochrome(id, cameraManager))
.toList();
String frontCamera = Stream.of(devices)
.filter(id -> isLensFacing(id, cameraManager, CameraMetadata.LENS_FACING_FRONT))
.findFirst()
.orElse(null);
if (frontCamera != null) {
cameraList.add(frontCamera);
}
String backCamera = Stream.of(devices)
.filter(id -> isLensFacing(id, cameraManager, CameraMetadata.LENS_FACING_BACK))
.findFirst()
.orElse(null);
if (backCamera != null) {
cameraList.add(backCamera);
}
}
this.deviceNames = cameraList.toArray(new String[0]);
} catch (CameraAccessException e) {
Log.e(TAG, "Camera access exception: " + e);
this.deviceNames = new String[] {};
}
return deviceNames;
}
@Override
public @NonNull CameraVideoCapturer createCapturer(@Nullable String deviceName,
@Nullable CameraVideoCapturer.CameraEventsHandler eventsHandler)
{
return new Camera2Capturer(context, deviceName, eventsHandler, new FilteredCamera2Enumerator(context));
}
}
}

View File

@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.ringrtc;
import androidx.annotation.NonNull;
public interface CameraEventListener {
void onCameraSwitchCompleted(@NonNull CameraState newCameraState);
}

View File

@ -26,6 +26,11 @@ public class CameraState {
return this.activeDirection != Direction.NONE;
}
@Override
public String toString() {
return "count: " + cameraCount + ", activeDirection: " + activeDirection;
}
public enum Direction {
FRONT, BACK, NONE, PENDING
}

View File

@ -0,0 +1,72 @@
package org.thoughtcrime.securesms.ringrtc;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import org.signal.ringrtc.CallId;
import org.webrtc.IceCandidate;
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
/**
* Utility class for passing ICE candidate objects via Intents.
*
* Also provides utility methods for converting to/from Signal ICE
* candidate messages.
*/
public class IceCandidateParcel implements Parcelable {
@NonNull private final IceCandidate iceCandidate;
public IceCandidateParcel(@NonNull IceCandidate iceCandidate) {
this.iceCandidate = iceCandidate;
}
public IceCandidateParcel(@NonNull IceUpdateMessage iceUpdateMessage) {
this.iceCandidate = new IceCandidate(iceUpdateMessage.getSdpMid(),
iceUpdateMessage.getSdpMLineIndex(),
iceUpdateMessage.getSdp());
}
private IceCandidateParcel(@NonNull Parcel in) {
this.iceCandidate = new IceCandidate(in.readString(),
in.readInt(),
in.readString());
}
public @NonNull IceCandidate getIceCandidate() {
return iceCandidate;
}
public @NonNull IceUpdateMessage getIceUpdateMessage(@NonNull CallId callId) {
return new IceUpdateMessage(callId.longValue(),
iceCandidate.sdpMid,
iceCandidate.sdpMLineIndex,
iceCandidate.sdp);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString(iceCandidate.sdpMid);
dest.writeInt(iceCandidate.sdpMLineIndex);
dest.writeString(iceCandidate.sdp);
}
public static final Creator<IceCandidateParcel> CREATOR = new Creator<IceCandidateParcel>() {
@Override
public IceCandidateParcel createFromParcel(@NonNull Parcel in) {
return new IceCandidateParcel(in);
}
@Override
public IceCandidateParcel[] newArray(int size) {
return new IceCandidateParcel[size];
}
};
}

View File

@ -1,107 +0,0 @@
package org.thoughtcrime.securesms.ringrtc;
import android.content.Context;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.util.List;
import org.signal.ringrtc.SignalMessageRecipient;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public final class MessageRecipient implements SignalMessageRecipient {
private static final String TAG = Log.tag(MessageRecipient.class);
@NonNull private final Recipient recipient;
@NonNull private final SignalServiceMessageSender messageSender;
public MessageRecipient(@NonNull SignalServiceMessageSender messageSender,
@NonNull Recipient recipient)
{
this.recipient = recipient;
this.messageSender = messageSender;
}
public @NonNull RecipientId getId() {
return recipient.getId();
}
@Override
public boolean isEqual(@NonNull SignalMessageRecipient inRecipient) {
if (!(inRecipient instanceof MessageRecipient)) {
return false;
}
if (getClass() != inRecipient.getClass()) {
Log.e(TAG, "CLASSES NOT EQUAL: " + getClass().toString() + ", " + recipient.getClass().toString());
return false;
}
MessageRecipient that = (MessageRecipient) inRecipient;
return recipient.equals(that.recipient);
}
private void sendMessage(Context context, SignalServiceCallMessage callMessage)
throws UntrustedIdentityException, IOException
{
messageSender.sendCallMessage(RecipientUtil.toSignalServiceAddress(context, recipient),
UnidentifiedAccessUtil.getAccessFor(context, recipient),
callMessage);
}
@Override
public void sendOfferMessage(Context context, long callId, String description)
throws UntrustedIdentityException, IOException
{
Log.i(TAG, "MessageRecipient::sendOfferMessage(): callId: 0x" + Long.toHexString(callId));
OfferMessage offerMessage = new OfferMessage(callId, description);
sendMessage(context, SignalServiceCallMessage.forOffer(offerMessage));
}
@Override
public void sendAnswerMessage(Context context, long callId, String description)
throws UntrustedIdentityException, IOException
{
Log.i(TAG, "MessageRecipient::sendAnswerMessage(): callId: 0x" + Long.toHexString(callId));
AnswerMessage answerMessage = new AnswerMessage(callId, description);
sendMessage(context, SignalServiceCallMessage.forAnswer(answerMessage));
}
@Override
public void sendIceUpdates(Context context, List<IceUpdateMessage> iceUpdateMessages)
throws UntrustedIdentityException, IOException
{
Log.i(TAG, "MessageRecipient::sendIceUpdates(): iceUpdates: " + iceUpdateMessages.size());
sendMessage(context, SignalServiceCallMessage.forIceUpdates(iceUpdateMessages));
}
@Override
public void sendHangupMessage(Context context, long callId)
throws UntrustedIdentityException, IOException
{
Log.i(TAG, "MessageRecipient::sendHangupMessage(): callId: 0x" + Long.toHexString(callId));
HangupMessage hangupMessage = new HangupMessage(callId);
sendMessage(context, SignalServiceCallMessage.forHangup(hangupMessage));
}
}

View File

@ -0,0 +1,149 @@
package org.thoughtcrime.securesms.ringrtc;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import org.signal.ringrtc.CallId;
import org.signal.ringrtc.Remote;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
/**
* Container class that represents the remote peer and current state
* of a video/voice call.
*
* The class is also Parcelable for passing around via an Intent.
*/
public final class RemotePeer implements Remote, Parcelable
{
private static final String TAG = Log.tag(RemotePeer.class);
@NonNull private final RecipientId recipientId;
@NonNull private CallState callState;
@NonNull private CallId callId;
public RemotePeer(@NonNull RecipientId recipientId) {
this.recipientId = recipientId;
this.callState = CallState.IDLE;
this.callId = new CallId(-1L);
}
private RemotePeer(@NonNull Parcel in) {
this.recipientId = RecipientId.CREATOR.createFromParcel(in);
this.callState = CallState.values()[in.readInt()];
this.callId = new CallId(in.readLong());
}
public @NonNull CallId getCallId() {
return callId;
}
public @NonNull CallState getState() {
return callState;
}
public @NonNull RecipientId getId() {
return recipientId;
}
public @NonNull Recipient getRecipient() {
return Recipient.resolved(recipientId);
}
@Override
public String toString() {
return "recipientId: " + this.recipientId +
", callId: " + this.callId +
", state: " + this.callState;
}
@Override
public boolean recipientEquals(Remote obj) {
if (obj != null && this.getClass() == obj.getClass()) {
RemotePeer that = (RemotePeer)obj;
return this.recipientId.equals(that.recipientId);
}
return false;
}
public boolean callIdEquals(RemotePeer remotePeer) {
return remotePeer != null && this.callId.equals(remotePeer.callId);
}
public void dialing(@NonNull CallId callId) {
if (callState != CallState.IDLE) {
throw new IllegalStateException("Cannot transition to DIALING from state: " + callState);
}
this.callId = callId;
this.callState = CallState.DIALING;
}
public void answering(@NonNull CallId callId) {
if (callState != CallState.IDLE) {
throw new IllegalStateException("Cannot transition to ANSWERING from state: " + callState);
}
this.callId = callId;
this.callState = CallState.ANSWERING;
}
public void remoteRinging() {
if (callState != CallState.DIALING) {
throw new IllegalStateException("Cannot transition to REMOTE_RINGING from state: " + callState);
}
this.callState = CallState.REMOTE_RINGING;
}
public void receivedBusy() {
if (callState != CallState.DIALING) {
Log.w(TAG, "RECEIVED_BUSY from unexpected state: " + callState);
}
this.callState = CallState.RECEIVED_BUSY;
}
public void localRinging() {
if (callState != CallState.ANSWERING) {
throw new IllegalStateException("Cannot transition to LOCAL_RINGING from state: " + callState);
}
this.callState = CallState.LOCAL_RINGING;
}
public void connected() {
if (callState != CallState.REMOTE_RINGING && callState != CallState.LOCAL_RINGING) {
throw new IllegalStateException("Cannot transition outgoing call to CONNECTED from state: " + callState);
}
this.callState = CallState.CONNECTED;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
recipientId.writeToParcel(dest, flags);
dest.writeInt(callState.ordinal());
dest.writeLong(callId.longValue());
}
public static final Creator<RemotePeer> CREATOR = new Creator<RemotePeer>() {
@Override
public RemotePeer createFromParcel(@NonNull Parcel in) {
return new RemotePeer(in);
}
@Override
public RemotePeer[] newArray(int size) {
return new RemotePeer[size];
}
};
}

View File

@ -0,0 +1,130 @@
package org.thoughtcrime.securesms.scribbles;
import android.content.Context;
import android.net.Uri;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.video.DecryptableUriVideoInput;
import org.thoughtcrime.securesms.video.videoconverter.VideoThumbnailsRangeSelectorView;
import java.io.IOException;
/**
* The HUD (heads-up display) that contains all of the tools for editing video.
*/
public final class VideoEditorHud extends LinearLayout {
@SuppressWarnings("unused")
private static final String TAG = Log.tag(VideoEditorHud.class);
private VideoThumbnailsRangeSelectorView videoTimeLine;
private EventListener eventListener;
private View playOverlay;
public VideoEditorHud(@NonNull Context context) {
super(context);
initialize();
}
public VideoEditorHud(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initialize();
}
public VideoEditorHud(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
private void initialize() {
View root = inflate(getContext(), R.layout.video_editor_hud, this);
setOrientation(VERTICAL);
videoTimeLine = root.findViewById(R.id.video_timeline);
playOverlay = root.findViewById(R.id.play_overlay);
playOverlay.setOnClickListener(v -> eventListener.onPlay());
}
public void setEventListener(EventListener eventListener) {
this.eventListener = eventListener;
}
@RequiresApi(api = 23)
public void setVideoSource(VideoSlide slide) throws IOException {
Uri uri = slide.getUri();
if (uri == null || !slide.hasVideo()) {
return;
}
videoTimeLine.setInput(DecryptableUriVideoInput.createForUri(getContext(), uri));
videoTimeLine.setOnRangeChangeListener(new VideoThumbnailsRangeSelectorView.OnRangeChangeListener() {
@Override
public void onPositionDrag(long position) {
if (eventListener != null) {
eventListener.onSeek(position, false);
}
}
@Override
public void onEndPositionDrag(long position) {
if (eventListener != null) {
eventListener.onSeek(position, true);
}
}
@Override
public void onRangeDrag(long minValue, long maxValue, long duration, VideoThumbnailsRangeSelectorView.Thumb thumb) {
if (eventListener != null) {
eventListener.onEditVideoDuration(duration, minValue, maxValue, thumb == VideoThumbnailsRangeSelectorView.Thumb.MIN, false);
}
}
@Override
public void onRangeDragEnd(long minValue, long maxValue, long duration, VideoThumbnailsRangeSelectorView.Thumb thumb) {
if (eventListener != null) {
eventListener.onEditVideoDuration(duration, minValue, maxValue, thumb == VideoThumbnailsRangeSelectorView.Thumb.MIN, true);
}
}
});
}
public void playing() {
playOverlay.setVisibility(INVISIBLE);
}
public void stopped() {
playOverlay.setVisibility(VISIBLE);
}
@RequiresApi(api = 23)
public void setDurationRange(long totalDuration, long fromDuration, long toDuration) {
videoTimeLine.setRange(fromDuration, toDuration);
}
@RequiresApi(api = 23)
public void setPosition(long playbackPositionUs) {
videoTimeLine.setActualPosition(playbackPositionUs);
}
public interface EventListener {
void onEditVideoDuration(long totalDurationUs, long startTimeUs, long endTimeUs, boolean fromEdited, boolean editingComplete);
void onPlay();
void onSeek(long position, boolean dragComplete);
}
}

View File

@ -9,7 +9,6 @@ import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.service.chooser.ChooserTarget;
import android.service.chooser.ChooserTargetService;
import androidx.annotation.NonNull;
@ -17,7 +16,7 @@ import androidx.annotation.RequiresApi;
import androidx.appcompat.view.ContextThemeWrapper;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.ShareActivity;
import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord;

View File

@ -15,26 +15,28 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
package org.thoughtcrime.securesms.sharing;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Process;
import android.provider.OpenableColumns;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;
import org.thoughtcrime.securesms.ContactSelectionListFragment;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.SearchToolbar;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
@ -42,31 +44,28 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FileUtils;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
/**
* An activity to quickly share content with contacts
* Entry point for sharing content into the app.
*
* @author Jake McGinty
* Handles contact selection when necessary, but also serves as an entry point for when the contact
* is known (such as choosing someone in a direct share).
*/
public class ShareActivity extends PassphraseRequiredActionBarActivity
implements ContactSelectionListFragment.OnContactSelectedListener, SwipeRefreshLayout.OnRefreshListener
@ -83,10 +82,8 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
private ContactSelectionListFragment contactsFragment;
private SearchToolbar searchToolbar;
private ImageView searchAction;
private View progressWheel;
private Uri resolvedExtra;
private String mimeType;
private boolean isPassingAlongMedia;
private ShareViewModel viewModel;
@Override
protected void onPreCreate() {
@ -114,15 +111,10 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
initializeToolbar();
initializeResources();
initializeSearch();
initializeViewModel();
initializeMedia();
}
@Override
protected void onNewIntent(Intent intent) {
Log.i(TAG, "onNewIntent()");
super.onNewIntent(intent);
setIntent(intent);
initializeMedia();
handleDestination();
}
@Override
@ -134,25 +126,22 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
}
@Override
public void onPause() {
super.onPause();
if (!isPassingAlongMedia && resolvedExtra != null) {
BlobProvider.getInstance().delete(this, resolvedExtra);
public void onStop() {
super.onStop();
if (!isFinishing()) {
finish();
}
if (!isFinishing()) {
finish();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
} else {
return super.onOptionsItemSelected(item);
}
return super.onOptionsItemSelected(item);
}
@Override
@ -161,6 +150,30 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
else super.onBackPressed();
}
@Override
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
SimpleTask.run(this.getLifecycle(), () -> {
Recipient recipient;
if (recipientId.isPresent()) {
recipient = Recipient.resolved(recipientId.get());
} else {
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
recipient = Recipient.external(this, number);
}
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient);
return new Pair<>(existingThread, recipient);
}, result -> onDestinationChosen(result.first(), result.second().getId()));
}
@Override
public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, String number) {
}
@Override
public void onRefresh() {
}
private void initializeToolbar() {
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
@ -173,15 +186,24 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
}
private void initializeResources() {
progressWheel = findViewById(R.id.progress_wheel);
searchToolbar = findViewById(R.id.search_toolbar);
searchAction = findViewById(R.id.search_action);
contactsFragment = (ContactSelectionListFragment) getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
if (contactsFragment == null) {
throw new IllegalStateException("Could not find contacts fragment!");
}
contactsFragment.setOnContactSelectedListener(this);
contactsFragment.setOnRefreshListener(this);
}
private void initializeViewModel() {
this.viewModel = ViewModelProviders.of(this, new ShareViewModel.Factory()).get(ShareViewModel.class);
}
private void initializeSearch() {
//noinspection IntegerDivisionInFloatingPointContext
searchAction.setOnClickListener(v -> searchToolbar.display(searchAction.getX() + (searchAction.getWidth() / 2),
searchAction.getY() + (searchAction.getHeight() / 2)));
@ -203,24 +225,25 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
}
private void initializeMedia() {
final Context context = this;
isPassingAlongMedia = false;
if (Intent.ACTION_SEND_MULTIPLE.equals(getIntent().getAction())) {
Log.i(TAG, "Multiple media share.");
List<Uri> uris = getIntent().getParcelableArrayListExtra(Intent.EXTRA_STREAM);
Uri streamExtra = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
mimeType = getMimeType(streamExtra);
viewModel.onMultipleMediaShared(uris);
} else if (Intent.ACTION_SEND.equals(getIntent().getAction()) || getIntent().hasExtra(Intent.EXTRA_STREAM)) {
Log.i(TAG, "Single media share.");
Uri uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
String type = getIntent().getType();
if (streamExtra != null && PartAuthority.isLocalUri(streamExtra)) {
isPassingAlongMedia = true;
resolvedExtra = streamExtra;
handleResolvedMedia(getIntent(), false);
viewModel.onSingleMediaShared(uri, type);
} else {
contactsFragment.getView().setVisibility(View.GONE);
progressWheel.setVisibility(View.VISIBLE);
new ResolveMediaTask(context).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, streamExtra);
Log.i(TAG, "Internal media share.");
viewModel.onNonExternalShare();
}
}
private void handleResolvedMedia(Intent intent, boolean animate) {
private void handleDestination() {
Intent intent = getIntent();
long threadId = intent.getLongExtra(EXTRA_THREAD_ID, -1);
int distributionType = intent.getIntExtra(EXTRA_DISTRIBUTION_TYPE, -1);
RecipientId recipientId = null;
@ -229,151 +252,75 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
recipientId = RecipientId.from(intent.getStringExtra(EXTRA_RECIPIENT_ID));
}
boolean hasResolvedDestination = threadId != -1 && recipientId != null && distributionType != -1;
boolean hasPreexistingDestination = threadId != -1 && recipientId != null && distributionType != -1;
if (hasResolvedDestination) {
createConversation(threadId, recipientId, distributionType);
} else if (animate) {
ViewUtil.fadeIn(contactsFragment.requireView(), 300);
ViewUtil.fadeOut(progressWheel, 300);
} else {
contactsFragment.requireView().setVisibility(View.VISIBLE);
progressWheel.setVisibility(View.GONE);
if (hasPreexistingDestination) {
if (contactsFragment.getView() != null) {
contactsFragment.getView().setVisibility(View.GONE);
}
onDestinationChosen(threadId, recipientId);
}
}
private void createConversation(long threadId, @NonNull RecipientId recipientId, int distributionType) {
final Intent intent = getBaseShareIntent(ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId);
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
private void onDestinationChosen(long threadId, @NonNull RecipientId recipientId) {
if (!viewModel.isExternalShare()) {
openConversation(threadId, recipientId, null);
return;
}
isPassingAlongMedia = true;
startActivity(intent);
AtomicReference<AlertDialog> progressWheel = new AtomicReference<>();
if (viewModel.getShareData().getValue() == null) {
progressWheel.set(SimpleProgressDialog.show(this));
}
viewModel.getShareData().observe(this, (data) -> {
if (data == null) return;
if (progressWheel.get() != null) {
progressWheel.get().dismiss();
progressWheel.set(null);
}
if (!data.isPresent()) {
Log.w(TAG, "No data to share!");
Toast.makeText(this, R.string.ShareActivity_multiple_attachments_are_only_supported, Toast.LENGTH_LONG).show();
finish();
return;
}
openConversation(threadId, recipientId, data.get());
});
}
private Intent getBaseShareIntent(final @NonNull Class<?> target) {
final Intent intent = new Intent(this, target);
final String textExtra = getIntent().getStringExtra(Intent.EXTRA_TEXT);
final ArrayList<Media> mediaExtra = getIntent().getParcelableArrayListExtra(ConversationActivity.MEDIA_EXTRA);
final StickerLocator stickerExtra = getIntent().getParcelableExtra(ConversationActivity.STICKER_EXTRA);
private void openConversation(long threadId, @NonNull RecipientId recipientId, @Nullable ShareData shareData) {
Intent intent = new Intent(this, ConversationActivity.class);
String textExtra = getIntent().getStringExtra(Intent.EXTRA_TEXT);
ArrayList<Media> mediaExtra = getIntent().getParcelableArrayListExtra(ConversationActivity.MEDIA_EXTRA);
StickerLocator stickerExtra = getIntent().getParcelableExtra(ConversationActivity.STICKER_EXTRA);
intent.putExtra(ConversationActivity.TEXT_EXTRA, textExtra);
intent.putExtra(ConversationActivity.MEDIA_EXTRA, mediaExtra);
intent.putExtra(ConversationActivity.STICKER_EXTRA, stickerExtra);
if (resolvedExtra != null) intent.setDataAndType(resolvedExtra, mimeType);
return intent;
}
private String getMimeType(@Nullable Uri uri) {
if (uri != null) {
final String mimeType = MediaUtil.getMimeType(getApplicationContext(), uri);
if (mimeType != null) return mimeType;
}
return MediaUtil.getCorrectedMimeType(getIntent().getType());
}
@Override
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
SimpleTask.run(this.getLifecycle(), () -> {
Recipient recipient;
if (recipientId.isPresent()) {
recipient = Recipient.resolved(recipientId.get());
} else {
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
recipient = Recipient.external(this, number);
}
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient);
return new Pair<>(existingThread, recipient);
}, result -> {
createConversation(result.first(), result.second().getId(), ThreadDatabase.DistributionTypes.DEFAULT);
});
}
@Override
public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, String number) {
}
@Override
public void onRefresh() {
}
@SuppressLint("StaticFieldLeak")
private class ResolveMediaTask extends AsyncTask<Uri, Void, Uri> {
private final Context context;
ResolveMediaTask(Context context) {
this.context = context;
if (shareData != null && shareData.isForIntent()) {
Log.i(TAG, "Shared data is a single file.");
intent.setDataAndType(shareData.getUri(), shareData.getMimeType());
} else if (shareData != null && shareData.isForMedia()) {
Log.i(TAG, "Shared data is set of media.");
intent.putExtra(ConversationActivity.MEDIA_EXTRA, shareData.getMedia());
} else if (shareData != null && shareData.isForPrimitive()) {
Log.i(TAG, "Shared data is a primitive type.");
} else {
Log.i(TAG, "Shared data was not external.");
}
@Override
protected Uri doInBackground(Uri... uris) {
try {
if (uris.length != 1 || uris[0] == null) {
return null;
}
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId);
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
InputStream inputStream;
viewModel.onSuccessulShare();
if ("file".equals(uris[0].getScheme())) {
inputStream = openFileUri(uris[0]);
} else {
inputStream = context.getContentResolver().openInputStream(uris[0]);
}
if (inputStream == null) {
return null;
}
Cursor cursor = getContentResolver().query(uris[0], new String[] {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}, null, null, null);
String fileName = null;
Long fileSize = null;
try {
if (cursor != null && cursor.moveToFirst()) {
try {
fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
} catch (IllegalArgumentException e) {
Log.w(TAG, e);
}
}
} finally {
if (cursor != null) cursor.close();
}
return BlobProvider.getInstance()
.forData(inputStream, fileSize == null ? 0 : fileSize)
.withMimeType(mimeType)
.withFileName(fileName)
.createForMultipleSessionsOnDisk(context);
} catch (IOException ioe) {
Log.w(TAG, ioe);
return null;
}
}
@Override
protected void onPostExecute(Uri uri) {
resolvedExtra = uri;
handleResolvedMedia(getIntent(), true);
}
private InputStream openFileUri(Uri uri) throws IOException {
FileInputStream fin = new FileInputStream(uri.getPath());
int owner = FileUtils.getFileDescriptorOwner(fin.getFD());
if (owner == -1 || owner == Process.myUid()) {
fin.close();
throw new IOException("File owned by application");
}
return fin;
}
startActivity(intent);
}
}

View File

@ -0,0 +1,66 @@
package org.thoughtcrime.securesms.sharing;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.mediasend.Media;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.ArrayList;
import java.util.List;
class ShareData {
private final Optional<Uri> uri;
private final Optional<String> mimeType;
private final Optional<ArrayList<Media>> media;
private final boolean external;
static ShareData forIntentData(@NonNull Uri uri, @NonNull String mimeType, boolean external) {
return new ShareData(Optional.of(uri), Optional.of(mimeType), Optional.absent(), external);
}
static ShareData forPrimitiveTypes() {
return new ShareData(Optional.absent(), Optional.absent(), Optional.absent(), true);
}
static ShareData forMedia(@NonNull List<Media> media) {
return new ShareData(Optional.absent(), Optional.absent(), Optional.of(new ArrayList<>(media)), true);
}
private ShareData(Optional<Uri> uri, Optional<String> mimeType, Optional<ArrayList<Media>> media, boolean external) {
this.uri = uri;
this.mimeType = mimeType;
this.media = media;
this.external = external;
}
boolean isForIntent() {
return uri.isPresent();
}
boolean isForPrimitive() {
return !uri.isPresent() && !media.isPresent();
}
boolean isForMedia() {
return media.isPresent();
}
public @NonNull Uri getUri() {
return uri.get();
}
public @NonNull String getMimeType() {
return mimeType.get();
}
public @NonNull ArrayList<Media> getMedia() {
return media.get();
}
public boolean isExternal() {
return external;
}
}

View File

@ -0,0 +1,209 @@
package org.thoughtcrime.securesms.sharing;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.MediaSendConstants;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
class ShareRepository {
private static final String TAG = Log.tag(ShareRepository.class);
/**
* Handles a single URI that may be local or external.
*/
void getResolved(@NonNull Uri uri, @Nullable String mimeType, @NonNull Callback<Optional<ShareData>> callback) {
SignalExecutors.BOUNDED.execute(() -> {
try {
callback.onResult(Optional.of(getResolvedInternal(uri, mimeType)));
} catch (IOException e) {
Log.w(TAG, "Failed to resolve!", e);
callback.onResult(Optional.absent());
}
});
}
/**
* Handles multiple URIs that are all assumed to be external images/videos.
*/
void getResolved(@NonNull List<Uri> uris, @NonNull Callback<Optional<ShareData>> callback) {
SignalExecutors.BOUNDED.execute(() -> {
try {
callback.onResult(Optional.fromNullable(getResolvedInternal(uris)));
} catch (IOException e) {
Log.w(TAG, "Failed to resolve!", e);
callback.onResult(Optional.absent());
}
});
}
@WorkerThread
private @NonNull ShareData getResolvedInternal(@Nullable Uri uri, @Nullable String mimeType) throws IOException {
Context context = ApplicationDependencies.getApplication();
if (uri == null) {
return ShareData.forPrimitiveTypes();
}
if (mimeType == null) {
mimeType = context.getContentResolver().getType(uri);
}
if (PartAuthority.isLocalUri(uri) && mimeType != null) {
return ShareData.forIntentData(uri, mimeType, false);
} else {
InputStream stream = context.getContentResolver().openInputStream(uri);
if (stream == null) {
throw new IOException("Failed to open stream!");
}
long size = getSize(context, uri);
String fileName = getFileName(context, uri);
String fillMimeType = Optional.fromNullable(mimeType).or(MediaUtil.UNKNOWN);
Uri blobUri;
if (MediaUtil.isImageType(fillMimeType) || MediaUtil.isVideoType(fillMimeType)) {
blobUri = BlobProvider.getInstance()
.forData(stream, size)
.withMimeType(fillMimeType)
.withFileName(fileName)
.createForSingleSessionOnDisk(context);
} else {
blobUri = BlobProvider.getInstance()
.forData(stream, size)
.withMimeType(fillMimeType)
.withFileName(fileName)
.createForMultipleSessionsOnDisk(context);
}
return ShareData.forIntentData(blobUri, fillMimeType, true);
}
}
@WorkerThread
private @Nullable
ShareData getResolvedInternal(@NonNull List<Uri> uris) throws IOException {
Context context = ApplicationDependencies.getApplication();
ContentResolver resolver = context.getContentResolver();
Map<Uri, String> mimeTypes = Stream.of(uris)
.map(uri -> new Pair<>(uri, Optional.fromNullable(resolver.getType(uri)).or(MediaUtil.UNKNOWN)))
.filter(p -> MediaUtil.isImageType(p.second) || MediaUtil.isVideoType(p.second))
.collect(Collectors.toMap(p -> p.first, p -> p.second));
if (mimeTypes.isEmpty()) {
return null;
}
List<Media> media = new ArrayList<>(mimeTypes.size());
for (Map.Entry<Uri, String> entry : mimeTypes.entrySet()) {
Uri uri = entry.getKey();
String mimeType = entry.getValue();
InputStream stream;
try {
stream = context.getContentResolver().openInputStream(uri);
if (stream == null) {
throw new IOException("Failed to open stream!");
}
} catch (IOException e) {
Log.w(TAG, "Failed to open: " + uri);
continue;
}
long size = getSize(context, uri);
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, mimeType, uri);
long duration = getDuration(context, uri);
Uri blobUri = BlobProvider.getInstance()
.forData(stream, size)
.withMimeType(mimeType)
.createForSingleSessionOnDisk(context);
media.add(new Media(blobUri,
mimeType,
System.currentTimeMillis(),
dimens.first,
dimens.second,
size,
duration,
Optional.of(Media.ALL_MEDIA_BUCKET_ID),
Optional.absent(),
Optional.absent()));
if (media.size() >= MediaSendConstants.MAX_PUSH) {
Log.w(TAG, "Exceeded the attachment limit! Skipping the rest.");
break;
}
}
if (media.size() > 0) {
return ShareData.forMedia(media);
} else {
return null;
}
}
private static long getSize(@NonNull Context context, @NonNull Uri uri) throws IOException {
long size = 0;
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst() && cursor.getColumnIndex(OpenableColumns.SIZE) >= 0) {
size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
}
}
if (size <= 0) {
size = MediaUtil.getMediaSize(context, uri);
}
return size;
}
private static @NonNull String getFileName(@NonNull Context context, @NonNull Uri uri) {
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst() && cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) >= 0) {
return cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
}
}
return "";
}
private static long getDuration(@NonNull Context context, @NonNull Uri uri) {
return 0;
}
interface Callback<E> {
void onResult(@NonNull E result);
}
}

View File

@ -0,0 +1,80 @@
package org.thoughtcrime.securesms.sharing;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.List;
public class ShareViewModel extends ViewModel {
private static final String TAG = Log.tag(ShareViewModel.class);
private final Context context;
private final ShareRepository shareRepository;
private final MutableLiveData<Optional<ShareData>> shareData;
private boolean mediaUsed;
private boolean externalShare;
private ShareViewModel() {
this.context = ApplicationDependencies.getApplication();
this.shareRepository = new ShareRepository();
this.shareData = new MutableLiveData<>();
}
void onSingleMediaShared(@NonNull Uri uri, @Nullable String mimeType) {
externalShare = true;
shareRepository.getResolved(uri, mimeType, shareData::postValue);
}
void onMultipleMediaShared(@NonNull List<Uri> uris) {
externalShare = true;
shareRepository.getResolved(uris, shareData::postValue);
}
void onNonExternalShare() {
externalShare = false;
}
void onSuccessulShare() {
mediaUsed = true;
}
@NonNull LiveData<Optional<ShareData>> getShareData() {
return shareData;
}
boolean isExternalShare() {
return externalShare;
}
@Override
protected void onCleared() {
ShareData data = shareData.getValue() != null ? shareData.getValue().orNull() : null;
if (data != null && data.isExternal() && data.isForIntent() && !mediaUsed) {
Log.i(TAG, "Clearing out unused data.");
BlobProvider.getInstance().delete(context, data.getUri());
}
}
public static class Factory extends ViewModelProvider.NewInstanceFactory {
@Override
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ShareViewModel());
}
}
}

View File

@ -46,8 +46,9 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.AttachmentCopyJob;
import org.thoughtcrime.securesms.jobs.AttachmentCompressionJob;
import org.thoughtcrime.securesms.jobs.AttachmentCopyJob;
import org.thoughtcrime.securesms.jobs.AttachmentMarkUploadedJob;
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob;
import org.thoughtcrime.securesms.jobs.MmsSendJob;
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
@ -437,15 +438,22 @@ public class MessageSender {
private static void sendLocalMediaSelf(Context context, long messageId) {
try {
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
OutgoingMediaMessage message = mmsDatabase.getOutgoingMessage(messageId);
SyncMessageId syncId = new SyncMessageId(Recipient.self().getId(), message.getSentTimeMillis());
for (Attachment attachment : message.getAttachments()) {
attachmentDatabase.markAttachmentUploaded(messageId, attachment);
}
List<AttachmentCompressionJob> compressionJobs = Stream.of(message.getAttachments())
.map(a -> AttachmentCompressionJob.fromAttachment((DatabaseAttachment) a, false, -1))
.toList();
List<AttachmentMarkUploadedJob> fakeUploadJobs = Stream.of(message.getAttachments())
.map(a -> new AttachmentMarkUploadedJob(messageId, ((DatabaseAttachment) a).getAttachmentId()))
.toList();
ApplicationDependencies.getJobManager().startChain(compressionJobs)
.then(fakeUploadJobs)
.enqueue();
mmsDatabase.markAsSent(messageId, true);
mmsDatabase.markUnidentified(messageId, true);

View File

@ -2,6 +2,11 @@ package org.thoughtcrime.securesms.stickers;
import androidx.annotation.NonNull;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
@ -10,12 +15,16 @@ import java.util.Set;
*/
public final class BlessedPacks {
public static final Pack ZOZO = new Pack("fb535407d2f6497ec074df8b9c51dd1d", "17e971c134035622781d2ee249e6473b774583750b68c11bb82b7509c68b6dfd");
public static final Pack BANDIT = new Pack("9acc9e8aba563d26a4994e69263e3b25", "5a6dff3948c28efb9b7aaf93ecc375c69fc316e78077ed26867a14d10a0f6a12");
public static final Pack ZOZO = new Pack("fb535407d2f6497ec074df8b9c51dd1d", "17e971c134035622781d2ee249e6473b774583750b68c11bb82b7509c68b6dfd");
public static final Pack BANDIT = new Pack("9acc9e8aba563d26a4994e69263e3b25", "5a6dff3948c28efb9b7aaf93ecc375c69fc316e78077ed26867a14d10a0f6a12");
public static final Pack SWOON_HANDS = new Pack("e61fa0867031597467ccc036cc65d403", "13ae7b1a7407318280e9b38c1261ded38e0e7138b9f964a6ccbb73e40f737a9b");
public static final Pack SWOON_FACES = new Pack("cca32f5b905208b7d0f1e17f23fdc185", "8bf8e95f7a45bdeafe0c8f5b002ef01ab95b8f1b5baac4019ccd6b6be0b1837a");
private static final Set<String> BLESSED_PACK_IDS = new HashSet<String>() {{
add(ZOZO.getPackId());
add(BANDIT.getPackId());
add(SWOON_HANDS.getPackId());
add(SWOON_FACES.getPackId());
}};
public static boolean contains(@NonNull String packId) {
@ -23,10 +32,12 @@ public final class BlessedPacks {
}
public static class Pack {
private final String packId;
private final String packKey;
@JsonProperty private final String packId;
@JsonProperty private final String packKey;
public Pack(@NonNull String packId, @NonNull String packKey) {
public Pack(@NonNull @JsonProperty("packId") String packId,
@NonNull @JsonProperty("packKey") String packKey)
{
this.packId = packId;
this.packKey = packKey;
}
@ -38,5 +49,17 @@ public final class BlessedPacks {
public @NonNull String getPackKey() {
return packKey;
}
public @NonNull String toJson() {
return JsonUtil.toJson(this);
}
public static @NonNull Pack fromJson(@NonNull String json) {
try {
return JsonUtil.fromJson(json, Pack.class);
} catch (IOException e) {
throw new AssertionError(e);
}
}
}
}

View File

@ -12,8 +12,7 @@ import android.view.MenuItem;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.ShareActivity;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.util.DynamicTheme;

View File

@ -5,8 +5,6 @@ import androidx.lifecycle.ViewModelProviders;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Point;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
@ -19,23 +17,15 @@ import android.widget.Toast;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.ShareActivity;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.stickers.StickerManifest.Sticker;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;

View File

@ -16,4 +16,12 @@ public final class Base64 {
public static @NonNull String encodeBytes(@NonNull byte[] source) {
return org.whispersystems.util.Base64.encodeBytes(source);
}
public static @NonNull byte[] decodeOrThrow(@NonNull String s) {
try {
return org.whispersystems.util.Base64.decode(s);
} catch (IOException e) {
throw new AssertionError();
}
}
}

View File

@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
@ -145,8 +146,8 @@ public class CommunicationActions {
.withPermanentDenialDialog(activity.getString(R.string.ConversationActivity_signal_needs_the_microphone_and_camera_permissions_in_order_to_call_s, recipient.getDisplayName(activity)))
.onAllGranted(() -> {
Intent intent = new Intent(activity, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL);
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_RECIPIENT, recipient.getId());
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(recipient.getId()));
activity.startService(intent);
Intent activityIntent = new Intent(activity, WebRtcCallActivity.class);

View File

@ -1,18 +1,16 @@
package org.thoughtcrime.securesms.util;
import android.app.Activity;
import androidx.annotation.StyleRes;
import org.thoughtcrime.securesms.R;
public class DynamicDarkActionBarTheme extends DynamicTheme {
@Override
protected int getSelectedTheme(Activity activity) {
String theme = TextSecurePreferences.getTheme(activity);
if (theme.equals("dark")) {
return R.style.TextSecure_DarkTheme_Conversation;
}
protected @StyleRes int getLightThemeStyle() {
return R.style.TextSecure_LightTheme_Conversation;
}
protected @StyleRes int getDarkThemeStyle() {
return R.style.TextSecure_DarkTheme_Conversation;
}
}

View File

@ -1,18 +1,16 @@
package org.thoughtcrime.securesms.util;
import android.app.Activity;
import androidx.annotation.StyleRes;
import org.thoughtcrime.securesms.R;
public class DynamicDarkToolbarTheme extends DynamicTheme {
@Override
protected int getSelectedTheme(Activity activity) {
String theme = TextSecurePreferences.getTheme(activity);
if (theme.equals("dark")) {
return R.style.TextSecure_DarkNoActionBar_DarkToolbar;
}
protected @StyleRes int getLightThemeStyle() {
return R.style.TextSecure_LightNoActionBar_DarkToolbar;
}
protected @StyleRes int getDarkThemeStyle() {
return R.style.TextSecure_DarkNoActionBar_DarkToolbar;
}
}

View File

@ -1,16 +1,16 @@
package org.thoughtcrime.securesms.util;
import android.app.Activity;
import androidx.annotation.StyleRes;
import org.thoughtcrime.securesms.R;
public class DynamicIntroTheme extends DynamicTheme {
@Override
protected int getSelectedTheme(Activity activity) {
String theme = TextSecurePreferences.getTheme(activity);
if (theme.equals("dark")) return R.style.TextSecure_DarkIntroTheme;
protected @StyleRes int getLightThemeStyle() {
return R.style.TextSecure_LightIntroTheme;
}
protected @StyleRes int getDarkThemeStyle() {
return R.style.TextSecure_DarkIntroTheme;
}
}

View File

@ -1,16 +1,16 @@
package org.thoughtcrime.securesms.util;
import android.app.Activity;
import androidx.annotation.StyleRes;
import org.thoughtcrime.securesms.R;
public class DynamicNoActionBarInviteTheme extends DynamicTheme {
@Override
protected int getSelectedTheme(Activity activity) {
String theme = TextSecurePreferences.getTheme(activity);
if (theme.equals("dark")) return R.style.Signal_NoActionBar_Invite;
protected @StyleRes int getLightThemeStyle() {
return R.style.Signal_Light_NoActionBar_Invite;
}
protected @StyleRes int getDarkThemeStyle() {
return R.style.Signal_NoActionBar_Invite;
}
}

View File

@ -1,16 +1,16 @@
package org.thoughtcrime.securesms.util;
import android.app.Activity;
import androidx.annotation.StyleRes;
import org.thoughtcrime.securesms.R;
public class DynamicNoActionBarTheme extends DynamicTheme {
@Override
protected int getSelectedTheme(Activity activity) {
String theme = TextSecurePreferences.getTheme(activity);
if (theme.equals("dark")) return R.style.TextSecure_DarkNoActionBar;
protected @StyleRes int getLightThemeStyle() {
return R.style.TextSecure_LightNoActionBar;
}
protected @StyleRes int getDarkThemeStyle() {
return R.style.TextSecure_DarkNoActionBar;
}
}

View File

@ -1,16 +1,16 @@
package org.thoughtcrime.securesms.util;
import android.app.Activity;
import androidx.annotation.StyleRes;
import org.thoughtcrime.securesms.R;
public class DynamicRegistrationTheme extends DynamicTheme {
@Override
protected int getSelectedTheme(Activity activity) {
String theme = TextSecurePreferences.getTheme(activity);
if (theme.equals("dark")) return R.style.TextSecure_DarkRegistrationTheme;
protected @StyleRes int getLightThemeStyle() {
return R.style.TextSecure_LightRegistrationTheme;
}
protected @StyleRes int getDarkThemeStyle() {
return R.style.TextSecure_DarkRegistrationTheme;
}
}

View File

@ -3,15 +3,19 @@ package org.thoughtcrime.securesms.util;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.StyleRes;
import androidx.appcompat.app.AppCompatDelegate;
import org.thoughtcrime.securesms.R;
public class DynamicTheme {
public static final String DARK = "dark";
public static final String LIGHT = "light";
public static final String DARK = "dark";
public static final String LIGHT = "light";
public static final String SYSTEM = "system";
private int currentTheme;
@ -30,16 +34,38 @@ public class DynamicTheme {
}
}
protected int getSelectedTheme(Activity activity) {
private @StyleRes int getSelectedTheme(Activity activity) {
String theme = TextSecurePreferences.getTheme(activity);
if (theme.equals(DARK)) {
return R.style.TextSecure_DarkTheme;
if (theme.equals(SYSTEM) && systemThemeAvailable()) {
if (isSystemInDarkTheme(activity)) {
return getDarkThemeStyle();
} else {
return getLightThemeStyle();
}
} else if (theme.equals(DARK)) {
return getDarkThemeStyle();
} else {
return getLightThemeStyle();
}
}
protected @StyleRes int getLightThemeStyle() {
return R.style.TextSecure_LightTheme;
}
protected @StyleRes int getDarkThemeStyle() {
return R.style.TextSecure_DarkTheme;
}
public static boolean systemThemeAvailable() {
return Build.VERSION.SDK_INT >= 29;
}
private static boolean isSystemInDarkTheme(@NonNull Activity activity) {
return (activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
}
private static final class OverridePendingTransition {
static void invoke(Activity activity) {
activity.overridePendingTransition(0, 0);

View File

@ -53,14 +53,19 @@ public final class FeatureFlags {
private static final String STORAGE_SERVICE = generateKey("storageService");
private static final String PINS_FOR_ALL = generateKey("pinsForAll");
private static final String PINS_MEGAPHONE_KILL_SWITCH = generateKey("pinsMegaphoneKillSwitch");
private static final String PROFILE_NAMES_MEGAPHONE = generateKey("profileNamesMegaphone");
private static final String VIDEO_TRIMMING = generateKey("videoTrimming");
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
* remotely, place it in here.
*/
private static final Set<String> REMOTE_CAPABLE = Sets.newHashSet(
VIDEO_TRIMMING,
PINS_FOR_ALL,
PINS_MEGAPHONE_KILL_SWITCH
PINS_MEGAPHONE_KILL_SWITCH,
PROFILE_NAMES_MEGAPHONE
);
/**
@ -81,6 +86,7 @@ public final class FeatureFlags {
* more burden on the reader to ensure that the app experience remains consistent.
*/
private static final Set<String> HOT_SWAPPABLE = Sets.newHashSet(
VIDEO_TRIMMING,
PINS_MEGAPHONE_KILL_SWITCH
);
@ -165,6 +171,17 @@ public final class FeatureFlags {
return getValue(PINS_MEGAPHONE_KILL_SWITCH, false);
}
/** Safety switch for disabling profile names megaphone */
public static boolean profileNamesMegaphoneEnabled() {
return getValue(PROFILE_NAMES_MEGAPHONE, false) &&
TextSecurePreferences.getFirstInstallVersion(ApplicationDependencies.getApplication()) < 600;
}
/** Allow trimming videos. */
public static boolean videoTrimming() {
return getValue(VIDEO_TRIMMING, false);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Boolean> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);
@ -303,4 +320,7 @@ public final class FeatureFlags {
return disk;
}
}
/** Read and write versioned profile information. */
public static final boolean VERSIONED_PROFILES = org.whispersystems.signalservice.FeatureFlags.VERSIONED_PROFILES;
}

View File

@ -58,6 +58,7 @@ public class MediaUtil {
public static final String VCARD = "text/x-vcard";
public static final String LONG_TEXT = "text/x-signal-plain";
public static final String VIEW_ONCE = "application/x-signal-view-once";
public static final String UNKNOWN = "*/*";
public static SlideType getSlideTypeFromContentType(@NonNull String contentType) {
if (isGif(contentType)) {
@ -300,7 +301,7 @@ public class MediaUtil {
}
@WorkerThread
public static @Nullable Bitmap getVideoThumbnail(Context context, Uri uri) {
public static @Nullable Bitmap getVideoThumbnail(Context context, Uri uri, long timeUs) {
if ("com.android.providers.media.documents".equals(uri.getAuthority())) {
long videoId = Long.parseLong(uri.getLastPathSegment().split(":")[1]);
@ -327,7 +328,7 @@ public class MediaUtil {
MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
MediaMetadataRetrieverUtil.setDataSource(mediaMetadataRetriever, mediaDataSource);
return mediaMetadataRetriever.getFrameAtTime(1000);
return mediaMetadataRetriever.getFrameAtTime(timeUs);
} catch (IOException e) {
Log.w(TAG, "failed to get thumbnail for video blob uri: " + uri, e);
return null;

View File

@ -6,6 +6,9 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
@ -19,6 +22,7 @@ import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
@ -28,22 +32,30 @@ import java.io.IOException;
/**
* Aids in the retrieval and decryption of profiles.
*/
public class ProfileUtil {
public final class ProfileUtil {
private ProfileUtil() {
}
private static final String TAG = Log.tag(ProfileUtil.class);
@WorkerThread
public static SignalServiceProfile retrieveProfile(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
public static @NonNull ProfileAndCredential retrieveProfile(@NonNull Context context,
@NonNull Recipient recipient,
@NonNull SignalServiceProfile.RequestType requestType)
throws IOException
{
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient);
Optional<UnidentifiedAccess> unidentifiedAccess = getUnidentifiedAccess(context, recipient);
Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey());
SignalServiceProfile profile;
ProfileAndCredential profile;
try {
profile = retrieveProfileInternal(address, unidentifiedAccess);
profile = retrieveProfileInternal(address, profileKey, unidentifiedAccess, requestType);
} catch (NonSuccessfulResponseCodeException e) {
if (unidentifiedAccess.isPresent()) {
profile = retrieveProfileInternal(address, Optional.absent());
profile = retrieveProfileInternal(address, profileKey, Optional.absent(), requestType);
} else {
throw e;
}
@ -52,7 +64,7 @@ public class ProfileUtil {
return profile;
}
public static @Nullable String decryptName(@NonNull byte[] profileKey, @Nullable String encryptedName)
public static @Nullable String decryptName(@NonNull ProfileKey profileKey, @Nullable String encryptedName)
throws InvalidCiphertextException, IOException
{
if (encryptedName == null) {
@ -64,8 +76,11 @@ public class ProfileUtil {
}
@WorkerThread
private static SignalServiceProfile retrieveProfileInternal(@NonNull SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess)
throws IOException
private static @NonNull ProfileAndCredential retrieveProfileInternal(@NonNull SignalServiceAddress address,
@NonNull Optional<ProfileKey> profileKey,
@NonNull Optional<UnidentifiedAccess> unidentifiedAccess,
@NonNull SignalServiceProfile.RequestType requestType)
throws IOException
{
SignalServiceMessagePipe authPipe = IncomingMessageObserver.getPipe();
SignalServiceMessagePipe unidentifiedPipe = IncomingMessageObserver.getUnidentifiedPipe();
@ -74,14 +89,18 @@ public class ProfileUtil {
if (pipe != null) {
try {
return pipe.getProfile(address, unidentifiedAccess);
return pipe.getProfile(address, profileKey, unidentifiedAccess, requestType);
} catch (IOException e) {
Log.w(TAG, e);
Log.w(TAG, "Websocket request failed. Falling back to REST.", e);
}
}
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver();
return receiver.retrieveProfile(address, unidentifiedAccess);
try {
return receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType);
} catch (VerificationFailedException e) {
throw new IOException("Verification Problem", e);
}
}
private static Optional<UnidentifiedAccess> getUnidentifiedAccess(@NonNull Context context, @NonNull Recipient recipient) {

View File

@ -5,6 +5,8 @@ import android.app.ActivityManager;
import android.app.AlarmManager;
import android.app.NotificationManager;
import android.app.job.JobScheduler;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.hardware.display.DisplayManager;
import android.location.LocationManager;
@ -69,6 +71,10 @@ public class ServiceUtil {
return (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
}
public static ClipboardManager getClipboardManager(@NonNull Context context) {
return (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
}
@RequiresApi(26)
public static JobScheduler getJobScheduler(Context context) {
return (JobScheduler) context.getSystemService(JobScheduler.class);

View File

@ -7,8 +7,6 @@ import androidx.annotation.NonNull;
import net.sqlcipher.database.SQLiteDatabase;
import org.whispersystems.libsignal.util.Pair;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -46,9 +44,9 @@ public final class SqlUtil {
* change. In other words, if {@link SQLiteDatabase#update(String, ContentValues, String, String[])}
* returns > 0, then you know something *actually* changed.
*/
public static @NonNull Pair<String, String[]> buildTrueUpdateQuery(@NonNull String selection,
@NonNull String[] args,
@NonNull ContentValues contentValues)
public static @NonNull UpdateQuery buildTrueUpdateQuery(@NonNull String selection,
@NonNull String[] args,
@NonNull ContentValues contentValues)
{
StringBuilder qualifier = new StringBuilder();
Set<Map.Entry<String, Object>> valueSet = contentValues.valueSet();
@ -73,6 +71,24 @@ public final class SqlUtil {
i++;
}
return new Pair<>("(" + selection + ") AND (" + qualifier + ")", fullArgs.toArray(new String[0]));
return new UpdateQuery("(" + selection + ") AND (" + qualifier + ")", fullArgs.toArray(new String[0]));
}
public static class UpdateQuery {
private final String where;
private final String[] whereArgs;
private UpdateQuery(@NonNull String where, @NonNull String[] whereArgs) {
this.where = where;
this.whereArgs = whereArgs;
}
public String getWhere() {
return where;
}
public String[] getWhereArgs() {
return whereArgs;
}
}
}

View File

@ -123,7 +123,6 @@ public class TextSecurePreferences {
private static final String MULTI_DEVICE_PROVISIONED_PREF = "pref_multi_device";
public static final String DIRECT_CAPTURE_CAMERA_ID = "pref_direct_capture_camera_id";
private static final String ALWAYS_RELAY_CALLS_PREF = "pref_turn_only";
private static final String PROFILE_KEY_PREF = "pref_profile_key";
private static final String PROFILE_NAME_PREF = "pref_profile_name";
private static final String PROFILE_AVATAR_ID_PREF = "pref_profile_avatar_id";
public static final String READ_RECEIPTS_PREF = "pref_read_receipts";
@ -435,14 +434,6 @@ public class TextSecurePreferences {
setBooleanPreference(context, GIF_GRID_LAYOUT, isGrid);
}
public static @Nullable String getProfileKey(Context context) {
return getStringPreference(context, PROFILE_KEY_PREF, null);
}
public static void setProfileKey(Context context, String key) {
setStringPreference(context, PROFILE_KEY_PREF, key);
}
public static void setProfileName(Context context, ProfileName name) {
setStringPreference(context, PROFILE_NAME_PREF, name.serialize());
}
@ -895,7 +886,7 @@ public class TextSecurePreferences {
}
public static String getTheme(Context context) {
return getStringPreference(context, THEME_PREF, "light");
return getStringPreference(context, THEME_PREF, DynamicTheme.systemThemeAvailable() ? DynamicTheme.SYSTEM : DynamicTheme.LIGHT);
}
public static boolean isVerifying(Context context) {

Some files were not shown because too many files have changed in this diff Show More