Copione merged onto master
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
commit
dd487c7329
|
@ -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
|
||||
|
|
74
BUILDING.md
74
BUILDING.md
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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())),
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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!");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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())) {
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package org.thoughtcrime.securesms.ringrtc;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface CameraEventListener {
|
||||
void onCameraSwitchCompleted(@NonNull CameraState newCameraState);
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue