Copione merged onto master
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
commit
0926a0a2f8
|
@ -1,6 +1,11 @@
|
|||
name: Android CI
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- '4.**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
|
@ -80,8 +80,8 @@ protobuf {
|
|||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 613
|
||||
def canonicalVersionName = "4.57.2"
|
||||
def canonicalVersionCode = 614
|
||||
def canonicalVersionName = "4.58.0"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['universal' : 0,
|
||||
|
@ -160,6 +160,7 @@ android {
|
|||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/NOTICE'
|
||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||
exclude 'lib/*/libzkgroup.so' // TODO: GV2 Remove line to include .so when used
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
@ -268,8 +269,10 @@ dependencies {
|
|||
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
|
||||
implementation "androidx.camera:camera-core:1.0.0-alpha06"
|
||||
implementation "androidx.camera:camera-camera2:1.0.0-alpha06"
|
||||
implementation "androidx.camera:camera-core:1.0.0-beta01"
|
||||
implementation "androidx.camera:camera-camera2:1.0.0-beta01"
|
||||
implementation "androidx.camera:camera-lifecycle:1.0.0-beta01"
|
||||
implementation "androidx.concurrent:concurrent-futures:1.0.0"
|
||||
|
||||
implementation('com.google.firebase:firebase-messaging:17.3.4') {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
|
@ -287,10 +290,11 @@ dependencies {
|
|||
implementation 'org.signal:aesgcmprovider:0.0.3'
|
||||
|
||||
implementation project(':libsignal-service')
|
||||
implementation 'org.signal:zkgroup-android:0.4.1'
|
||||
|
||||
implementation 'org.signal:argon2:13.1@aar'
|
||||
|
||||
implementation 'org.signal:ringrtc-android:1.0.2'
|
||||
implementation 'org.signal:ringrtc-android:1.1.0'
|
||||
|
||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
|
@ -350,7 +354,9 @@ dependencies {
|
|||
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
|
||||
|
||||
testImplementation 'androidx.test:core:1.2.0'
|
||||
testImplementation 'org.robolectric:robolectric:4.2'
|
||||
testImplementation ('org.robolectric:robolectric:4.2') {
|
||||
exclude group: 'com.google.protobuf', module: 'protobuf-java'
|
||||
}
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.thoughtcrime.securesms">
|
||||
|
||||
<uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2"/>
|
||||
<uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2,androidx.camera.lifecycle" />
|
||||
|
||||
<permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"
|
||||
android:label="Access to TextSecure Secrets"
|
||||
|
@ -261,12 +261,7 @@
|
|||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ExperienceUpgradeActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".PassphraseCreateActivity"
|
||||
<activity android:name=".PassphraseCreateActivity"
|
||||
android:label="@string/AndroidManifest__create_passphrase"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
|
@ -690,17 +685,6 @@
|
|||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".ExperienceUpgradeActivity$AppUpgradeReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
|
||||
<data android:scheme="package" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.ExperienceUpgradeActivity.DISMISS_ACTION"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".service.PanicResponderListener"
|
||||
android:exported="true">
|
||||
|
|
|
@ -19,7 +19,7 @@ package org.thoughtcrime.securesms;
|
|||
import android.annotation.SuppressLint;
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.camera.camera2.Camera2AppConfig;
|
||||
import androidx.camera.camera2.Camera2Config;
|
||||
import androidx.camera.core.CameraX;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
@ -71,6 +71,7 @@ import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
|
|||
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
@ -136,7 +137,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||
FeatureFlags.init();
|
||||
NotificationChannels.create(this);
|
||||
RefreshPreKeysJob.scheduleIfNecessary();
|
||||
StorageSyncJob.scheduleIfNecessary();
|
||||
StorageSyncHelper.scheduleRoutineSync();
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
||||
|
||||
if (Build.VERSION.SDK_INT < 21) {
|
||||
|
@ -385,7 +386,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||
if (CameraXUtil.isSupported()) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
CameraX.init(this, Camera2AppConfig.create(this));
|
||||
CameraX.initialize(this, Camera2Config.defaultConfig());
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "Failed to initialize CameraX.");
|
||||
}
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class BasicIntroFragment extends Fragment {
|
||||
|
||||
private static final String ARG_DRAWABLE = "drawable";
|
||||
private static final String ARG_TEXT = "text";
|
||||
private static final String ARG_SUBTEXT = "subtext";
|
||||
|
||||
private int drawable;
|
||||
private int text;
|
||||
private int subtext;
|
||||
|
||||
public static BasicIntroFragment newInstance(int drawable, int text, int subtext) {
|
||||
BasicIntroFragment fragment = new BasicIntroFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putInt(ARG_DRAWABLE, drawable);
|
||||
args.putInt(ARG_TEXT, text);
|
||||
args.putInt(ARG_SUBTEXT, subtext);
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public BasicIntroFragment() {}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (getArguments() != null) {
|
||||
drawable = getArguments().getInt(ARG_DRAWABLE);
|
||||
text = getArguments().getInt(ARG_TEXT);
|
||||
subtext = getArguments().getInt(ARG_SUBTEXT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View v = inflater.inflate(R.layout.color_fragment, container, false);
|
||||
|
||||
((ImageView)v.findViewById(R.id.watermark)).setImageResource(drawable);
|
||||
((TextView)v.findViewById(R.id.blurb)).setText(text);
|
||||
((TextView)v.findViewById(R.id.subblurb)).setText(subtext);
|
||||
|
||||
return v;
|
||||
}
|
||||
}
|
|
@ -1,322 +0,0 @@
|
|||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.melnykov.fab.FloatingActionButton;
|
||||
|
||||
import org.thoughtcrime.securesms.IntroPagerAdapter.IntroPage;
|
||||
import org.thoughtcrime.securesms.experienceupgrades.StickersIntroFragment;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationIds;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ExperienceUpgradeActivity extends BaseActionBarActivity
|
||||
implements TypingIndicatorIntroFragment.Controller,
|
||||
LinkPreviewsIntroFragment.Controller,
|
||||
StickersIntroFragment.Controller
|
||||
{
|
||||
private static final String TAG = ExperienceUpgradeActivity.class.getSimpleName();
|
||||
private static final String DISMISS_ACTION = "org.thoughtcrime.securesms.ExperienceUpgradeActivity.DISMISS_ACTION";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
|
||||
private enum ExperienceUpgrade {
|
||||
SIGNAL_REBRANDING(157,
|
||||
new IntroPage(0xFF2090EA,
|
||||
BasicIntroFragment.newInstance(R.drawable.splash_logo,
|
||||
R.string.ExperienceUpgradeActivity_welcome_to_signal_dgaf,
|
||||
R.string.ExperienceUpgradeActivity_textsecure_is_now_called_signal)),
|
||||
R.string.ExperienceUpgradeActivity_welcome_to_signal_excited,
|
||||
R.string.ExperienceUpgradeActivity_textsecure_is_now_signal,
|
||||
R.string.ExperienceUpgradeActivity_textsecure_is_now_signal_long,
|
||||
null,
|
||||
false),
|
||||
VIDEO_CALLS(245,
|
||||
new IntroPage(0xFF2090EA,
|
||||
BasicIntroFragment.newInstance(R.drawable.video_splash,
|
||||
R.string.ExperienceUpgradeActivity_say_hello_to_video_calls,
|
||||
R.string.ExperienceUpgradeActivity_signal_now_supports_secure_video_calls)),
|
||||
R.string.ExperienceUpgradeActivity_say_hello_to_video_calls,
|
||||
R.string.ExperienceUpgradeActivity_signal_now_supports_secure_video_calling,
|
||||
R.string.ExperienceUpgradeActivity_signal_now_supports_secure_video_calling_long,
|
||||
null,
|
||||
false),
|
||||
PROFILES(286,
|
||||
new IntroPage(0xFF2090EA,
|
||||
BasicIntroFragment.newInstance(R.drawable.profile_splash,
|
||||
R.string.ExperienceUpgradeActivity_ready_for_your_closeup,
|
||||
R.string.ExperienceUpgradeActivity_now_you_can_share_a_profile_photo_and_name_with_friends_on_signal)),
|
||||
R.string.ExperienceUpgradeActivity_signal_profiles_are_here,
|
||||
R.string.ExperienceUpgradeActivity_now_you_can_share_a_profile_photo_and_name_with_friends_on_signal,
|
||||
R.string.ExperienceUpgradeActivity_now_you_can_share_a_profile_photo_and_name_with_friends_on_signal,
|
||||
EditProfileActivity.class,
|
||||
false),
|
||||
READ_RECEIPTS(299,
|
||||
new IntroPage(0xFF2090EA,
|
||||
ReadReceiptsIntroFragment.newInstance()),
|
||||
R.string.experience_upgrade_preference_fragment__read_receipts_are_here,
|
||||
R.string.experience_upgrade_preference_fragment__optionally_see_and_share_when_messages_have_been_read,
|
||||
R.string.experience_upgrade_preference_fragment__optionally_see_and_share_when_messages_have_been_read,
|
||||
null,
|
||||
false),
|
||||
TYPING_INDICATORS(432,
|
||||
new IntroPage(0xFF2090EA,
|
||||
TypingIndicatorIntroFragment.newInstance()),
|
||||
R.string.ExperienceUpgradeActivity_introducing_typing_indicators,
|
||||
R.string.ExperienceUpgradeActivity_now_you_can_optionally_see_and_share_when_messages_are_being_typed,
|
||||
R.string.ExperienceUpgradeActivity_now_you_can_optionally_see_and_share_when_messages_are_being_typed,
|
||||
null,
|
||||
true),
|
||||
LINK_PREVIEWS(449,
|
||||
new IntroPage(0xFF2090EA, LinkPreviewsIntroFragment.newInstance()),
|
||||
R.string.ExperienceUpgradeActivity_introducing_link_previews,
|
||||
R.string.ExperienceUpgradeActivity_optional_link_previews_are_now_supported,
|
||||
R.string.ExperienceUpgradeActivity_optional_link_previews_are_now_supported,
|
||||
null,
|
||||
true),
|
||||
STICKERS(580,
|
||||
new IntroPage(0xFF2090EA, StickersIntroFragment.newInstance()),
|
||||
R.string.ExperienceUpgradeActivity_introducing_stickers,
|
||||
R.string.ExperienceUpgradeActivity_why_use_words_when_you_can_use_stickers,
|
||||
R.string.ExperienceUpgradeActivity_why_use_words_when_you_can_use_stickers,
|
||||
null,
|
||||
true);
|
||||
|
||||
private int version;
|
||||
private List<IntroPage> pages;
|
||||
private @StringRes int notificationTitle;
|
||||
private @StringRes int notificationText;
|
||||
private @StringRes int notificationBigText;
|
||||
private @Nullable Class nextIntent;
|
||||
private boolean handlesNavigation;
|
||||
|
||||
ExperienceUpgrade(int version,
|
||||
@NonNull List<IntroPage> pages,
|
||||
@StringRes int notificationTitle,
|
||||
@StringRes int notificationText,
|
||||
@StringRes int notificationBigText,
|
||||
@Nullable Class nextIntent,
|
||||
boolean handlesNavigation)
|
||||
{
|
||||
this.version = version;
|
||||
this.pages = pages;
|
||||
this.notificationTitle = notificationTitle;
|
||||
this.notificationText = notificationText;
|
||||
this.notificationBigText = notificationBigText;
|
||||
this.nextIntent = nextIntent;
|
||||
this.handlesNavigation = handlesNavigation;
|
||||
}
|
||||
|
||||
ExperienceUpgrade(int version,
|
||||
@NonNull IntroPage page,
|
||||
@StringRes int notificationTitle,
|
||||
@StringRes int notificationText,
|
||||
@StringRes int notificationBigText,
|
||||
@Nullable Class nextIntent,
|
||||
boolean handlesNavigation)
|
||||
{
|
||||
this(version, Collections.singletonList(page), notificationTitle, notificationText, notificationBigText, nextIntent, handlesNavigation);
|
||||
}
|
||||
|
||||
public int getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public List<IntroPage> getPages() {
|
||||
return pages;
|
||||
}
|
||||
|
||||
public IntroPage getPage(int i) {
|
||||
return pages.get(i);
|
||||
}
|
||||
|
||||
public int getNotificationTitle() {
|
||||
return notificationTitle;
|
||||
}
|
||||
|
||||
public int getNotificationText() {
|
||||
return notificationText;
|
||||
}
|
||||
|
||||
public int getNotificationBigText() {
|
||||
return notificationBigText;
|
||||
}
|
||||
|
||||
public boolean handlesNavigation() {
|
||||
return handlesNavigation;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
dynamicTheme.onCreate(this);
|
||||
|
||||
final Optional<ExperienceUpgrade> upgrade = getExperienceUpgrade(this);
|
||||
if (!upgrade.isPresent()) {
|
||||
onContinue(upgrade);
|
||||
return;
|
||||
}
|
||||
|
||||
setContentView(R.layout.experience_upgrade_activity);
|
||||
final ViewPager pager = ViewUtil.findById(this, R.id.pager);
|
||||
final FloatingActionButton fab = ViewUtil.findById(this, R.id.fab);
|
||||
|
||||
pager.setAdapter(new IntroPagerAdapter(getSupportFragmentManager(), upgrade.get().getPages()));
|
||||
|
||||
if (upgrade.get().handlesNavigation()) {
|
||||
fab.setVisibility(View.GONE);
|
||||
} else {
|
||||
fab.setVisibility(View.VISIBLE);
|
||||
fab.setOnClickListener(v -> onContinue(upgrade));
|
||||
}
|
||||
|
||||
getWindow().setBackgroundDrawable(new ColorDrawable(upgrade.get().getPage(0).backgroundColor));
|
||||
ServiceUtil.getNotificationManager(this).cancel(NotificationIds.EXPERIENCE_UPGRADE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
private void onContinue(Optional<ExperienceUpgrade> seenUpgrade) {
|
||||
ServiceUtil.getNotificationManager(this).cancel(NotificationIds.EXPERIENCE_UPGRADE);
|
||||
int latestVersion = seenUpgrade.isPresent() ? seenUpgrade.get().getVersion()
|
||||
: Util.getCanonicalVersionCode();
|
||||
TextSecurePreferences.setLastExperienceVersionCode(this, latestVersion);
|
||||
if (seenUpgrade.isPresent() && seenUpgrade.get().nextIntent != null) {
|
||||
Intent intent = new Intent(this, seenUpgrade.get().nextIntent);
|
||||
// TODO [greyson] Navigation
|
||||
Intent nextIntent = new Intent(this, MainActivity.class);
|
||||
intent.putExtra("next_intent", nextIntent);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
startActivity(getIntent().getParcelableExtra("next_intent"));
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
public static boolean isUpdate(Context context) {
|
||||
return getExperienceUpgrade(context).isPresent();
|
||||
}
|
||||
|
||||
public static Optional<ExperienceUpgrade> getExperienceUpgrade(Context context) {
|
||||
final int currentVersionCode = Util.getCanonicalVersionCode();
|
||||
final int lastSeenVersion = TextSecurePreferences.getLastExperienceVersionCode(context);
|
||||
Log.i(TAG, "getExperienceUpgrade(" + lastSeenVersion + ")");
|
||||
|
||||
if (lastSeenVersion >= currentVersionCode) {
|
||||
TextSecurePreferences.setLastExperienceVersionCode(context, currentVersionCode);
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
Optional<ExperienceUpgrade> eligibleUpgrade = Optional.absent();
|
||||
for (ExperienceUpgrade upgrade : ExperienceUpgrade.values()) {
|
||||
if (lastSeenVersion < upgrade.getVersion()) eligibleUpgrade = Optional.of(upgrade);
|
||||
}
|
||||
|
||||
return eligibleUpgrade;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTypingIndicatorsFinished() {
|
||||
onContinue(Optional.of(ExperienceUpgrade.TYPING_INDICATORS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLinkPreviewsFinished() {
|
||||
onContinue(Optional.of(ExperienceUpgrade.LINK_PREVIEWS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStickersFinished() {
|
||||
onContinue(Optional.of(ExperienceUpgrade.STICKERS));
|
||||
}
|
||||
|
||||
public static class AppUpgradeReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction()) &&
|
||||
intent.getData().getSchemeSpecificPart().equals(context.getPackageName()))
|
||||
{
|
||||
if (TextSecurePreferences.getLastExperienceVersionCode(context) < 339 &&
|
||||
!TextSecurePreferences.isPasswordDisabled(context))
|
||||
{
|
||||
Notification notification = new NotificationCompat.Builder(context, NotificationChannels.OTHER)
|
||||
.setSmallIcon(R.drawable.icon_notification)
|
||||
.setColor(context.getResources().getColor(R.color.signal_primary))
|
||||
.setContentTitle(context.getString(R.string.ExperienceUpgradeActivity_unlock_to_complete_update))
|
||||
.setContentText(context.getString(R.string.ExperienceUpgradeActivity_please_unlock_signal_to_complete_update))
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(R.string.ExperienceUpgradeActivity_please_unlock_signal_to_complete_update)))
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0,
|
||||
context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.build();
|
||||
|
||||
ServiceUtil.getNotificationManager(context).notify(NotificationIds.EXPERIENCE_UPGRADE, notification);
|
||||
}
|
||||
|
||||
Optional<ExperienceUpgrade> experienceUpgrade = getExperienceUpgrade(context);
|
||||
|
||||
if (!experienceUpgrade.isPresent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (experienceUpgrade.get().getVersion() == TextSecurePreferences.getExperienceDismissedVersionCode(context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent targetIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
|
||||
Intent dismissIntent = new Intent(context, AppUpgradeReceiver.class);
|
||||
dismissIntent.setAction(DISMISS_ACTION);
|
||||
|
||||
Notification notification = new NotificationCompat.Builder(context, NotificationChannels.OTHER)
|
||||
.setSmallIcon(R.drawable.icon_notification)
|
||||
.setColor(context.getResources().getColor(R.color.signal_primary))
|
||||
.setContentTitle(context.getString(experienceUpgrade.get().getNotificationTitle()))
|
||||
.setContentText(context.getString(experienceUpgrade.get().getNotificationText()))
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(experienceUpgrade.get().getNotificationBigText())))
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0,
|
||||
targetIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
|
||||
.setDeleteIntent(PendingIntent.getBroadcast(context, 0,
|
||||
dismissIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.build();
|
||||
ServiceUtil.getNotificationManager(context).notify(NotificationIds.EXPERIENCE_UPGRADE, notification);
|
||||
} else if (DISMISS_ACTION.equals(intent.getAction())) {
|
||||
TextSecurePreferences.setExperienceDismissedVersionCode(context, Util.getCanonicalVersionCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -54,6 +54,7 @@ import org.thoughtcrime.securesms.database.GroupDatabase;
|
|||
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
@ -208,7 +209,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||
}
|
||||
|
||||
private void initializeExistingGroup() {
|
||||
final String groupId = getIntent().getStringExtra(GROUP_ID_EXTRA);
|
||||
final GroupId groupId = GroupId.parseNullable(getIntent().getStringExtra(GROUP_ID_EXTRA));
|
||||
|
||||
if (groupId != null) {
|
||||
new FillExistingGroupInfoAsyncTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, groupId);
|
||||
|
@ -361,7 +362,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||
}
|
||||
memberAddresses.add(Recipient.self().getId());
|
||||
|
||||
String groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateGroupForMembers(memberAddresses, true);
|
||||
GroupId groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateGroupForMembers(memberAddresses, true);
|
||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(activity).getOrInsertFromGroupId(groupId);
|
||||
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(activity).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT);
|
||||
|
@ -443,9 +444,9 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||
}
|
||||
|
||||
private static class UpdateSignalGroupTask extends SignalGroupTask {
|
||||
private String groupId;
|
||||
private final GroupId groupId;
|
||||
|
||||
public UpdateSignalGroupTask(GroupCreateActivity activity, String groupId,
|
||||
public UpdateSignalGroupTask(GroupCreateActivity activity, GroupId groupId,
|
||||
Bitmap avatar, String name, Set<Recipient> members)
|
||||
{
|
||||
super(activity, avatar, name, members);
|
||||
|
@ -467,7 +468,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||
if (!activity.isFinishing()) {
|
||||
Intent intent = activity.getIntent();
|
||||
intent.putExtra(GROUP_THREAD_EXTRA, result.get().getThreadId());
|
||||
intent.putExtra(GROUP_ID_EXTRA, result.get().getGroupRecipient().requireGroupId());
|
||||
intent.putExtra(GROUP_ID_EXTRA, result.get().getGroupRecipient().requireGroupId().toString());
|
||||
activity.setResult(RESULT_OK, intent);
|
||||
activity.finish();
|
||||
}
|
||||
|
@ -534,7 +535,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||
}
|
||||
}
|
||||
|
||||
private static class FillExistingGroupInfoAsyncTask extends ProgressDialogAsyncTask<String,Void,Optional<GroupData>> {
|
||||
private static class FillExistingGroupInfoAsyncTask extends ProgressDialogAsyncTask<GroupId, Void, Optional<GroupData>> {
|
||||
private GroupCreateActivity activity;
|
||||
|
||||
public FillExistingGroupInfoAsyncTask(GroupCreateActivity activity) {
|
||||
|
@ -545,7 +546,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
protected Optional<GroupData> doInBackground(String... groupIds) {
|
||||
protected Optional<GroupData> doInBackground(GroupId... groupIds) {
|
||||
final GroupDatabase db = DatabaseFactory.getGroupDatabase(activity);
|
||||
final List<Recipient> recipients = db.getGroupMembers(groupIds[0], false);
|
||||
final Optional<GroupRecord> group = db.getGroup(groupIds[0]);
|
||||
|
@ -593,13 +594,13 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||
}
|
||||
|
||||
private static class GroupData {
|
||||
String id;
|
||||
GroupId id;
|
||||
Set<Recipient> recipients;
|
||||
Bitmap avatarBmp;
|
||||
byte[] avatarBytes;
|
||||
String name;
|
||||
|
||||
public GroupData(String id, Set<Recipient> recipients, Bitmap avatarBmp, byte[] avatarBytes, String name) {
|
||||
GroupData(GroupId id, Set<Recipient> recipients, Bitmap avatarBmp, byte[] avatarBytes, String name) {
|
||||
this.id = id;
|
||||
this.recipients = recipients;
|
||||
this.avatarBmp = avatarBmp;
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
package org.thoughtcrime.securesms;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class IntroPagerAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
public static class IntroPage {
|
||||
final int backgroundColor;
|
||||
final Fragment fragment;
|
||||
|
||||
public IntroPage(int backgroundColor, Fragment fragment) {
|
||||
this.backgroundColor = backgroundColor;
|
||||
this.fragment = fragment;
|
||||
}
|
||||
}
|
||||
|
||||
private List<IntroPage> pages;
|
||||
|
||||
public IntroPagerAdapter(FragmentManager fm, List<IntroPage> pages) {
|
||||
super(fm);
|
||||
this.pages = pages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int i) {
|
||||
IntroPage page = pages.get(i);
|
||||
return page.fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return pages.size();
|
||||
}
|
||||
}
|
|
@ -175,17 +175,17 @@ public class InviteActivity extends PassphraseRequiredActionBarActivity implemen
|
|||
}
|
||||
|
||||
private void setPrimaryColorsToolbarForSms() {
|
||||
primaryToolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.signal_primary));
|
||||
primaryToolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.core_ultramarine));
|
||||
primaryToolbar.getNavigationIcon().setColorFilter(ThemeUtil.getThemedColor(this, R.attr.conversation_subtitle_color), PorterDuff.Mode.SRC_IN);
|
||||
primaryToolbar.setTitleTextColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.signal_primary));
|
||||
getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.core_ultramarine));
|
||||
WindowUtil.clearLightStatusBar(getWindow());
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 27) {
|
||||
getWindow().setNavigationBarColor(ContextCompat.getColor(this, R.color.signal_primary));
|
||||
getWindow().setNavigationBarColor(ContextCompat.getColor(this, R.color.core_ultramarine));
|
||||
WindowUtil.clearLightNavigationBar(getWindow());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public class LinkPreviewsIntroFragment extends Fragment {
|
||||
|
||||
private Controller controller;
|
||||
|
||||
public static LinkPreviewsIntroFragment newInstance() {
|
||||
LinkPreviewsIntroFragment fragment = new LinkPreviewsIntroFragment();
|
||||
Bundle args = new Bundle();
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public LinkPreviewsIntroFragment() {}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (!(getActivity() instanceof Controller)) {
|
||||
throw new IllegalStateException("Parent activity must implement the Controller interface.");
|
||||
}
|
||||
|
||||
controller = (Controller) getActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.experience_upgrade_link_previews_fragment, container, false);
|
||||
|
||||
view.findViewById(R.id.experience_ok_button).setOnClickListener(v -> {
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
|
||||
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(requireContext()),
|
||||
TextSecurePreferences.isLinkPreviewsEnabled(requireContext())));
|
||||
controller.onLinkPreviewsFinished();
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
public interface Controller {
|
||||
void onLinkPreviewsFinished();
|
||||
}
|
||||
}
|
|
@ -238,7 +238,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
|||
EditorInfo.IME_ACTION_DONE);
|
||||
|
||||
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.core_ultramarine), PorterDuff.Mode.SRC_IN);
|
||||
|
||||
lockScreenButton.setOnClickListener(v -> resumeScreenLock());
|
||||
}
|
||||
|
@ -358,7 +358,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
|||
handleAuthenticated();
|
||||
|
||||
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.core_ultramarine), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
@ -381,7 +381,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
|||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.core_ultramarine), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
|||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.CensorshipUtil;
|
||||
|
@ -39,10 +40,9 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
|||
private static final int STATE_CREATE_PASSPHRASE = 1;
|
||||
private static final int STATE_PROMPT_PASSPHRASE = 2;
|
||||
private static final int STATE_UI_BLOCKING_UPGRADE = 3;
|
||||
private static final int STATE_EXPERIENCE_UPGRADE = 4;
|
||||
private static final int STATE_WELCOME_PUSH_SCREEN = 5;
|
||||
private static final int STATE_CREATE_PROFILE_NAME = 6;
|
||||
private static final int STATE_CREATE_KBS_PIN = 7;
|
||||
private static final int STATE_WELCOME_PUSH_SCREEN = 4;
|
||||
private static final int STATE_CREATE_PROFILE_NAME = 5;
|
||||
private static final int STATE_CREATE_KBS_PIN = 6;
|
||||
|
||||
private SignalServiceNetworkAccess networkAccess;
|
||||
private BroadcastReceiver clearKeyReceiver;
|
||||
|
@ -157,7 +157,6 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
|||
case STATE_PROMPT_PASSPHRASE: return getPromptPassphraseIntent();
|
||||
case STATE_UI_BLOCKING_UPGRADE: return getUiBlockingUpgradeIntent();
|
||||
case STATE_WELCOME_PUSH_SCREEN: return getPushRegistrationIntent();
|
||||
case STATE_EXPERIENCE_UPGRADE: return getExperienceUpgradeIntent();
|
||||
case STATE_CREATE_KBS_PIN: return getCreateKbsPinIntent();
|
||||
case STATE_CREATE_PROFILE_NAME: return getCreateProfileNameIntent();
|
||||
default: return null;
|
||||
|
@ -173,8 +172,6 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
|||
return STATE_UI_BLOCKING_UPGRADE;
|
||||
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
|
||||
return STATE_WELCOME_PUSH_SCREEN;
|
||||
} else if (ExperienceUpgradeActivity.isUpdate(this)) {
|
||||
return STATE_EXPERIENCE_UPGRADE;
|
||||
} else if (userMustSetProfileName()) {
|
||||
return STATE_CREATE_PROFILE_NAME;
|
||||
} else if (userMustSetKbsPin()) {
|
||||
|
@ -191,7 +188,7 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
|||
}
|
||||
|
||||
private boolean userMustSetProfileName() {
|
||||
return !SignalStore.registrationValues().isRegistrationComplete() && TextSecurePreferences.getProfileName(this) == ProfileName.EMPTY;
|
||||
return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName() == ProfileName.EMPTY;
|
||||
}
|
||||
|
||||
private Intent getCreatePassphraseIntent() {
|
||||
|
@ -209,10 +206,6 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
|||
: getPushRegistrationIntent());
|
||||
}
|
||||
|
||||
private Intent getExperienceUpgradeIntent() {
|
||||
return getRoutedIntent(ExperienceUpgradeActivity.class, getIntent());
|
||||
}
|
||||
|
||||
private Intent getPushRegistrationIntent() {
|
||||
return RegistrationNavigationActivity.newIntentForNewRegistration(this);
|
||||
}
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class ReadReceiptsIntroFragment extends Fragment {
|
||||
|
||||
public static ReadReceiptsIntroFragment newInstance() {
|
||||
ReadReceiptsIntroFragment fragment = new ReadReceiptsIntroFragment();
|
||||
Bundle args = new Bundle();
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public ReadReceiptsIntroFragment() {}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View v = inflater.inflate(R.layout.experience_upgrade_preference_fragment, container, false);
|
||||
SwitchCompat preference = ViewUtil.findById(v, R.id.preference);
|
||||
|
||||
preference.setChecked(TextSecurePreferences.isReadReceiptsEnabled(getContext()));
|
||||
preference.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
TextSecurePreferences.setReadReceiptsEnabled(getContext(), isChecked);
|
||||
ApplicationDependencies.getJobManager()
|
||||
.add(new MultiDeviceConfigurationUpdateJob(isChecked,
|
||||
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
|
||||
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
||||
});
|
||||
|
||||
return v;
|
||||
}
|
||||
}
|
|
@ -221,7 +221,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
|||
}
|
||||
|
||||
private void setHeader(@NonNull Recipient recipient) {
|
||||
ContactPhoto contactPhoto = recipient.isLocalNumber() ? new ProfileContactPhoto(recipient.getId(), String.valueOf(TextSecurePreferences.getProfileAvatarId(this)))
|
||||
ContactPhoto contactPhoto = recipient.isLocalNumber() ? new ProfileContactPhoto(recipient, recipient.getProfileAvatar())
|
||||
: recipient.getContactPhoto();
|
||||
FallbackContactPhoto fallbackPhoto = recipient.isLocalNumber() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
|
||||
: recipient.getFallbackContactPhoto();
|
||||
|
|
|
@ -109,7 +109,7 @@ public class TransportOptions {
|
|||
public static @NonNull TransportOption getPushTransportOption(@NonNull Context context) {
|
||||
return new TransportOption(Type.TEXTSECURE,
|
||||
R.drawable.ic_send_lock_24,
|
||||
context.getResources().getColor(R.color.textsecure_primary),
|
||||
context.getResources().getColor(R.color.core_ultramarine),
|
||||
context.getString(R.string.ConversationActivity_transport_signal),
|
||||
context.getString(R.string.conversation_activity__type_message_push),
|
||||
new PushCharacterCalculator());
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.thoughtcrime.securesms.components.TypingIndicatorView;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public class TypingIndicatorIntroFragment extends Fragment {
|
||||
|
||||
private Controller controller;
|
||||
|
||||
public static TypingIndicatorIntroFragment newInstance() {
|
||||
TypingIndicatorIntroFragment fragment = new TypingIndicatorIntroFragment();
|
||||
Bundle args = new Bundle();
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public TypingIndicatorIntroFragment() {}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (!(getActivity() instanceof Controller)) {
|
||||
throw new IllegalStateException("Parent activity must implement the Controller interface.");
|
||||
}
|
||||
|
||||
controller = (Controller) getActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.experience_upgrade_typing_indicators_fragment, container, false);
|
||||
View yesButton = view.findViewById(R.id.experience_yes_button);
|
||||
View noButton = view.findViewById(R.id.experience_no_button);
|
||||
|
||||
((TypingIndicatorView) view.findViewById(R.id.typing_indicator)).startAnimation();
|
||||
|
||||
yesButton.setOnClickListener(v -> onButtonClicked(true));
|
||||
noButton.setOnClickListener(v -> onButtonClicked(false));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void onButtonClicked(boolean typingEnabled) {
|
||||
TextSecurePreferences.setTypingIndicatorsEnabled(getContext(), typingEnabled);
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
|
||||
typingEnabled,
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
|
||||
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
||||
|
||||
controller.onTypingIndicatorsFinished();
|
||||
}
|
||||
|
||||
public interface Controller {
|
||||
void onTypingIndicatorsFinished();
|
||||
}
|
||||
}
|
|
@ -77,6 +77,7 @@ import org.thoughtcrime.securesms.qr.ScanningThread;
|
|||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
@ -605,7 +606,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
|
|||
remoteIdentity,
|
||||
isChecked ? VerifiedStatus.VERIFIED :
|
||||
VerifiedStatus.DEFAULT));
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
|
||||
IdentityUtil.markIdentityVerified(getActivity(), recipient.get(), isChecked, false);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class EmojiUtil {
|
||||
|
||||
private static final Map<String, String> VARIATION_MAP = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (EmojiPageModel page : EmojiPages.DATA_PAGES) {
|
||||
for (Emoji emoji : page.getDisplayEmoji()) {
|
||||
for (String variation : emoji.getVariations()) {
|
||||
VARIATION_MAP.put(variation, emoji.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final int MAX_EMOJI_LENGTH;
|
||||
static {
|
||||
int max = 0;
|
||||
for (EmojiPageModel page : EmojiPages.DATA_PAGES) {
|
||||
for (String emoji : page.getEmoji()) {
|
||||
max = Math.max(max, emoji.length());
|
||||
}
|
||||
}
|
||||
MAX_EMOJI_LENGTH = max;
|
||||
}
|
||||
|
||||
private EmojiUtil() {}
|
||||
|
||||
/**
|
||||
* This will return all ways we know of expressing a singular emoji. This is to aid in search,
|
||||
* where some platforms may send an emoji we've locally marked as 'obsolete'.
|
||||
*/
|
||||
public static @NonNull Set<String> getAllRepresentations(@NonNull String emoji) {
|
||||
Set<String> out = new HashSet<>();
|
||||
|
||||
out.add(emoji);
|
||||
|
||||
for (Pair<String, String> pair : EmojiPages.OBSOLETE) {
|
||||
if (pair.first().equals(emoji)) {
|
||||
out.add(pair.second());
|
||||
} else if (pair.second().equals(emoji)) {
|
||||
out.add(pair.first());
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* When provided an emoji that is a skin variation of another, this will return the default yellow
|
||||
* version. This is to aid in search, so using a variation will still find all emojis tagged with
|
||||
* the default version.
|
||||
*
|
||||
* If the emoji has no skin variations, this function will return the original emoji.
|
||||
*/
|
||||
public static @NonNull String getCanonicalRepresentation(@NonNull String emoji) {
|
||||
String canonical = VARIATION_MAP.get(emoji);
|
||||
return canonical != null ? canonical : emoji;
|
||||
}
|
||||
}
|
|
@ -202,7 +202,7 @@ public class ContactAccessor {
|
|||
reader = DatabaseFactory.getGroupDatabase(context).getGroupsFilteredByTitle(constraint, true);
|
||||
|
||||
while ((record = reader.getNext()) != null) {
|
||||
numberList.add(record.getEncodedId());
|
||||
numberList.add(record.getId().toString());
|
||||
}
|
||||
} finally {
|
||||
if (reader != null)
|
||||
|
|
|
@ -235,7 +235,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
|||
final String headerString = getHeaderString(position);
|
||||
if (isPush(position)) {
|
||||
SpannableString spannable = new SpannableString(headerString);
|
||||
spannable.setSpan(new ForegroundColorSpan(getContext().getResources().getColor(R.color.signal_primary)), 0, headerString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannable.setSpan(new ForegroundColorSpan(getContext().getResources().getColor(R.color.core_ultramarine)), 0, headerString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return spannable;
|
||||
} else {
|
||||
return headerString;
|
||||
|
|
|
@ -2,24 +2,24 @@ package org.thoughtcrime.securesms.contacts;
|
|||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.FromTextView;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
|
@ -106,7 +106,7 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
|
|||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private void setText(@Nullable Recipient recipient, int type, String name, String number, String label) {
|
||||
if (number == null || number.isEmpty() || GroupUtil.isEncodedGroup(number)) {
|
||||
if (number == null || number.isEmpty() || GroupId.isEncodedGroup(number)) {
|
||||
this.nameView.setEnabled(false);
|
||||
this.numberView.setText("");
|
||||
this.labelView.setVisibility(View.GONE);
|
||||
|
|
|
@ -22,10 +22,11 @@ import android.database.Cursor;
|
|||
import android.database.MatrixCursor;
|
||||
import android.database.MergeCursor;
|
||||
import android.provider.ContactsContract;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.loader.content.CursorLoader;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
|
@ -35,8 +36,8 @@ import org.thoughtcrime.securesms.database.ThreadDatabase;
|
|||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
|
@ -226,7 +227,7 @@ public class ContactsCursorLoader extends CursorLoader {
|
|||
ThreadRecord threadRecord;
|
||||
while ((threadRecord = reader.getNext()) != null) {
|
||||
Recipient recipient = threadRecord.getRecipient();
|
||||
String stringId = recipient.isGroup() ? recipient.requireGroupId() : recipient.getE164().or(recipient.getEmail()).or("");
|
||||
String stringId = recipient.isGroup() ? recipient.requireGroupId().toString() : recipient.getE164().or(recipient.getEmail()).or("");
|
||||
|
||||
recentConversations.addRow(new Object[] { recipient.getId().serialize(),
|
||||
recipient.toShortString(getContext()),
|
||||
|
@ -265,7 +266,7 @@ public class ContactsCursorLoader extends CursorLoader {
|
|||
while ((groupRecord = reader.getNext()) != null) {
|
||||
groupContacts.addRow(new Object[] { groupRecord.getRecipientId().serialize(),
|
||||
groupRecord.getTitle(),
|
||||
groupRecord.getEncodedId(),
|
||||
groupRecord.getId(),
|
||||
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
|
||||
"",
|
||||
ContactRepository.NORMAL_TYPE });
|
||||
|
|
|
@ -3,11 +3,13 @@ package org.thoughtcrime.securesms.contacts.avatars;
|
|||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
|
@ -16,12 +18,12 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
public class GroupRecordContactPhoto implements ContactPhoto {
|
||||
public final class GroupRecordContactPhoto implements ContactPhoto {
|
||||
|
||||
private final String groupId;
|
||||
private final long avatarId;
|
||||
private final GroupId groupId;
|
||||
private final long avatarId;
|
||||
|
||||
public GroupRecordContactPhoto(@NonNull String groupId, long avatarId) {
|
||||
public GroupRecordContactPhoto(@NonNull GroupId groupId, long avatarId) {
|
||||
this.groupId = groupId;
|
||||
this.avatarId = avatarId;
|
||||
}
|
||||
|
@ -50,13 +52,13 @@ public class GroupRecordContactPhoto implements ContactPhoto {
|
|||
|
||||
@Override
|
||||
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
|
||||
messageDigest.update(groupId.getBytes());
|
||||
messageDigest.update(groupId.toString().getBytes());
|
||||
messageDigest.update(Conversions.longToByteArray(avatarId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null || !(other instanceof GroupRecordContactPhoto)) return false;
|
||||
if (!(other instanceof GroupRecordContactPhoto)) return false;
|
||||
|
||||
GroupRecordContactPhoto that = (GroupRecordContactPhoto)other;
|
||||
return this.groupId.equals(that.groupId) && this.avatarId == that.avatarId;
|
||||
|
|
|
@ -6,32 +6,37 @@ import android.net.Uri;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Objects;
|
||||
|
||||
public class ProfileContactPhoto implements ContactPhoto {
|
||||
|
||||
private final @NonNull RecipientId recipient;
|
||||
private final @NonNull String avatarObject;
|
||||
private final @NonNull Recipient recipient;
|
||||
private final @NonNull String avatarObject;
|
||||
|
||||
public ProfileContactPhoto(@NonNull RecipientId recipient, @NonNull String avatarObject) {
|
||||
public ProfileContactPhoto(@NonNull Recipient recipient, @Nullable String avatarObject) {
|
||||
this.recipient = recipient;
|
||||
this.avatarObject = avatarObject;
|
||||
this.avatarObject = avatarObject == null ? "" : avatarObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull InputStream openInputStream(Context context) throws IOException {
|
||||
return AvatarHelper.getInputStreamFor(context, recipient);
|
||||
return AvatarHelper.getInputStreamFor(context, recipient.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Uri getUri(@NonNull Context context) {
|
||||
File avatarFile = AvatarHelper.getAvatarFile(context, recipient);
|
||||
File avatarFile = AvatarHelper.getAvatarFile(context, recipient.getId());
|
||||
return avatarFile.exists() ? Uri.fromFile(avatarFile) : null;
|
||||
}
|
||||
|
||||
|
@ -42,21 +47,37 @@ public class ProfileContactPhoto implements ContactPhoto {
|
|||
|
||||
@Override
|
||||
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
|
||||
messageDigest.update(recipient.serialize().getBytes());
|
||||
messageDigest.update(recipient.getId().serialize().getBytes());
|
||||
messageDigest.update(avatarObject.getBytes());
|
||||
messageDigest.update(ByteUtil.longToByteArray(getFileLastModified()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null || !(other instanceof ProfileContactPhoto)) return false;
|
||||
|
||||
ProfileContactPhoto that = (ProfileContactPhoto)other;
|
||||
|
||||
return this.recipient.equals(that.recipient) && this.avatarObject.equals(that.avatarObject);
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ProfileContactPhoto that = (ProfileContactPhoto) o;
|
||||
return recipient.equals(that.recipient) &&
|
||||
avatarObject.equals(that.avatarObject) &&
|
||||
getFileLastModified() == that.getFileLastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return recipient.hashCode() ^ avatarObject.hashCode();
|
||||
return Objects.hash(recipient, avatarObject, getFileLastModified());
|
||||
}
|
||||
|
||||
private long getFileLastModified() {
|
||||
if (!recipient.isLocalNumber()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
File avatarFile = AvatarHelper.getAvatarFile(ApplicationDependencies.getApplication(), recipient.getId());
|
||||
|
||||
if (avatarFile.exists()) {
|
||||
return avatarFile.lastModified();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
|||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -21,11 +22,6 @@ public class DirectoryHelper {
|
|||
|
||||
@WorkerThread
|
||||
public static void refreshDirectory(@NonNull Context context, boolean notifyOfNewUsers) throws IOException {
|
||||
if (!SignalStore.storageServiceValues().hasFirstStorageSyncCompleted()) {
|
||||
Log.i(TAG, "First storage sync has not completed. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (FeatureFlags.uuids()) {
|
||||
// TODO [greyson] Create a DirectoryHelperV2 when appropriate.
|
||||
DirectoryHelperV1.refreshDirectory(context, notifyOfNewUsers);
|
||||
|
@ -33,7 +29,7 @@ public class DirectoryHelper {
|
|||
DirectoryHelperV1.refreshDirectory(context, notifyOfNewUsers);
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
@ -49,7 +45,7 @@ public class DirectoryHelper {
|
|||
}
|
||||
|
||||
if (newRegisteredState != originalRegisteredState) {
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
|
||||
return newRegisteredState;
|
||||
|
|
|
@ -1,767 +0,0 @@
|
|||
package org.thoughtcrime.securesms.contacts.sync;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord.IdentityState;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||
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;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
|
||||
public final class StorageSyncHelper {
|
||||
|
||||
private static final String TAG = Log.tag(StorageSyncHelper.class);
|
||||
|
||||
private static final KeyGenerator KEY_GENERATOR = () -> Util.getSecretBytes(16);
|
||||
|
||||
private static KeyGenerator testKeyGenerator = null;
|
||||
|
||||
/**
|
||||
* Given the local state of pending storage mutations, this will generate a result that will
|
||||
* include that data that needs to be written to the storage service, as well as any changes you
|
||||
* need to write back to local storage (like storage keys that might have changed for updated
|
||||
* contacts).
|
||||
*
|
||||
* @param currentManifestVersion What you think the version is locally.
|
||||
* @param currentLocalKeys All local keys you have. This assumes that 'inserts' were given keys
|
||||
* already, and that deletes still have keys.
|
||||
* @param updates Contacts that have been altered.
|
||||
* @param inserts Contacts that have been inserted (or newly marked as registered).
|
||||
* @param deletes Contacts that are no longer registered.
|
||||
*
|
||||
* @return If changes need to be written, then it will return those changes. If no changes need
|
||||
* to be written, this will return {@link Optional#absent()}.
|
||||
*/
|
||||
public static @NonNull Optional<LocalWriteResult> buildStorageUpdatesForLocal(long currentManifestVersion,
|
||||
@NonNull List<byte[]> currentLocalKeys,
|
||||
@NonNull List<RecipientSettings> updates,
|
||||
@NonNull List<RecipientSettings> inserts,
|
||||
@NonNull List<RecipientSettings> deletes)
|
||||
{
|
||||
Set<ByteBuffer> completeKeys = new LinkedHashSet<>(Stream.of(currentLocalKeys).map(ByteBuffer::wrap).toList());
|
||||
Set<SignalStorageRecord> storageInserts = new LinkedHashSet<>();
|
||||
Set<ByteBuffer> storageDeletes = new LinkedHashSet<>();
|
||||
Map<RecipientId, byte[]> storageKeyUpdates = new HashMap<>();
|
||||
|
||||
for (RecipientSettings insert : inserts) {
|
||||
storageInserts.add(localToRemoteRecord(insert));
|
||||
}
|
||||
|
||||
for (RecipientSettings delete : deletes) {
|
||||
byte[] key = Objects.requireNonNull(delete.getStorageKey());
|
||||
storageDeletes.add(ByteBuffer.wrap(key));
|
||||
completeKeys.remove(ByteBuffer.wrap(key));
|
||||
}
|
||||
|
||||
for (RecipientSettings update : updates) {
|
||||
byte[] oldKey = Objects.requireNonNull(update.getStorageKey());
|
||||
byte[] newKey = generateKey();
|
||||
|
||||
storageInserts.add(localToRemoteRecord(update, newKey));
|
||||
storageDeletes.add(ByteBuffer.wrap(oldKey));
|
||||
completeKeys.remove(ByteBuffer.wrap(oldKey));
|
||||
completeKeys.add(ByteBuffer.wrap(newKey));
|
||||
storageKeyUpdates.put(update.getId(), newKey);
|
||||
}
|
||||
|
||||
if (storageInserts.isEmpty() && storageDeletes.isEmpty()) {
|
||||
return Optional.absent();
|
||||
} else {
|
||||
List<byte[]> contactDeleteBytes = Stream.of(storageDeletes).map(ByteBuffer::array).toList();
|
||||
List<byte[]> completeKeysBytes = Stream.of(completeKeys).map(ByteBuffer::array).toList();
|
||||
SignalStorageManifest manifest = new SignalStorageManifest(currentManifestVersion + 1, completeKeysBytes);
|
||||
WriteOperationResult writeOperationResult = new WriteOperationResult(manifest, new ArrayList<>(storageInserts), contactDeleteBytes);
|
||||
|
||||
return Optional.of(new LocalWriteResult(writeOperationResult, storageKeyUpdates));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of all the local and remote keys you know about, this will return a result telling
|
||||
* you which keys are exclusively remote and which are exclusively local.
|
||||
*
|
||||
* @param remoteKeys All remote keys available.
|
||||
* @param localKeys All local keys available.
|
||||
*
|
||||
* @return An object describing which keys are exclusive to the remote data set and which keys are
|
||||
* exclusive to the local data set.
|
||||
*/
|
||||
public static @NonNull KeyDifferenceResult findKeyDifference(@NonNull List<byte[]> remoteKeys,
|
||||
@NonNull List<byte[]> localKeys)
|
||||
{
|
||||
Set<ByteBuffer> allRemoteKeys = Stream.of(remoteKeys).map(ByteBuffer::wrap).collect(LinkedHashSet::new, HashSet::add);
|
||||
Set<ByteBuffer> allLocalKeys = Stream.of(localKeys).map(ByteBuffer::wrap).collect(LinkedHashSet::new, HashSet::add);
|
||||
|
||||
Set<ByteBuffer> remoteOnlyKeys = SetUtil.difference(allRemoteKeys, allLocalKeys);
|
||||
Set<ByteBuffer> localOnlyKeys = SetUtil.difference(allLocalKeys, allRemoteKeys);
|
||||
|
||||
return new KeyDifferenceResult(Stream.of(remoteOnlyKeys).map(ByteBuffer::array).toList(),
|
||||
Stream.of(localOnlyKeys).map(ByteBuffer::array).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two sets of storage records, this will resolve the data into a set of actions that need
|
||||
* to be applied to resolve the differences. This will handle discovering which records between
|
||||
* the two collections refer to the same contacts and are actually updates, which are brand new,
|
||||
* etc.
|
||||
*
|
||||
* @param remoteOnlyRecords Records that are only present remotely.
|
||||
* @param localOnlyRecords Records that are only present locally.
|
||||
*
|
||||
* @return A set of actions that should be applied to resolve the conflict.
|
||||
*/
|
||||
public static @NonNull MergeResult resolveConflict(@NonNull Collection<SignalStorageRecord> remoteOnlyRecords,
|
||||
@NonNull Collection<SignalStorageRecord> localOnlyRecords)
|
||||
{
|
||||
List<SignalContactRecord> remoteOnlyContacts = Stream.of(remoteOnlyRecords).filter(r -> r.getContact().isPresent()).map(r -> r.getContact().get()).toList();
|
||||
List<SignalContactRecord> localOnlyContacts = Stream.of(localOnlyRecords).filter(r -> r.getContact().isPresent()).map(r -> r.getContact().get()).toList();
|
||||
|
||||
List<SignalGroupV1Record> remoteOnlyGroupV1 = Stream.of(remoteOnlyRecords).filter(r -> r.getGroupV1().isPresent()).map(r -> r.getGroupV1().get()).toList();
|
||||
List<SignalGroupV1Record> localOnlyGroupV1 = Stream.of(localOnlyRecords).filter(r -> r.getGroupV1().isPresent()).map(r -> r.getGroupV1().get()).toList();
|
||||
|
||||
List<SignalStorageRecord> remoteOnlyUnknowns = Stream.of(remoteOnlyRecords).filter(SignalStorageRecord::isUnknown).toList();
|
||||
List<SignalStorageRecord> localOnlyUnknowns = Stream.of(localOnlyRecords).filter(SignalStorageRecord::isUnknown).toList();
|
||||
|
||||
ContactRecordMergeResult contactMergeResult = resolveContactConflict(remoteOnlyContacts, localOnlyContacts);
|
||||
GroupV1RecordMergeResult groupV1MergeResult = resolveGroupV1Conflict(remoteOnlyGroupV1, localOnlyGroupV1);
|
||||
|
||||
Set<SignalStorageRecord> remoteInserts = new HashSet<>();
|
||||
remoteInserts.addAll(Stream.of(contactMergeResult.remoteInserts).map(SignalStorageRecord::forContact).toList());
|
||||
remoteInserts.addAll(Stream.of(groupV1MergeResult.remoteInserts).map(SignalStorageRecord::forGroupV1).toList());
|
||||
|
||||
Set<RecordUpdate> remoteUpdates = new HashSet<>();
|
||||
remoteUpdates.addAll(Stream.of(contactMergeResult.remoteUpdates)
|
||||
.map(c -> new RecordUpdate(SignalStorageRecord.forContact(c.getOld()), SignalStorageRecord.forContact(c.getNew())))
|
||||
.toList());
|
||||
remoteUpdates.addAll(Stream.of(groupV1MergeResult.remoteUpdates)
|
||||
.map(c -> new RecordUpdate(SignalStorageRecord.forGroupV1(c.getOld()), SignalStorageRecord.forGroupV1(c.getNew())))
|
||||
.toList());
|
||||
|
||||
return new MergeResult(contactMergeResult.localInserts,
|
||||
contactMergeResult.localUpdates,
|
||||
groupV1MergeResult.localInserts,
|
||||
groupV1MergeResult.localUpdates,
|
||||
new LinkedHashSet<>(remoteOnlyUnknowns),
|
||||
new LinkedHashSet<>(localOnlyUnknowns),
|
||||
remoteInserts,
|
||||
remoteUpdates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assumes that the merge result has *not* yet been applied to the local data. That means that
|
||||
* this method will handle generating the correct final key set based on the merge result.
|
||||
*/
|
||||
public static @NonNull WriteOperationResult createWriteOperation(long currentManifestVersion,
|
||||
@NonNull List<byte[]> currentLocalStorageKeys,
|
||||
@NonNull MergeResult mergeResult)
|
||||
{
|
||||
Set<ByteBuffer> completeKeys = new LinkedHashSet<>(Stream.of(currentLocalStorageKeys).map(ByteBuffer::wrap).toList());
|
||||
|
||||
for (SignalContactRecord insert : mergeResult.getLocalContactInserts()) {
|
||||
completeKeys.add(ByteBuffer.wrap(insert.getKey()));
|
||||
}
|
||||
|
||||
for (SignalGroupV1Record insert : mergeResult.getLocalGroupV1Inserts()) {
|
||||
completeKeys.add(ByteBuffer.wrap(insert.getKey()));
|
||||
}
|
||||
|
||||
for (SignalStorageRecord insert : mergeResult.getRemoteInserts()) {
|
||||
completeKeys.add(ByteBuffer.wrap(insert.getKey()));
|
||||
}
|
||||
|
||||
for (SignalStorageRecord insert : mergeResult.getLocalUnknownInserts()) {
|
||||
completeKeys.add(ByteBuffer.wrap(insert.getKey()));
|
||||
}
|
||||
|
||||
for (ContactUpdate update : mergeResult.getLocalContactUpdates()) {
|
||||
completeKeys.remove(ByteBuffer.wrap(update.getOld().getKey()));
|
||||
completeKeys.add(ByteBuffer.wrap(update.getNew().getKey()));
|
||||
}
|
||||
|
||||
for (GroupV1Update update : mergeResult.getLocalGroupV1Updates()) {
|
||||
completeKeys.remove(ByteBuffer.wrap(update.getOld().getKey()));
|
||||
completeKeys.add(ByteBuffer.wrap(update.getNew().getKey()));
|
||||
}
|
||||
|
||||
for (RecordUpdate update : mergeResult.getRemoteUpdates()) {
|
||||
completeKeys.remove(ByteBuffer.wrap(update.getOld().getKey()));
|
||||
completeKeys.add(ByteBuffer.wrap(update.getNew().getKey()));
|
||||
}
|
||||
|
||||
SignalStorageManifest manifest = new SignalStorageManifest(currentManifestVersion + 1, Stream.of(completeKeys).map(ByteBuffer::array).toList());
|
||||
|
||||
List<SignalStorageRecord> inserts = new ArrayList<>();
|
||||
inserts.addAll(mergeResult.getRemoteInserts());
|
||||
inserts.addAll(Stream.of(mergeResult.getRemoteUpdates()).map(RecordUpdate::getNew).toList());
|
||||
|
||||
List<byte[]> deletes = Stream.of(mergeResult.getRemoteUpdates()).map(RecordUpdate::getOld).map(SignalStorageRecord::getKey).toList();
|
||||
|
||||
return new WriteOperationResult(manifest, inserts, deletes);
|
||||
}
|
||||
|
||||
public static @NonNull SignalStorageRecord localToRemoteRecord(@NonNull RecipientSettings settings) {
|
||||
if (settings.getStorageKey() == null) {
|
||||
throw new AssertionError("Must have a storage key!");
|
||||
}
|
||||
|
||||
return localToRemoteRecord(settings, settings.getStorageKey());
|
||||
}
|
||||
|
||||
public static @NonNull SignalStorageRecord localToRemoteRecord(@NonNull RecipientSettings settings, @NonNull byte[] key) {
|
||||
if (settings.getGroupType() == RecipientDatabase.GroupType.NONE) {
|
||||
return SignalStorageRecord.forContact(localToRemoteContact(settings, key));
|
||||
} else if (settings.getGroupType() == RecipientDatabase.GroupType.SIGNAL_V1) {
|
||||
return SignalStorageRecord.forGroupV1(localToRemoteGroupV1(settings, key));
|
||||
} else {
|
||||
throw new AssertionError("Unsupported type!");
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull SignalContactRecord localToRemoteContact(@NonNull RecipientSettings recipient, byte[] storageKey) {
|
||||
if (recipient.getUuid() == null && recipient.getE164() == null) {
|
||||
throw new AssertionError("Must have either a UUID or a phone number!");
|
||||
}
|
||||
|
||||
return new SignalContactRecord.Builder(storageKey, new SignalServiceAddress(recipient.getUuid(), recipient.getE164()))
|
||||
.setProfileKey(recipient.getProfileKey())
|
||||
.setGivenName(recipient.getProfileName().getGivenName())
|
||||
.setFamilyName(recipient.getProfileName().getFamilyName())
|
||||
.setBlocked(recipient.isBlocked())
|
||||
.setProfileSharingEnabled(recipient.isProfileSharing())
|
||||
.setIdentityKey(recipient.getIdentityKey())
|
||||
.setIdentityState(localToRemoteIdentityState(recipient.getIdentityStatus()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static @NonNull SignalGroupV1Record localToRemoteGroupV1(@NonNull RecipientSettings recipient, byte[] storageKey) {
|
||||
if (recipient.getGroupId() == null) {
|
||||
throw new AssertionError("Must have a groupId!");
|
||||
}
|
||||
|
||||
return new SignalGroupV1Record.Builder(storageKey, GroupUtil.getDecodedIdOrThrow(recipient.getGroupId()))
|
||||
.setBlocked(recipient.isBlocked())
|
||||
.setProfileSharingEnabled(recipient.isProfileSharing())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static @NonNull IdentityDatabase.VerifiedStatus remoteToLocalIdentityStatus(@NonNull IdentityState identityState) {
|
||||
switch (identityState) {
|
||||
case VERIFIED: return IdentityDatabase.VerifiedStatus.VERIFIED;
|
||||
case UNVERIFIED: return IdentityDatabase.VerifiedStatus.UNVERIFIED;
|
||||
default: return IdentityDatabase.VerifiedStatus.DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull byte[] generateKey() {
|
||||
if (testKeyGenerator != null) {
|
||||
return testKeyGenerator.generate();
|
||||
} else {
|
||||
return KEY_GENERATOR.generate();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static @NonNull SignalContactRecord mergeContacts(@NonNull SignalContactRecord remote,
|
||||
@NonNull SignalContactRecord local)
|
||||
{
|
||||
UUID uuid = remote.getAddress().getUuid().or(local.getAddress().getUuid()).orNull();
|
||||
String e164 = remote.getAddress().getNumber().or(local.getAddress().getNumber()).orNull();
|
||||
SignalServiceAddress address = new SignalServiceAddress(uuid, e164);
|
||||
String givenName = remote.getGivenName().or(local.getGivenName()).or("");
|
||||
String familyName = remote.getFamilyName().or(local.getFamilyName()).or("");
|
||||
byte[] profileKey = remote.getProfileKey().or(local.getProfileKey()).orNull();
|
||||
String username = remote.getUsername().or(local.getUsername()).or("");
|
||||
IdentityState identityState = remote.getIdentityState();
|
||||
byte[] identityKey = remote.getIdentityKey().or(local.getIdentityKey()).orNull();
|
||||
String nickname = local.getNickname().or(""); // TODO [greyson] Update this when we add real nickname support
|
||||
boolean blocked = remote.isBlocked();
|
||||
boolean profileSharing = remote.isProfileSharingEnabled() || local.isProfileSharingEnabled();
|
||||
boolean matchesRemote = doParamsMatchContact(remote, address, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, nickname);
|
||||
boolean matchesLocal = doParamsMatchContact(local, address, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, nickname);
|
||||
|
||||
if (remote.getProtoVersion() > 0) {
|
||||
Log.w(TAG, "Inbound model has version " + remote.getProtoVersion() + ", but our version is 0.");
|
||||
}
|
||||
|
||||
if (matchesRemote) {
|
||||
return remote;
|
||||
} else if (matchesLocal) {
|
||||
return local;
|
||||
} else {
|
||||
return new SignalContactRecord.Builder(generateKey(), address)
|
||||
.setGivenName(givenName)
|
||||
.setFamilyName(familyName)
|
||||
.setProfileKey(profileKey)
|
||||
.setUsername(username)
|
||||
.setIdentityState(identityState)
|
||||
.setIdentityKey(identityKey)
|
||||
.setBlocked(blocked)
|
||||
.setProfileSharingEnabled(profileSharing)
|
||||
.setNickname(nickname)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static @NonNull SignalGroupV1Record mergeGroupV1(@NonNull SignalGroupV1Record remote,
|
||||
@NonNull SignalGroupV1Record local)
|
||||
{
|
||||
boolean blocked = remote.isBlocked();
|
||||
boolean profileSharing = remote.isProfileSharingEnabled() || local.isProfileSharingEnabled();
|
||||
|
||||
boolean matchesRemote = blocked == remote.isBlocked() && profileSharing == remote.isProfileSharingEnabled();
|
||||
boolean matchesLocal = blocked == local.isBlocked() && profileSharing == local.isProfileSharingEnabled();
|
||||
|
||||
if (matchesRemote) {
|
||||
return remote;
|
||||
} else if (matchesLocal) {
|
||||
return local;
|
||||
} else {
|
||||
return new SignalGroupV1Record.Builder(generateKey(), remote.getGroupId())
|
||||
.setBlocked(blocked)
|
||||
.setProfileSharingEnabled(blocked)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static void setTestKeyGenerator(@Nullable KeyGenerator keyGenerator) {
|
||||
testKeyGenerator = keyGenerator;
|
||||
}
|
||||
|
||||
private static IdentityState localToRemoteIdentityState(@NonNull IdentityDatabase.VerifiedStatus local) {
|
||||
switch (local) {
|
||||
case VERIFIED: return IdentityState.VERIFIED;
|
||||
case UNVERIFIED: return IdentityState.UNVERIFIED;
|
||||
default: return IdentityState.DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean doParamsMatchContact(@NonNull SignalContactRecord contact,
|
||||
@NonNull SignalServiceAddress address,
|
||||
@Nullable String givenName,
|
||||
@Nullable String familyName,
|
||||
@Nullable byte[] profileKey,
|
||||
@Nullable String username,
|
||||
@Nullable IdentityState identityState,
|
||||
@Nullable byte[] identityKey,
|
||||
boolean blocked,
|
||||
boolean profileSharing,
|
||||
@Nullable String nickname)
|
||||
{
|
||||
return Objects.equals(contact.getAddress(), address) &&
|
||||
Objects.equals(contact.getGivenName().or(""), givenName) &&
|
||||
Objects.equals(contact.getFamilyName().or(""), familyName) &&
|
||||
Arrays.equals(contact.getProfileKey().orNull(), profileKey) &&
|
||||
Objects.equals(contact.getUsername().or(""), username) &&
|
||||
Objects.equals(contact.getIdentityState(), identityState) &&
|
||||
Arrays.equals(contact.getIdentityKey().orNull(), identityKey) &&
|
||||
contact.isBlocked() == blocked &&
|
||||
contact.isProfileSharingEnabled() == profileSharing &&
|
||||
Objects.equals(contact.getNickname().or(""), nickname);
|
||||
}
|
||||
|
||||
private static @NonNull ContactRecordMergeResult resolveContactConflict(@NonNull Collection<SignalContactRecord> remoteOnlyRecords,
|
||||
@NonNull Collection<SignalContactRecord> localOnlyRecords)
|
||||
{
|
||||
Map<UUID, SignalContactRecord> localByUuid = new HashMap<>();
|
||||
Map<String, SignalContactRecord> localByE164 = new HashMap<>();
|
||||
|
||||
for (SignalContactRecord contact : localOnlyRecords) {
|
||||
if (contact.getAddress().getUuid().isPresent()) {
|
||||
localByUuid.put(contact.getAddress().getUuid().get(), contact);
|
||||
}
|
||||
if (contact.getAddress().getNumber().isPresent()) {
|
||||
localByE164.put(contact.getAddress().getNumber().get(), contact);
|
||||
}
|
||||
}
|
||||
|
||||
Set<SignalContactRecord> localInserts = new LinkedHashSet<>(remoteOnlyRecords);
|
||||
Set<SignalContactRecord> remoteInserts = new LinkedHashSet<>(localOnlyRecords);
|
||||
Set<ContactUpdate> localUpdates = new LinkedHashSet<>();
|
||||
Set<ContactUpdate> remoteUpdates = new LinkedHashSet<>();
|
||||
|
||||
for (SignalContactRecord remote : remoteOnlyRecords) {
|
||||
SignalContactRecord localUuid = remote.getAddress().getUuid().isPresent() ? localByUuid.get(remote.getAddress().getUuid().get()) : null;
|
||||
SignalContactRecord localE164 = remote.getAddress().getNumber().isPresent() ? localByE164.get(remote.getAddress().getNumber().get()) : null;
|
||||
|
||||
Optional<SignalContactRecord> local = Optional.fromNullable(localUuid).or(Optional.fromNullable(localE164));
|
||||
|
||||
if (local.isPresent()) {
|
||||
SignalContactRecord merged = mergeContacts(remote, local.get());
|
||||
|
||||
if (!merged.equals(remote)) {
|
||||
remoteUpdates.add(new ContactUpdate(remote, merged));
|
||||
}
|
||||
|
||||
if (!merged.equals(local.get())) {
|
||||
localUpdates.add(new ContactUpdate(local.get(), merged));
|
||||
}
|
||||
|
||||
localInserts.remove(remote);
|
||||
remoteInserts.remove(local.get());
|
||||
}
|
||||
}
|
||||
|
||||
return new ContactRecordMergeResult(localInserts, localUpdates, remoteInserts, remoteUpdates);
|
||||
}
|
||||
|
||||
private static @NonNull GroupV1RecordMergeResult resolveGroupV1Conflict(@NonNull Collection<SignalGroupV1Record> remoteOnlyRecords,
|
||||
@NonNull Collection<SignalGroupV1Record> localOnlyRecords)
|
||||
{
|
||||
Map<String, SignalGroupV1Record> remoteByGroupId = Stream.of(remoteOnlyRecords).collect(Collectors.toMap(g -> GroupUtil.getEncodedId(g.getGroupId(), false), g -> g));
|
||||
Map<String, SignalGroupV1Record> localByGroupId = Stream.of(localOnlyRecords).collect(Collectors.toMap(g -> GroupUtil.getEncodedId(g.getGroupId(), false), g -> g));
|
||||
|
||||
Set<SignalGroupV1Record> localInserts = new LinkedHashSet<>(remoteOnlyRecords);
|
||||
Set<SignalGroupV1Record> remoteInserts = new LinkedHashSet<>(localOnlyRecords);
|
||||
Set<GroupV1Update> localUpdates = new LinkedHashSet<>();
|
||||
Set<GroupV1Update> remoteUpdates = new LinkedHashSet<>();
|
||||
|
||||
for (Map.Entry<String, SignalGroupV1Record> entry : remoteByGroupId.entrySet()) {
|
||||
SignalGroupV1Record remote = entry.getValue();
|
||||
SignalGroupV1Record local = localByGroupId.get(entry.getKey());
|
||||
|
||||
if (local != null) {
|
||||
SignalGroupV1Record merged = mergeGroupV1(remote, local);
|
||||
|
||||
if (!merged.equals(remote)) {
|
||||
remoteUpdates.add(new GroupV1Update(remote, merged));
|
||||
}
|
||||
|
||||
if (!merged.equals(local)) {
|
||||
localUpdates.add(new GroupV1Update(local, merged));
|
||||
}
|
||||
|
||||
localInserts.remove(remote);
|
||||
remoteInserts.remove(local);
|
||||
}
|
||||
}
|
||||
|
||||
return new GroupV1RecordMergeResult(localInserts, localUpdates, remoteInserts, remoteUpdates);
|
||||
}
|
||||
|
||||
public static final class ContactUpdate {
|
||||
private final SignalContactRecord oldContact;
|
||||
private final SignalContactRecord newContact;
|
||||
|
||||
ContactUpdate(@NonNull SignalContactRecord oldContact, @NonNull SignalContactRecord newContact) {
|
||||
this.oldContact = oldContact;
|
||||
this.newContact = newContact;
|
||||
}
|
||||
|
||||
public @NonNull SignalContactRecord getOld() {
|
||||
return oldContact;
|
||||
}
|
||||
|
||||
public @NonNull SignalContactRecord getNew() {
|
||||
return newContact;
|
||||
}
|
||||
|
||||
public boolean profileKeyChanged() {
|
||||
return !OptionalUtil.byteArrayEquals(oldContact.getProfileKey(), newContact.getProfileKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ContactUpdate that = (ContactUpdate) o;
|
||||
return oldContact.equals(that.oldContact) &&
|
||||
newContact.equals(that.newContact);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(oldContact, newContact);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class GroupV1Update {
|
||||
private final SignalGroupV1Record oldGroup;
|
||||
private final SignalGroupV1Record newGroup;
|
||||
|
||||
|
||||
public GroupV1Update(@NonNull SignalGroupV1Record oldGroup, @NonNull SignalGroupV1Record newGroup) {
|
||||
this.oldGroup = oldGroup;
|
||||
this.newGroup = newGroup;
|
||||
}
|
||||
|
||||
public @NonNull SignalGroupV1Record getOld() {
|
||||
return oldGroup;
|
||||
}
|
||||
|
||||
public @NonNull SignalGroupV1Record getNew() {
|
||||
return newGroup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
GroupV1Update that = (GroupV1Update) o;
|
||||
return oldGroup.equals(that.oldGroup) &&
|
||||
newGroup.equals(that.newGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(oldGroup, newGroup);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class RecordUpdate {
|
||||
private final SignalStorageRecord oldRecord;
|
||||
private final SignalStorageRecord newRecord;
|
||||
|
||||
RecordUpdate(@NonNull SignalStorageRecord oldRecord, @NonNull SignalStorageRecord newRecord) {
|
||||
this.oldRecord = oldRecord;
|
||||
this.newRecord = newRecord;
|
||||
}
|
||||
|
||||
public @NonNull SignalStorageRecord getOld() {
|
||||
return oldRecord;
|
||||
}
|
||||
|
||||
public @NonNull SignalStorageRecord getNew() {
|
||||
return newRecord;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
RecordUpdate that = (RecordUpdate) o;
|
||||
return oldRecord.equals(that.oldRecord) &&
|
||||
newRecord.equals(that.newRecord);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(oldRecord, newRecord);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class KeyDifferenceResult {
|
||||
private final List<byte[]> remoteOnlyKeys;
|
||||
private final List<byte[]> localOnlyKeys;
|
||||
|
||||
private KeyDifferenceResult(@NonNull List<byte[]> remoteOnlyKeys, @NonNull List<byte[]> localOnlyKeys) {
|
||||
this.remoteOnlyKeys = remoteOnlyKeys;
|
||||
this.localOnlyKeys = localOnlyKeys;
|
||||
}
|
||||
|
||||
public @NonNull List<byte[]> getRemoteOnlyKeys() {
|
||||
return remoteOnlyKeys;
|
||||
}
|
||||
|
||||
public @NonNull List<byte[]> getLocalOnlyKeys() {
|
||||
return localOnlyKeys;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return remoteOnlyKeys.isEmpty() && localOnlyKeys.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public static final class MergeResult {
|
||||
private final Set<SignalContactRecord> localContactInserts;
|
||||
private final Set<ContactUpdate> localContactUpdates;
|
||||
private final Set<SignalGroupV1Record> localGroupV1Inserts;
|
||||
private final Set<GroupV1Update> localGroupV1Updates;
|
||||
private final Set<SignalStorageRecord> localUnknownInserts;
|
||||
private final Set<SignalStorageRecord> localUnknownDeletes;
|
||||
private final Set<SignalStorageRecord> remoteInserts;
|
||||
private final Set<RecordUpdate> remoteUpdates;
|
||||
|
||||
@VisibleForTesting
|
||||
MergeResult(@NonNull Set<SignalContactRecord> localContactInserts,
|
||||
@NonNull Set<ContactUpdate> localContactUpdates,
|
||||
@NonNull Set<SignalGroupV1Record> localGroupV1Inserts,
|
||||
@NonNull Set<GroupV1Update> localGroupV1Updates,
|
||||
@NonNull Set<SignalStorageRecord> localUnknownInserts,
|
||||
@NonNull Set<SignalStorageRecord> localUnknownDeletes,
|
||||
@NonNull Set<SignalStorageRecord> remoteInserts,
|
||||
@NonNull Set<RecordUpdate> remoteUpdates)
|
||||
{
|
||||
this.localContactInserts = localContactInserts;
|
||||
this.localContactUpdates = localContactUpdates;
|
||||
this.localGroupV1Inserts = localGroupV1Inserts;
|
||||
this.localGroupV1Updates = localGroupV1Updates;
|
||||
this.localUnknownInserts = localUnknownInserts;
|
||||
this.localUnknownDeletes = localUnknownDeletes;
|
||||
this.remoteInserts = remoteInserts;
|
||||
this.remoteUpdates = remoteUpdates;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalContactRecord> getLocalContactInserts() {
|
||||
return localContactInserts;
|
||||
}
|
||||
|
||||
public @NonNull Set<ContactUpdate> getLocalContactUpdates() {
|
||||
return localContactUpdates;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalGroupV1Record> getLocalGroupV1Inserts() {
|
||||
return localGroupV1Inserts;
|
||||
}
|
||||
|
||||
public @NonNull Set<GroupV1Update> getLocalGroupV1Updates() {
|
||||
return localGroupV1Updates;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalStorageRecord> getLocalUnknownInserts() {
|
||||
return localUnknownInserts;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalStorageRecord> getLocalUnknownDeletes() {
|
||||
return localUnknownDeletes;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalStorageRecord> getRemoteInserts() {
|
||||
return remoteInserts;
|
||||
}
|
||||
|
||||
public @NonNull Set<RecordUpdate> getRemoteUpdates() {
|
||||
return remoteUpdates;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return String.format(Locale.ENGLISH,
|
||||
"localContactInserts: %d, localContactUpdates: %d, localGroupInserts: %d, localGroupUpdates: %d, localUnknownInserts: %d, localUnknownDeletes: %d, remoteInserts: %d, remoteUpdates: %d",
|
||||
localContactInserts.size(), localContactUpdates.size(), localGroupV1Inserts.size(), localGroupV1Updates.size(), localUnknownInserts.size(), localUnknownDeletes.size(), remoteInserts.size(), remoteUpdates.size());
|
||||
}
|
||||
}
|
||||
|
||||
public static final class WriteOperationResult {
|
||||
private final SignalStorageManifest manifest;
|
||||
private final List<SignalStorageRecord> inserts;
|
||||
private final List<byte[]> deletes;
|
||||
|
||||
private WriteOperationResult(@NonNull SignalStorageManifest manifest,
|
||||
@NonNull List<SignalStorageRecord> inserts,
|
||||
@NonNull List<byte[]> deletes)
|
||||
{
|
||||
this.manifest = manifest;
|
||||
this.inserts = inserts;
|
||||
this.deletes = deletes;
|
||||
}
|
||||
|
||||
public @NonNull SignalStorageManifest getManifest() {
|
||||
return manifest;
|
||||
}
|
||||
|
||||
public @NonNull List<SignalStorageRecord> getInserts() {
|
||||
return inserts;
|
||||
}
|
||||
|
||||
public @NonNull List<byte[]> getDeletes() {
|
||||
return deletes;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return inserts.isEmpty() && deletes.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return String.format(Locale.ENGLISH,
|
||||
"ManifestVersion: %d, Total Keys: %d, Inserts: %d, Deletes: %d",
|
||||
manifest.getVersion(),
|
||||
manifest.getStorageKeys().size(),
|
||||
inserts.size(),
|
||||
deletes.size());
|
||||
}
|
||||
}
|
||||
|
||||
public static class LocalWriteResult {
|
||||
private final WriteOperationResult writeResult;
|
||||
private final Map<RecipientId, byte[]> storageKeyUpdates;
|
||||
|
||||
private LocalWriteResult(WriteOperationResult writeResult, Map<RecipientId, byte[]> storageKeyUpdates) {
|
||||
this.writeResult = writeResult;
|
||||
this.storageKeyUpdates = storageKeyUpdates;
|
||||
}
|
||||
|
||||
public @NonNull WriteOperationResult getWriteResult() {
|
||||
return writeResult;
|
||||
}
|
||||
|
||||
public @NonNull Map<RecipientId, byte[]> getStorageKeyUpdates() {
|
||||
return storageKeyUpdates;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ContactRecordMergeResult {
|
||||
final Set<SignalContactRecord> localInserts;
|
||||
final Set<ContactUpdate> localUpdates;
|
||||
final Set<SignalContactRecord> remoteInserts;
|
||||
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.remoteInserts = remoteInserts;
|
||||
this.remoteUpdates = remoteUpdates;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class GroupV1RecordMergeResult {
|
||||
final Set<SignalGroupV1Record> localInserts;
|
||||
final Set<GroupV1Update> localUpdates;
|
||||
final Set<SignalGroupV1Record> remoteInserts;
|
||||
final Set<GroupV1Update> remoteUpdates;
|
||||
|
||||
GroupV1RecordMergeResult(@NonNull Set<SignalGroupV1Record> localInserts,
|
||||
@NonNull Set<GroupV1Update> localUpdates,
|
||||
@NonNull Set<SignalGroupV1Record> remoteInserts,
|
||||
@NonNull Set<GroupV1Update> remoteUpdates)
|
||||
{
|
||||
this.localInserts = localInserts;
|
||||
this.localUpdates = localUpdates;
|
||||
this.remoteInserts = remoteInserts;
|
||||
this.remoteUpdates = remoteUpdates;
|
||||
}
|
||||
}
|
||||
|
||||
interface KeyGenerator {
|
||||
@NonNull byte[] generate();
|
||||
}
|
||||
}
|
|
@ -28,10 +28,10 @@ import android.content.pm.PackageManager;
|
|||
import android.content.res.Configuration;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.hardware.Camera;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
|
@ -73,6 +73,8 @@ import androidx.core.graphics.drawable.IconCompat;
|
|||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
|
@ -207,7 +209,7 @@ import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
|
|||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.Dialogs;
|
||||
import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
@ -261,6 +263,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
StickerKeyboardProvider.StickerEventListener,
|
||||
AttachmentKeyboard.Callback
|
||||
{
|
||||
|
||||
private static final int SHORTCUT_ICON_SIZE = Build.VERSION.SDK_INT >= 26 ? ViewUtil.dpToPx(72) : ViewUtil.dpToPx(48 + 16 * 2);
|
||||
|
||||
private static final String TAG = ConversationActivity.class.getSimpleName();
|
||||
|
||||
public static final String RECIPIENT_EXTRA = "recipient_id";
|
||||
|
@ -431,9 +436,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
return;
|
||||
}
|
||||
|
||||
if (!Util.isEmpty(composeText) || attachmentManager.isAttachmentPresent()) {
|
||||
if (!Util.isEmpty(composeText) || attachmentManager.isAttachmentPresent() || inputPanel.getQuote().isPresent()) {
|
||||
saveDraft();
|
||||
attachmentManager.clear(glideRequests, false);
|
||||
inputPanel.clearQuote();
|
||||
silentlySetComposeText("");
|
||||
}
|
||||
|
||||
|
@ -1046,47 +1052,58 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
private void handleAddShortcut() {
|
||||
Log.i(TAG, "Creating home screen shortcut for recipient " + recipient.get().getId());
|
||||
|
||||
new AsyncTask<Void, Void, IconCompat>() {
|
||||
final Context context = getApplicationContext();
|
||||
final Recipient recipient = this.recipient.get();
|
||||
|
||||
@Override
|
||||
protected IconCompat doInBackground(Void... voids) {
|
||||
Context context = getApplicationContext();
|
||||
IconCompat icon = null;
|
||||
GlideApp.with(this)
|
||||
.asBitmap()
|
||||
.load(recipient.getContactPhoto())
|
||||
.error(recipient.getFallbackContactPhoto().asDrawable(this, recipient.getColor().toAvatarColor(this), false))
|
||||
.into(new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onLoadFailed(@Nullable Drawable errorDrawable) {
|
||||
if (errorDrawable == null) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
if (recipient.get().getContactPhoto() != null) {
|
||||
try {
|
||||
Bitmap bitmap = BitmapFactory.decodeStream(recipient.get().getContactPhoto().openInputStream(context));
|
||||
bitmap = BitmapUtil.createScaledBitmap(bitmap, 300, 300);
|
||||
icon = IconCompat.createWithAdaptiveBitmap(bitmap);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to decode contact photo during shortcut creation. Falling back to generic icon.", e);
|
||||
}
|
||||
}
|
||||
Log.w(TAG, "Utilizing fallback photo for shortcut for recipient " + recipient.getId());
|
||||
|
||||
if (icon == null) {
|
||||
icon = IconCompat.createWithResource(context, recipient.get().isGroup() ? R.mipmap.ic_group_shortcut
|
||||
: R.mipmap.ic_person_shortcut);
|
||||
}
|
||||
SimpleTask.run(() -> DrawableUtil.toBitmap(errorDrawable, SHORTCUT_ICON_SIZE, SHORTCUT_ICON_SIZE),
|
||||
bitmap -> addIconToHomeScreen(context, bitmap, recipient));
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
SimpleTask.run(() -> BitmapUtil.createScaledBitmap(resource, SHORTCUT_ICON_SIZE, SHORTCUT_ICON_SIZE),
|
||||
bitmap -> addIconToHomeScreen(context, bitmap, recipient));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(IconCompat icon) {
|
||||
Context context = getApplicationContext();
|
||||
String name = recipient.get().getDisplayName(ConversationActivity.this);
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
||||
}
|
||||
});
|
||||
|
||||
ShortcutInfoCompat shortcutInfo = new ShortcutInfoCompat.Builder(context, recipient.get().getId().serialize() + '-' + System.currentTimeMillis())
|
||||
.setShortLabel(name)
|
||||
.setIcon(icon)
|
||||
.setIntent(ShortcutLauncherActivity.createIntent(context, recipient.getId()))
|
||||
.build();
|
||||
}
|
||||
|
||||
if (ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null)) {
|
||||
Toast.makeText(context, getString(R.string.ConversationActivity_added_to_home_screen), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
private static void addIconToHomeScreen(@NonNull Context context,
|
||||
@NonNull Bitmap bitmap,
|
||||
@NonNull Recipient recipient)
|
||||
{
|
||||
IconCompat icon = IconCompat.createWithAdaptiveBitmap(bitmap);
|
||||
String name = recipient.isLocalNumber() ? context.getString(R.string.note_to_self)
|
||||
: recipient.getDisplayName(context);
|
||||
|
||||
ShortcutInfoCompat shortcutInfoCompat = new ShortcutInfoCompat.Builder(context, recipient.getId().serialize() + '-' + System.currentTimeMillis())
|
||||
.setShortLabel(name)
|
||||
.setIcon(icon)
|
||||
.setIntent(ShortcutLauncherActivity.createIntent(context, recipient.getId()))
|
||||
.build();
|
||||
|
||||
if (ShortcutManagerCompat.requestPinShortcut(context, shortcutInfoCompat, null)) {
|
||||
Toast.makeText(context, context.getString(R.string.ConversationActivity_added_to_home_screen), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
bitmap.recycle();
|
||||
}
|
||||
|
||||
private void handleSearch() {
|
||||
|
@ -1123,7 +1140,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
|
||||
private void handleEditPushGroup() {
|
||||
Intent intent = new Intent(ConversationActivity.this, GroupCreateActivity.class);
|
||||
intent.putExtra(GroupCreateActivity.GROUP_ID_EXTRA, recipient.get().requireGroupId());
|
||||
intent.putExtra(GroupCreateActivity.GROUP_ID_EXTRA, recipient.get().requireGroupId().toString());
|
||||
startActivityForResult(intent, GROUP_EDIT);
|
||||
}
|
||||
|
||||
|
@ -1787,7 +1804,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
inputPanel.setMediaKeyboardToggleMode(true);
|
||||
|
||||
TooltipPopup.forTarget(inputPanel.getMediaKeyboardToggleAnchorView())
|
||||
.setBackgroundTint(getResources().getColor(R.color.core_blue))
|
||||
.setBackgroundTint(getResources().getColor(R.color.core_ultramarine))
|
||||
.setTextColor(getResources().getColor(R.color.core_white))
|
||||
.setText(R.string.ConversationActivity_new_say_it_with_stickers)
|
||||
.setOnDismissListener(() -> {
|
||||
|
|
|
@ -336,10 +336,11 @@ public class ConversationFragment extends Fragment
|
|||
return;
|
||||
}
|
||||
|
||||
Recipient recipient = recipientInfo.getRecipient();
|
||||
boolean isSelf = Recipient.self().equals(recipient);
|
||||
int memberCount = recipientInfo.getGroupMemberCount();
|
||||
List<String> groups = recipientInfo.getSharedGroups();
|
||||
Recipient recipient = recipientInfo.getRecipient();
|
||||
boolean isSelf = Recipient.self().equals(recipient);
|
||||
int memberCount = recipientInfo.getGroupMemberCount();
|
||||
int pendingMemberCount = recipientInfo.getGroupPendingMemberCount();
|
||||
List<String> groups = recipientInfo.getSharedGroups();
|
||||
|
||||
if (recipient != null) {
|
||||
conversationBanner.setAvatar(GlideApp.with(context), recipient);
|
||||
|
@ -348,7 +349,14 @@ public class ConversationFragment extends Fragment
|
|||
conversationBanner.setTitle(title);
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
conversationBanner.setSubtitle(context.getResources().getQuantityString(R.plurals.MessageRequestProfileView_members, memberCount, memberCount));
|
||||
if (pendingMemberCount > 0) {
|
||||
conversationBanner.setSubtitle(context.getResources()
|
||||
.getQuantityString(R.plurals.MessageRequestProfileView_members_and_invited, memberCount,
|
||||
memberCount, pendingMemberCount));
|
||||
} else {
|
||||
conversationBanner.setSubtitle(context.getResources().getQuantityString(R.plurals.MessageRequestProfileView_members, memberCount,
|
||||
memberCount));
|
||||
}
|
||||
} else if (isSelf) {
|
||||
conversationBanner.setSubtitle(context.getString(R.string.ConversationFragment__you_can_add_notes_for_yourself_in_this_conversation));
|
||||
} else {
|
||||
|
@ -1015,7 +1023,7 @@ public class ConversationFragment extends Fragment
|
|||
TooltipPopup.forTarget(requireActivity().findViewById(R.id.menu_context_reply))
|
||||
.setText(text)
|
||||
.setTextColor(getResources().getColor(R.color.core_white))
|
||||
.setBackgroundTint(getResources().getColor(R.color.core_blue))
|
||||
.setBackgroundTint(getResources().getColor(R.color.core_ultramarine))
|
||||
.show(TooltipPopup.POSITION_BELOW);
|
||||
|
||||
TextSecurePreferences.setHasSeenSwipeToReplyTooltip(requireContext(), true);
|
||||
|
|
|
@ -408,10 +408,10 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||
|
||||
private void setAudioViewTint(MessageRecord messageRecord, Recipient recipient) {
|
||||
if (messageRecord.isOutgoing()) {
|
||||
if (DynamicTheme.LIGHT.equals(TextSecurePreferences.getTheme(context))) {
|
||||
audioViewStub.get().setTint(getContext().getResources().getColor(R.color.core_grey_60), defaultBubbleColor);
|
||||
} else {
|
||||
if (DynamicTheme.isDarkTheme(context)) {
|
||||
audioViewStub.get().setTint(Color.WHITE, defaultBubbleColor);
|
||||
} else {
|
||||
audioViewStub.get().setTint(getContext().getResources().getColor(R.color.core_grey_60), defaultBubbleColor);
|
||||
}
|
||||
} else {
|
||||
audioViewStub.get().setTint(Color.WHITE, recipient.getColor().toConversationColor(context));
|
||||
|
|
|
@ -10,6 +10,7 @@ import android.os.Handler;
|
|||
import androidx.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
||||
import org.thoughtcrime.securesms.database.CursorList;
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
|
@ -17,21 +18,21 @@ import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
|
|||
import org.thoughtcrime.securesms.util.CloseableLiveData;
|
||||
import org.thoughtcrime.securesms.util.Throttler;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
class ConversationStickerViewModel extends ViewModel {
|
||||
|
||||
private static final int SEARCH_LIMIT = 10;
|
||||
|
||||
private final Application application;
|
||||
private final StickerSearchRepository repository;
|
||||
private final CloseableLiveData<CursorList<StickerRecord>> stickers;
|
||||
private final MutableLiveData<Boolean> stickersAvailable;
|
||||
private final Throttler availabilityThrottler;
|
||||
private final ContentObserver packObserver;
|
||||
private final Application application;
|
||||
private final StickerSearchRepository repository;
|
||||
private final MutableLiveData<List<StickerRecord>> stickers;
|
||||
private final MutableLiveData<Boolean> stickersAvailable;
|
||||
private final Throttler availabilityThrottler;
|
||||
private final ContentObserver packObserver;
|
||||
|
||||
private ConversationStickerViewModel(@NonNull Application application, @NonNull StickerSearchRepository repository) {
|
||||
this.application = application;
|
||||
this.repository = repository;
|
||||
this.stickers = new CloseableLiveData<>();
|
||||
this.stickers = new MutableLiveData<>();
|
||||
this.stickersAvailable = new MutableLiveData<>();
|
||||
this.availabilityThrottler = new Throttler(500);
|
||||
this.packObserver = new ContentObserver(new Handler()) {
|
||||
|
@ -44,7 +45,7 @@ class ConversationStickerViewModel extends ViewModel {
|
|||
application.getContentResolver().registerContentObserver(DatabaseContentProviders.StickerPack.CONTENT_URI, true, packObserver);
|
||||
}
|
||||
|
||||
@NonNull LiveData<CursorList<StickerRecord>> getStickerResults() {
|
||||
@NonNull LiveData<List<StickerRecord>> getStickerResults() {
|
||||
return stickers;
|
||||
}
|
||||
|
||||
|
@ -54,7 +55,7 @@ class ConversationStickerViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
void onInputTextUpdated(@NonNull String text) {
|
||||
if (TextUtils.isEmpty(text) || text.length() > SEARCH_LIMIT) {
|
||||
if (TextUtils.isEmpty(text) || text.length() > EmojiUtil.MAX_EMOJI_LENGTH) {
|
||||
stickers.setValue(CursorList.emptyList());
|
||||
} else {
|
||||
repository.searchByEmoji(text, stickers::postValue);
|
||||
|
@ -63,7 +64,6 @@ class ConversationStickerViewModel extends ViewModel {
|
|||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
stickers.close();
|
||||
application.getContentResolver().unregisterContentObserver(packObserver);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,26 +6,27 @@ import android.content.ContentValues;
|
|||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
@ -94,9 +95,9 @@ public class GroupDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
public Optional<GroupRecord> getGroup(String groupId) {
|
||||
public Optional<GroupRecord> getGroup(@NonNull GroupId groupId) {
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, GROUP_ID + " = ?",
|
||||
new String[] {groupId},
|
||||
new String[] {groupId.toString()},
|
||||
null, null, null))
|
||||
{
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
|
@ -112,7 +113,7 @@ public class GroupDatabase extends Database {
|
|||
return Optional.fromNullable(reader.getCurrent());
|
||||
}
|
||||
|
||||
public boolean isUnknownGroup(String groupId) {
|
||||
public boolean isUnknownGroup(@NonNull GroupId groupId) {
|
||||
Optional<GroupRecord> group = getGroup(groupId);
|
||||
|
||||
if (!group.isPresent()) {
|
||||
|
@ -142,7 +143,7 @@ public class GroupDatabase extends Database {
|
|||
return new Reader(cursor);
|
||||
}
|
||||
|
||||
public String getOrCreateGroupForMembers(List<RecipientId> members, boolean mms) {
|
||||
public GroupId getOrCreateGroupForMembers(List<RecipientId> members, boolean mms) {
|
||||
Collections.sort(members);
|
||||
|
||||
Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] {GROUP_ID},
|
||||
|
@ -151,9 +152,9 @@ public class GroupDatabase extends Database {
|
|||
null, null, null);
|
||||
try {
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
return cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID));
|
||||
return GroupId.parse(cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)));
|
||||
} else {
|
||||
String groupId = GroupUtil.getEncodedId(allocateGroupId(), mms);
|
||||
GroupId groupId = allocateGroupId(mms);
|
||||
create(groupId, null, members, null, null);
|
||||
return groupId;
|
||||
}
|
||||
|
@ -163,25 +164,31 @@ public class GroupDatabase extends Database {
|
|||
}
|
||||
|
||||
public List<String> getGroupNamesContainingMember(RecipientId recipientId) {
|
||||
return Stream.of(getGroupsContainingMember(recipientId))
|
||||
.map(GroupRecord::getTitle)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<GroupRecord> getGroupsContainingMember(RecipientId recipientId) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
String table = TABLE_NAME + " INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + RECIPIENT_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID;
|
||||
List<String> groupNames = new LinkedList<>();
|
||||
String[] projection = new String[]{TITLE, MEMBERS};
|
||||
String query = MEMBERS + " LIKE ?";
|
||||
String[] args = new String[]{"%" + recipientId.serialize() + "%"};
|
||||
String orderBy = ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.DATE + " DESC";
|
||||
|
||||
try (Cursor cursor = database.query(table, projection, query, args, null, null, orderBy)) {
|
||||
List<GroupRecord> groups = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = database.query(table, null, query, args, null, null, orderBy)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
List<String> members = Util.split(cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS)), ",");
|
||||
|
||||
if (members.contains(recipientId.serialize())) {
|
||||
groupNames.add(cursor.getString(cursor.getColumnIndexOrThrow(TITLE)));
|
||||
groups.add(new Reader(cursor).getCurrent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groupNames;
|
||||
return groups;
|
||||
}
|
||||
|
||||
public Reader getGroups() {
|
||||
|
@ -190,7 +197,7 @@ public class GroupDatabase extends Database {
|
|||
return new Reader(cursor);
|
||||
}
|
||||
|
||||
public @NonNull List<Recipient> getGroupMembers(String groupId, boolean includeSelf) {
|
||||
public @NonNull List<Recipient> getGroupMembers(@NonNull GroupId groupId, boolean includeSelf) {
|
||||
List<RecipientId> members = getCurrentMembers(groupId);
|
||||
List<Recipient> recipients = new LinkedList<>();
|
||||
|
||||
|
@ -205,14 +212,14 @@ public class GroupDatabase extends Database {
|
|||
return recipients;
|
||||
}
|
||||
|
||||
public void create(@NonNull String groupId, @Nullable String title, @NonNull List<RecipientId> members,
|
||||
public void create(@NonNull GroupId groupId, @Nullable String title, @NonNull List<RecipientId> members,
|
||||
@Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay)
|
||||
{
|
||||
Collections.sort(members);
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(RECIPIENT_ID, DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId).serialize());
|
||||
contentValues.put(GROUP_ID, groupId);
|
||||
contentValues.put(GROUP_ID, groupId.toString());
|
||||
contentValues.put(TITLE, title);
|
||||
contentValues.put(MEMBERS, RecipientId.toSerializedList(members));
|
||||
|
||||
|
@ -226,7 +233,7 @@ public class GroupDatabase extends Database {
|
|||
contentValues.put(AVATAR_RELAY, relay);
|
||||
contentValues.put(TIMESTAMP, System.currentTimeMillis());
|
||||
contentValues.put(ACTIVE, 1);
|
||||
contentValues.put(MMS, GroupUtil.isMmsGroup(groupId));
|
||||
contentValues.put(MMS, groupId.isMmsGroup());
|
||||
|
||||
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
|
||||
|
||||
|
@ -236,7 +243,7 @@ public class GroupDatabase extends Database {
|
|||
notifyConversationListListeners();
|
||||
}
|
||||
|
||||
public void update(String groupId, String title, SignalServiceAttachmentPointer avatar) {
|
||||
public void update(@NonNull GroupId groupId, String title, SignalServiceAttachmentPointer avatar) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
if (title != null) contentValues.put(TITLE, title);
|
||||
|
||||
|
@ -249,7 +256,7 @@ public class GroupDatabase extends Database {
|
|||
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues,
|
||||
GROUP_ID + " = ?",
|
||||
new String[] {groupId});
|
||||
new String[] {groupId.toString()});
|
||||
|
||||
RecipientId groupRecipient = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||
Recipient.live(groupRecipient).refresh();
|
||||
|
@ -257,21 +264,21 @@ public class GroupDatabase extends Database {
|
|||
notifyConversationListListeners();
|
||||
}
|
||||
|
||||
public void updateTitle(String groupId, String title) {
|
||||
public void updateTitle(@NonNull GroupId groupId, String title) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(TITLE, title);
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?",
|
||||
new String[] {groupId});
|
||||
new String[] {groupId.toString()});
|
||||
|
||||
RecipientId groupRecipient = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||
Recipient.live(groupRecipient).refresh();
|
||||
}
|
||||
|
||||
public void updateAvatar(String groupId, Bitmap avatar) {
|
||||
public void updateAvatar(@NonNull GroupId groupId, @Nullable Bitmap avatar) {
|
||||
updateAvatar(groupId, BitmapUtil.toByteArray(avatar));
|
||||
}
|
||||
|
||||
public void updateAvatar(String groupId, byte[] avatar) {
|
||||
public void updateAvatar(@NonNull GroupId groupId, @Nullable byte[] avatar) {
|
||||
long avatarId;
|
||||
|
||||
if (avatar != null) avatarId = Math.abs(new SecureRandom().nextLong());
|
||||
|
@ -283,13 +290,13 @@ public class GroupDatabase extends Database {
|
|||
contentValues.put(AVATAR_ID, avatarId);
|
||||
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?",
|
||||
new String[] {groupId});
|
||||
new String[] {groupId.toString()});
|
||||
|
||||
RecipientId groupRecipient = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||
Recipient.live(groupRecipient).refresh();
|
||||
}
|
||||
|
||||
public void updateMembers(String groupId, List<RecipientId> members) {
|
||||
public void updateMembers(@NonNull GroupId groupId, List<RecipientId> members) {
|
||||
Collections.sort(members);
|
||||
|
||||
ContentValues contents = new ContentValues();
|
||||
|
@ -297,13 +304,13 @@ public class GroupDatabase extends Database {
|
|||
contents.put(ACTIVE, 1);
|
||||
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?",
|
||||
new String[] {groupId});
|
||||
new String[] {groupId.toString()});
|
||||
|
||||
RecipientId groupRecipient = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||
Recipient.live(groupRecipient).refresh();
|
||||
}
|
||||
|
||||
public void remove(String groupId, RecipientId source) {
|
||||
public void remove(@NonNull GroupId groupId, RecipientId source) {
|
||||
List<RecipientId> currentMembers = getCurrentMembers(groupId);
|
||||
currentMembers.remove(source);
|
||||
|
||||
|
@ -311,19 +318,19 @@ public class GroupDatabase extends Database {
|
|||
contents.put(MEMBERS, RecipientId.toSerializedList(currentMembers));
|
||||
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?",
|
||||
new String[] {groupId});
|
||||
new String[] {groupId.toString()});
|
||||
|
||||
RecipientId groupRecipient = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||
Recipient.live(groupRecipient).refresh();
|
||||
}
|
||||
|
||||
private List<RecipientId> getCurrentMembers(String groupId) {
|
||||
private List<RecipientId> getCurrentMembers(@NonNull GroupId groupId) {
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] {MEMBERS},
|
||||
GROUP_ID + " = ?",
|
||||
new String[] {groupId},
|
||||
new String[] {groupId.toString()},
|
||||
null, null, null);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
|
@ -338,23 +345,22 @@ public class GroupDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
public boolean isActive(String groupId) {
|
||||
public boolean isActive(@NonNull GroupId groupId) {
|
||||
Optional<GroupRecord> record = getGroup(groupId);
|
||||
return record.isPresent() && record.get().isActive();
|
||||
}
|
||||
|
||||
public void setActive(String groupId, boolean active) {
|
||||
public void setActive(@NonNull GroupId groupId, boolean active) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(ACTIVE, active ? 1 : 0);
|
||||
database.update(TABLE_NAME, values, GROUP_ID + " = ?", new String[] {groupId});
|
||||
database.update(TABLE_NAME, values, GROUP_ID + " = ?", new String[] {groupId.toString()});
|
||||
}
|
||||
|
||||
|
||||
public byte[] allocateGroupId() {
|
||||
public static GroupId allocateGroupId(boolean mms) {
|
||||
byte[] groupId = new byte[16];
|
||||
new SecureRandom().nextBytes(groupId);
|
||||
return groupId;
|
||||
return mms ? GroupId.mms(groupId) : GroupId.v1(groupId);
|
||||
}
|
||||
|
||||
public static class Reader implements Closeable {
|
||||
|
@ -378,7 +384,7 @@ public class GroupDatabase extends Database {
|
|||
return null;
|
||||
}
|
||||
|
||||
return new GroupRecord(cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)),
|
||||
return new GroupRecord(GroupId.parse(cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID))),
|
||||
RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(TITLE)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS)),
|
||||
|
@ -401,7 +407,7 @@ public class GroupDatabase extends Database {
|
|||
|
||||
public static class GroupRecord {
|
||||
|
||||
private final String id;
|
||||
private final GroupId id;
|
||||
private final RecipientId recipientId;
|
||||
private final String title;
|
||||
private final List<RecipientId> members;
|
||||
|
@ -414,7 +420,7 @@ public class GroupDatabase extends Database {
|
|||
private final boolean active;
|
||||
private final boolean mms;
|
||||
|
||||
public GroupRecord(String id, @NonNull RecipientId recipientId, String title, String members, byte[] avatar,
|
||||
public GroupRecord(@NonNull GroupId id, @NonNull RecipientId recipientId, String title, String members, byte[] avatar,
|
||||
long avatarId, byte[] avatarKey, String avatarContentType,
|
||||
String relay, boolean active, byte[] avatarDigest, boolean mms)
|
||||
{
|
||||
|
@ -434,22 +440,14 @@ public class GroupDatabase extends Database {
|
|||
else this.members = new LinkedList<>();
|
||||
}
|
||||
|
||||
public byte[] getId() {
|
||||
try {
|
||||
return GroupUtil.getDecodedId(id);
|
||||
} catch (IOException ioe) {
|
||||
throw new AssertionError(ioe);
|
||||
}
|
||||
public GroupId getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getRecipientId() {
|
||||
return recipientId;
|
||||
}
|
||||
|
||||
public String getEncodedId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
|
|
@ -17,29 +17,30 @@ 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;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.RecordUpdate;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncModels;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
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.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
@ -93,7 +94,7 @@ public class RecipientDatabase extends Database {
|
|||
private static final String FORCE_SMS_SELECTION = "force_sms_selection";
|
||||
private static final String UUID_CAPABILITY = "uuid_supported";
|
||||
private static final String GROUPS_V2_CAPABILITY = "gv2_capability";
|
||||
private static final String STORAGE_SERVICE_KEY = "storage_service_key";
|
||||
private static final String STORAGE_SERVICE_ID = "storage_service_key";
|
||||
private static final String DIRTY = "dirty";
|
||||
private static final String PROFILE_GIVEN_NAME = "signal_profile_name";
|
||||
private static final String PROFILE_FAMILY_NAME = "profile_family_name";
|
||||
|
@ -114,7 +115,7 @@ public class RecipientDatabase extends Database {
|
|||
UNIDENTIFIED_ACCESS_MODE,
|
||||
FORCE_SMS_SELECTION,
|
||||
UUID_CAPABILITY, GROUPS_V2_CAPABILITY,
|
||||
STORAGE_SERVICE_KEY, DIRTY
|
||||
STORAGE_SERVICE_ID, DIRTY
|
||||
};
|
||||
|
||||
private static final String[] RECIPIENT_FULL_PROJECTION = ArrayUtils.concat(
|
||||
|
@ -132,7 +133,7 @@ public class RecipientDatabase extends Database {
|
|||
};
|
||||
|
||||
private static final String[] ID_PROJECTION = new String[]{ID};
|
||||
private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, "COALESCE(" + PROFILE_JOINED_NAME + ", " + PROFILE_GIVEN_NAME + ") AS " + SEARCH_PROFILE_NAME, "COALESCE(" + SYSTEM_DISPLAY_NAME + ", " + PROFILE_JOINED_NAME + ", " + PROFILE_GIVEN_NAME + ", " + USERNAME + ") AS " + SORT_NAME};
|
||||
private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, "COALESCE(" + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ") AS " + SEARCH_PROFILE_NAME, "COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ") AS " + SORT_NAME};
|
||||
public static final String[] SEARCH_PROJECTION_NAMES = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, SEARCH_PROFILE_NAME, SORT_NAME};
|
||||
static final List<String> TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
|
||||
.map(columnName -> TABLE_NAME + "." + columnName)
|
||||
|
@ -214,7 +215,7 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
enum DirtyState {
|
||||
public enum DirtyState {
|
||||
CLEAN(0), UPDATE(1), INSERT(2), DELETE(3);
|
||||
|
||||
private final int id;
|
||||
|
@ -226,6 +227,10 @@ public class RecipientDatabase extends Database {
|
|||
int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public static DirtyState fromId(int id) {
|
||||
return values()[id];
|
||||
}
|
||||
}
|
||||
|
||||
public enum GroupType {
|
||||
|
@ -283,7 +288,7 @@ public class RecipientDatabase extends Database {
|
|||
FORCE_SMS_SELECTION + " INTEGER DEFAULT 0, " +
|
||||
UUID_CAPABILITY + " INTEGER DEFAULT " + Recipient.Capability.UNKNOWN.serialize() + ", " +
|
||||
GROUPS_V2_CAPABILITY + " INTEGER DEFAULT " + Recipient.Capability.UNKNOWN.serialize() + ", " +
|
||||
STORAGE_SERVICE_KEY + " TEXT UNIQUE DEFAULT NULL, " +
|
||||
STORAGE_SERVICE_ID + " TEXT UNIQUE DEFAULT NULL, " +
|
||||
DIRTY + " INTEGER DEFAULT " + DirtyState.CLEAN.getId() + ");";
|
||||
|
||||
private static final String INSIGHTS_INVITEE_LIST = "SELECT " + TABLE_NAME + "." + ID +
|
||||
|
@ -345,18 +350,18 @@ public class RecipientDatabase extends Database {
|
|||
return getOrInsertByColumn(EMAIL, email).recipientId;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getOrInsertFromGroupId(@NonNull String groupId) {
|
||||
GetOrInsertResult result = getOrInsertByColumn(GROUP_ID, groupId);
|
||||
public @NonNull RecipientId getOrInsertFromGroupId(@NonNull GroupId groupId) {
|
||||
GetOrInsertResult result = getOrInsertByColumn(GROUP_ID, groupId.toString());
|
||||
|
||||
if (result.neededInsert) {
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
if (GroupUtil.isMmsGroup(groupId)) {
|
||||
if (groupId.isMmsGroup()) {
|
||||
values.put(GROUP_TYPE, GroupType.MMS.getId());
|
||||
} else {
|
||||
values.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId());
|
||||
values.put(DIRTY, DirtyState.INSERT.getId());
|
||||
values.put(STORAGE_SERVICE_KEY, Base64.encodeBytes(StorageSyncHelper.generateKey()));
|
||||
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()));
|
||||
}
|
||||
|
||||
update(result.recipientId, values);
|
||||
|
@ -399,29 +404,41 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
public @NonNull DirtyState getDirtyState(@NonNull RecipientId recipientId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[] { DIRTY }, ID_WHERE, new String[] { recipientId.serialize() }, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return DirtyState.fromId(cursor.getInt(cursor.getColumnIndexOrThrow(DIRTY)));
|
||||
}
|
||||
}
|
||||
|
||||
return DirtyState.CLEAN;
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientSettings> getPendingRecipientSyncUpdates() {
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_KEY + " NOT NULL";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.UPDATE.getId()) };
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.UPDATE.getId()), Recipient.self().getId().serialize() };
|
||||
|
||||
return getRecipientSettings(query, args);
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientSettings> getPendingRecipientSyncInsertions() {
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_KEY + " NOT NULL";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.INSERT.getId()) };
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.INSERT.getId()), Recipient.self().getId().serialize() };
|
||||
|
||||
return getRecipientSettings(query, args);
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientSettings> getPendingRecipientSyncDeletions() {
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_KEY + " NOT NULL";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.DELETE.getId()) };
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.DELETE.getId()), Recipient.self().getId().serialize() };
|
||||
|
||||
return getRecipientSettings(query, args);
|
||||
}
|
||||
|
||||
public @Nullable RecipientSettings getByStorageSyncKey(@NonNull byte[] key) {
|
||||
List<RecipientSettings> result = getRecipientSettings(STORAGE_SERVICE_KEY + " = ?", new String[] { Base64.encodeBytes(key) });
|
||||
public @Nullable RecipientSettings getByStorageId(@NonNull byte[] storageId) {
|
||||
List<RecipientSettings> result = getRecipientSettings(STORAGE_SERVICE_ID + " = ?", new String[] { Base64.encodeBytes(storageId) });
|
||||
|
||||
if (result.size() > 0) {
|
||||
return result.get(0);
|
||||
|
@ -430,16 +447,20 @@ public class RecipientDatabase extends Database {
|
|||
return null;
|
||||
}
|
||||
|
||||
public void applyStorageSyncKeyUpdates(@NonNull Map<RecipientId, byte[]> keys) {
|
||||
public void markNeedsSync(@NonNull RecipientId recipientId) {
|
||||
markDirty(recipientId, DirtyState.UPDATE);
|
||||
}
|
||||
|
||||
public void applyStorageIdUpdates(@NonNull Map<RecipientId, StorageId> storageIds) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
String query = ID + " = ?";
|
||||
|
||||
for (Map.Entry<RecipientId, byte[]> entry : keys.entrySet()) {
|
||||
for (Map.Entry<RecipientId, StorageId> entry : storageIds.entrySet()) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(STORAGE_SERVICE_KEY, Base64.encodeBytes(entry.getValue()));
|
||||
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(entry.getValue().getRaw()));
|
||||
values.put(DIRTY, DirtyState.CLEAN.getId());
|
||||
|
||||
db.update(TABLE_NAME, values, query, new String[] { entry.getKey().serialize() });
|
||||
|
@ -450,13 +471,14 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
public void applyStorageSyncUpdates(@NonNull Collection<SignalContactRecord> contactInserts,
|
||||
@NonNull Collection<StorageSyncHelper.ContactUpdate> contactUpdates,
|
||||
@NonNull Collection<SignalGroupV1Record> groupV1Inserts,
|
||||
@NonNull Collection<StorageSyncHelper.GroupV1Update> groupV1Updates)
|
||||
public void applyStorageSyncUpdates(@NonNull Collection<SignalContactRecord> contactInserts,
|
||||
@NonNull Collection<RecordUpdate<SignalContactRecord>> contactUpdates,
|
||||
@NonNull Collection<SignalGroupV1Record> groupV1Inserts,
|
||||
@NonNull Collection<RecordUpdate<SignalGroupV1Record>> groupV1Updates)
|
||||
{
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
|
@ -483,30 +505,29 @@ public class RecipientDatabase extends Database {
|
|||
try {
|
||||
IdentityKey identityKey = new IdentityKey(insert.getIdentityKey().get(), 0);
|
||||
|
||||
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(recipientId, identityKey, StorageSyncHelper.remoteToLocalIdentityStatus(insert.getIdentityState()));
|
||||
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(insert.getIdentityState()));
|
||||
IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), true, true);
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w(TAG, "Failed to process identity key during insert! Skipping.", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (Recipient.self().getId().equals(recipientId)) {
|
||||
TextSecurePreferences.setProfileName(context, ProfileName.fromParts(insert.getGivenName().orNull(), insert.getFamilyName().orNull()));
|
||||
}
|
||||
threadDatabase.setArchived(recipientId, insert.isArchived());
|
||||
Recipient.live(recipientId).refresh();
|
||||
}
|
||||
}
|
||||
|
||||
for (StorageSyncHelper.ContactUpdate update : contactUpdates) {
|
||||
for (RecordUpdate<SignalContactRecord> update : contactUpdates) {
|
||||
ContentValues values = getValuesForStorageContact(update.getNew());
|
||||
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_KEY + " = ?", new String[]{Base64.encodeBytes(update.getOld().getKey())});
|
||||
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(update.getOld().getId().getRaw())});
|
||||
|
||||
if (updateCount < 1) {
|
||||
throw new AssertionError("Had an update, but it didn't match any rows!");
|
||||
}
|
||||
|
||||
RecipientId recipientId = getByStorageKeyOrThrow(update.getNew().getKey());
|
||||
RecipientId recipientId = getByStorageKeyOrThrow(update.getNew().getId().getRaw());
|
||||
|
||||
if (update.profileKeyChanged()) {
|
||||
if (StorageSyncHelper.profileKeyChanged(update)) {
|
||||
clearProfileKeyCredential(recipientId);
|
||||
}
|
||||
|
||||
|
@ -515,7 +536,7 @@ public class RecipientDatabase extends Database {
|
|||
|
||||
if (update.getNew().getIdentityKey().isPresent()) {
|
||||
IdentityKey identityKey = new IdentityKey(update.getNew().getIdentityKey().get(), 0);
|
||||
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(recipientId, identityKey, StorageSyncHelper.remoteToLocalIdentityStatus(update.getNew().getIdentityState()));
|
||||
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(update.getNew().getIdentityState()));
|
||||
}
|
||||
|
||||
Optional<IdentityRecord> newIdentityRecord = identityDatabase.getIdentity(recipientId);
|
||||
|
@ -532,19 +553,32 @@ public class RecipientDatabase extends Database {
|
|||
} catch (InvalidKeyException e) {
|
||||
Log.w(TAG, "Failed to process identity key during update! Skipping.", e);
|
||||
}
|
||||
|
||||
threadDatabase.setArchived(recipientId, update.getNew().isArchived());
|
||||
Recipient.live(recipientId).refresh();
|
||||
}
|
||||
|
||||
for (SignalGroupV1Record insert : groupV1Inserts) {
|
||||
db.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV1(insert));
|
||||
|
||||
Recipient recipient = Recipient.externalGroup(context, GroupId.v1(insert.getGroupId()));
|
||||
|
||||
threadDatabase.setArchived(recipient.getId(), insert.isArchived());
|
||||
recipient.live().refresh();
|
||||
}
|
||||
|
||||
for (StorageSyncHelper.GroupV1Update update : groupV1Updates) {
|
||||
for (RecordUpdate<SignalGroupV1Record> update : groupV1Updates) {
|
||||
ContentValues values = getValuesForStorageGroupV1(update.getNew());
|
||||
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_KEY + " = ?", new String[]{Base64.encodeBytes(update.getOld().getKey())});
|
||||
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(update.getOld().getId().getRaw())});
|
||||
|
||||
if (updateCount < 1) {
|
||||
throw new AssertionError("Had an update, but it didn't match any rows!");
|
||||
}
|
||||
|
||||
Recipient recipient = Recipient.externalGroup(context, GroupId.v1(update.getOld().getGroupId()));
|
||||
|
||||
threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived());
|
||||
recipient.live().refresh();
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
|
@ -553,6 +587,27 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
public void applyStorageSyncUpdates(@NonNull StorageId storageId, SignalAccountRecord update) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
ProfileName profileName = ProfileName.fromParts(update.getGivenName().orNull(), update.getFamilyName().orNull());
|
||||
|
||||
values.put(PROFILE_GIVEN_NAME, profileName.getGivenName());
|
||||
values.put(PROFILE_FAMILY_NAME, profileName.getFamilyName());
|
||||
values.put(PROFILE_JOINED_NAME, profileName.toString());
|
||||
values.put(PROFILE_KEY, update.getProfileKey().transform(Base64::encodeBytes).orNull());
|
||||
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(update.getId().getRaw()));
|
||||
values.put(DIRTY, DirtyState.CLEAN.getId());
|
||||
|
||||
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(storageId.getRaw())});
|
||||
if (updateCount < 1) {
|
||||
throw new AssertionError("Account update didn't match any rows!");
|
||||
}
|
||||
|
||||
Recipient.self().live().refresh();
|
||||
}
|
||||
|
||||
public void updatePhoneNumbers(@NonNull Map<String, String> mapping) {
|
||||
if (mapping.isEmpty()) return;
|
||||
|
||||
|
@ -577,7 +632,7 @@ public class RecipientDatabase extends Database {
|
|||
|
||||
private @NonNull RecipientId getByStorageKeyOrThrow(byte[] storageKey) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String query = STORAGE_SERVICE_KEY + " = ?";
|
||||
String query = STORAGE_SERVICE_ID + " = ?";
|
||||
String[] args = new String[]{Base64.encodeBytes(storageKey)};
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, ID_PROJECTION, query, args, null, null, null)) {
|
||||
|
@ -598,27 +653,28 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
|
||||
ProfileName profileName = ProfileName.fromParts(contact.getGivenName().orNull(), contact.getFamilyName().orNull());
|
||||
String username = contact.getUsername().orNull();
|
||||
|
||||
values.put(PHONE, contact.getAddress().getNumber().orNull());
|
||||
values.put(PROFILE_GIVEN_NAME, profileName.getGivenName());
|
||||
values.put(PROFILE_FAMILY_NAME, profileName.getFamilyName());
|
||||
values.put(PROFILE_JOINED_NAME, profileName.toString());
|
||||
values.put(PROFILE_KEY, contact.getProfileKey().transform(Base64::encodeBytes).orNull());
|
||||
values.put(USERNAME, contact.getUsername().orNull());
|
||||
values.put(USERNAME, TextUtils.isEmpty(username) ? null : username);
|
||||
values.put(PROFILE_SHARING, contact.isProfileSharingEnabled() ? "1" : "0");
|
||||
values.put(BLOCKED, contact.isBlocked() ? "1" : "0");
|
||||
values.put(STORAGE_SERVICE_KEY, Base64.encodeBytes(contact.getKey()));
|
||||
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(contact.getId().getRaw()));
|
||||
values.put(DIRTY, DirtyState.CLEAN.getId());
|
||||
return values;
|
||||
}
|
||||
|
||||
private static @NonNull ContentValues getValuesForStorageGroupV1(@NonNull SignalGroupV1Record groupV1) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(GROUP_ID, GroupUtil.getEncodedId(groupV1.getGroupId(), false));
|
||||
values.put(GROUP_ID, GroupId.v1(groupV1.getGroupId()).toString());
|
||||
values.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId());
|
||||
values.put(PROFILE_SHARING, groupV1.isProfileSharingEnabled() ? "1" : "0");
|
||||
values.put(BLOCKED, groupV1.isBlocked() ? "1" : "0");
|
||||
values.put(STORAGE_SERVICE_KEY, Base64.encodeBytes(groupV1.getKey()));
|
||||
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV1.getId().getRaw()));
|
||||
values.put(DIRTY, DirtyState.CLEAN.getId());
|
||||
return values;
|
||||
}
|
||||
|
@ -638,40 +694,46 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
|
||||
/**
|
||||
* @return All storage keys, excluding the ones that need to be deleted.
|
||||
* @return All storage ids for ContactRecords, excluding the ones that need to be deleted.
|
||||
*/
|
||||
public List<byte[]> getAllStorageSyncKeys() {
|
||||
return new ArrayList<>(getAllStorageSyncKeysMap().values());
|
||||
public List<StorageId> getContactStorageSyncIds() {
|
||||
return new ArrayList<>(getContactStorageSyncIdsMap().values());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return All storage keys, excluding the ones that need to be deleted.
|
||||
* @return All storage IDs for ContactRecords, excluding the ones that need to be deleted.
|
||||
*/
|
||||
public Map<RecipientId, byte[]> getAllStorageSyncKeysMap() {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String query = STORAGE_SERVICE_KEY + " NOT NULL AND " + DIRTY + " != ?";
|
||||
String[] args = new String[]{String.valueOf(DirtyState.DELETE)};
|
||||
Map<RecipientId, byte[]> out = new HashMap<>();
|
||||
public @NonNull Map<RecipientId, StorageId> getContactStorageSyncIdsMap() {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String query = STORAGE_SERVICE_ID + " NOT NULL AND " + DIRTY + " != ? AND " + ID + " != ?";
|
||||
String[] args = new String[]{String.valueOf(DirtyState.DELETE), Recipient.self().getId().serialize() };
|
||||
Map<RecipientId, StorageId> out = new HashMap<>();
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[] { ID, STORAGE_SERVICE_KEY }, query, args, null, null, null)) {
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[] { ID, STORAGE_SERVICE_ID, GROUP_TYPE }, query, args, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
RecipientId id = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID)));
|
||||
String encodedKey = cursor.getString(cursor.getColumnIndexOrThrow(STORAGE_SERVICE_KEY));
|
||||
String encodedKey = cursor.getString(cursor.getColumnIndexOrThrow(STORAGE_SERVICE_ID));
|
||||
GroupType groupType = GroupType.fromId(cursor.getInt(cursor.getColumnIndexOrThrow(GROUP_TYPE)));
|
||||
byte[] key = Base64.decodeOrThrow(encodedKey);
|
||||
|
||||
out.put(id, Base64.decodeOrThrow(encodedKey));
|
||||
if (groupType == GroupType.NONE) {
|
||||
out.put(id, StorageId.forContact(key));
|
||||
} else {
|
||||
out.put(id, StorageId.forGroupV1(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private @NonNull RecipientSettings getRecipientSettings(@NonNull Cursor cursor) {
|
||||
private static @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));
|
||||
GroupId groupId = GroupId.parseNullable(cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)));
|
||||
int groupType = cursor.getInt(cursor.getColumnIndexOrThrow(GROUP_TYPE));
|
||||
boolean blocked = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCKED)) == 1;
|
||||
String messageRingtone = cursor.getString(cursor.getColumnIndexOrThrow(MESSAGE_RINGTONE));
|
||||
|
@ -699,7 +761,7 @@ public class RecipientDatabase extends Database {
|
|||
boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1;
|
||||
int uuidCapabilityValue = cursor.getInt(cursor.getColumnIndexOrThrow(UUID_CAPABILITY));
|
||||
int groupsV2CapabilityValue = cursor.getInt(cursor.getColumnIndexOrThrow(GROUPS_V2_CAPABILITY));
|
||||
String storageKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(STORAGE_SERVICE_KEY));
|
||||
String storageKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(STORAGE_SERVICE_ID));
|
||||
String identityKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
|
||||
int identityStatusRaw = cursor.getInt(cursor.getColumnIndexOrThrow(IDENTITY_STATUS));
|
||||
|
||||
|
@ -911,7 +973,7 @@ public class RecipientDatabase extends Database {
|
|||
if (update(updateQuery, valuesToSet)) {
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
Recipient.live(id).refresh();
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -957,7 +1019,7 @@ public class RecipientDatabase extends Database {
|
|||
if (update(id, contentValues)) {
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
Recipient.live(id).refresh();
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -966,6 +1028,11 @@ public class RecipientDatabase extends Database {
|
|||
contentValues.put(SIGNAL_PROFILE_AVATAR, profileAvatar);
|
||||
if (update(id, contentValues)) {
|
||||
Recipient.live(id).refresh();
|
||||
|
||||
if (id.equals(Recipient.self().getId())) {
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -975,7 +1042,7 @@ public class RecipientDatabase extends Database {
|
|||
if (update(id, contentValues)) {
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
Recipient.live(id).refresh();
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -993,6 +1060,7 @@ public class RecipientDatabase extends Database {
|
|||
if (update(id, contentValues)) {
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
Recipient.live(id).refresh();
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1008,8 +1076,10 @@ public class RecipientDatabase extends Database {
|
|||
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(USERNAME, username);
|
||||
update(id, contentValues);
|
||||
Recipient.live(id).refresh();
|
||||
if (update(id, contentValues)) {
|
||||
Recipient.live(id).refresh();
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void clearUsernameIfExists(@NonNull String username) {
|
||||
|
@ -1106,7 +1176,7 @@ public class RecipientDatabase extends Database {
|
|||
contentValues.put(REGISTERED, registeredState.getId());
|
||||
|
||||
if (registeredState == RegisteredState.REGISTERED) {
|
||||
contentValues.put(STORAGE_SERVICE_KEY, Base64.encodeBytes(StorageSyncHelper.generateKey()));
|
||||
contentValues.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()));
|
||||
}
|
||||
|
||||
if (update(id, contentValues)) {
|
||||
|
@ -1213,9 +1283,8 @@ public class RecipientDatabase extends Database {
|
|||
String selection = BLOCKED + " = ? AND " +
|
||||
REGISTERED + " = ? AND " +
|
||||
GROUP_ID + " IS NULL AND " +
|
||||
"(" + SYSTEM_DISPLAY_NAME + " NOT NULL OR " + PROFILE_SHARING + " = ?) AND " +
|
||||
"(" + SYSTEM_DISPLAY_NAME + " NOT NULL OR " + SEARCH_PROFILE_NAME + " NOT NULL OR " + USERNAME + " NOT NULL)";
|
||||
String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), "1" };
|
||||
String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()) };
|
||||
String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SEARCH_PROFILE_NAME + ", " + USERNAME + ", " + PHONE;
|
||||
|
||||
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
|
||||
|
@ -1228,14 +1297,13 @@ public class RecipientDatabase extends Database {
|
|||
String selection = BLOCKED + " = ? AND " +
|
||||
REGISTERED + " = ? AND " +
|
||||
GROUP_ID + " IS NULL AND " +
|
||||
"(" + SYSTEM_DISPLAY_NAME + " NOT NULL OR " + PROFILE_SHARING + " = ? OR " + USERNAME + " NOT NULL) AND " +
|
||||
"(" +
|
||||
PHONE + " LIKE ? OR " +
|
||||
SYSTEM_DISPLAY_NAME + " LIKE ? OR " +
|
||||
SEARCH_PROFILE_NAME + " LIKE ? OR " +
|
||||
USERNAME + " LIKE ?" +
|
||||
")";
|
||||
String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), "1", query, query, query, query };
|
||||
String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), query, query, query, query };
|
||||
String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SEARCH_PROFILE_NAME + ", " + PHONE;
|
||||
|
||||
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
|
||||
|
@ -1338,10 +1406,10 @@ public class RecipientDatabase extends Database {
|
|||
db.update(TABLE_NAME, setBlocked, UUID + " = ?", new String[] { uuid });
|
||||
}
|
||||
|
||||
List<String> groupIdStrings = Stream.of(groupIds).map(g -> GroupUtil.getEncodedId(g, false)).toList();
|
||||
List<GroupId> groupIdStrings = Stream.of(groupIds).map(GroupId::v1).toList();
|
||||
|
||||
for (String groupId : groupIdStrings) {
|
||||
db.update(TABLE_NAME, setBlocked, GROUP_ID + " = ?", new String[] { groupId });
|
||||
for (GroupId groupId : groupIdStrings) {
|
||||
db.update(TABLE_NAME, setBlocked, GROUP_ID + " = ?", new String[] { groupId.toString() });
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
|
@ -1359,7 +1427,7 @@ public class RecipientDatabase extends Database {
|
|||
try {
|
||||
for (Map.Entry<RecipientId, byte[]> entry : keys.entrySet()) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(STORAGE_SERVICE_KEY, Base64.encodeBytes(entry.getValue()));
|
||||
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(entry.getValue()));
|
||||
db.update(TABLE_NAME, values, ID_WHERE, new String[] { entry.getKey().serialize() });
|
||||
}
|
||||
|
||||
|
@ -1399,7 +1467,7 @@ public class RecipientDatabase extends Database {
|
|||
query += "(" + DIRTY + " < ? OR " + DIRTY + " = ?)";
|
||||
args = SqlUtil.appendArg(args, String.valueOf(DirtyState.DELETE.getId()));
|
||||
|
||||
contentValues.put(STORAGE_SERVICE_KEY, Base64.encodeBytes(StorageSyncHelper.generateKey()));
|
||||
contentValues.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()));
|
||||
break;
|
||||
case DELETE:
|
||||
query += "(" + DIRTY + " < ? OR " + DIRTY + " = ?)";
|
||||
|
@ -1528,7 +1596,7 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
|
||||
private void markAllRelevantEntriesDirty() {
|
||||
String query = SYSTEM_INFO_PENDING + " = ? AND " + STORAGE_SERVICE_KEY + " NOT NULL AND " + DIRTY + " < ?";
|
||||
String query = SYSTEM_INFO_PENDING + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + DIRTY + " < ?";
|
||||
String[] args = new String[] { "1", String.valueOf(DirtyState.UPDATE.getId()) };
|
||||
|
||||
ContentValues values = new ContentValues(1);
|
||||
|
@ -1553,6 +1621,10 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
private static @NonNull String nullIfEmpty(String column) {
|
||||
return "NULLIF(" + column + ", '')";
|
||||
}
|
||||
|
||||
public interface ColorUpdater {
|
||||
MaterialColor update(@NonNull String name, @Nullable String color);
|
||||
}
|
||||
|
@ -1563,7 +1635,7 @@ public class RecipientDatabase extends Database {
|
|||
private final String username;
|
||||
private final String e164;
|
||||
private final String email;
|
||||
private final String groupId;
|
||||
private final GroupId groupId;
|
||||
private final GroupType groupType;
|
||||
private final boolean blocked;
|
||||
private final long muteUntil;
|
||||
|
@ -1590,7 +1662,7 @@ public class RecipientDatabase extends Database {
|
|||
private final Recipient.Capability uuidCapability;
|
||||
private final Recipient.Capability groupsV2Capability;
|
||||
private final InsightsBannerTier insightsBannerTier;
|
||||
private final byte[] storageKey;
|
||||
private final byte[] storageId;
|
||||
private final byte[] identityKey;
|
||||
private final IdentityDatabase.VerifiedStatus identityStatus;
|
||||
|
||||
|
@ -1599,7 +1671,7 @@ public class RecipientDatabase extends Database {
|
|||
@Nullable String username,
|
||||
@Nullable String e164,
|
||||
@Nullable String email,
|
||||
@Nullable String groupId,
|
||||
@Nullable GroupId groupId,
|
||||
@NonNull GroupType groupType,
|
||||
boolean blocked,
|
||||
long muteUntil,
|
||||
|
@ -1626,7 +1698,7 @@ public class RecipientDatabase extends Database {
|
|||
Recipient.Capability uuidCapability,
|
||||
Recipient.Capability groupsV2Capability,
|
||||
@NonNull InsightsBannerTier insightsBannerTier,
|
||||
@Nullable byte[] storageKey,
|
||||
@Nullable byte[] storageId,
|
||||
@Nullable byte[] identityKey,
|
||||
@NonNull IdentityDatabase.VerifiedStatus identityStatus)
|
||||
{
|
||||
|
@ -1662,7 +1734,7 @@ public class RecipientDatabase extends Database {
|
|||
this.uuidCapability = uuidCapability;
|
||||
this.groupsV2Capability = groupsV2Capability;
|
||||
this.insightsBannerTier = insightsBannerTier;
|
||||
this.storageKey = storageKey;
|
||||
this.storageId = storageId;
|
||||
this.identityKey = identityKey;
|
||||
this.identityStatus = identityStatus;
|
||||
}
|
||||
|
@ -1687,7 +1759,7 @@ public class RecipientDatabase extends Database {
|
|||
return email;
|
||||
}
|
||||
|
||||
public @Nullable String getGroupId() {
|
||||
public @Nullable GroupId getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
|
@ -1795,8 +1867,8 @@ public class RecipientDatabase extends Database {
|
|||
return groupsV2Capability;
|
||||
}
|
||||
|
||||
public @Nullable byte[] getStorageKey() {
|
||||
return storageKey;
|
||||
public @Nullable byte[] getStorageId() {
|
||||
return storageId;
|
||||
}
|
||||
|
||||
public @Nullable byte[] getIdentityKey() {
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Context;
|
|||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
@ -27,6 +28,7 @@ import com.annimon.stream.Stream;
|
|||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteStatement;
|
||||
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
@ -231,7 +233,7 @@ public class SmsMigrator {
|
|||
|
||||
List<RecipientId> recipientIds = Stream.of(ourRecipients).map(Recipient::getId).toList();
|
||||
|
||||
String ourGroupId = DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(recipientIds, true);
|
||||
GroupId ourGroupId = DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(recipientIds, true);
|
||||
RecipientId ourGroupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(ourGroupId);
|
||||
Recipient ourGroupRecipient = Recipient.resolved(ourGroupRecipientId);
|
||||
long ourThreadId = threadDatabase.getThreadIdFor(ourGroupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
|
||||
|
|
|
@ -30,6 +30,8 @@ import java.io.File;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.List;
|
||||
|
||||
public class StickerDatabase extends Database {
|
||||
|
|
|
@ -12,11 +12,11 @@ import net.sqlcipher.database.SQLiteDatabase;
|
|||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
@ -28,11 +28,11 @@ public class StorageKeyDatabase extends Database {
|
|||
private static final String TABLE_NAME = "storage_key";
|
||||
private static final String ID = "_id";
|
||||
private static final String TYPE = "type";
|
||||
private static final String KEY = "key";
|
||||
private static final String STORAGE_ID = "key";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
TYPE + " INTEGER, " +
|
||||
KEY + " TEXT UNIQUE)";
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
TYPE + " INTEGER, " +
|
||||
STORAGE_ID + " TEXT UNIQUE)";
|
||||
|
||||
public static final String[] CREATE_INDEXES = new String[] {
|
||||
"CREATE INDEX IF NOT EXISTS storage_key_type_index ON " + TABLE_NAME + " (" + TYPE + ");"
|
||||
|
@ -42,14 +42,15 @@ public class StorageKeyDatabase extends Database {
|
|||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
public List<byte[]> getAllKeys() {
|
||||
List<byte[]> keys = new ArrayList<>();
|
||||
public List<StorageId> getAllKeys() {
|
||||
List<StorageId> keys = new ArrayList<>();
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, null, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String keyEncoded = cursor.getString(cursor.getColumnIndexOrThrow(KEY));
|
||||
String keyEncoded = cursor.getString(cursor.getColumnIndexOrThrow(STORAGE_ID));
|
||||
int type = cursor.getInt(cursor.getColumnIndexOrThrow(TYPE));
|
||||
try {
|
||||
keys.add(Base64.decode(keyEncoded));
|
||||
keys.add(StorageId.forType(Base64.decode(keyEncoded), type));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
@ -59,14 +60,14 @@ public class StorageKeyDatabase extends Database {
|
|||
return keys;
|
||||
}
|
||||
|
||||
public @Nullable SignalStorageRecord getByKey(@NonNull byte[] key) {
|
||||
String query = KEY + " = ?";
|
||||
String[] args = new String[] { Base64.encodeBytes(key) };
|
||||
public @Nullable SignalStorageRecord getById(@NonNull byte[] rawId) {
|
||||
String query = STORAGE_ID + " = ?";
|
||||
String[] args = new String[] { Base64.encodeBytes(rawId) };
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, query, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int type = cursor.getInt(cursor.getColumnIndexOrThrow(TYPE));
|
||||
return SignalStorageRecord.forUnknown(key, type);
|
||||
return SignalStorageRecord.forUnknown(StorageId.forType(rawId, type));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -83,15 +84,15 @@ public class StorageKeyDatabase extends Database {
|
|||
for (SignalStorageRecord insert : inserts) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(TYPE, insert.getType());
|
||||
values.put(KEY, Base64.encodeBytes(insert.getKey()));
|
||||
values.put(STORAGE_ID, Base64.encodeBytes(insert.getId().getRaw()));
|
||||
|
||||
db.insert(TABLE_NAME, null, values);
|
||||
}
|
||||
|
||||
String deleteQuery = KEY + " = ?";
|
||||
String deleteQuery = STORAGE_ID + " = ?";
|
||||
|
||||
for (SignalStorageRecord delete : deletes) {
|
||||
String[] args = new String[] { Base64.encodeBytes(delete.getKey()) };
|
||||
String[] args = new String[] { Base64.encodeBytes(delete.getId().getRaw()) };
|
||||
db.delete(TABLE_NAME, deleteQuery, args);
|
||||
}
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.mms.SlideDeck;
|
|||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
@ -53,6 +54,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
|||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
|
@ -422,10 +424,48 @@ public class ThreadDatabase extends Database {
|
|||
return getConversationList("1");
|
||||
}
|
||||
|
||||
public boolean isArchived(@NonNull RecipientId recipientId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String query = RECIPIENT_ID + " = ?";
|
||||
String[] args = new String[]{ recipientId.serialize() };
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[] { ARCHIVED }, query, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getInt(cursor.getColumnIndexOrThrow(ARCHIVED)) == 1;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setArchived(@NonNull RecipientId recipientId, boolean status) {
|
||||
setArchived(Collections.singletonMap(recipientId, status));
|
||||
}
|
||||
|
||||
public void setArchived(@NonNull Map<RecipientId, Boolean> status) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
String query = RECIPIENT_ID + " = ?";
|
||||
|
||||
for (Map.Entry<RecipientId, Boolean> entry : status.entrySet()) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(ARCHIVED, entry.getValue() ? "1" : "0");
|
||||
db.update(TABLE_NAME, values, query, new String[] { entry.getKey().serialize() });
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull Set<RecipientId> getArchivedRecipients() {
|
||||
Set<RecipientId> archived = new HashSet<>();
|
||||
|
||||
try (Cursor cursor = DatabaseFactory.getThreadDatabase(context).getArchivedConversationList()) {
|
||||
try (Cursor cursor = getArchivedConversationList()) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
archived.add(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.RECIPIENT_ID))));
|
||||
}
|
||||
|
@ -488,6 +528,12 @@ public class ThreadDatabase extends Database {
|
|||
|
||||
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""});
|
||||
notifyConversationListListeners();
|
||||
|
||||
Recipient recipient = getRecipientForThreadId(threadId);
|
||||
if (recipient != null) {
|
||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipient.getId());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void unarchiveConversation(long threadId) {
|
||||
|
@ -497,6 +543,12 @@ public class ThreadDatabase extends Database {
|
|||
|
||||
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""});
|
||||
notifyConversationListListeners();
|
||||
|
||||
Recipient recipient = getRecipientForThreadId(threadId);
|
||||
if (recipient != null) {
|
||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipient.getId());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void setLastSeen(long threadId) {
|
||||
|
|
|
@ -10,9 +10,9 @@ import android.database.sqlite.SQLiteDatabase;
|
|||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.net.Uri;
|
||||
import android.provider.ContactsContract;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.i18n.phonenumbers.NumberParseException;
|
||||
|
@ -35,13 +35,14 @@ import org.thoughtcrime.securesms.database.PushDatabase;
|
|||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.DelimiterUtil;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
@ -1274,7 +1275,7 @@ public class ClassicOpenHelper extends SQLiteOpenHelper {
|
|||
while (cursor != null && cursor.moveToNext()) {
|
||||
String address = cursor.getString(cursor.getColumnIndexOrThrow("recipient_ids"));
|
||||
|
||||
if (!TextUtils.isEmpty(address) && !GroupUtil.isEncodedGroup(address) && !NumberUtil.isValidEmail(address)) {
|
||||
if (!TextUtils.isEmpty(address) && !GroupId.isEncodedGroup(address) && !NumberUtil.isValidEmail(address)) {
|
||||
Uri lookup = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(address));
|
||||
|
||||
try (Cursor contactCursor = context.getContentResolver().query(lookup, new String[] {ContactsContract.PhoneLookup.DISPLAY_NAME,
|
||||
|
|
|
@ -9,10 +9,10 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
|
||||
import org.thoughtcrime.securesms.util.DelimiterUtil;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.HashSet;
|
||||
|
@ -153,7 +153,7 @@ public class RecipientIdMigrationHelper {
|
|||
try (Cursor cursor = db.query("recipient_preferences", null, null, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String address = cursor.getString(cursor.getColumnIndexOrThrow("recipient_ids"));
|
||||
boolean isGroup = GroupUtil.isEncodedGroup(address);
|
||||
boolean isGroup = GroupId.isEncodedGroup(address);
|
||||
boolean isEmail = !isGroup && NumberUtil.isValidEmail(address);
|
||||
boolean isPhone = !isGroup && !isEmail;
|
||||
|
||||
|
|
|
@ -21,7 +21,8 @@ import net.sqlcipher.database.SQLiteDatabase;
|
|||
import net.sqlcipher.database.SQLiteDatabaseHook;
|
||||
import net.sqlcipher.database.SQLiteOpenHelper;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.sync.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
|
@ -44,20 +45,20 @@ import org.thoughtcrime.securesms.database.StickerDatabase;
|
|||
import org.thoughtcrime.securesms.database.StorageKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||
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.storage.StorageSyncHelper;
|
||||
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.io.FilenameFilter;
|
||||
import java.util.List;
|
||||
|
||||
public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
|
@ -116,8 +117,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
private static final int STORAGE_SERVICE_ACTIVE = 50;
|
||||
private static final int GROUPS_V2_RECIPIENT_CAPABILITY = 51;
|
||||
private static final int TRANSFER_FILE_CLEANUP = 52;
|
||||
private static final int PROFILE_DATA_MIGRATION = 53;
|
||||
|
||||
private static final int DATABASE_VERSION = 52;
|
||||
private static final int DATABASE_VERSION = 53;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
|
@ -350,7 +352,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
String displayName = NotificationChannels.getChannelDisplayNameFor(context, systemName, profileName, null, address);
|
||||
boolean vibrateEnabled = vibrateState == 0 ? TextSecurePreferences.isNotificationVibrateEnabled(context) : vibrateState == 1;
|
||||
|
||||
if (GroupUtil.isEncodedGroup(address)) {
|
||||
if (GroupId.isEncodedGroup(address)) {
|
||||
try(Cursor groupCursor = db.rawQuery("SELECT title FROM groups WHERE group_id = ?", new String[] { address })) {
|
||||
if (groupCursor != null && groupCursor.moveToFirst()) {
|
||||
String title = groupCursor.getString(groupCursor.getColumnIndexOrThrow("title"));
|
||||
|
@ -546,11 +548,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
values.put("phone", localNumber);
|
||||
values.put("registered", 1);
|
||||
values.put("profile_sharing", 1);
|
||||
values.put("signal_profile_name", TextSecurePreferences.getProfileName(context).getGivenName());
|
||||
db.insert("recipient", null, values);
|
||||
} else {
|
||||
db.execSQL("UPDATE recipient SET registered = ?, profile_sharing = ?, signal_profile_name = ? WHERE phone = ?",
|
||||
new String[] { "1", "1", TextSecurePreferences.getProfileName(context).getGivenName(), localNumber });
|
||||
db.execSQL("UPDATE recipient SET registered = ?, profile_sharing = ? WHERE phone = ?",
|
||||
new String[] { "1", "1", localNumber });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -790,6 +791,17 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
}
|
||||
}
|
||||
|
||||
if (oldVersion < PROFILE_DATA_MIGRATION) {
|
||||
String localNumber = TextSecurePreferences.getLocalNumber(context);
|
||||
if (localNumber != null) {
|
||||
String encodedProfileName = PreferenceManager.getDefaultSharedPreferences(context).getString("pref_profile_name", null);
|
||||
ProfileName profileName = ProfileName.fromSerialized(encodedProfileName);
|
||||
|
||||
db.execSQL("UPDATE recipient SET signal_profile_name = ?, profile_family_name = ?, profile_joined_name = ? WHERE phone = ?",
|
||||
new String[] { profileName.getGivenName(), profileName.getFamilyName(), profileName.toString(), localNumber });
|
||||
}
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
package org.thoughtcrime.securesms.experienceupgrades;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.TypingIndicatorView;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public class StickersIntroFragment extends Fragment {
|
||||
|
||||
private Controller controller;
|
||||
|
||||
public static StickersIntroFragment newInstance() {
|
||||
return new StickersIntroFragment();
|
||||
}
|
||||
|
||||
public StickersIntroFragment() {}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (!(getActivity() instanceof Controller)) {
|
||||
throw new IllegalStateException("Parent activity must implement the Controller interface.");
|
||||
}
|
||||
|
||||
controller = (Controller) getActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.experience_upgrade_stickers_fragment, container, false);
|
||||
View goButton = view.findViewById(R.id.stickers_experience_go_button);
|
||||
|
||||
((LottieAnimationView) view.findViewById(R.id.stickers_experience_animation)).playAnimation();
|
||||
|
||||
goButton.setOnClickListener(v -> controller.onStickersFinished());
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
public interface Controller {
|
||||
void onStickersFinished();
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import androidx.annotation.WorkerThread;
|
|||
|
||||
import org.thoughtcrime.securesms.IncomingMessageProcessor;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker;
|
||||
import org.thoughtcrime.securesms.jobs.MarkerJob;
|
||||
|
@ -75,7 +76,7 @@ public class RestStrategy implements MessageRetriever.Strategy {
|
|||
|
||||
jobManager.addListener(markerJob.getId(), new JobTracker.JobListener() {
|
||||
@Override
|
||||
public void onStateChanged(@NonNull JobTracker.JobState jobState) {
|
||||
public void onStateChanged(@NonNull Job job, @NonNull JobTracker.JobState jobState) {
|
||||
if (jobState.isComplete()) {
|
||||
jobManager.removeListener(this);
|
||||
latch.countDown();
|
||||
|
|
|
@ -108,7 +108,7 @@ public class GiphyActivity extends PassphraseRequiredActionBarActivity
|
|||
}
|
||||
|
||||
private @ColorInt int getConversationColor() {
|
||||
return getIntent().getIntExtra(EXTRA_COLOR, ActivityCompat.getColor(this, R.color.signal_primary));
|
||||
return getIntent().getIntExtra(EXTRA_COLOR, ActivityCompat.getColor(this, R.color.core_ultramarine));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -13,6 +13,7 @@ import java.io.File;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.SocketException;
|
||||
|
||||
public class EncryptedCacheEncoder extends EncryptedCoder implements Encoder<InputStream> {
|
||||
|
||||
|
@ -42,12 +43,14 @@ public class EncryptedCacheEncoder extends EncryptedCoder implements Encoder<Inp
|
|||
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
if (e instanceof SocketException) {
|
||||
Log.d(TAG, "Socket exception. Likely a cancellation.");
|
||||
} else {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
byteArrayPool.put(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
package org.thoughtcrime.securesms.groups;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class GroupId {
|
||||
|
||||
private static final String ENCODED_SIGNAL_GROUP_PREFIX = "__textsecure_group__!";
|
||||
private static final String ENCODED_MMS_GROUP_PREFIX = "__signal_mms_group__!";
|
||||
|
||||
private final String encodedId;
|
||||
|
||||
private GroupId(@NonNull String encodedId) {
|
||||
this.encodedId = encodedId;
|
||||
}
|
||||
|
||||
public static @NonNull GroupId v1(byte[] gv1GroupIdBytes) {
|
||||
return new GroupId(ENCODED_SIGNAL_GROUP_PREFIX + Hex.toStringCondensed(gv1GroupIdBytes));
|
||||
}
|
||||
|
||||
public static @NonNull GroupId mms(byte[] mmsGroupIdBytes) {
|
||||
return new GroupId(ENCODED_MMS_GROUP_PREFIX + Hex.toStringCondensed(mmsGroupIdBytes));
|
||||
}
|
||||
|
||||
public static @NonNull GroupId parse(@NonNull String encodedGroupId) {
|
||||
try {
|
||||
if (!isEncodedGroup(encodedGroupId)) {
|
||||
throw new IOException("Invalid encoding");
|
||||
}
|
||||
|
||||
byte[] bytes = extractDecodedId(encodedGroupId);
|
||||
return isMmsGroup(encodedGroupId) ? mms(bytes) : v1(bytes);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable GroupId parseNullable(@Nullable String encodedGroupId) {
|
||||
if (encodedGroupId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parse(encodedGroupId);
|
||||
}
|
||||
|
||||
public static boolean isEncodedGroup(@NonNull String groupId) {
|
||||
return groupId.startsWith(ENCODED_SIGNAL_GROUP_PREFIX) || groupId.startsWith(ENCODED_MMS_GROUP_PREFIX);
|
||||
}
|
||||
|
||||
private static byte[] extractDecodedId(@NonNull String encodedGroupId) throws IOException {
|
||||
return Hex.fromStringCondensed(encodedGroupId.split("!", 2)[1]);
|
||||
}
|
||||
|
||||
private static boolean isMmsGroup(@NonNull String groupId) {
|
||||
return groupId.startsWith(ENCODED_MMS_GROUP_PREFIX);
|
||||
}
|
||||
|
||||
public byte[] getDecodedId() {
|
||||
try {
|
||||
return extractDecodedId(encodedId);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isMmsGroup() {
|
||||
return isMmsGroup(encodedId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (obj instanceof GroupId) {
|
||||
return ((GroupId) obj).encodedId.equals(encodedId);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return encodedId.hashCode();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return encodedId;
|
||||
}
|
||||
}
|
|
@ -29,7 +29,7 @@ public final class GroupManager {
|
|||
}
|
||||
|
||||
public static GroupActionResult updateGroup(@NonNull Context context,
|
||||
@NonNull String groupId,
|
||||
@NonNull GroupId groupId,
|
||||
@NonNull Set<Recipient> members,
|
||||
@Nullable Bitmap avatar,
|
||||
@Nullable String name)
|
||||
|
@ -51,7 +51,7 @@ public final class GroupManager {
|
|||
|
||||
@WorkerThread
|
||||
public static boolean leaveGroup(@NonNull Context context, @NonNull Recipient groupRecipient) {
|
||||
String groupId = groupRecipient.requireGroupId();
|
||||
GroupId groupId = groupRecipient.requireGroupId();
|
||||
|
||||
return V1GroupManager.leaveGroup(context, groupId, groupRecipient);
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ import org.thoughtcrime.securesms.sms.IncomingGroupMessage;
|
|||
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
|
||||
|
@ -63,7 +62,7 @@ public class GroupMessageProcessor {
|
|||
|
||||
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
|
||||
SignalServiceGroup group = message.getGroupInfo().get();
|
||||
String id = GroupUtil.getEncodedId(group.getGroupId(), false);
|
||||
GroupId id = GroupId.v1(group.getGroupId());
|
||||
Optional<GroupRecord> record = database.getGroup(id);
|
||||
|
||||
if (record.isPresent() && group.getType() == Type.UPDATE) {
|
||||
|
@ -73,7 +72,7 @@ public class GroupMessageProcessor {
|
|||
} else if (record.isPresent() && group.getType() == Type.QUIT) {
|
||||
return handleGroupLeave(context, content, group, record.get(), outgoing);
|
||||
} else if (record.isPresent() && group.getType() == Type.REQUEST_INFO) {
|
||||
return handleGroupInfoRequest(context, content, group, record.get());
|
||||
return handleGroupInfoRequest(context, content, record.get());
|
||||
} else {
|
||||
Log.w(TAG, "Received unknown type, ignoring...");
|
||||
return null;
|
||||
|
@ -86,7 +85,7 @@ public class GroupMessageProcessor {
|
|||
boolean outgoing)
|
||||
{
|
||||
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
|
||||
String id = GroupUtil.getEncodedId(group.getGroupId(), false);
|
||||
GroupId id = GroupId.v1(group.getGroupId());
|
||||
GroupContext.Builder builder = createGroupContext(group);
|
||||
builder.setType(GroupContext.Type.UPDATE);
|
||||
|
||||
|
@ -106,7 +105,7 @@ public class GroupMessageProcessor {
|
|||
|
||||
if (FeatureFlags.messageRequests() && (sender.isSystemContact() || sender.isProfileSharing())) {
|
||||
Log.i(TAG, "Auto-enabling profile sharing because 'adder' is trusted. contact: " + sender.isSystemContact() + ", profileSharing: " + sender.isProfileSharing());
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.external(context, id).getId(), true);
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.externalGroup(context, id).getId(), true);
|
||||
}
|
||||
|
||||
return storeMessage(context, content, group, builder.build(), outgoing);
|
||||
|
@ -120,7 +119,7 @@ public class GroupMessageProcessor {
|
|||
{
|
||||
|
||||
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
|
||||
String id = GroupUtil.getEncodedId(group.getGroupId(), false);
|
||||
GroupId id = GroupId.v1(group.getGroupId());
|
||||
|
||||
Set<RecipientId> recordMembers = new HashSet<>(groupRecord.getMembers());
|
||||
Set<RecipientId> messageMembers = new HashSet<>();
|
||||
|
@ -178,13 +177,12 @@ public class GroupMessageProcessor {
|
|||
|
||||
private static Long handleGroupInfoRequest(@NonNull Context context,
|
||||
@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceGroup group,
|
||||
@NonNull GroupRecord record)
|
||||
{
|
||||
Recipient sender = Recipient.externalPush(context, content.getSender());
|
||||
|
||||
if (record.getMembers().contains(sender.getId())) {
|
||||
ApplicationDependencies.getJobManager().add(new PushGroupUpdateJob(sender.getId(), group.getGroupId()));
|
||||
ApplicationDependencies.getJobManager().add(new PushGroupUpdateJob(sender.getId(), record.getId()));
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -197,7 +195,7 @@ public class GroupMessageProcessor {
|
|||
boolean outgoing)
|
||||
{
|
||||
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
|
||||
String id = GroupUtil.getEncodedId(group.getGroupId(), false);
|
||||
GroupId id = GroupId.v1(group.getGroupId());
|
||||
List<RecipientId> members = record.getMembers();
|
||||
|
||||
GroupContext.Builder builder = createGroupContext(group);
|
||||
|
@ -222,13 +220,13 @@ public class GroupMessageProcessor {
|
|||
{
|
||||
if (group.getAvatar().isPresent()) {
|
||||
ApplicationDependencies.getJobManager()
|
||||
.add(new AvatarDownloadJob(group.getGroupId()));
|
||||
.add(new AvatarDownloadJob(GroupId.v1(group.getGroupId())));
|
||||
}
|
||||
|
||||
try {
|
||||
if (outgoing) {
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
RecipientId recipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(GroupUtil.getEncodedId(group.getGroupId(), false));
|
||||
RecipientId recipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(GroupId.v1(group.getGroupId()));
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, content.getTimestamp(), 0, false, null, Collections.emptyList(), Collections.emptyList());
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
|
@ -240,7 +238,7 @@ public class GroupMessageProcessor {
|
|||
} else {
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
String body = Base64.encodeBytes(storage.toByteArray());
|
||||
IncomingTextMessage incoming = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(), content.getSenderDevice(), content.getTimestamp(), body, Optional.of(GroupUtil.getEncodedId(group.getGroupId(), false)), 0, content.isNeedsReceipt());
|
||||
IncomingTextMessage incoming = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(), content.getSenderDevice(), content.getTimestamp(), body, Optional.of(GroupId.v1(group.getGroupId())), 0, content.isNeedsReceipt());
|
||||
IncomingGroupMessage groupMessage = new IncomingGroupMessage(incoming, storage, body);
|
||||
|
||||
Optional<InsertResult> insertResult = smsDatabase.insertMessageInbox(groupMessage);
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.groups;
|
|||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
@ -11,7 +12,6 @@ import com.google.protobuf.ByteString;
|
|||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
|
@ -28,12 +28,10 @@ import org.thoughtcrime.securesms.sms.MessageSender;
|
|||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
@ -49,7 +47,7 @@ final class V1GroupManager {
|
|||
{
|
||||
final byte[] avatarBytes = BitmapUtil.toByteArray(avatar);
|
||||
final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
final String groupId = GroupUtil.getEncodedId(groupDatabase.allocateGroupId(), mms);
|
||||
final GroupId groupId = GroupDatabase.allocateGroupId(mms);
|
||||
final RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||
final Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
||||
|
||||
|
@ -67,7 +65,7 @@ final class V1GroupManager {
|
|||
}
|
||||
|
||||
static GroupActionResult updateGroup(@NonNull Context context,
|
||||
@NonNull String groupId,
|
||||
@NonNull GroupId groupId,
|
||||
@NonNull Set<RecipientId> memberAddresses,
|
||||
@Nullable Bitmap avatar,
|
||||
@Nullable String name)
|
||||
|
@ -81,7 +79,7 @@ final class V1GroupManager {
|
|||
groupDatabase.updateTitle(groupId, name);
|
||||
groupDatabase.updateAvatar(groupId, avatarBytes);
|
||||
|
||||
if (!GroupUtil.isMmsGroup(groupId)) {
|
||||
if (!groupId.isMmsGroup()) {
|
||||
return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes);
|
||||
} else {
|
||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||
|
@ -92,48 +90,44 @@ final class V1GroupManager {
|
|||
}
|
||||
|
||||
private static GroupActionResult sendGroupUpdate(@NonNull Context context,
|
||||
@NonNull String groupId,
|
||||
@NonNull GroupId groupId,
|
||||
@NonNull Set<RecipientId> members,
|
||||
@Nullable String groupName,
|
||||
@Nullable byte[] avatar)
|
||||
{
|
||||
try {
|
||||
Attachment avatarAttachment = null;
|
||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
||||
Attachment avatarAttachment = null;
|
||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
||||
|
||||
List<GroupContext.Member> uuidMembers = new LinkedList<>();
|
||||
List<String> e164Members = new LinkedList<>();
|
||||
List<GroupContext.Member> uuidMembers = new LinkedList<>();
|
||||
List<String> e164Members = new LinkedList<>();
|
||||
|
||||
for (RecipientId member : members) {
|
||||
Recipient recipient = Recipient.resolved(member);
|
||||
uuidMembers.add(GroupMessageProcessor.createMember(RecipientUtil.toSignalServiceAddress(context, recipient)));
|
||||
}
|
||||
|
||||
GroupContext.Builder groupContextBuilder = GroupContext.newBuilder()
|
||||
.setId(ByteString.copyFrom(GroupUtil.getDecodedId(groupId)))
|
||||
.setType(GroupContext.Type.UPDATE)
|
||||
.addAllMembersE164(e164Members)
|
||||
.addAllMembers(uuidMembers);
|
||||
if (groupName != null) groupContextBuilder.setName(groupName);
|
||||
GroupContext groupContext = groupContextBuilder.build();
|
||||
|
||||
if (avatar != null) {
|
||||
Uri avatarUri = BlobProvider.getInstance().forData(avatar).createForSingleUseInMemory();
|
||||
avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null, null, null, null);
|
||||
}
|
||||
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, false, null, Collections.emptyList(), Collections.emptyList());
|
||||
long threadId = MessageSender.send(context, outgoingMessage, -1, false, null);
|
||||
|
||||
return new GroupActionResult(groupRecipient, threadId);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
for (RecipientId member : members) {
|
||||
Recipient recipient = Recipient.resolved(member);
|
||||
uuidMembers.add(GroupMessageProcessor.createMember(RecipientUtil.toSignalServiceAddress(context, recipient)));
|
||||
}
|
||||
|
||||
GroupContext.Builder groupContextBuilder = GroupContext.newBuilder()
|
||||
.setId(ByteString.copyFrom(groupId.getDecodedId()))
|
||||
.setType(GroupContext.Type.UPDATE)
|
||||
.addAllMembersE164(e164Members)
|
||||
.addAllMembers(uuidMembers);
|
||||
if (groupName != null) groupContextBuilder.setName(groupName);
|
||||
GroupContext groupContext = groupContextBuilder.build();
|
||||
|
||||
if (avatar != null) {
|
||||
Uri avatarUri = BlobProvider.getInstance().forData(avatar).createForSingleUseInMemory();
|
||||
avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null, null, null, null);
|
||||
}
|
||||
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, false, null, Collections.emptyList(), Collections.emptyList());
|
||||
long threadId = MessageSender.send(context, outgoingMessage, -1, false, null);
|
||||
|
||||
return new GroupActionResult(groupRecipient, threadId);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
static boolean leaveGroup(@NonNull Context context, @NonNull String groupId, @NonNull Recipient groupRecipient) {
|
||||
static boolean leaveGroup(@NonNull Context context, @NonNull GroupId groupId, @NonNull Recipient groupRecipient) {
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
||||
Optional<OutgoingGroupMediaMessage> leaveMessage = GroupUtil.createGroupLeaveMessage(context, groupRecipient);
|
||||
|
||||
|
|
|
@ -320,7 +320,7 @@ public final class ImageEditorView extends FrameLayout {
|
|||
|
||||
private EditSession startADrawingSession(@NonNull PointF point) {
|
||||
BezierDrawingRenderer renderer = new BezierDrawingRenderer(color, thickness * Bounds.FULL_BOUNDS.width(), cap, model.findCropRelativeToRoot());
|
||||
EditorElement element = new EditorElement(renderer);
|
||||
EditorElement element = new EditorElement(renderer, EditorModel.Z_DRAWING);
|
||||
model.addElementCentered(element, 1);
|
||||
|
||||
Matrix elementInverseMatrix = model.findElementInverseMatrix(element, viewMatrix);
|
||||
|
|
|
@ -3,12 +3,15 @@ package org.thoughtcrime.securesms.imageeditor.model;
|
|||
import android.graphics.Matrix;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.imageeditor.Renderer;
|
||||
import org.thoughtcrime.securesms.imageeditor.RendererContext;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
@ -32,10 +35,13 @@ import java.util.UUID;
|
|||
*/
|
||||
public final class EditorElement implements Parcelable {
|
||||
|
||||
private static final Comparator<EditorElement> Z_ORDER_COMPARATOR = (e1, e2) -> Integer.compare(e1.zOrder, e2.zOrder);
|
||||
|
||||
private final UUID id;
|
||||
private final EditorFlags flags;
|
||||
private final Matrix localMatrix = new Matrix();
|
||||
private final Matrix editorMatrix = new Matrix();
|
||||
private final int zOrder;
|
||||
|
||||
@Nullable
|
||||
private final Renderer renderer;
|
||||
|
@ -54,9 +60,14 @@ public final class EditorElement implements Parcelable {
|
|||
private AlphaAnimation alphaAnimation = AlphaAnimation.NULL_1;
|
||||
|
||||
public EditorElement(@Nullable Renderer renderer) {
|
||||
this(renderer, 0);
|
||||
}
|
||||
|
||||
public EditorElement(@Nullable Renderer renderer, int zOrder) {
|
||||
this.id = UUID.randomUUID();
|
||||
this.flags = new EditorFlags();
|
||||
this.renderer = renderer;
|
||||
this.zOrder = zOrder;
|
||||
}
|
||||
|
||||
private EditorElement(Parcel in) {
|
||||
|
@ -64,6 +75,7 @@ public final class EditorElement implements Parcelable {
|
|||
flags = new EditorFlags(in.readInt());
|
||||
ParcelUtils.readMatrix(localMatrix, in);
|
||||
renderer = in.readParcelable(Renderer.class.getClassLoader());
|
||||
zOrder = in.readInt();
|
||||
in.readTypedList(children, EditorElement.CREATOR);
|
||||
}
|
||||
|
||||
|
@ -127,6 +139,7 @@ public final class EditorElement implements Parcelable {
|
|||
|
||||
public void addElement(@NonNull EditorElement element) {
|
||||
children.add(element);
|
||||
Collections.sort(children, Z_ORDER_COMPARATOR);
|
||||
}
|
||||
|
||||
public Matrix getLocalMatrix() {
|
||||
|
@ -328,6 +341,7 @@ public final class EditorElement implements Parcelable {
|
|||
dest.writeInt(this.flags.asInt());
|
||||
ParcelUtils.writeMatrix(dest, localMatrix);
|
||||
dest.writeParcelable(renderer, flags);
|
||||
dest.writeInt(zOrder);
|
||||
dest.writeTypedList(children);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,10 @@ import java.util.UUID;
|
|||
*/
|
||||
public final class EditorModel implements Parcelable, RendererContext.Ready {
|
||||
|
||||
public static final int Z_DRAWING = 0;
|
||||
public static final int Z_STICKERS = 0;
|
||||
public static final int Z_TEXT = 1;
|
||||
|
||||
private static final Runnable NULL_RUNNABLE = () -> {
|
||||
};
|
||||
|
||||
|
@ -545,9 +549,17 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
|||
*/
|
||||
@WorkerThread
|
||||
public @NonNull Bitmap render(@NonNull Context context) {
|
||||
return render(context, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocking render of the model.
|
||||
*/
|
||||
@WorkerThread
|
||||
public @NonNull Bitmap render(@NonNull Context context, @Nullable Point size) {
|
||||
EditorElement image = editorElementHierarchy.getFlipRotate();
|
||||
RectF cropRect = editorElementHierarchy.getCropRect();
|
||||
Point outputSize = getOutputSize();
|
||||
Point outputSize = size != null ? size : getOutputSize();
|
||||
|
||||
Bitmap bitmap = Bitmap.createBitmap(outputSize.x, outputSize.y, Bitmap.Config.ARGB_8888);
|
||||
try {
|
||||
|
|
|
@ -67,14 +67,14 @@ public class InsightsRepository implements InsightsDashboardViewModel.Repository
|
|||
public void getUserAvatar(@NonNull Consumer<InsightsUserAvatar> avatarConsumer) {
|
||||
SimpleTask.run(() -> {
|
||||
Recipient self = Recipient.self().resolve();
|
||||
String name = Optional.fromNullable(self.getName(context)).or(Optional.fromNullable(TextSecurePreferences.getProfileName(context).toString())).or("");
|
||||
String name = Optional.fromNullable(self.getName(context)).or("");
|
||||
MaterialColor fallbackColor = self.getColor();
|
||||
|
||||
if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) {
|
||||
fallbackColor = ContactColors.generateFor(name);
|
||||
}
|
||||
|
||||
return new InsightsUserAvatar(new ProfileContactPhoto(self.getId(), String.valueOf(TextSecurePreferences.getProfileAvatarId(context))),
|
||||
return new InsightsUserAvatar(new ProfileContactPhoto(self, self.getProfileAvatar()),
|
||||
fallbackColor,
|
||||
new GeneratedContactPhoto(name, R.drawable.ic_profile_outline_40));
|
||||
}, avatarConsumer::accept);
|
||||
|
|
|
@ -85,7 +85,7 @@ class JobController {
|
|||
|
||||
if (chainExceedsMaximumInstances(chain)) {
|
||||
Job solo = chain.get(0).get(0);
|
||||
jobTracker.onStateChange(solo.getId(), JobTracker.JobState.IGNORED);
|
||||
jobTracker.onStateChange(solo, JobTracker.JobState.IGNORED);
|
||||
Log.w(TAG, JobLogger.format(solo, "Already at the max instance count of " + solo.getParameters().getMaxInstances() + ". Skipping."));
|
||||
return;
|
||||
}
|
||||
|
@ -101,7 +101,7 @@ class JobController {
|
|||
List<List<Job>> chain = Collections.singletonList(Collections.singletonList(job));
|
||||
|
||||
if (chainExceedsMaximumInstances(chain)) {
|
||||
jobTracker.onStateChange(job.getId(), JobTracker.JobState.IGNORED);
|
||||
jobTracker.onStateChange(job, JobTracker.JobState.IGNORED);
|
||||
Log.w(TAG, JobLogger.format(job, "Already at the max instance count of " + job.getParameters().getMaxInstances() + ". Skipping."));
|
||||
return;
|
||||
}
|
||||
|
@ -149,7 +149,7 @@ class JobController {
|
|||
String serializedData = dataSerializer.serialize(job.serialize());
|
||||
|
||||
jobStorage.updateJobAfterRetry(job.getId(), false, nextRunAttempt, nextRunAttemptTime, serializedData);
|
||||
jobTracker.onStateChange(job.getId(), JobTracker.JobState.PENDING);
|
||||
jobTracker.onStateChange(job, JobTracker.JobState.PENDING);
|
||||
|
||||
List<Constraint> constraints = Stream.of(jobStorage.getConstraintSpecs(job.getId()))
|
||||
.map(ConstraintSpec::getFactoryKey)
|
||||
|
@ -172,7 +172,7 @@ class JobController {
|
|||
@WorkerThread
|
||||
synchronized void onSuccess(@NonNull Job job) {
|
||||
jobStorage.deleteJob(job.getId());
|
||||
jobTracker.onStateChange(job.getId(), JobTracker.JobState.SUCCESS);
|
||||
jobTracker.onStateChange(job, JobTracker.JobState.SUCCESS);
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
|
@ -196,7 +196,7 @@ class JobController {
|
|||
all.addAll(dependents);
|
||||
|
||||
jobStorage.deleteJobs(Stream.of(all).map(Job::getId).toList());
|
||||
Stream.of(all).forEach(j -> jobTracker.onStateChange(j.getId(), JobTracker.JobState.FAILURE));
|
||||
Stream.of(all).forEach(j -> jobTracker.onStateChange(j, JobTracker.JobState.FAILURE));
|
||||
|
||||
return dependents;
|
||||
}
|
||||
|
@ -224,7 +224,7 @@ class JobController {
|
|||
|
||||
jobStorage.updateJobRunningState(job.getId(), true);
|
||||
runningJobs.put(job.getId(), job);
|
||||
jobTracker.onStateChange(job.getId(), JobTracker.JobState.RUNNING);
|
||||
jobTracker.onStateChange(job, JobTracker.JobState.RUNNING);
|
||||
|
||||
return job;
|
||||
} catch (InterruptedException e) {
|
||||
|
|
|
@ -100,13 +100,21 @@ public class JobManager implements ConstraintObserver.Notifier {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for {@link #addListener(JobTracker.JobFilter, JobTracker.JobListener)} that
|
||||
* takes in an ID to filter on.
|
||||
*/
|
||||
public void addListener(@NonNull String id, @NonNull JobTracker.JobListener listener) {
|
||||
jobTracker.addListener(new JobIdFilter(id), listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener to subscribe to job state updates. Listeners will be invoked on an arbitrary
|
||||
* background thread. You must eventually call {@link #removeListener(JobTracker.JobListener)} to avoid
|
||||
* memory leaks.
|
||||
*/
|
||||
public void addListener(@NonNull String id, @NonNull JobTracker.JobListener listener) {
|
||||
jobTracker.addListener(id, listener);
|
||||
public void addListener(@NonNull JobTracker.JobFilter filter, @NonNull JobTracker.JobListener listener) {
|
||||
jobTracker.addListener(filter, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -127,7 +135,7 @@ public class JobManager implements ConstraintObserver.Notifier {
|
|||
* Enqueues a single job that depends on a collection of job ID's.
|
||||
*/
|
||||
public void add(@NonNull Job job, @NonNull Collection<String> dependsOn) {
|
||||
jobTracker.onStateChange(job.getId(), JobTracker.JobState.PENDING);
|
||||
jobTracker.onStateChange(job, JobTracker.JobState.PENDING);
|
||||
|
||||
executor.execute(() -> {
|
||||
jobController.submitJobWithExistingDependencies(job, dependsOn);
|
||||
|
@ -177,7 +185,7 @@ public class JobManager implements ConstraintObserver.Notifier {
|
|||
|
||||
addListener(job.getId(), new JobTracker.JobListener() {
|
||||
@Override
|
||||
public void onStateChanged(@NonNull JobTracker.JobState jobState) {
|
||||
public void onStateChanged(@NonNull Job job, @NonNull JobTracker.JobState jobState) {
|
||||
if (jobState.isComplete()) {
|
||||
removeListener(this);
|
||||
resultState.set(jobState);
|
||||
|
@ -248,7 +256,7 @@ public class JobManager implements ConstraintObserver.Notifier {
|
|||
private void enqueueChain(@NonNull Chain chain) {
|
||||
for (List<Job> jobList : chain.getJobListChain()) {
|
||||
for (Job job : jobList) {
|
||||
jobTracker.onStateChange(job.getId(), JobTracker.JobState.PENDING);
|
||||
jobTracker.onStateChange(job, JobTracker.JobState.PENDING);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -270,6 +278,19 @@ public class JobManager implements ConstraintObserver.Notifier {
|
|||
void onQueueEmpty();
|
||||
}
|
||||
|
||||
public static class JobIdFilter implements JobTracker.JobFilter {
|
||||
private final String id;
|
||||
|
||||
public JobIdFilter(@NonNull String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(@NonNull Job job) {
|
||||
return id.equals(job.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows enqueuing work that depends on each other. Jobs that appear later in the chain will
|
||||
* only run after all jobs earlier in the chain have been completed. If a job fails, all jobs
|
||||
|
|
|
@ -3,13 +3,15 @@ package org.thoughtcrime.securesms.jobmanager;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.util.LRUCache;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
|
@ -17,11 +19,13 @@ import java.util.concurrent.Executor;
|
|||
*/
|
||||
public class JobTracker {
|
||||
|
||||
private final Map<String, TrackingState> trackingStates;
|
||||
private final Executor listenerExecutor;
|
||||
private final Map<String, JobInfo> jobInfos;
|
||||
private final List<ListenerInfo> jobListeners;
|
||||
private final Executor listenerExecutor;
|
||||
|
||||
JobTracker() {
|
||||
this.trackingStates = new LRUCache<>(1000);
|
||||
this.jobInfos = new LRUCache<>(1000);
|
||||
this.jobListeners = new ArrayList<>();
|
||||
this.listenerExecutor = SignalExecutors.BOUNDED;
|
||||
}
|
||||
|
||||
|
@ -30,54 +34,63 @@ public class JobTracker {
|
|||
* background thread. You must eventually call {@link #removeListener(JobListener)} to avoid
|
||||
* memory leaks.
|
||||
*/
|
||||
synchronized void addListener(@NonNull String id, @NonNull JobListener jobListener) {
|
||||
TrackingState state = getOrCreateTrackingState(id);
|
||||
JobState currentJobState = state.getJobState();
|
||||
synchronized void addListener(@NonNull JobFilter filter, @NonNull JobListener listener) {
|
||||
jobListeners.add(new ListenerInfo(filter, listener));
|
||||
|
||||
state.addListener(jobListener);
|
||||
|
||||
if (currentJobState != null) {
|
||||
listenerExecutor.execute(() -> jobListener.onStateChanged(currentJobState));
|
||||
}
|
||||
Stream.of(jobInfos.values())
|
||||
.filter(info -> info.getJobState() != null)
|
||||
.filter(info -> filter.matches(info.getJob()))
|
||||
.forEach(state-> {
|
||||
//noinspection ConstantConditions We already filter for nulls above
|
||||
listenerExecutor.execute(() -> listener.onStateChanged(state.getJob(), state.getJobState()));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe the provided listener from all job updates.
|
||||
*/
|
||||
synchronized void removeListener(@NonNull JobListener jobListener) {
|
||||
Collection<TrackingState> allTrackingState = trackingStates.values();
|
||||
synchronized void removeListener(@NonNull JobListener listener) {
|
||||
Iterator<ListenerInfo> iter = jobListeners.iterator();
|
||||
|
||||
for (TrackingState state : allTrackingState) {
|
||||
state.removeListener(jobListener);
|
||||
while (iter.hasNext()) {
|
||||
if (listener.equals(iter.next().getListener())) {
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the state of a job with the associated ID.
|
||||
*/
|
||||
synchronized void onStateChange(@NonNull String id, @NonNull JobState jobState) {
|
||||
TrackingState trackingState = getOrCreateTrackingState(id);
|
||||
trackingState.setJobState(jobState);
|
||||
synchronized void onStateChange(@NonNull Job job, @NonNull JobState state) {
|
||||
getOrCreateJobInfo(job).setJobState(state);
|
||||
|
||||
for (JobListener listener : trackingState.getListeners()) {
|
||||
listenerExecutor.execute(() -> listener.onStateChanged(jobState));
|
||||
}
|
||||
Stream.of(jobListeners)
|
||||
.filter(info -> info.getFilter().matches(job))
|
||||
.map(ListenerInfo::getListener)
|
||||
.forEach(listener -> {
|
||||
listenerExecutor.execute(() -> listener.onStateChanged(job, state));
|
||||
});
|
||||
}
|
||||
|
||||
private @NonNull TrackingState getOrCreateTrackingState(@NonNull String id) {
|
||||
TrackingState state = trackingStates.get(id);
|
||||
private @NonNull JobInfo getOrCreateJobInfo(@NonNull Job job) {
|
||||
JobInfo jobInfo = jobInfos.get(job.getId());
|
||||
|
||||
if (state == null) {
|
||||
state = new TrackingState();
|
||||
if (jobInfo == null) {
|
||||
jobInfo = new JobInfo(job);
|
||||
}
|
||||
|
||||
trackingStates.put(id, state);
|
||||
jobInfos.put(job.getId(), jobInfo);
|
||||
|
||||
return state;
|
||||
return jobInfo;
|
||||
}
|
||||
|
||||
public interface JobFilter {
|
||||
boolean matches(@NonNull Job job);
|
||||
}
|
||||
|
||||
public interface JobListener {
|
||||
void onStateChanged(@NonNull JobState jobState);
|
||||
void onStateChanged(@NonNull Job job, @NonNull JobState jobState);
|
||||
}
|
||||
|
||||
public enum JobState {
|
||||
|
@ -94,21 +107,34 @@ public class JobTracker {
|
|||
}
|
||||
}
|
||||
|
||||
private static class TrackingState {
|
||||
private JobState jobState;
|
||||
private static class ListenerInfo {
|
||||
private final JobFilter filter;
|
||||
private final JobListener listener;
|
||||
|
||||
private final CopyOnWriteArraySet<JobListener> listeners = new CopyOnWriteArraySet<>();
|
||||
|
||||
void addListener(@NonNull JobListener jobListener) {
|
||||
listeners.add(jobListener);
|
||||
private ListenerInfo(JobFilter filter, JobListener listener) {
|
||||
this.filter = filter;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
void removeListener(@NonNull JobListener jobListener) {
|
||||
listeners.remove(jobListener);
|
||||
@NonNull JobFilter getFilter() {
|
||||
return filter;
|
||||
}
|
||||
|
||||
@NonNull Collection<JobListener> getListeners() {
|
||||
return listeners;
|
||||
@NonNull JobListener getListener() {
|
||||
return listener;
|
||||
}
|
||||
}
|
||||
|
||||
private static class JobInfo {
|
||||
private final Job job;
|
||||
private JobState jobState;
|
||||
|
||||
private JobInfo(Job job) {
|
||||
this.job = job;
|
||||
}
|
||||
|
||||
@NonNull Job getJob() {
|
||||
return job;
|
||||
}
|
||||
|
||||
void setJobState(@NonNull JobState jobState) {
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
|
@ -14,7 +16,6 @@ import org.thoughtcrime.securesms.logging.Log;
|
|||
import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel;
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
@ -36,9 +37,9 @@ public class AvatarDownloadJob extends BaseJob {
|
|||
|
||||
private static final String KEY_GROUP_ID = "group_id";
|
||||
|
||||
private byte[] groupId;
|
||||
private @NonNull GroupId groupId;
|
||||
|
||||
public AvatarDownloadJob(@NonNull byte[] groupId) {
|
||||
public AvatarDownloadJob(@NonNull GroupId groupId) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setMaxAttempts(10)
|
||||
|
@ -46,14 +47,14 @@ public class AvatarDownloadJob extends BaseJob {
|
|||
groupId);
|
||||
}
|
||||
|
||||
private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull byte[] groupId) {
|
||||
private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull GroupId groupId) {
|
||||
super(parameters);
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return new Data.Builder().putString(KEY_GROUP_ID, GroupUtil.getEncodedId(groupId, false)).build();
|
||||
return new Data.Builder().putString(KEY_GROUP_ID, groupId.toString()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -63,9 +64,8 @@ public class AvatarDownloadJob extends BaseJob {
|
|||
|
||||
@Override
|
||||
public void onRun() throws IOException {
|
||||
String encodeId = GroupUtil.getEncodedId(groupId, false);
|
||||
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
|
||||
Optional<GroupRecord> record = database.getGroup(encodeId);
|
||||
Optional<GroupRecord> record = database.getGroup(groupId);
|
||||
File attachment = null;
|
||||
|
||||
try {
|
||||
|
@ -93,7 +93,7 @@ public class AvatarDownloadJob extends BaseJob {
|
|||
InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, MAX_AVATAR_SIZE);
|
||||
Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500);
|
||||
|
||||
database.updateAvatar(encodeId, avatar);
|
||||
database.updateAvatar(groupId, avatar);
|
||||
inputStream.close();
|
||||
}
|
||||
} catch (BitmapDecodingException | NonSuccessfulResponseCodeException | InvalidMessageException e) {
|
||||
|
@ -116,11 +116,7 @@ public class AvatarDownloadJob extends BaseJob {
|
|||
public static final class Factory implements Job.Factory<AvatarDownloadJob> {
|
||||
@Override
|
||||
public @NonNull AvatarDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
try {
|
||||
return new AvatarDownloadJob(parameters, GroupUtil.getDecodedId(data.getString(KEY_GROUP_ID)));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
return new AvatarDownloadJob(parameters, GroupId.parse(data.getString(KEY_GROUP_ID)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -125,7 +125,7 @@ public class FcmRefreshJob extends BaseJob {
|
|||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 1122, intent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationChannels.FAILURES);
|
||||
|
||||
builder.setSmallIcon(R.drawable.icon_notification);
|
||||
builder.setSmallIcon(R.drawable.ic_notification);
|
||||
builder.setLargeIcon(BitmapFactory.decodeResource(context.getResources(),
|
||||
R.drawable.ic_action_warning_red));
|
||||
builder.setContentTitle(context.getString(R.string.GcmRefreshJob_Permanent_Signal_communication_failure));
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdFollowUpJobMi
|
|||
import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdFollowUpJobMigration2;
|
||||
import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdJobMigration;
|
||||
import org.thoughtcrime.securesms.jobmanager.migrations.SendReadReceiptsJobMigration;
|
||||
import org.thoughtcrime.securesms.migrations.AvatarIdRemovalMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.PassingMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.AvatarMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.CachedAttachmentsMigrationJob;
|
||||
|
@ -85,6 +86,7 @@ public final class JobManagerFactories {
|
|||
put(RefreshPreKeysJob.KEY, new RefreshPreKeysJob.Factory());
|
||||
put(RemoteConfigRefreshJob.KEY, new RemoteConfigRefreshJob.Factory());
|
||||
put(RequestGroupInfoJob.KEY, new RequestGroupInfoJob.Factory());
|
||||
put(StorageAccountRestoreJob.KEY, new StorageAccountRestoreJob.Factory());
|
||||
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory());
|
||||
put(RetrieveProfileJob.KEY, new RetrieveProfileJob.Factory());
|
||||
put(RotateCertificateJob.KEY, new RotateCertificateJob.Factory());
|
||||
|
@ -107,6 +109,7 @@ public final class JobManagerFactories {
|
|||
put(ProfileUploadJob.KEY, new ProfileUploadJob.Factory());
|
||||
|
||||
// Migrations
|
||||
put(AvatarIdRemovalMigrationJob.KEY, new AvatarIdRemovalMigrationJob.Factory());
|
||||
put(AvatarMigrationJob.KEY, new AvatarMigrationJob.Factory());
|
||||
put(CachedAttachmentsMigrationJob.KEY, new CachedAttachmentsMigrationJob.Factory());
|
||||
put(DatabaseMigrationJob.KEY, new DatabaseMigrationJob.Factory());
|
||||
|
|
|
@ -3,12 +3,12 @@ package org.thoughtcrime.securesms.jobs;
|
|||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
|
@ -18,7 +18,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
|
@ -27,8 +26,6 @@ import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
|||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
|
@ -54,7 +51,7 @@ public class LeaveGroupJob extends BaseJob {
|
|||
private static final String KEY_MEMBERS = "members";
|
||||
private static final String KEY_RECIPIENTS = "recipients";
|
||||
|
||||
private final byte[] groupId;
|
||||
private final GroupId groupId;
|
||||
private final String name;
|
||||
private final List<RecipientId> members;
|
||||
private final List<RecipientId> recipients;
|
||||
|
@ -63,7 +60,7 @@ public class LeaveGroupJob extends BaseJob {
|
|||
List<RecipientId> members = Stream.of(group.resolve().getParticipants()).map(Recipient::getId).toList();
|
||||
members.remove(Recipient.self().getId());
|
||||
|
||||
return new LeaveGroupJob(GroupUtil.getDecodedIdOrThrow(group.getGroupId().get()),
|
||||
return new LeaveGroupJob(group.getGroupId().get(),
|
||||
group.resolve().getDisplayName(ApplicationDependencies.getApplication()),
|
||||
members,
|
||||
members,
|
||||
|
@ -75,7 +72,7 @@ public class LeaveGroupJob extends BaseJob {
|
|||
.build());
|
||||
}
|
||||
|
||||
private LeaveGroupJob(@NonNull byte[] groupId,
|
||||
private LeaveGroupJob(@NonNull GroupId groupId,
|
||||
@NonNull String name,
|
||||
@NonNull List<RecipientId> members,
|
||||
@NonNull List<RecipientId> recipients,
|
||||
|
@ -90,7 +87,7 @@ public class LeaveGroupJob extends BaseJob {
|
|||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return new Data.Builder().putString(KEY_GROUP_ID, Base64.encodeBytes(groupId))
|
||||
return new Data.Builder().putString(KEY_GROUP_ID, Base64.encodeBytes(groupId.getDecodedId()))
|
||||
.putString(KEY_GROUP_NAME, name)
|
||||
.putString(KEY_MEMBERS, RecipientId.toSerializedList(members))
|
||||
.putString(KEY_RECIPIENTS, RecipientId.toSerializedList(recipients))
|
||||
|
@ -128,7 +125,7 @@ public class LeaveGroupJob extends BaseJob {
|
|||
}
|
||||
|
||||
private static @NonNull List<Recipient> deliver(@NonNull Context context,
|
||||
@NonNull byte[] groupId,
|
||||
@NonNull GroupId groupId,
|
||||
@NonNull String name,
|
||||
@NonNull List<RecipientId> members,
|
||||
@NonNull List<RecipientId> destinations)
|
||||
|
@ -138,7 +135,7 @@ public class LeaveGroupJob extends BaseJob {
|
|||
List<SignalServiceAddress> addresses = Stream.of(destinations).map(Recipient::resolved).map(t -> RecipientUtil.toSignalServiceAddress(context, t)).toList();
|
||||
List<SignalServiceAddress> memberAddresses = Stream.of(members).map(Recipient::resolved).map(t -> RecipientUtil.toSignalServiceAddress(context, t)).toList();
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = Stream.of(destinations).map(Recipient::resolved).map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient)).toList();
|
||||
SignalServiceGroup serviceGroup = new SignalServiceGroup(SignalServiceGroup.Type.QUIT, groupId, name, memberAddresses, null);
|
||||
SignalServiceGroup serviceGroup = new SignalServiceGroup(SignalServiceGroup.Type.QUIT, groupId.getDecodedId(), name, memberAddresses, null);
|
||||
SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(System.currentTimeMillis())
|
||||
.asGroupMessage(serviceGroup);
|
||||
|
@ -169,7 +166,7 @@ public class LeaveGroupJob extends BaseJob {
|
|||
public static class Factory implements Job.Factory<LeaveGroupJob> {
|
||||
@Override
|
||||
public @NonNull LeaveGroupJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new LeaveGroupJob(Base64.decodeOrThrow(data.getString(KEY_GROUP_ID)),
|
||||
return new LeaveGroupJob(GroupId.v1(Base64.decodeOrThrow(data.getString(KEY_GROUP_ID))),
|
||||
data.getString(KEY_GROUP_NAME),
|
||||
RecipientId.fromSerializedList(data.getString(KEY_MEMBERS)),
|
||||
RecipientId.fromSerializedList(data.getString(KEY_RECIPIENTS)),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
|
@ -16,6 +17,7 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
|||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
@ -177,11 +179,11 @@ public class MmsDownloadJob extends BaseJob {
|
|||
int subscriptionId, @Nullable RecipientId notificationFrom)
|
||||
throws MmsException
|
||||
{
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
Optional<String> group = Optional.absent();
|
||||
Set<RecipientId> members = new HashSet<>();
|
||||
String body = null;
|
||||
List<Attachment> attachments = new LinkedList<>();
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
Optional<GroupId> group = Optional.absent();
|
||||
Set<RecipientId> members = new HashSet<>();
|
||||
String body = null;
|
||||
List<Attachment> attachments = new LinkedList<>();
|
||||
|
||||
RecipientId from = null;
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
|||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
|
@ -76,7 +75,7 @@ public class MultiDeviceBlockedUpdateJob extends BaseJob {
|
|||
|
||||
while ((recipient = reader.getNext()) != null) {
|
||||
if (recipient.isPushGroup()) {
|
||||
blockedGroups.add(GroupUtil.getDecodedId(recipient.requireGroupId()));
|
||||
blockedGroups.add(recipient.requireGroupId().getDecodedId());
|
||||
} else if (recipient.hasServiceIdentifier()) {
|
||||
blockedIndividuals.add(RecipientUtil.toSignalServiceAddress(context, recipient));
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ 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.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
|
@ -92,13 +91,13 @@ public class MultiDeviceGroupUpdateJob extends BaseJob {
|
|||
members.add(RecipientUtil.toSignalServiceAddress(context, Recipient.resolved(member)));
|
||||
}
|
||||
|
||||
RecipientId recipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(GroupUtil.getEncodedId(record.getId(), record.isMms()));
|
||||
RecipientId recipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(record.getId());
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
Optional<Integer> expirationTimer = recipient.getExpireMessages() > 0 ? Optional.of(recipient.getExpireMessages()) : Optional.absent();
|
||||
Map<RecipientId, Integer> inboxPositions = DatabaseFactory.getThreadDatabase(context).getInboxPositions();
|
||||
Set<RecipientId> archived = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
|
||||
|
||||
out.write(new DeviceGroup(record.getId(),
|
||||
out.write(new DeviceGroup(record.getId().getDecodedId(),
|
||||
Optional.fromNullable(record.getTitle()),
|
||||
members,
|
||||
getAvatar(record.getAvatar()),
|
||||
|
|
|
@ -8,18 +8,13 @@ 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.keyvalue.SignalStore;
|
||||
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.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
|
@ -100,7 +95,7 @@ public class MultiDeviceMessageRequestResponseJob extends BaseJob {
|
|||
MessageRequestResponseMessage response;
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
response = MessageRequestResponseMessage.forGroup(GroupUtil.getDecodedId(recipient.getGroupId().get()), localToRemoteType(type));
|
||||
response = MessageRequestResponseMessage.forGroup(recipient.getGroupId().get().getDecodedId(), localToRemoteType(type));
|
||||
} else {
|
||||
response = MessageRequestResponseMessage.forIndividual(RecipientUtil.toSignalServiceAddress(context, recipient), localToRemoteType(type));
|
||||
}
|
||||
|
|
|
@ -6,15 +6,20 @@ 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.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.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.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
|
||||
public final class ProfileUploadJob extends BaseJob {
|
||||
|
@ -44,16 +49,19 @@ public final class ProfileUploadJob extends BaseJob {
|
|||
@Override
|
||||
protected void onRun() throws Exception {
|
||||
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
|
||||
ProfileName profileName = TextSecurePreferences.getProfileName(context);
|
||||
ProfileName profileName = Recipient.self().getProfileName();
|
||||
String avatarPath = null;
|
||||
|
||||
try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) {
|
||||
if (FeatureFlags.VERSIONED_PROFILES) {
|
||||
accountManager.setVersionedProfile(profileKey, profileName.serialize(), avatar);
|
||||
avatarPath = accountManager.setVersionedProfile(Recipient.self().getUuid().get(), profileKey, profileName.serialize(), avatar).orNull();
|
||||
} else {
|
||||
accountManager.setProfileName(profileKey, profileName.serialize());
|
||||
accountManager.setProfileAvatar(profileKey, avatar);
|
||||
avatarPath = accountManager.setProfileAvatar(profileKey, avatar).orNull();
|
||||
}
|
||||
}
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileAvatar(Recipient.self().getId(), avatarPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -29,13 +29,13 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.state.SignalProtocolStore;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
@ -137,7 +137,7 @@ public final class PushDecryptMessageJob extends BaseJob {
|
|||
// TODO [greyson] Navigation
|
||||
NotificationManagerCompat.from(context).notify(494949,
|
||||
new NotificationCompat.Builder(context, NotificationChannels.getMessagesChannel(context))
|
||||
.setSmallIcon(R.drawable.icon_notification)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setContentTitle(context.getString(R.string.PushDecryptJob_new_locked_message))
|
||||
|
@ -233,7 +233,7 @@ public final class PushDecryptMessageJob extends BaseJob {
|
|||
|
||||
return new PushProcessMessageJob.ExceptionMetadata(sender,
|
||||
e.getSenderDevice(),
|
||||
e.getGroup().transform(g -> GroupUtil.getEncodedId(g.getGroupId(), false)).orNull());
|
||||
e.getGroup().transform(g -> GroupId.v1(g.getGroupId())).orNull());
|
||||
}
|
||||
|
||||
private static PushProcessMessageJob.ExceptionMetadata toExceptionMetadata(@NonNull ProtocolException e) throws NoSenderException {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
@ -18,6 +19,7 @@ import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
|||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
|
@ -31,8 +33,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
|
@ -242,7 +242,7 @@ public class PushGroupSendJob extends PushSendJob {
|
|||
rotateSenderCertificateIfNecessary();
|
||||
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
String groupId = groupRecipient.requireGroupId();
|
||||
GroupId groupId = groupRecipient.requireGroupId();
|
||||
Optional<byte[]> profileKey = getProfileKey(groupRecipient);
|
||||
Optional<Quote> quote = getQuoteFor(message);
|
||||
Optional<SignalServiceDataMessage.Sticker> sticker = getStickerFor(message);
|
||||
|
@ -266,7 +266,7 @@ public class PushGroupSendJob extends PushSendJob {
|
|||
List<SignalServiceAddress> members = Stream.of(groupContext.getMembersList())
|
||||
.map(m -> new SignalServiceAddress(UuidUtil.parseOrNull(m.getUuid()), m.getE164()))
|
||||
.toList();
|
||||
SignalServiceGroup group = new SignalServiceGroup(type, GroupUtil.getDecodedId(groupId), groupContext.getName(), members, avatar);
|
||||
SignalServiceGroup group = new SignalServiceGroup(type, groupId.getDecodedId(), groupContext.getName(), members, avatar);
|
||||
SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(message.getSentTimeMillis())
|
||||
.withExpiration(groupRecipient.getExpireMessages())
|
||||
|
@ -275,7 +275,7 @@ public class PushGroupSendJob extends PushSendJob {
|
|||
|
||||
return messageSender.sendMessage(addresses, unidentifiedAccess, isRecipientUpdate, groupDataMessage);
|
||||
} else {
|
||||
SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedId(groupId));
|
||||
SignalServiceGroup group = new SignalServiceGroup(groupId.getDecodedId());
|
||||
SignalServiceDataMessage groupMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(message.getSentTimeMillis())
|
||||
.asGroupMessage(group)
|
||||
|
@ -295,7 +295,7 @@ public class PushGroupSendJob extends PushSendJob {
|
|||
}
|
||||
}
|
||||
|
||||
private @NonNull List<RecipientId> getGroupMessageRecipients(String groupId, long messageId) {
|
||||
private @NonNull List<RecipientId> getGroupMessageRecipients(@NonNull GroupId groupId, long messageId) {
|
||||
List<GroupReceiptInfo> destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId);
|
||||
if (!destinations.isEmpty()) return Stream.of(destinations).map(GroupReceiptInfo::getRecipientId).toList();
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
|
@ -15,7 +16,6 @@ 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.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
|
@ -26,8 +26,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
|||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup.Type;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
|
@ -44,10 +42,10 @@ public class PushGroupUpdateJob extends BaseJob {
|
|||
private static final String KEY_SOURCE = "source";
|
||||
private static final String KEY_GROUP_ID = "group_id";
|
||||
|
||||
private RecipientId source;
|
||||
private byte[] groupId;
|
||||
private final RecipientId source;
|
||||
private final GroupId groupId;
|
||||
|
||||
public PushGroupUpdateJob(@NonNull RecipientId source, byte[] groupId) {
|
||||
public PushGroupUpdateJob(@NonNull RecipientId source, @NonNull GroupId groupId) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
|
@ -57,7 +55,7 @@ public class PushGroupUpdateJob extends BaseJob {
|
|||
groupId);
|
||||
}
|
||||
|
||||
private PushGroupUpdateJob(@NonNull Job.Parameters parameters, RecipientId source, byte[] groupId) {
|
||||
private PushGroupUpdateJob(@NonNull Job.Parameters parameters, RecipientId source, @NonNull GroupId groupId) {
|
||||
super(parameters);
|
||||
|
||||
this.source = source;
|
||||
|
@ -67,7 +65,7 @@ public class PushGroupUpdateJob extends BaseJob {
|
|||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return new Data.Builder().putString(KEY_SOURCE, source.serialize())
|
||||
.putString(KEY_GROUP_ID, GroupUtil.getEncodedId(groupId, false))
|
||||
.putString(KEY_GROUP_ID, groupId.toString())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -79,11 +77,11 @@ public class PushGroupUpdateJob extends BaseJob {
|
|||
@Override
|
||||
public void onRun() throws IOException, UntrustedIdentityException {
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
Optional<GroupRecord> record = groupDatabase.getGroup(GroupUtil.getEncodedId(groupId, false));
|
||||
Optional<GroupRecord> record = groupDatabase.getGroup(groupId);
|
||||
SignalServiceAttachment avatar = null;
|
||||
|
||||
if (record == null) {
|
||||
Log.w(TAG, "No information for group record info request: " + new String(groupId));
|
||||
Log.w(TAG, "No information for group record info request: " + groupId.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -104,12 +102,12 @@ public class PushGroupUpdateJob extends BaseJob {
|
|||
|
||||
SignalServiceGroup groupContext = SignalServiceGroup.newBuilder(Type.UPDATE)
|
||||
.withAvatar(avatar)
|
||||
.withId(groupId)
|
||||
.withId(groupId.getDecodedId())
|
||||
.withMembers(members)
|
||||
.withName(record.get().getTitle())
|
||||
.build();
|
||||
|
||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(GroupUtil.getEncodedId(groupId, false));
|
||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
||||
|
||||
SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
|
||||
|
@ -139,13 +137,9 @@ public class PushGroupUpdateJob extends BaseJob {
|
|||
public static final class Factory implements Job.Factory<PushGroupUpdateJob> {
|
||||
@Override
|
||||
public @NonNull PushGroupUpdateJob create(@NonNull Parameters parameters, @NonNull org.thoughtcrime.securesms.jobmanager.Data data) {
|
||||
try {
|
||||
return new PushGroupUpdateJob(parameters,
|
||||
RecipientId.from(data.getString(KEY_SOURCE)),
|
||||
GroupUtil.getDecodedId(data.getString(KEY_GROUP_ID)));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
return new PushGroupUpdateJob(parameters,
|
||||
RecipientId.from(data.getString(KEY_SOURCE)),
|
||||
GroupId.parse(data.getString(KEY_GROUP_ID)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
|||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupMessageProcessor;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
|
@ -62,9 +63,9 @@ import org.thoughtcrime.securesms.mms.StickerSlide;
|
|||
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.service.WebRtcCallService;
|
||||
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
|
||||
import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage;
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
||||
|
@ -72,8 +73,8 @@ import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
|
|||
import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
@ -216,7 +217,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
//noinspection ConstantConditions
|
||||
dataBuilder.putString(KEY_EXCEPTION_SENDER, exceptionMetadata.sender)
|
||||
.putInt(KEY_EXCEPTION_DEVICE, exceptionMetadata.senderDevice)
|
||||
.putString(KEY_EXCEPTION_GROUP_ID, exceptionMetadata.groupId);
|
||||
.putString(KEY_EXCEPTION_GROUP_ID, exceptionMetadata.groupId == null ? null : exceptionMetadata.groupId.toString());
|
||||
}
|
||||
|
||||
return dataBuilder.build();
|
||||
|
@ -271,7 +272,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId);
|
||||
else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId);
|
||||
|
||||
if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false))) {
|
||||
if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupId.v1(message.getGroupInfo().get().getGroupId()))) {
|
||||
handleUnknownGroupMessage(content, message.getGroupInfo().get());
|
||||
}
|
||||
|
||||
|
@ -326,8 +327,8 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
}
|
||||
}
|
||||
|
||||
private static @NonNull Optional<String> toEncodedId(@NonNull Optional<SignalServiceGroup> groupInfo) {
|
||||
return groupInfo.transform(g -> GroupUtil.getEncodedId(g.getGroupId(), false));
|
||||
private static @NonNull Optional<GroupId> toEncodedId(@NonNull Optional<SignalServiceGroup> groupInfo) {
|
||||
return groupInfo.transform(g -> GroupId.v1(g.getGroupId()));
|
||||
}
|
||||
|
||||
private void handleExceptionMessage(@NonNull ExceptionMetadata e, @NonNull Optional<Long> smsMessageId) {
|
||||
|
@ -544,7 +545,11 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
private void handleUnknownGroupMessage(@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceGroup group)
|
||||
{
|
||||
ApplicationDependencies.getJobManager().add(new RequestGroupInfoJob(Recipient.externalPush(context, content.getSender()).getId(), group.getGroupId()));
|
||||
if (group.getType() != SignalServiceGroup.Type.REQUEST_INFO) {
|
||||
ApplicationDependencies.getJobManager().add(new RequestGroupInfoJob(Recipient.externalPush(context, content.getSender()).getId(), GroupId.v1(group.getGroupId())));
|
||||
} else {
|
||||
Log.w(TAG, "Received a REQUEST_INFO message for a group we don't know about. Ignoring.");
|
||||
}
|
||||
}
|
||||
|
||||
private void handleExpirationUpdate(@NonNull SignalServiceContent content,
|
||||
|
@ -661,7 +666,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
ApplicationDependencies.getJobManager().add(new RefreshOwnProfileJob());
|
||||
break;
|
||||
case STORAGE_MANIFEST:
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "Received a fetch message for an unknown type.");
|
||||
|
@ -677,7 +682,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
if (response.getPerson().isPresent()) {
|
||||
recipient = Recipient.externalPush(context, response.getPerson().get());
|
||||
} else if (response.getGroupId().isPresent()) {
|
||||
String groupId = GroupUtil.getEncodedId(response.getGroupId().get(), false);
|
||||
GroupId groupId = GroupId.v1(response.getGroupId().get());
|
||||
recipient = Recipient.externalGroup(context, groupId);
|
||||
} else {
|
||||
Log.w(TAG, "Message request response was missing a thread recipient! Skipping.");
|
||||
|
@ -738,7 +743,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
threadId = handleSynchronizeSentTextMessage(message);
|
||||
}
|
||||
|
||||
if (message.getMessage().getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get().getGroupId(), false))) {
|
||||
if (message.getMessage().getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupId.v1(message.getMessage().getGroupInfo().get().getGroupId()))) {
|
||||
handleUnknownGroupMessage(content, message.getMessage().getGroupInfo().get());
|
||||
}
|
||||
|
||||
|
@ -1012,7 +1017,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
updateGroupReceiptStatus(message, record.getId(), recipient.requireGroupId());
|
||||
}
|
||||
|
||||
private void updateGroupReceiptStatus(@NonNull SentTranscriptMessage message, long messageId, @NonNull String groupString) {
|
||||
private void updateGroupReceiptStatus(@NonNull SentTranscriptMessage message, long messageId, @NonNull GroupId groupString) {
|
||||
GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
|
||||
List<Recipient> messageRecipients = Stream.of(message.getRecipients()).map(address -> Recipient.externalPush(context, address)).toList();
|
||||
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupString, false);
|
||||
|
@ -1178,7 +1183,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
|
||||
private void handleUnsupportedDataMessage(@NonNull String sender,
|
||||
int senderDevice,
|
||||
@NonNull Optional<String> groupId,
|
||||
@NonNull Optional<GroupId> groupId,
|
||||
long timestamp,
|
||||
@NonNull Optional<Long> smsMessageId)
|
||||
{
|
||||
|
@ -1198,7 +1203,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
|
||||
private void handleInvalidMessage(@NonNull SignalServiceAddress sender,
|
||||
int senderDevice,
|
||||
@NonNull Optional<String> groupId,
|
||||
@NonNull Optional<GroupId> groupId,
|
||||
long timestamp,
|
||||
@NonNull Optional<Long> smsMessageId)
|
||||
{
|
||||
|
@ -1308,7 +1313,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
long threadId;
|
||||
|
||||
if (typingMessage.getGroupId().isPresent()) {
|
||||
RecipientId recipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(GroupUtil.getEncodedId(typingMessage.getGroupId().get(), false));
|
||||
RecipientId recipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(GroupId.v1(typingMessage.getGroupId().get()));
|
||||
Recipient groupRecipient = Recipient.resolved(recipientId);
|
||||
|
||||
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
||||
|
@ -1473,7 +1478,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
return insertPlaceholder(sender, senderDevice, timestamp, Optional.absent());
|
||||
}
|
||||
|
||||
private Optional<InsertResult> insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp, Optional<String> groupId) {
|
||||
private Optional<InsertResult> insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp, Optional<GroupId> groupId) {
|
||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
IncomingTextMessage textMessage = new IncomingTextMessage(Recipient.external(context, sender).getId(),
|
||||
senderDevice, timestamp, "",
|
||||
|
@ -1485,7 +1490,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
|
||||
private Recipient getSyncMessageDestination(SentTranscriptMessage message) {
|
||||
if (message.getMessage().getGroupInfo().isPresent()) {
|
||||
return Recipient.external(context, GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get().getGroupId(), false));
|
||||
return Recipient.externalGroup(context, GroupId.v1(message.getMessage().getGroupInfo().get().getGroupId()));
|
||||
} else {
|
||||
return Recipient.externalPush(context, message.getDestination().get());
|
||||
}
|
||||
|
@ -1493,7 +1498,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
|
||||
private Recipient getMessageDestination(SignalServiceContent content, SignalServiceDataMessage message) {
|
||||
if (message.getGroupInfo().isPresent()) {
|
||||
return Recipient.external(context, GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false));
|
||||
return Recipient.externalGroup(context, GroupId.v1(message.getGroupInfo().get().getGroupId()));
|
||||
} else {
|
||||
return Recipient.externalPush(context, content.getSender());
|
||||
}
|
||||
|
@ -1524,9 +1529,9 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
if (conversation.isGroup() && conversation.isBlocked()) {
|
||||
return true;
|
||||
} else if (conversation.isGroup()) {
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
Optional<String> groupId = message.getGroupInfo().isPresent() ? Optional.of(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false))
|
||||
: Optional.absent();
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
Optional<GroupId> groupId = message.getGroupInfo().isPresent() ? Optional.of(GroupId.v1(message.getGroupInfo().get().getGroupId()))
|
||||
: Optional.absent();
|
||||
|
||||
if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
|
||||
return false;
|
||||
|
@ -1611,7 +1616,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
} else {
|
||||
ExceptionMetadata exceptionMetadata = new ExceptionMetadata(data.getString(KEY_EXCEPTION_SENDER),
|
||||
data.getInt(KEY_EXCEPTION_DEVICE),
|
||||
data.getStringOrDefault(KEY_EXCEPTION_GROUP_ID, null));
|
||||
GroupId.parseNullable(data.getStringOrDefault(KEY_EXCEPTION_GROUP_ID, null)));
|
||||
|
||||
return new PushProcessMessageJob(parameters,
|
||||
state,
|
||||
|
@ -1638,11 +1643,11 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
}
|
||||
|
||||
static class ExceptionMetadata {
|
||||
@NonNull private final String sender;
|
||||
private final int senderDevice;
|
||||
@Nullable private final String groupId;
|
||||
@NonNull private final String sender;
|
||||
private final int senderDevice;
|
||||
@Nullable private final GroupId groupId;
|
||||
|
||||
ExceptionMetadata(@NonNull String sender, int senderDevice, @Nullable String groupId) {
|
||||
ExceptionMetadata(@NonNull String sender, int senderDevice, @Nullable GroupId groupId) {
|
||||
this.sender = sender;
|
||||
this.senderDevice = senderDevice;
|
||||
this.groupId = groupId;
|
||||
|
|
|
@ -21,7 +21,6 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
|||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
|
@ -217,7 +216,7 @@ public class ReactionSendJob extends BaseJob {
|
|||
.withReaction(buildReaction(context, reaction, remove, targetAuthor, targetSentTimestamp));
|
||||
|
||||
if (conversationRecipient.isGroup()) {
|
||||
dataMessage.asGroupMessage(new SignalServiceGroup(GroupUtil.getDecodedId(conversationRecipient.requireGroupId())));
|
||||
dataMessage.asGroupMessage(new SignalServiceGroup(conversationRecipient.requireGroupId().getDecodedId()));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -106,7 +106,6 @@ public class RefreshOwnProfileJob extends BaseJob {
|
|||
ProfileName profileName = ProfileName.fromSerialized(plaintextName);
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileName(Recipient.self().getId(), profileName);
|
||||
TextSecurePreferences.setProfileName(context, profileName);
|
||||
} catch (InvalidCiphertextException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
|
|
@ -4,20 +4,18 @@ import androidx.annotation.NonNull;
|
|||
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup.Type;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -33,10 +31,10 @@ public class RequestGroupInfoJob extends BaseJob {
|
|||
private static final String KEY_SOURCE = "source";
|
||||
private static final String KEY_GROUP_ID = "group_id";
|
||||
|
||||
private RecipientId source;
|
||||
private byte[] groupId;
|
||||
private final RecipientId source;
|
||||
private final GroupId groupId;
|
||||
|
||||
public RequestGroupInfoJob(@NonNull RecipientId source, @NonNull byte[] groupId) {
|
||||
public RequestGroupInfoJob(@NonNull RecipientId source, @NonNull GroupId groupId) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
|
@ -47,7 +45,7 @@ public class RequestGroupInfoJob extends BaseJob {
|
|||
|
||||
}
|
||||
|
||||
private RequestGroupInfoJob(@NonNull Job.Parameters parameters, @NonNull RecipientId source, @NonNull byte[] groupId) {
|
||||
private RequestGroupInfoJob(@NonNull Job.Parameters parameters, @NonNull RecipientId source, @NonNull GroupId groupId) {
|
||||
super(parameters);
|
||||
|
||||
this.source = source;
|
||||
|
@ -57,7 +55,7 @@ public class RequestGroupInfoJob extends BaseJob {
|
|||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return new Data.Builder().putString(KEY_SOURCE, source.serialize())
|
||||
.putString(KEY_GROUP_ID, GroupUtil.getEncodedId(groupId, false))
|
||||
.putString(KEY_GROUP_ID, groupId.toString())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -69,7 +67,7 @@ public class RequestGroupInfoJob extends BaseJob {
|
|||
@Override
|
||||
public void onRun() throws IOException, UntrustedIdentityException {
|
||||
SignalServiceGroup group = SignalServiceGroup.newBuilder(Type.REQUEST_INFO)
|
||||
.withId(groupId)
|
||||
.withId(groupId.getDecodedId())
|
||||
.build();
|
||||
|
||||
SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
|
||||
|
@ -99,13 +97,9 @@ public class RequestGroupInfoJob extends BaseJob {
|
|||
|
||||
@Override
|
||||
public @NonNull RequestGroupInfoJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
try {
|
||||
return new RequestGroupInfoJob(parameters,
|
||||
RecipientId.from(data.getString(KEY_SOURCE)),
|
||||
GroupUtil.getDecodedId(data.getString(KEY_GROUP_ID)));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
return new RequestGroupInfoJob(parameters,
|
||||
RecipientId.from(data.getString(KEY_SOURCE)),
|
||||
GroupId.parse(data.getString(KEY_GROUP_ID)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ public class RetrieveProfileAvatarJob extends BaseJob {
|
|||
.setQueue("RetrieveProfileAvatarJob::" + recipient.getId().toQueueKey())
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.HOURS.toMillis(1))
|
||||
.setMaxInstances(1)
|
||||
.build(),
|
||||
recipient,
|
||||
profileAvatar);
|
||||
|
@ -121,10 +120,6 @@ public class RetrieveProfileAvatarJob extends BaseJob {
|
|||
}
|
||||
|
||||
database.setProfileAvatar(recipient.getId(), profileAvatar);
|
||||
|
||||
if (recipient.isLocalNumber()) {
|
||||
TextSecurePreferences.setProfileAvatarId(context, Util.getSecureRandom().nextInt());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -18,6 +18,8 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
|||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class RotateProfileKeyJob extends BaseJob {
|
||||
|
||||
public static String KEY = "RotateProfileKeyJob";
|
||||
|
@ -55,11 +57,12 @@ public class RotateProfileKeyJob extends BaseJob {
|
|||
recipientDatabase.setProfileKey(self.getId(), profileKey);
|
||||
try (StreamDetails avatarStream = AvatarHelper.getSelfProfileAvatarStream(context)) {
|
||||
if (FeatureFlags.VERSIONED_PROFILES) {
|
||||
accountManager.setVersionedProfile(profileKey,
|
||||
TextSecurePreferences.getProfileName(context).serialize(),
|
||||
accountManager.setVersionedProfile(self.getUuid().get(),
|
||||
profileKey,
|
||||
Recipient.self().getProfileName().serialize(),
|
||||
avatarStream);
|
||||
} else {
|
||||
accountManager.setProfileName(profileKey, TextSecurePreferences.getProfileName(context).serialize());
|
||||
accountManager.setProfileName(profileKey, Recipient.self().getProfileName().serialize());
|
||||
accountManager.setProfileAvatar(profileKey, avatarStream);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
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.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Restored the AccountRecord present in the storage service, if any. This will overwrite any local
|
||||
* data that is stored in AccountRecord, so this should only be done immediately after registration.
|
||||
*/
|
||||
public class StorageAccountRestoreJob extends BaseJob {
|
||||
|
||||
public static String KEY = "StorageAccountRestoreJob";
|
||||
|
||||
public static long LIFESPAN = TimeUnit.SECONDS.toMillis(10);
|
||||
|
||||
private static final String TAG = Log.tag(StorageAccountRestoreJob.class);
|
||||
|
||||
public StorageAccountRestoreJob() {
|
||||
this(new Parameters.Builder()
|
||||
.setQueue(StorageSyncJob.QUEUE_KEY)
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setMaxInstances(1)
|
||||
.setMaxAttempts(1)
|
||||
.setLifespan(LIFESPAN)
|
||||
.build());
|
||||
}
|
||||
|
||||
private StorageAccountRestoreJob(@NonNull Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return Data.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRun() throws Exception {
|
||||
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
StorageKey storageServiceKey = SignalStore.storageServiceValues().getOrCreateStorageMasterKey().deriveStorageServiceKey();
|
||||
|
||||
Optional<SignalStorageManifest> manifest = accountManager.getStorageManifest(storageServiceKey);
|
||||
|
||||
if (!manifest.isPresent()) {
|
||||
Log.w(TAG, "Manifest did not exist or was undecryptable (bad key). Not restoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<StorageId> accountId = manifest.get().getAccountStorageId();
|
||||
|
||||
if (!accountId.isPresent()) {
|
||||
Log.w(TAG, "Manifest had no account record! Not restoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
List<SignalStorageRecord> records = accountManager.readStorageRecords(storageServiceKey, Collections.singletonList(accountId.get()));
|
||||
SignalStorageRecord record = records.size() > 0 ? records.get(0) : null;
|
||||
|
||||
if (record == null) {
|
||||
Log.w(TAG, "Could not find account record, even though we had an ID! Not restoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
SignalAccountRecord accountRecord = record.getAccount().orNull();
|
||||
if (accountRecord == null) {
|
||||
Log.w(TAG, "The storage record didn't actually have an account on it! Not restoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
StorageId selfStorageId = StorageId.forAccount(Recipient.self().getStorageServiceId());
|
||||
StorageSyncHelper.applyAccountStorageSyncUpdates(context, selfStorageId, accountRecord);
|
||||
|
||||
if (accountRecord.getAvatarUrlPath().isPresent()) {
|
||||
RetrieveProfileAvatarJob avatarJob = new RetrieveProfileAvatarJob(Recipient.self(), accountRecord.getAvatarUrlPath().get());
|
||||
try {
|
||||
avatarJob.setContext(context);
|
||||
avatarJob.onRun();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to download avatar. Scheduling for later.");
|
||||
ApplicationDependencies.getJobManager().add(avatarJob);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(@NonNull Exception e) {
|
||||
return e instanceof PushNetworkException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<StorageAccountRestoreJob> {
|
||||
@Override
|
||||
public @NonNull
|
||||
StorageAccountRestoreJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new StorageAccountRestoreJob(parameters);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,10 @@ import androidx.annotation.NonNull;
|
|||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.sync.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncModels;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.StorageKeyDatabase;
|
||||
|
@ -16,12 +19,10 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
|||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
|
@ -35,6 +36,7 @@ import java.util.List;
|
|||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
|
@ -76,16 +78,18 @@ public class StorageForcePushJob extends BaseJob {
|
|||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context);
|
||||
|
||||
long currentVersion = accountManager.getStorageManifestVersion();
|
||||
Map<RecipientId, byte[]> oldStorageKeys = recipientDatabase.getAllStorageSyncKeysMap();
|
||||
long currentVersion = accountManager.getStorageManifestVersion();
|
||||
Map<RecipientId, StorageId> oldStorageKeys = recipientDatabase.getContactStorageSyncIdsMap();
|
||||
|
||||
long newVersion = currentVersion + 1;
|
||||
Map<RecipientId, byte[]> newStorageKeys = generateNewKeys(oldStorageKeys);
|
||||
List<SignalStorageRecord> inserts = Stream.of(oldStorageKeys.keySet())
|
||||
.map(recipientDatabase::getRecipientSettings)
|
||||
.withoutNulls()
|
||||
.map(s -> StorageSyncHelper.localToRemoteRecord(s, Objects.requireNonNull(newStorageKeys.get(s.getId()))))
|
||||
.toList();
|
||||
long newVersion = currentVersion + 1;
|
||||
Map<RecipientId, StorageId> newStorageKeys = generateNewKeys(oldStorageKeys);
|
||||
Set<RecipientId> archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
|
||||
List<SignalStorageRecord> inserts = Stream.of(oldStorageKeys.keySet())
|
||||
.map(recipientDatabase::getRecipientSettings)
|
||||
.withoutNulls()
|
||||
.map(s -> StorageSyncModels.localToRemoteRecord(s, Objects.requireNonNull(newStorageKeys.get(s.getId())).getRaw(), archivedRecipients))
|
||||
.toList();
|
||||
inserts.add(StorageSyncHelper.buildAccountRecord(context, StorageId.forAccount(Recipient.self().fresh().getStorageServiceId())));
|
||||
|
||||
SignalStorageManifest manifest = new SignalStorageManifest(newVersion, new ArrayList<>(newStorageKeys.values()));
|
||||
|
||||
|
@ -110,7 +114,7 @@ public class StorageForcePushJob extends BaseJob {
|
|||
|
||||
Log.i(TAG, "Force push succeeded. Updating local manifest version to: " + newVersion);
|
||||
TextSecurePreferences.setStorageManifestVersion(context, newVersion);
|
||||
recipientDatabase.applyStorageSyncKeyUpdates(newStorageKeys);
|
||||
recipientDatabase.applyStorageIdUpdates(newStorageKeys);
|
||||
storageKeyDatabase.deleteAll();
|
||||
}
|
||||
|
||||
|
@ -123,11 +127,11 @@ public class StorageForcePushJob extends BaseJob {
|
|||
public void onFailure() {
|
||||
}
|
||||
|
||||
private static @NonNull Map<RecipientId, byte[]> generateNewKeys(@NonNull Map<RecipientId, byte[]> oldKeys) {
|
||||
Map<RecipientId, byte[]> out = new HashMap<>();
|
||||
private static @NonNull Map<RecipientId, StorageId> generateNewKeys(@NonNull Map<RecipientId, StorageId> oldKeys) {
|
||||
Map<RecipientId, StorageId> out = new HashMap<>();
|
||||
|
||||
for (Map.Entry<RecipientId, byte[]> entry : oldKeys.entrySet()) {
|
||||
out.put(entry.getKey(), StorageSyncHelper.generateKey());
|
||||
for (Map.Entry<RecipientId, StorageId> entry : oldKeys.entrySet()) {
|
||||
out.put(entry.getKey(), entry.getValue().withNewBytes(StorageSyncHelper.generateKey()));
|
||||
}
|
||||
|
||||
return out;
|
||||
|
|
|
@ -6,11 +6,13 @@ import androidx.annotation.NonNull;
|
|||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.sync.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.contacts.sync.StorageSyncHelper.KeyDifferenceResult;
|
||||
import org.thoughtcrime.securesms.contacts.sync.StorageSyncHelper.LocalWriteResult;
|
||||
import org.thoughtcrime.securesms.contacts.sync.StorageSyncHelper.MergeResult;
|
||||
import org.thoughtcrime.securesms.contacts.sync.StorageSyncHelper.WriteOperationResult;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.KeyDifferenceResult;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.LocalWriteResult;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.MergeResult;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.WriteOperationResult;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncModels;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||
|
@ -22,6 +24,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
|||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncValidations;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
@ -29,16 +32,20 @@ import org.thoughtcrime.securesms.util.Util;
|
|||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
|
@ -55,27 +62,14 @@ public class StorageSyncJob extends BaseJob {
|
|||
|
||||
private static final String TAG = Log.tag(StorageSyncJob.class);
|
||||
|
||||
private static final long REFRESH_INTERVAL = TimeUnit.HOURS.toMillis(2);
|
||||
|
||||
public StorageSyncJob() {
|
||||
this(new Job.Parameters.Builder().addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(QUEUE_KEY)
|
||||
.setMaxInstances(1)
|
||||
.setMaxInstances(2)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.build());
|
||||
}
|
||||
|
||||
public static void scheduleIfNecessary() {
|
||||
long timeSinceLastSync = System.currentTimeMillis() - SignalStore.storageServiceValues().getLastSyncTime();
|
||||
|
||||
if (timeSinceLastSync > REFRESH_INTERVAL) {
|
||||
Log.d(TAG, "Scheduling a sync. Last sync was " + timeSinceLastSync + " ms ago.");
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
} else {
|
||||
Log.d(TAG, "No need for sync. Last sync was " + timeSinceLastSync + " ms ago.");
|
||||
}
|
||||
}
|
||||
|
||||
private StorageSyncJob(@NonNull Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
@ -97,6 +91,11 @@ public class StorageSyncJob extends BaseJob {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!TextSecurePreferences.isPushRegistered(context)) {
|
||||
Log.i(TAG, "Not registered. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
boolean needsMultiDeviceSync = performSync();
|
||||
|
||||
|
@ -112,11 +111,6 @@ public class StorageSyncJob extends BaseJob {
|
|||
.then(new StorageForcePushJob())
|
||||
.then(new MultiDeviceStorageSyncRequestJob())
|
||||
.enqueue();
|
||||
} finally {
|
||||
if (!SignalStore.storageServiceValues().hasFirstStorageSyncCompleted()) {
|
||||
SignalStore.storageServiceValues().setFirstStorageSyncCompleted(true);
|
||||
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,26 +139,29 @@ public class StorageSyncJob extends BaseJob {
|
|||
if (remoteManifest.isPresent() && remoteManifestVersion > localManifestVersion) {
|
||||
Log.i(TAG, "[Remote Newer] Newer manifest version found!");
|
||||
|
||||
List<byte[]> allLocalStorageKeys = getAllLocalStorageKeys(context);
|
||||
KeyDifferenceResult keyDifference = StorageSyncHelper.findKeyDifference(remoteManifest.get().getStorageKeys(), allLocalStorageKeys);
|
||||
List<StorageId> allLocalStorageKeys = getAllLocalStorageIds(context);
|
||||
KeyDifferenceResult keyDifference = StorageSyncHelper.findKeyDifference(remoteManifest.get().getStorageIds(), allLocalStorageKeys);
|
||||
|
||||
if (!keyDifference.isEmpty()) {
|
||||
Log.i(TAG, "[Remote Newer] There's a difference in keys. Local-only: " + keyDifference.getLocalOnlyKeys().size() + ", Remote-only: " + keyDifference.getRemoteOnlyKeys().size());
|
||||
|
||||
List<SignalStorageRecord> localOnly = buildLocalStorageRecords(context, keyDifference.getLocalOnlyKeys());
|
||||
Set<RecipientId> archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
|
||||
List<SignalStorageRecord> localOnly = buildLocalStorageRecords(context, keyDifference.getLocalOnlyKeys(), archivedRecipients);
|
||||
List<SignalStorageRecord> remoteOnly = accountManager.readStorageRecords(storageServiceKey, keyDifference.getRemoteOnlyKeys());
|
||||
MergeResult mergeResult = StorageSyncHelper.resolveConflict(remoteOnly, localOnly);
|
||||
WriteOperationResult writeOperationResult = StorageSyncHelper.createWriteOperation(remoteManifest.get().getVersion(), allLocalStorageKeys, mergeResult);
|
||||
|
||||
StorageSyncValidations.validate(writeOperationResult);
|
||||
|
||||
Log.i(TAG, "[Remote Newer] MergeResult :: " + mergeResult);
|
||||
|
||||
if (!writeOperationResult.isEmpty()) {
|
||||
Log.i(TAG, "[Remote Newer] WriteOperationResult :: " + writeOperationResult);
|
||||
Log.i(TAG, "[Remote Newer] We have something to write remotely.");
|
||||
|
||||
if (writeOperationResult.getManifest().getStorageKeys().size() != remoteManifest.get().getStorageKeys().size() + writeOperationResult.getInserts().size() - writeOperationResult.getDeletes().size()) {
|
||||
if (writeOperationResult.getManifest().getStorageIds().size() != remoteManifest.get().getStorageIds().size() + writeOperationResult.getInserts().size() - writeOperationResult.getDeletes().size()) {
|
||||
Log.w(TAG, String.format(Locale.ENGLISH, "Bad storage key management! originalRemoteKeys: %d, newRemoteKeys: %d, insertedKeys: %d, deletedKeys: %d",
|
||||
remoteManifest.get().getStorageKeys().size(), writeOperationResult.getManifest().getStorageKeys().size(), writeOperationResult.getInserts().size(), writeOperationResult.getDeletes().size()));
|
||||
remoteManifest.get().getStorageIds().size(), writeOperationResult.getManifest().getStorageIds().size(), writeOperationResult.getInserts().size(), writeOperationResult.getDeletes().size()));
|
||||
}
|
||||
|
||||
Optional<SignalStorageManifest> conflict = accountManager.writeStorageRecords(storageServiceKey, writeOperationResult.getManifest(), writeOperationResult.getInserts(), writeOperationResult.getDeletes());
|
||||
|
@ -181,6 +178,7 @@ public class StorageSyncJob extends BaseJob {
|
|||
|
||||
recipientDatabase.applyStorageSyncUpdates(mergeResult.getLocalContactInserts(), mergeResult.getLocalContactUpdates(), mergeResult.getLocalGroupV1Inserts(), mergeResult.getLocalGroupV1Updates());
|
||||
storageKeyDatabase.applyStorageSyncUpdates(mergeResult.getLocalUnknownInserts(), mergeResult.getLocalUnknownDeletes());
|
||||
StorageSyncHelper.applyAccountStorageSyncUpdates(context, mergeResult.getLocalAccountUpdate());
|
||||
needsMultiDeviceSync = true;
|
||||
|
||||
Log.i(TAG, "[Remote Newer] Updating local manifest version to: " + remoteManifestVersion);
|
||||
|
@ -194,20 +192,27 @@ public class StorageSyncJob extends BaseJob {
|
|||
|
||||
localManifestVersion = TextSecurePreferences.getStorageManifestVersion(context);
|
||||
|
||||
List<byte[]> allLocalStorageKeys = recipientDatabase.getAllStorageSyncKeys();
|
||||
List<RecipientSettings> pendingUpdates = recipientDatabase.getPendingRecipientSyncUpdates();
|
||||
List<RecipientSettings> pendingInsertions = recipientDatabase.getPendingRecipientSyncInsertions();
|
||||
List<RecipientSettings> pendingDeletions = recipientDatabase.getPendingRecipientSyncDeletions();
|
||||
Optional<LocalWriteResult> localWriteResult = StorageSyncHelper.buildStorageUpdatesForLocal(localManifestVersion,
|
||||
allLocalStorageKeys,
|
||||
pendingUpdates,
|
||||
pendingInsertions,
|
||||
pendingDeletions);
|
||||
List<StorageId> allLocalStorageKeys = getAllLocalStorageIds(context);
|
||||
List<RecipientSettings> pendingUpdates = recipientDatabase.getPendingRecipientSyncUpdates();
|
||||
List<RecipientSettings> pendingInsertions = recipientDatabase.getPendingRecipientSyncInsertions();
|
||||
List<RecipientSettings> pendingDeletions = recipientDatabase.getPendingRecipientSyncDeletions();
|
||||
Optional<SignalAccountRecord> pendingAccountUpdate = StorageSyncHelper.getPendingAccountSyncUpdate(context);
|
||||
Optional<SignalAccountRecord> pendingAccountInsert = StorageSyncHelper.getPendingAccountSyncInsert(context);
|
||||
Set<RecipientId> archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
|
||||
Optional<LocalWriteResult> localWriteResult = StorageSyncHelper.buildStorageUpdatesForLocal(localManifestVersion,
|
||||
allLocalStorageKeys,
|
||||
pendingUpdates,
|
||||
pendingInsertions,
|
||||
pendingDeletions,
|
||||
pendingAccountUpdate,
|
||||
pendingAccountInsert,
|
||||
archivedRecipients);
|
||||
|
||||
if (localWriteResult.isPresent()) {
|
||||
Log.i(TAG, String.format(Locale.ENGLISH, "[Local Changes] Local changes present. %d updates, %d inserts, %d deletes.", pendingUpdates.size(), pendingInsertions.size(), pendingDeletions.size()));
|
||||
Log.i(TAG, String.format(Locale.ENGLISH, "[Local Changes] Local changes present. %d updates, %d inserts, %d deletes, account update: %b, account insert %b.", pendingUpdates.size(), pendingInsertions.size(), pendingDeletions.size(), pendingAccountUpdate.isPresent(), pendingAccountInsert.isPresent()));
|
||||
|
||||
WriteOperationResult localWrite = localWriteResult.get().getWriteResult();
|
||||
StorageSyncValidations.validate(localWrite);
|
||||
|
||||
Log.i(TAG, "[Local Changes] WriteOperationResult :: " + localWrite);
|
||||
|
||||
|
@ -215,18 +220,19 @@ public class StorageSyncJob extends BaseJob {
|
|||
throw new AssertionError("Decided there were local writes, but our write result was empty!");
|
||||
}
|
||||
|
||||
Optional<SignalStorageManifest> conflict = accountManager.writeStorageRecords(storageServiceKey, localWrite.getManifest(), localWrite.getInserts(), localWrite.getDeletes());
|
||||
Optional<SignalStorageManifest> conflict = accountManager.writeStorageRecords(storageServiceKey, localWrite.getManifest(), localWrite.getInserts(), localWrite.getDeletes());
|
||||
|
||||
if (conflict.isPresent()) {
|
||||
Log.w(TAG, "[Local Changes] Hit a conflict when trying to upload our local writes! Retrying.");
|
||||
throw new RetryLaterException();
|
||||
}
|
||||
|
||||
List<RecipientId> clearIds = new ArrayList<>(pendingUpdates.size() + pendingInsertions.size() + pendingDeletions.size());
|
||||
List<RecipientId> clearIds = new ArrayList<>(pendingUpdates.size() + pendingInsertions.size() + pendingDeletions.size() + 1);
|
||||
|
||||
clearIds.addAll(Stream.of(pendingUpdates).map(RecipientSettings::getId).toList());
|
||||
clearIds.addAll(Stream.of(pendingInsertions).map(RecipientSettings::getId).toList());
|
||||
clearIds.addAll(Stream.of(pendingDeletions).map(RecipientSettings::getId).toList());
|
||||
clearIds.add(Recipient.self().getId());
|
||||
|
||||
recipientDatabase.clearDirtyState(clearIds);
|
||||
recipientDatabase.updateStorageKeys(localWriteResult.get().getStorageKeyUpdates());
|
||||
|
@ -242,22 +248,44 @@ public class StorageSyncJob extends BaseJob {
|
|||
return needsMultiDeviceSync;
|
||||
}
|
||||
|
||||
private static @NonNull List<byte[]> getAllLocalStorageKeys(@NonNull Context context) {
|
||||
return Util.concatenatedList(DatabaseFactory.getRecipientDatabase(context).getAllStorageSyncKeys(),
|
||||
private static @NonNull List<StorageId> getAllLocalStorageIds(@NonNull Context context) {
|
||||
Recipient self = Recipient.self().fresh();
|
||||
|
||||
return Util.concatenatedList(DatabaseFactory.getRecipientDatabase(context).getContactStorageSyncIds(),
|
||||
Collections.singletonList(StorageId.forAccount(self.getStorageServiceId())),
|
||||
DatabaseFactory.getStorageKeyDatabase(context).getAllKeys());
|
||||
}
|
||||
|
||||
private static @NonNull List<SignalStorageRecord> buildLocalStorageRecords(@NonNull Context context, @NonNull List<byte[]> keys) {
|
||||
private static @NonNull List<SignalStorageRecord> buildLocalStorageRecords(@NonNull Context context, @NonNull List<StorageId> ids, @NonNull Set<RecipientId> archivedRecipients) {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context);
|
||||
|
||||
List<SignalStorageRecord> records = new ArrayList<>(keys.size());
|
||||
List<SignalStorageRecord> records = new ArrayList<>(ids.size());
|
||||
|
||||
for (byte[] key : keys) {
|
||||
SignalStorageRecord record = Optional.fromNullable(recipientDatabase.getByStorageSyncKey(key))
|
||||
.transform(StorageSyncHelper::localToRemoteRecord)
|
||||
.or(() -> storageKeyDatabase.getByKey(key));
|
||||
records.add(record);
|
||||
for (StorageId id : ids) {
|
||||
switch (id.getType()) {
|
||||
case ManifestRecord.Identifier.Type.CONTACT_VALUE:
|
||||
case ManifestRecord.Identifier.Type.GROUPV1_VALUE:
|
||||
case ManifestRecord.Identifier.Type.GROUPV2_VALUE:
|
||||
RecipientSettings settings = recipientDatabase.getByStorageId(id.getRaw());
|
||||
if (settings != null) {
|
||||
records.add(StorageSyncModels.localToRemoteRecord(settings, archivedRecipients));
|
||||
} else {
|
||||
Log.w(TAG, "Missing local recipient model! Type: " + id.getType());
|
||||
}
|
||||
break;
|
||||
case ManifestRecord.Identifier.Type.ACCOUNT_VALUE:
|
||||
records.add(StorageSyncHelper.buildAccountRecord(context, id));
|
||||
break;
|
||||
default:
|
||||
SignalStorageRecord unknown = storageKeyDatabase.getById(id.getRaw());
|
||||
if (unknown != null) {
|
||||
records.add(unknown);
|
||||
} else {
|
||||
Log.w(TAG, "Missing local unknown model! Type: " + id.getType());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return records;
|
||||
|
|
|
@ -12,7 +12,6 @@ import org.thoughtcrime.securesms.jobmanager.Job;
|
|||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
|
@ -87,7 +86,7 @@ public class TypingSendJob extends BaseJob {
|
|||
|
||||
if (recipient.isGroup()) {
|
||||
recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), false);
|
||||
groupId = Optional.of(GroupUtil.getDecodedId(recipient.requireGroupId()));
|
||||
groupId = Optional.of(recipient.requireGroupId().getDecodedId());
|
||||
}
|
||||
|
||||
recipients = Stream.of(recipients).map(Recipient::resolve).toList();
|
||||
|
|
|
@ -18,7 +18,6 @@ public final class SignalStore {
|
|||
|
||||
public static void onFirstEverAppLaunch() {
|
||||
registrationValues().onFirstEverAppLaunch();
|
||||
storageServiceValues().setFirstStorageSyncCompleted(false);
|
||||
}
|
||||
|
||||
public static @NonNull KbsValues kbsValues() {
|
||||
|
|
|
@ -9,9 +9,8 @@ import java.security.SecureRandom;
|
|||
|
||||
public class StorageServiceValues {
|
||||
|
||||
private static final String STORAGE_MASTER_KEY = "storage.storage_master_key";
|
||||
private static final String FIRST_STORAGE_SYNC_COMPLETED = "storage.first_storage_sync_completed";
|
||||
private static final String LAST_SYNC_TIME = "storage.last_sync_time";
|
||||
private static final String STORAGE_MASTER_KEY = "storage.storage_master_key";
|
||||
private static final String LAST_SYNC_TIME = "storage.last_sync_time";
|
||||
|
||||
private final KeyValueStore store;
|
||||
|
||||
|
@ -38,14 +37,6 @@ public class StorageServiceValues {
|
|||
.commit();
|
||||
}
|
||||
|
||||
public boolean hasFirstStorageSyncCompleted() {
|
||||
return !FeatureFlags.storageServiceRestore() || store.getBoolean(FIRST_STORAGE_SYNC_COMPLETED, true);
|
||||
}
|
||||
|
||||
public void setFirstStorageSyncCompleted(boolean completed) {
|
||||
store.beginWrite().putBoolean(FIRST_STORAGE_SYNC_COMPLETED, completed).apply();
|
||||
}
|
||||
|
||||
public long getLastSyncTime() {
|
||||
return store.getLong(LAST_SYNC_TIME, 0);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.mediasend;
|
|||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Point;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
@ -26,6 +27,8 @@ import java.util.Collections;
|
|||
|
||||
public class AvatarSelectionActivity extends AppCompatActivity implements CameraFragment.Controller, ImageEditorFragment.Controller, MediaPickerFolderFragment.Controller, MediaPickerItemFragment.Controller {
|
||||
|
||||
private static final Point AVATAR_DIMENSIONS = new Point(1024, 1024);
|
||||
|
||||
private static final String IMAGE_CAPTURE = "IMAGE_CAPTURE";
|
||||
private static final String IMAGE_EDITOR = "IMAGE_EDITOR";
|
||||
private static final String ARG_GALLERY = "ARG_GALLERY";
|
||||
|
@ -199,9 +202,6 @@ public class AvatarSelectionActivity extends AppCompatActivity implements Camera
|
|||
}
|
||||
|
||||
ImageEditorFragment.Data data = (ImageEditorFragment.Data) fragment.saveState();
|
||||
if (data == null) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
EditorModel model = data.readModel();
|
||||
if (model == null) {
|
||||
|
@ -210,7 +210,7 @@ public class AvatarSelectionActivity extends AppCompatActivity implements Camera
|
|||
|
||||
MediaRepository.transformMedia(this,
|
||||
Collections.singletonList(currentMedia),
|
||||
Collections.singletonMap(currentMedia, new ImageEditorModelRenderMediaTransform(model)),
|
||||
Collections.singletonMap(currentMedia, new ImageEditorModelRenderMediaTransform(model, AVATAR_DIMENSIONS)),
|
||||
output -> {
|
||||
Media transformed = output.get(currentMedia);
|
||||
|
||||
|
|
|
@ -44,7 +44,6 @@ public class CameraButtonView extends View {
|
|||
|
||||
private boolean isRecordingVideo;
|
||||
private float progressPercent = 0f;
|
||||
private float latestIncrement = 0f;
|
||||
|
||||
private @NonNull CameraButtonMode cameraButtonMode = CameraButtonMode.IMAGE;
|
||||
private @Nullable VideoCaptureListener videoCaptureListener;
|
||||
|
@ -247,7 +246,6 @@ public class CameraButtonView extends View {
|
|||
int action = event.getAction();
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
latestIncrement = 0f;
|
||||
if (isEnabled()) {
|
||||
startAnimation(shrinkAnimation);
|
||||
}
|
||||
|
@ -258,11 +256,6 @@ public class CameraButtonView extends View {
|
|||
float deltaY = Math.abs(event.getY() - deadzoneRect.top);
|
||||
float increment = Math.min(1f, deltaY / maxRange);
|
||||
|
||||
if (Math.abs(increment - latestIncrement) < MINIMUM_ALLOWED_ZOOM_STEP) {
|
||||
break;
|
||||
}
|
||||
|
||||
latestIncrement = increment;
|
||||
notifyZoomPercent(ZOOM_INTERPOLATOR.getInterpolation(increment));
|
||||
invalidate();
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ class CameraContactsRepository {
|
|||
try (GroupDatabase.Reader reader = groupDatabase.getGroupsFilteredByTitle(query, false)) {
|
||||
GroupDatabase.GroupRecord groupRecord;
|
||||
while ((groupRecord = reader.getNext()) != null) {
|
||||
RecipientId recipientId = recipientDatabase.getOrInsertFromGroupId(groupRecord.getEncodedId());
|
||||
RecipientId recipientId = recipientDatabase.getOrInsertFromGroupId(groupRecord.getId());
|
||||
recipients.add(Recipient.resolved(recipientId));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,8 +22,9 @@ import android.widget.TextView;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.camera.core.CameraX;
|
||||
import androidx.camera.core.CameraSelector;
|
||||
import androidx.camera.core.ImageCapture;
|
||||
import androidx.camera.core.ImageCaptureException;
|
||||
import androidx.camera.core.ImageProxy;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
@ -204,7 +205,9 @@ public class CameraXFragment extends Fragment implements CameraFragment {
|
|||
onCaptureClicked();
|
||||
});
|
||||
|
||||
if (camera.hasCameraWithLensFacing(CameraX.LensFacing.FRONT) && camera.hasCameraWithLensFacing(CameraX.LensFacing.BACK)) {
|
||||
camera.setScaleType(CameraXView.ScaleType.CENTER_INSIDE);
|
||||
|
||||
if (camera.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT) && camera.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK)) {
|
||||
flipButton.setVisibility(View.VISIBLE);
|
||||
flipButton.setOnClickListener(v -> {
|
||||
camera.toggleCamera();
|
||||
|
@ -308,7 +311,7 @@ public class CameraXFragment extends Fragment implements CameraFragment {
|
|||
|
||||
TooltipPopup.forTarget(captureButton)
|
||||
.setOnDismissListener(this::neverDisplayVideoRecordingTooltipAgain)
|
||||
.setBackgroundTint(ContextCompat.getColor(requireContext(), R.color.signal_primary))
|
||||
.setBackgroundTint(ContextCompat.getColor(requireContext(), R.color.core_ultramarine))
|
||||
.setTextColor(ThemeUtil.getThemedColor(requireContext(), R.attr.conversation_title_color))
|
||||
.setText(R.string.CameraXFragment_tap_for_photo_hold_for_video)
|
||||
.show(displayRotation == Surface.ROTATION_0 || displayRotation == Surface.ROTATION_180 ? TooltipPopup.POSITION_ABOVE : TooltipPopup.POSITION_START);
|
||||
|
@ -361,15 +364,15 @@ public class CameraXFragment extends Fragment implements CameraFragment {
|
|||
selfieFlash
|
||||
);
|
||||
|
||||
camera.takePicture(Executors.mainThreadExecutor(), new ImageCapture.OnImageCapturedListener() {
|
||||
camera.takePicture(Executors.mainThreadExecutor(), new ImageCapture.OnImageCapturedCallback() {
|
||||
@Override
|
||||
public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
|
||||
public void onCaptureSuccess(ImageProxy image) {
|
||||
flashHelper.endFlash();
|
||||
|
||||
SimpleTask.run(CameraXFragment.this.getViewLifecycleOwner().getLifecycle(), () -> {
|
||||
stopwatch.split("captured");
|
||||
try {
|
||||
return CameraXUtil.toJpeg(image, rotationDegrees, camera.getCameraLensFacing() == CameraX.LensFacing.FRONT);
|
||||
return CameraXUtil.toJpeg(image, camera.getCameraLensFacing() == CameraSelector.LENS_FACING_FRONT);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
} finally {
|
||||
|
@ -388,7 +391,7 @@ public class CameraXFragment extends Fragment implements CameraFragment {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onError(ImageCapture.ImageCaptureError useCaseError, String message, @Nullable Throwable cause) {
|
||||
public void onError(ImageCaptureException exception) {
|
||||
flashHelper.endFlash();
|
||||
controller.onCameraError();
|
||||
}
|
||||
|
|
|
@ -6,8 +6,9 @@ import android.view.WindowManager;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.camera.core.CameraSelector;
|
||||
import androidx.camera.core.CameraX;
|
||||
import androidx.camera.core.FlashMode;
|
||||
import androidx.camera.core.ImageCapture;
|
||||
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXView;
|
||||
|
||||
|
@ -65,8 +66,10 @@ final class CameraXSelfieFlashHelper {
|
|||
}
|
||||
|
||||
private boolean shouldUseViewBasedFlash() {
|
||||
return camera.getFlash() == FlashMode.ON &&
|
||||
Integer cameraLensFacing = camera.getCameraLensFacing();
|
||||
|
||||
return camera.getFlash() == ImageCapture.FLASH_MODE_ON &&
|
||||
!camera.hasFlash() &&
|
||||
camera.getCameraLensFacing() == CameraX.LensFacing.FRONT;
|
||||
cameraLensFacing != null && cameraLensFacing == CameraSelector.LENS_FACING_BACK;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import org.thoughtcrime.securesms.mediasend.camerax.CameraXView;
|
|||
import org.thoughtcrime.securesms.mediasend.camerax.VideoCapture;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.MemoryFileDescriptor;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.video.VideoUtil;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
|
@ -45,12 +44,12 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
|
|||
private boolean isRecording;
|
||||
private ValueAnimator cameraMetricsAnimator;
|
||||
|
||||
private final VideoCapture.OnVideoSavedListener videoSavedListener = new VideoCapture.OnVideoSavedListener() {
|
||||
private final VideoCapture.OnVideoSavedCallback videoSavedListener = new VideoCapture.OnVideoSavedCallback() {
|
||||
@Override
|
||||
public void onVideoSaved(@NonNull FileDescriptor fileDescriptor) {
|
||||
try {
|
||||
isRecording = false;
|
||||
camera.setZoomLevel(0f);
|
||||
camera.setZoomRatio(camera.getMinZoomRatio());
|
||||
memoryFileDescriptor.seek(0);
|
||||
callback.onVideoSaved(fileDescriptor);
|
||||
} catch (IOException e) {
|
||||
|
@ -59,13 +58,9 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull VideoCapture.VideoCaptureError videoCaptureError,
|
||||
@NonNull String message,
|
||||
@Nullable Throwable cause)
|
||||
{
|
||||
public void onError(int videoCaptureError, @NonNull String message, @Nullable Throwable cause) {
|
||||
isRecording = false;
|
||||
callback.onVideoError(cause);
|
||||
Util.runOnMain(() -> resetCameraSizing());
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -119,7 +114,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
|
|||
}
|
||||
|
||||
private void beginCameraRecording() {
|
||||
this.camera.setZoomLevel(0f);
|
||||
this.camera.setZoomRatio(this.camera.getMinZoomRatio());
|
||||
callback.onVideoRecordStarted();
|
||||
shrinkCaptureArea();
|
||||
camera.startRecording(memoryFileDescriptor.getFileDescriptor(), Executors.mainThreadExecutor(), videoSavedListener);
|
||||
|
@ -135,22 +130,24 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
|
|||
|
||||
if (scaleX == 1f) {
|
||||
float targetHeightForAnimation = videoRecordingSize.getHeight() * scale;
|
||||
|
||||
if (screenSize.getHeight() == targetHeightForAnimation) {
|
||||
return;
|
||||
}
|
||||
|
||||
cameraMetricsAnimator = ValueAnimator.ofFloat(screenSize.getHeight(), targetHeightForAnimation);
|
||||
} else {
|
||||
|
||||
if (screenSize.getWidth() == targetWidthForAnimation) {
|
||||
return;
|
||||
}
|
||||
|
||||
cameraMetricsAnimator = ValueAnimator.ofFloat(screenSize.getWidth(), targetWidthForAnimation);
|
||||
}
|
||||
|
||||
ViewGroup.LayoutParams params = camera.getLayoutParams();
|
||||
cameraMetricsAnimator.setInterpolator(new LinearInterpolator());
|
||||
cameraMetricsAnimator.setDuration(200);
|
||||
cameraMetricsAnimator.addListener(new AnimationEndCallback() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
if (!isRecording) return;
|
||||
|
||||
scaleCameraViewToMatchRecordingSizeAndAspectRatio();
|
||||
}
|
||||
});
|
||||
cameraMetricsAnimator.addUpdateListener(animation -> {
|
||||
if (scaleX == 1f) {
|
||||
params.height = Math.round((float) animation.getAnimatedValue());
|
||||
|
@ -162,20 +159,6 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
|
|||
cameraMetricsAnimator.start();
|
||||
}
|
||||
|
||||
private void scaleCameraViewToMatchRecordingSizeAndAspectRatio() {
|
||||
ViewGroup.LayoutParams layoutParams = camera.getLayoutParams();
|
||||
|
||||
Size videoRecordingSize = VideoUtil.getVideoRecordingSize();
|
||||
float scale = getSurfaceScaleForRecording();
|
||||
|
||||
layoutParams.height = videoRecordingSize.getHeight();
|
||||
layoutParams.width = videoRecordingSize.getWidth();
|
||||
|
||||
camera.setLayoutParams(layoutParams);
|
||||
camera.setScaleX(scale);
|
||||
camera.setScaleY(scale);
|
||||
}
|
||||
|
||||
private Size getScreenSize() {
|
||||
DisplayMetrics metrics = camera.getResources().getDisplayMetrics();
|
||||
return new Size(metrics.widthPixels, metrics.heightPixels);
|
||||
|
@ -187,16 +170,6 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
|
|||
return Math.min(screenSize.getHeight(), screenSize.getWidth()) / (float) Math.min(videoRecordingSize.getHeight(), videoRecordingSize.getWidth());
|
||||
}
|
||||
|
||||
private void resetCameraSizing() {
|
||||
ViewGroup.LayoutParams layoutParams = camera.getLayoutParams();
|
||||
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
|
||||
camera.setLayoutParams(layoutParams);
|
||||
camera.setScaleX(1);
|
||||
camera.setScaleY(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoCaptureComplete() {
|
||||
isRecording = false;
|
||||
|
@ -214,8 +187,8 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
|
|||
|
||||
@Override
|
||||
public void onZoomIncremented(float increment) {
|
||||
float range = camera.getMaxZoomLevel() - camera.getMinZoomLevel();
|
||||
camera.setZoomLevel(range * increment);
|
||||
float range = camera.getMaxZoomRatio() - camera.getMinZoomRatio();
|
||||
camera.setZoomRatio((range * increment) + camera.getMinZoomRatio());
|
||||
}
|
||||
|
||||
static MemoryFileDescriptor createFileDescriptor(@NonNull Context context) throws MemoryFileDescriptor.MemoryFileException {
|
||||
|
@ -226,7 +199,7 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener
|
|||
);
|
||||
}
|
||||
|
||||
private abstract class AnimationEndCallback implements Animator.AnimatorListener {
|
||||
private static abstract class AnimationEndCallback implements Animator.AnimatorListener {
|
||||
|
||||
@Override
|
||||
public final void onAnimationStart(Animator animation) {
|
||||
|
|
|
@ -2,15 +2,18 @@ package org.thoughtcrime.securesms.mediasend;
|
|||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Point;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
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.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
@ -20,10 +23,16 @@ public final class ImageEditorModelRenderMediaTransform implements MediaTransfor
|
|||
|
||||
private static final String TAG = Log.tag(ImageEditorModelRenderMediaTransform.class);
|
||||
|
||||
private final EditorModel modelToRender;
|
||||
@NonNull private final EditorModel modelToRender;
|
||||
@Nullable private final Point size;
|
||||
|
||||
ImageEditorModelRenderMediaTransform(@NonNull EditorModel modelToRender) {
|
||||
this(modelToRender, null);
|
||||
}
|
||||
|
||||
ImageEditorModelRenderMediaTransform(@NonNull EditorModel modelToRender, @Nullable Point size) {
|
||||
this.modelToRender = modelToRender;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
@ -31,7 +40,7 @@ public final class ImageEditorModelRenderMediaTransform implements MediaTransfor
|
|||
public @NonNull Media transform(@NonNull Context context, @NonNull Media media) {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
||||
Bitmap bitmap = modelToRender.render(context);
|
||||
Bitmap bitmap = modelToRender.render(context, size);
|
||||
try {
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
|
||||
|
||||
|
@ -46,11 +55,7 @@ public final class ImageEditorModelRenderMediaTransform implements MediaTransfor
|
|||
return media;
|
||||
} finally {
|
||||
bitmap.recycle();
|
||||
try {
|
||||
outputStream.close();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
Util.close(outputStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -727,7 +727,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
case VIEW_ONCE_TOOLTIP:
|
||||
TooltipPopup.forTarget(revealButton)
|
||||
.setText(R.string.MediaSendActivity_tap_here_to_make_this_message_disappear_after_it_is_viewed)
|
||||
.setBackgroundTint(getResources().getColor(R.color.core_blue))
|
||||
.setBackgroundTint(getResources().getColor(R.color.core_ultramarine))
|
||||
.setTextColor(getResources().getColor(R.color.core_white))
|
||||
.setOnDismissListener(() -> TextSecurePreferences.setHasSeenViewOnceTooltip(this, true))
|
||||
.show(TooltipPopup.POSITION_ABOVE);
|
||||
|
|
|
@ -5,10 +5,9 @@ import android.os.Bundle;
|
|||
import android.os.Parcelable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
import androidx.camera.core.FlashMode;
|
||||
import androidx.camera.core.ImageCapture;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
|
@ -43,7 +42,7 @@ public final class CameraXFlashToggleView extends AppCompatImageView {
|
|||
public CameraXFlashToggleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
super.setOnClickListener((v) -> setFlash(FLASH_MODES.get((flashIndex + 1) % FLASH_ENUM.length)));
|
||||
super.setOnClickListener((v) -> setFlash(FLASH_MODES.get((flashIndex + 1) % FLASH_ENUM.length).getFlashMode()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -61,10 +60,12 @@ public final class CameraXFlashToggleView extends AppCompatImageView {
|
|||
|
||||
public void setAutoFlashEnabled(boolean isAutoEnabled) {
|
||||
supportsFlashModeAuto = isAutoEnabled;
|
||||
setFlash(FLASH_MODES.get(flashIndex));
|
||||
setFlash(FLASH_MODES.get(flashIndex).getFlashMode());
|
||||
}
|
||||
|
||||
public void setFlash(@NonNull FlashMode flashMode) {
|
||||
public void setFlash(@ImageCapture.FlashMode int mode) {
|
||||
FlashMode flashMode = FlashMode.fromImageCaptureFlashMode(mode);
|
||||
|
||||
flashIndex = resolveFlashIndex(FLASH_MODES.indexOf(flashMode), supportsFlashModeAuto);
|
||||
refreshDrawableState();
|
||||
notifyListener();
|
||||
|
@ -92,7 +93,7 @@ public final class CameraXFlashToggleView extends AppCompatImageView {
|
|||
|
||||
supportsFlashModeAuto = savedState.getBoolean(STATE_SUPPORT_AUTO);
|
||||
setFlash(FLASH_MODES.get(
|
||||
resolveFlashIndex(savedState.getInt(STATE_FLASH_INDEX), supportsFlashModeAuto))
|
||||
resolveFlashIndex(savedState.getInt(STATE_FLASH_INDEX), supportsFlashModeAuto)).getFlashMode()
|
||||
);
|
||||
|
||||
super.onRestoreInstanceState(savedState.getParcelable(STATE_PARENT));
|
||||
|
@ -104,7 +105,7 @@ public final class CameraXFlashToggleView extends AppCompatImageView {
|
|||
private void notifyListener() {
|
||||
if (flashModeChangedListener == null) return;
|
||||
|
||||
flashModeChangedListener.flashModeChanged(FLASH_MODES.get(flashIndex));
|
||||
flashModeChangedListener.flashModeChanged(FLASH_MODES.get(flashIndex).getFlashMode());
|
||||
}
|
||||
|
||||
private static int resolveFlashIndex(int desiredFlashIndex, boolean supportsFlashModeAuto) {
|
||||
|
@ -126,6 +127,33 @@ public final class CameraXFlashToggleView extends AppCompatImageView {
|
|||
}
|
||||
|
||||
public interface OnFlashModeChangedListener {
|
||||
void flashModeChanged(FlashMode flashMode);
|
||||
void flashModeChanged(@ImageCapture.CaptureMode int flashMode);
|
||||
}
|
||||
|
||||
private enum FlashMode {
|
||||
|
||||
AUTO(ImageCapture.FLASH_MODE_AUTO),
|
||||
OFF(ImageCapture.FLASH_MODE_OFF),
|
||||
ON(ImageCapture.FLASH_MODE_ON);
|
||||
|
||||
private final @ImageCapture.FlashMode int flashMode;
|
||||
|
||||
FlashMode(@ImageCapture.FlashMode int flashMode) {
|
||||
this.flashMode = flashMode;
|
||||
}
|
||||
|
||||
@ImageCapture.FlashMode int getFlashMode() {
|
||||
return flashMode;
|
||||
}
|
||||
|
||||
private static FlashMode fromImageCaptureFlashMode(@ImageCapture.FlashMode int flashMode) {
|
||||
for (FlashMode mode : values()) {
|
||||
if (mode.getFlashMode() == flashMode) {
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,14 +20,6 @@ import android.Manifest.permission;
|
|||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.hardware.camera2.CameraAccessException;
|
||||
import android.hardware.camera2.CameraCharacteristics;
|
||||
import android.hardware.camera2.CameraManager;
|
||||
import android.os.Build;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.util.Rational;
|
||||
import android.util.Size;
|
||||
|
@ -36,41 +28,53 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.RequiresPermission;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.camera.core.AspectRatio;
|
||||
import androidx.camera.core.CameraInfo;
|
||||
import androidx.camera.core.CameraInfoUnavailableException;
|
||||
import androidx.camera.core.CameraOrientationUtil;
|
||||
import androidx.camera.core.Camera;
|
||||
import androidx.camera.core.CameraSelector;
|
||||
import androidx.camera.core.CameraX;
|
||||
import androidx.camera.core.FlashMode;
|
||||
import androidx.camera.core.ImageCapture;
|
||||
import androidx.camera.core.ImageCaptureConfig;
|
||||
import androidx.camera.core.ImageCapture.OnImageCapturedCallback;
|
||||
import androidx.camera.core.Preview;
|
||||
import androidx.camera.core.PreviewConfig;
|
||||
import androidx.camera.core.VideoCaptureConfig;
|
||||
import androidx.camera.core.TorchState;
|
||||
import androidx.camera.core.UseCase;
|
||||
import androidx.camera.core.impl.CameraInternal;
|
||||
import androidx.camera.core.impl.LensFacingConverter;
|
||||
import androidx.camera.core.impl.VideoCaptureConfig;
|
||||
import androidx.camera.core.impl.utils.CameraOrientationUtil;
|
||||
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
||||
import androidx.camera.core.impl.utils.futures.FutureCallback;
|
||||
import androidx.camera.core.impl.utils.futures.Futures;
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider;
|
||||
import androidx.core.util.Preconditions;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.OnLifecycleEvent;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.video.VideoUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
|
||||
|
||||
/** CameraX use case operation built on @{link androidx.camera.core}. */
|
||||
// Begin Signal Custom Code Block
|
||||
@RequiresApi(21)
|
||||
// End Signal Custom Code Block
|
||||
final class CameraXModule {
|
||||
public static final String TAG = "CameraXModule";
|
||||
|
||||
private static final int MAX_VIEW_DIMENSION = 2000;
|
||||
private static final float UNITY_ZOOM_SCALE = 1f;
|
||||
private static final float ZOOM_NOT_SUPPORTED = UNITY_ZOOM_SCALE;
|
||||
private static final Rational ASPECT_RATIO_16_9 = new Rational(16, 9);
|
||||
|
@ -78,22 +82,27 @@ final class CameraXModule {
|
|||
private static final Rational ASPECT_RATIO_9_16 = new Rational(9, 16);
|
||||
private static final Rational ASPECT_RATIO_3_4 = new Rational(3, 4);
|
||||
|
||||
private final CameraManager mCameraManager;
|
||||
private final PreviewConfig.Builder mPreviewConfigBuilder;
|
||||
private final Preview.Builder mPreviewBuilder;
|
||||
private final VideoCaptureConfig.Builder mVideoCaptureConfigBuilder;
|
||||
private final ImageCaptureConfig.Builder mImageCaptureConfigBuilder;
|
||||
private final CameraXView mCameraView;
|
||||
private final ImageCapture.Builder mImageCaptureBuilder;
|
||||
private final CameraXView mCameraXView;
|
||||
final AtomicBoolean mVideoIsRecording = new AtomicBoolean(false);
|
||||
private CameraXView.CaptureMode mCaptureMode = CameraXView.CaptureMode.IMAGE;
|
||||
private long mMaxVideoDuration = CameraXView.INDEFINITE_VIDEO_DURATION;
|
||||
private long mMaxVideoSize = CameraXView.INDEFINITE_VIDEO_SIZE;
|
||||
private FlashMode mFlash = FlashMode.OFF;
|
||||
@ImageCapture.FlashMode
|
||||
private int mFlash = FLASH_MODE_OFF;
|
||||
@Nullable
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
|
||||
Camera mCamera;
|
||||
@Nullable
|
||||
private ImageCapture mImageCapture;
|
||||
@Nullable
|
||||
private VideoCapture mVideoCapture;
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
|
||||
@Nullable
|
||||
Preview mPreview;
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
|
||||
@Nullable
|
||||
LifecycleOwner mCurrentLifecycle;
|
||||
private final LifecycleObserver mCurrentLifecycleObserver =
|
||||
|
@ -102,27 +111,44 @@ final class CameraXModule {
|
|||
public void onDestroy(LifecycleOwner owner) {
|
||||
if (owner == mCurrentLifecycle) {
|
||||
clearCurrentLifecycle();
|
||||
mPreview.removePreviewOutputListener();
|
||||
mPreview.setSurfaceProvider(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
@Nullable
|
||||
private LifecycleOwner mNewLifecycle;
|
||||
private float mZoomLevel = UNITY_ZOOM_SCALE;
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
|
||||
@Nullable
|
||||
private Rect mCropRegion;
|
||||
Integer mCameraLensFacing = CameraSelector.LENS_FACING_BACK;
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
|
||||
@Nullable
|
||||
private CameraX.LensFacing mCameraLensFacing = CameraX.LensFacing.BACK;
|
||||
ProcessCameraProvider mCameraProvider;
|
||||
|
||||
CameraXModule(CameraXView view) {
|
||||
this.mCameraView = view;
|
||||
mCameraXView = view;
|
||||
|
||||
mCameraManager = (CameraManager) view.getContext().getSystemService(Context.CAMERA_SERVICE);
|
||||
Futures.addCallback(ProcessCameraProvider.getInstance(view.getContext()),
|
||||
new FutureCallback<ProcessCameraProvider>() {
|
||||
// TODO(b/124269166): Rethink how we can handle permissions here.
|
||||
@SuppressLint("MissingPermission")
|
||||
@Override
|
||||
public void onSuccess(@Nullable ProcessCameraProvider provider) {
|
||||
Preconditions.checkNotNull(provider);
|
||||
mCameraProvider = provider;
|
||||
if (mCurrentLifecycle != null) {
|
||||
bindToLifecycle(mCurrentLifecycle);
|
||||
}
|
||||
}
|
||||
|
||||
mPreviewConfigBuilder = new PreviewConfig.Builder().setTargetName("Preview");
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
throw new RuntimeException("CameraX failed to initialize.", t);
|
||||
}
|
||||
}, CameraXExecutors.mainThreadExecutor());
|
||||
|
||||
mImageCaptureConfigBuilder =
|
||||
new ImageCaptureConfig.Builder().setTargetName("ImageCapture");
|
||||
mPreviewBuilder = new Preview.Builder().setTargetName("Preview");
|
||||
|
||||
mImageCaptureBuilder = new ImageCapture.Builder().setTargetName("ImageCapture");
|
||||
|
||||
// Begin Signal Custom Code Block
|
||||
mVideoCaptureConfigBuilder =
|
||||
|
@ -132,42 +158,8 @@ final class CameraXModule {
|
|||
.setBitRate(VideoUtil.VIDEO_BIT_RATE);
|
||||
// End Signal Custom Code Block
|
||||
}
|
||||
|
||||
/**
|
||||
* Rescales view rectangle with dimensions in [-1000, 1000] to a corresponding rectangle in the
|
||||
* sensor coordinate frame.
|
||||
*/
|
||||
private static Rect rescaleViewRectToSensorRect(Rect view, Rect sensor) {
|
||||
// Scale width and height.
|
||||
int newWidth = Math.round(view.width() * sensor.width() / (float) MAX_VIEW_DIMENSION);
|
||||
int newHeight = Math.round(view.height() * sensor.height() / (float) MAX_VIEW_DIMENSION);
|
||||
|
||||
// Scale top/left corner.
|
||||
int halfViewDimension = MAX_VIEW_DIMENSION / 2;
|
||||
int leftOffset =
|
||||
Math.round(
|
||||
(view.left + halfViewDimension)
|
||||
* sensor.width()
|
||||
/ (float) MAX_VIEW_DIMENSION)
|
||||
+ sensor.left;
|
||||
int topOffset =
|
||||
Math.round(
|
||||
(view.top + halfViewDimension)
|
||||
* sensor.height()
|
||||
/ (float) MAX_VIEW_DIMENSION)
|
||||
+ sensor.top;
|
||||
|
||||
// Now, produce the scaled rect.
|
||||
Rect scaled = new Rect();
|
||||
scaled.left = leftOffset;
|
||||
scaled.top = topOffset;
|
||||
scaled.right = scaled.left + newWidth;
|
||||
scaled.bottom = scaled.top + newHeight;
|
||||
return scaled;
|
||||
}
|
||||
|
||||
@RequiresPermission(permission.CAMERA)
|
||||
public void bindToLifecycle(LifecycleOwner lifecycleOwner) {
|
||||
void bindToLifecycle(LifecycleOwner lifecycleOwner) {
|
||||
mNewLifecycle = lifecycleOwner;
|
||||
|
||||
if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) {
|
||||
|
@ -189,38 +181,33 @@ final class CameraXModule {
|
|||
throw new IllegalArgumentException("Cannot bind to lifecycle in a destroyed state.");
|
||||
}
|
||||
|
||||
final int cameraOrientation;
|
||||
try {
|
||||
Set<CameraX.LensFacing> available = getAvailableCameraLensFacing();
|
||||
if (mCameraProvider == null) {
|
||||
// try again once the camera provider is no longer null
|
||||
return;
|
||||
}
|
||||
|
||||
if (available.isEmpty()) {
|
||||
Log.w(TAG, "Unable to bindToLifeCycle since no cameras available");
|
||||
mCameraLensFacing = null;
|
||||
}
|
||||
Set<Integer> available = getAvailableCameraLensFacing();
|
||||
|
||||
// Ensure the current camera exists, or default to another camera
|
||||
if (mCameraLensFacing != null && !available.contains(mCameraLensFacing)) {
|
||||
Log.w(TAG, "Camera does not exist with direction " + mCameraLensFacing);
|
||||
if (available.isEmpty()) {
|
||||
Log.w(TAG, "Unable to bindToLifeCycle since no cameras available");
|
||||
mCameraLensFacing = null;
|
||||
}
|
||||
|
||||
// Default to the first available camera direction
|
||||
mCameraLensFacing = available.iterator().next();
|
||||
// Ensure the current camera exists, or default to another camera
|
||||
if (mCameraLensFacing != null && !available.contains(mCameraLensFacing)) {
|
||||
Log.w(TAG, "Camera does not exist with direction " + mCameraLensFacing);
|
||||
|
||||
Log.w(TAG, "Defaulting to primary camera with direction " + mCameraLensFacing);
|
||||
}
|
||||
// Default to the first available camera direction
|
||||
mCameraLensFacing = available.iterator().next();
|
||||
|
||||
// Do not attempt to create use cases for a null cameraLensFacing. This could occur if
|
||||
// the
|
||||
// user explicitly sets the LensFacing to null, or if we determined there
|
||||
// were no available cameras, which should be logged in the logic above.
|
||||
if (mCameraLensFacing == null) {
|
||||
return;
|
||||
}
|
||||
CameraInfo cameraInfo = CameraX.getCameraInfo(getLensFacing());
|
||||
cameraOrientation = cameraInfo.getSensorRotationDegrees();
|
||||
} catch (CameraInfoUnavailableException e) {
|
||||
throw new IllegalStateException("Unable to get Camera Info.", e);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Unable to bind to lifecycle.", e);
|
||||
Log.w(TAG, "Defaulting to primary camera with direction " + mCameraLensFacing);
|
||||
}
|
||||
|
||||
// Do not attempt to create use cases for a null cameraLensFacing. This could occur if
|
||||
// the user explicitly sets the LensFacing to null, or if we determined there
|
||||
// were no available cameras, which should be logged in the logic above.
|
||||
if (mCameraLensFacing == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the preferred aspect ratio as 4:3 if it is IMAGE only mode. Set the preferred aspect
|
||||
|
@ -230,23 +217,32 @@ final class CameraXModule {
|
|||
boolean isDisplayPortrait = getDisplayRotationDegrees() == 0
|
||||
|| getDisplayRotationDegrees() == 180;
|
||||
|
||||
// Begin Signal Custom Code Block
|
||||
Rational targetAspectRatio;
|
||||
|
||||
// Begin Signal Custom Code Block
|
||||
int resolution = CameraXUtil.getIdealResolution(Resources.getSystem().getDisplayMetrics().widthPixels, Resources.getSystem().getDisplayMetrics().heightPixels);
|
||||
Log.i(TAG, "Ideal resolution: " + resolution);
|
||||
if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) {
|
||||
mImageCaptureConfigBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_4_3, isDisplayPortrait));
|
||||
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3;
|
||||
} else {
|
||||
mImageCaptureConfigBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait));
|
||||
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
|
||||
}
|
||||
mImageCaptureConfigBuilder.setCaptureMode(CameraXUtil.getOptimalCaptureMode());
|
||||
mImageCaptureConfigBuilder.setLensFacing(mCameraLensFacing);
|
||||
// End Signal Custom Code Block
|
||||
|
||||
mImageCaptureConfigBuilder.setTargetRotation(getDisplaySurfaceRotation());
|
||||
mImageCapture = new ImageCapture(mImageCaptureConfigBuilder.build());
|
||||
if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) {
|
||||
// mImageCaptureBuilder.setTargetAspectRatio(AspectRatio.RATIO_4_3);
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_4_3, isDisplayPortrait));
|
||||
// End Signal Custom Code Block
|
||||
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3;
|
||||
} else {
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait));
|
||||
// End Signal Custom Code Block
|
||||
// mImageCaptureBuilder.setTargetAspectRatio(AspectRatio.RATIO_16_9);
|
||||
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
|
||||
}
|
||||
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setCaptureMode(CameraXUtil.getOptimalCaptureMode());
|
||||
// End Signal Custom Code Block
|
||||
|
||||
mImageCaptureBuilder.setTargetRotation(getDisplaySurfaceRotation());
|
||||
mImageCapture = mImageCaptureBuilder.build();
|
||||
|
||||
// Begin Signal Custom Code Block
|
||||
Size size = VideoUtil.getVideoRecordingSize();
|
||||
|
@ -255,46 +251,37 @@ final class CameraXModule {
|
|||
// End Signal Custom Code Block
|
||||
|
||||
mVideoCaptureConfigBuilder.setTargetRotation(getDisplaySurfaceRotation());
|
||||
mVideoCaptureConfigBuilder.setLensFacing(mCameraLensFacing);
|
||||
|
||||
// Begin Signal Custom Code Block
|
||||
if (MediaConstraints.isVideoTranscodeAvailable()) {
|
||||
mVideoCapture = new VideoCapture(mVideoCaptureConfigBuilder.build());
|
||||
mVideoCapture = new VideoCapture(mVideoCaptureConfigBuilder.getUseCaseConfig());
|
||||
}
|
||||
mPreviewConfigBuilder.setLensFacing(mCameraLensFacing);
|
||||
// End Signal Custom Code Block
|
||||
|
||||
// Adjusts the preview resolution according to the view size and the target aspect ratio.
|
||||
int height = (int) (getMeasuredWidth() / targetAspectRatio.floatValue());
|
||||
mPreviewConfigBuilder.setTargetResolution(new Size(getMeasuredWidth(), height));
|
||||
mPreviewBuilder.setTargetResolution(new Size(getMeasuredWidth(), height));
|
||||
|
||||
mPreview = new Preview(mPreviewConfigBuilder.build());
|
||||
mPreview.setOnPreviewOutputUpdateListener(
|
||||
new Preview.OnPreviewOutputUpdateListener() {
|
||||
@Override
|
||||
public void onUpdated(@NonNull Preview.PreviewOutput output) {
|
||||
boolean needReverse = cameraOrientation != 0 && cameraOrientation != 180;
|
||||
int textureWidth =
|
||||
needReverse
|
||||
? output.getTextureSize().getHeight()
|
||||
: output.getTextureSize().getWidth();
|
||||
int textureHeight =
|
||||
needReverse
|
||||
? output.getTextureSize().getWidth()
|
||||
: output.getTextureSize().getHeight();
|
||||
CameraXModule.this.onPreviewSourceDimensUpdated(textureWidth,
|
||||
textureHeight);
|
||||
CameraXModule.this.setSurfaceTexture(output.getSurfaceTexture());
|
||||
}
|
||||
});
|
||||
mPreview = mPreviewBuilder.build();
|
||||
mPreview.setSurfaceProvider(mCameraXView.getPreviewView().getPreviewSurfaceProvider());
|
||||
|
||||
CameraSelector cameraSelector =
|
||||
new CameraSelector.Builder().requireLensFacing(mCameraLensFacing).build();
|
||||
if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) {
|
||||
CameraX.bindToLifecycle(mCurrentLifecycle, mImageCapture, mPreview);
|
||||
mCamera = mCameraProvider.bindToLifecycle(mCurrentLifecycle, cameraSelector,
|
||||
mImageCapture,
|
||||
mPreview);
|
||||
} else if (getCaptureMode() == CameraXView.CaptureMode.VIDEO) {
|
||||
CameraX.bindToLifecycle(mCurrentLifecycle, mVideoCapture, mPreview);
|
||||
mCamera = mCameraProvider.bindToLifecycle(mCurrentLifecycle, cameraSelector,
|
||||
mVideoCapture,
|
||||
mPreview);
|
||||
} else {
|
||||
CameraX.bindToLifecycle(mCurrentLifecycle, mImageCapture, mVideoCapture, mPreview);
|
||||
mCamera = mCameraProvider.bindToLifecycle(mCurrentLifecycle, cameraSelector,
|
||||
mImageCapture,
|
||||
mVideoCapture, mPreview);
|
||||
}
|
||||
setZoomLevel(mZoomLevel);
|
||||
|
||||
setZoomRatio(UNITY_ZOOM_SCALE);
|
||||
mCurrentLifecycle.getLifecycle().addObserver(mCurrentLifecycleObserver);
|
||||
// Enable flash setting in ImageCapture after use cases are created and binded.
|
||||
setFlash(getFlash());
|
||||
|
@ -310,7 +297,7 @@ final class CameraXModule {
|
|||
"Explicit open/close of camera not yet supported. Use bindtoLifecycle() instead.");
|
||||
}
|
||||
|
||||
public void takePicture(Executor executor, ImageCapture.OnImageCapturedListener listener) {
|
||||
public void takePicture(Executor executor, OnImageCapturedCallback callback) {
|
||||
if (mImageCapture == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -319,35 +306,19 @@ final class CameraXModule {
|
|||
throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
|
||||
}
|
||||
|
||||
if (listener == null) {
|
||||
throw new IllegalArgumentException("OnImageCapturedListener should not be empty");
|
||||
if (callback == null) {
|
||||
throw new IllegalArgumentException("OnImageCapturedCallback should not be empty");
|
||||
}
|
||||
|
||||
mImageCapture.takePicture(executor, listener);
|
||||
}
|
||||
|
||||
public void takePicture(File saveLocation, Executor executor, ImageCapture.OnImageSavedListener listener) {
|
||||
if (mImageCapture == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getCaptureMode() == CameraXView.CaptureMode.VIDEO) {
|
||||
throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
|
||||
}
|
||||
|
||||
if (listener == null) {
|
||||
throw new IllegalArgumentException("OnImageSavedListener should not be empty");
|
||||
}
|
||||
|
||||
ImageCapture.Metadata metadata = new ImageCapture.Metadata();
|
||||
metadata.isReversedHorizontal = mCameraLensFacing == CameraX.LensFacing.FRONT;
|
||||
mImageCapture.takePicture(saveLocation, metadata, executor, listener);
|
||||
mImageCapture.takePicture(executor, callback);
|
||||
}
|
||||
|
||||
// Begin Signal Custom Code Block
|
||||
@RequiresApi(26)
|
||||
public void startRecording(FileDescriptor file, Executor executor, final VideoCapture.OnVideoSavedListener listener) {
|
||||
// End Signal Custom Code Block
|
||||
public void startRecording(FileDescriptor file,
|
||||
// End Signal Custom Code Block
|
||||
Executor executor,
|
||||
final VideoCapture.OnVideoSavedCallback callback) {
|
||||
if (mVideoCapture == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -356,31 +327,31 @@ final class CameraXModule {
|
|||
throw new IllegalStateException("Can not record video under IMAGE capture mode.");
|
||||
}
|
||||
|
||||
if (listener == null) {
|
||||
throw new IllegalArgumentException("OnVideoSavedListener should not be empty");
|
||||
if (callback == null) {
|
||||
throw new IllegalArgumentException("OnVideoSavedCallback should not be empty");
|
||||
}
|
||||
|
||||
mVideoIsRecording.set(true);
|
||||
mVideoCapture.startRecording(
|
||||
file,
|
||||
executor,
|
||||
new VideoCapture.OnVideoSavedListener() {
|
||||
new VideoCapture.OnVideoSavedCallback() {
|
||||
@Override
|
||||
// Begin Signal Custom Code block
|
||||
// Begin Signal Custom Code Block
|
||||
public void onVideoSaved(@NonNull FileDescriptor savedFile) {
|
||||
// End Signal Custom Code Block
|
||||
mVideoIsRecording.set(false);
|
||||
listener.onVideoSaved(savedFile);
|
||||
callback.onVideoSaved(savedFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(
|
||||
@NonNull VideoCapture.VideoCaptureError videoCaptureError,
|
||||
@VideoCapture.VideoCaptureError int videoCaptureError,
|
||||
@NonNull String message,
|
||||
@Nullable Throwable cause) {
|
||||
mVideoIsRecording.set(false);
|
||||
Log.e(TAG, message, cause);
|
||||
listener.onError(videoCaptureError, message, cause);
|
||||
callback.onError(videoCaptureError, message, cause);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -402,9 +373,9 @@ final class CameraXModule {
|
|||
|
||||
// TODO(b/124269166): Rethink how we can handle permissions here.
|
||||
@SuppressLint("MissingPermission")
|
||||
public void setCameraLensFacing(@Nullable CameraX.LensFacing lensFacing) {
|
||||
public void setCameraLensFacing(@Nullable Integer lensFacing) {
|
||||
// Setting same lens facing is a no-op, so check for that first
|
||||
if (mCameraLensFacing != lensFacing) {
|
||||
if (!Objects.equals(mCameraLensFacing, lensFacing)) {
|
||||
// If we're not bound to a lifecycle, just update the camera that will be opened when we
|
||||
// attach to a lifecycle.
|
||||
mCameraLensFacing = lensFacing;
|
||||
|
@ -417,7 +388,7 @@ final class CameraXModule {
|
|||
}
|
||||
|
||||
@RequiresPermission(permission.CAMERA)
|
||||
public boolean hasCameraWithLensFacing(CameraX.LensFacing lensFacing) {
|
||||
public boolean hasCameraWithLensFacing(@CameraSelector.LensFacing int lensFacing) {
|
||||
String cameraId;
|
||||
try {
|
||||
cameraId = CameraX.getCameraWithLensFacing(lensFacing);
|
||||
|
@ -429,14 +400,14 @@ final class CameraXModule {
|
|||
}
|
||||
|
||||
@Nullable
|
||||
public CameraX.LensFacing getLensFacing() {
|
||||
public Integer getLensFacing() {
|
||||
return mCameraLensFacing;
|
||||
}
|
||||
|
||||
public void toggleCamera() {
|
||||
// TODO(b/124269166): Rethink how we can handle permissions here.
|
||||
@SuppressLint("MissingPermission")
|
||||
Set<CameraX.LensFacing> availableCameraLensFacing = getAvailableCameraLensFacing();
|
||||
Set<Integer> availableCameraLensFacing = getAvailableCameraLensFacing();
|
||||
|
||||
if (availableCameraLensFacing.isEmpty()) {
|
||||
return;
|
||||
|
@ -447,106 +418,65 @@ final class CameraXModule {
|
|||
return;
|
||||
}
|
||||
|
||||
if (mCameraLensFacing == CameraX.LensFacing.BACK
|
||||
&& availableCameraLensFacing.contains(CameraX.LensFacing.FRONT)) {
|
||||
setCameraLensFacing(CameraX.LensFacing.FRONT);
|
||||
if (mCameraLensFacing == CameraSelector.LENS_FACING_BACK
|
||||
&& availableCameraLensFacing.contains(CameraSelector.LENS_FACING_FRONT)) {
|
||||
setCameraLensFacing(CameraSelector.LENS_FACING_FRONT);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mCameraLensFacing == CameraX.LensFacing.FRONT
|
||||
&& availableCameraLensFacing.contains(CameraX.LensFacing.BACK)) {
|
||||
setCameraLensFacing(CameraX.LensFacing.BACK);
|
||||
if (mCameraLensFacing == CameraSelector.LENS_FACING_FRONT
|
||||
&& availableCameraLensFacing.contains(CameraSelector.LENS_FACING_BACK)) {
|
||||
setCameraLensFacing(CameraSelector.LENS_FACING_BACK);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public float getZoomLevel() {
|
||||
return mZoomLevel;
|
||||
public float getZoomRatio() {
|
||||
if (mCamera != null) {
|
||||
return mCamera.getCameraInfo().getZoomState().getValue().getZoomRatio();
|
||||
} else {
|
||||
return UNITY_ZOOM_SCALE;
|
||||
}
|
||||
}
|
||||
|
||||
public void setZoomLevel(float zoomLevel) {
|
||||
// Set the zoom level in case it is set before binding to a lifecycle
|
||||
this.mZoomLevel = zoomLevel;
|
||||
public void setZoomRatio(float zoomRatio) {
|
||||
if (mCamera != null) {
|
||||
ListenableFuture<Void> future = mCamera.getCameraControl().setZoomRatio(
|
||||
zoomRatio);
|
||||
Futures.addCallback(future, new FutureCallback<Void>() {
|
||||
@Override
|
||||
public void onSuccess(@Nullable Void result) {
|
||||
}
|
||||
|
||||
if (mPreview == null) {
|
||||
// Nothing to zoom on yet since we don't have a preview. Defer calculating crop
|
||||
// region.
|
||||
return;
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
// Throw the unexpected error.
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
}, CameraXExecutors.directExecutor());
|
||||
} else {
|
||||
Log.e(TAG, "Failed to set zoom ratio");
|
||||
}
|
||||
|
||||
Rect sensorSize;
|
||||
try {
|
||||
sensorSize = getSensorSize(getActiveCamera());
|
||||
if (sensorSize == null) {
|
||||
Log.e(TAG, "Failed to get the sensor size.");
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to get the sensor size.", e);
|
||||
return;
|
||||
}
|
||||
|
||||
float minZoom = getMinZoomLevel();
|
||||
float maxZoom = getMaxZoomLevel();
|
||||
|
||||
if (this.mZoomLevel < minZoom) {
|
||||
Log.e(TAG, "Requested zoom level is less than minimum zoom level.");
|
||||
}
|
||||
if (this.mZoomLevel > maxZoom) {
|
||||
Log.e(TAG, "Requested zoom level is greater than maximum zoom level.");
|
||||
}
|
||||
this.mZoomLevel = Math.max(minZoom, Math.min(maxZoom, this.mZoomLevel));
|
||||
|
||||
float zoomScaleFactor =
|
||||
(maxZoom == minZoom) ? minZoom : (this.mZoomLevel - minZoom) / (maxZoom - minZoom);
|
||||
int minWidth = Math.round(sensorSize.width() / maxZoom);
|
||||
int minHeight = Math.round(sensorSize.height() / maxZoom);
|
||||
int diffWidth = sensorSize.width() - minWidth;
|
||||
int diffHeight = sensorSize.height() - minHeight;
|
||||
float cropWidth = diffWidth * zoomScaleFactor;
|
||||
float cropHeight = diffHeight * zoomScaleFactor;
|
||||
|
||||
Rect cropRegion =
|
||||
new Rect(
|
||||
/*left=*/ (int) Math.ceil(cropWidth / 2 - 0.5f),
|
||||
/*top=*/ (int) Math.ceil(cropHeight / 2 - 0.5f),
|
||||
/*right=*/ (int) Math.floor(sensorSize.width() - cropWidth / 2 + 0.5f),
|
||||
/*bottom=*/ (int) Math.floor(sensorSize.height() - cropHeight / 2 + 0.5f));
|
||||
|
||||
if (cropRegion.width() < 50 || cropRegion.height() < 50) {
|
||||
Log.e(TAG, "Crop region is too small to compute 3A stats, so ignoring further zoom.");
|
||||
return;
|
||||
}
|
||||
this.mCropRegion = cropRegion;
|
||||
|
||||
mPreview.zoom(cropRegion);
|
||||
}
|
||||
|
||||
public float getMinZoomLevel() {
|
||||
return UNITY_ZOOM_SCALE;
|
||||
public float getMinZoomRatio() {
|
||||
if (mCamera != null) {
|
||||
return mCamera.getCameraInfo().getZoomState().getValue().getMinZoomRatio();
|
||||
} else {
|
||||
return UNITY_ZOOM_SCALE;
|
||||
}
|
||||
}
|
||||
|
||||
public float getMaxZoomLevel() {
|
||||
try {
|
||||
CameraCharacteristics characteristics =
|
||||
mCameraManager.getCameraCharacteristics(getActiveCamera());
|
||||
Float maxZoom =
|
||||
characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM);
|
||||
if (maxZoom == null) {
|
||||
return ZOOM_NOT_SUPPORTED;
|
||||
}
|
||||
if (maxZoom == ZOOM_NOT_SUPPORTED) {
|
||||
return ZOOM_NOT_SUPPORTED;
|
||||
}
|
||||
return maxZoom;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to get SCALER_AVAILABLE_MAX_DIGITAL_ZOOM.", e);
|
||||
public float getMaxZoomRatio() {
|
||||
if (mCamera != null) {
|
||||
return mCamera.getCameraInfo().getZoomState().getValue().getMaxZoomRatio();
|
||||
} else {
|
||||
return ZOOM_NOT_SUPPORTED;
|
||||
}
|
||||
return ZOOM_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
public boolean isZoomSupported() {
|
||||
return getMaxZoomLevel() != ZOOM_NOT_SUPPORTED;
|
||||
return getMaxZoomRatio() != ZOOM_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
// TODO(b/124269166): Rethink how we can handle permissions here.
|
||||
|
@ -559,80 +489,47 @@ final class CameraXModule {
|
|||
|
||||
int getRelativeCameraOrientation(boolean compensateForMirroring) {
|
||||
int rotationDegrees = 0;
|
||||
try {
|
||||
CameraInfo cameraInfo = CameraX.getCameraInfo(getLensFacing());
|
||||
rotationDegrees = cameraInfo.getSensorRotationDegrees(getDisplaySurfaceRotation());
|
||||
if (mCamera != null) {
|
||||
rotationDegrees =
|
||||
mCamera.getCameraInfo().getSensorRotationDegrees(getDisplaySurfaceRotation());
|
||||
if (compensateForMirroring) {
|
||||
rotationDegrees = (360 - rotationDegrees) % 360;
|
||||
}
|
||||
} catch (CameraInfoUnavailableException e) {
|
||||
Log.e(TAG, "Failed to get CameraInfo", e);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to query camera", e);
|
||||
}
|
||||
|
||||
return rotationDegrees;
|
||||
}
|
||||
|
||||
public void invalidateView() {
|
||||
transformPreview();
|
||||
updateViewInfo();
|
||||
}
|
||||
|
||||
void clearCurrentLifecycle() {
|
||||
if (mCurrentLifecycle != null) {
|
||||
if (mCurrentLifecycle != null && mCameraProvider != null) {
|
||||
// Remove previous use cases
|
||||
// Begin Signal Custom Code Block
|
||||
CameraX.unbind(mImageCapture, mPreview);
|
||||
if (mVideoCapture != null) {
|
||||
CameraX.unbind(mVideoCapture);
|
||||
List<UseCase> toUnbind = new ArrayList<>();
|
||||
if (mImageCapture != null && mCameraProvider.isBound(mImageCapture)) {
|
||||
toUnbind.add(mImageCapture);
|
||||
}
|
||||
if (mVideoCapture != null && mCameraProvider.isBound(mVideoCapture)) {
|
||||
toUnbind.add(mVideoCapture);
|
||||
}
|
||||
if (mPreview != null && mCameraProvider.isBound(mPreview)) {
|
||||
toUnbind.add(mPreview);
|
||||
}
|
||||
// End Signal Custom Code Block
|
||||
}
|
||||
|
||||
if (!toUnbind.isEmpty()) {
|
||||
mCameraProvider.unbind(toUnbind.toArray((new UseCase[0])));
|
||||
}
|
||||
}
|
||||
mCamera = null;
|
||||
mCurrentLifecycle = null;
|
||||
}
|
||||
|
||||
private Rect getSensorSize(String cameraId) throws CameraAccessException {
|
||||
CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(cameraId);
|
||||
return characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
|
||||
}
|
||||
|
||||
String getActiveCamera() throws CameraInfoUnavailableException {
|
||||
return CameraX.getCameraWithLensFacing(mCameraLensFacing);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void transformPreview() {
|
||||
int previewWidth = getPreviewWidth();
|
||||
int previewHeight = getPreviewHeight();
|
||||
int displayOrientation = getDisplayRotationDegrees();
|
||||
|
||||
Matrix matrix = new Matrix();
|
||||
|
||||
// Apply rotation of the display
|
||||
int rotation = -displayOrientation;
|
||||
|
||||
int px = (int) Math.round(previewWidth / 2d);
|
||||
int py = (int) Math.round(previewHeight / 2d);
|
||||
|
||||
matrix.postRotate(rotation, px, py);
|
||||
|
||||
if (displayOrientation == 90 || displayOrientation == 270) {
|
||||
// Swap width and height
|
||||
float xScale = previewWidth / (float) previewHeight;
|
||||
float yScale = previewHeight / (float) previewWidth;
|
||||
|
||||
matrix.postScale(xScale, yScale, px, py);
|
||||
}
|
||||
|
||||
setTransform(matrix);
|
||||
}
|
||||
|
||||
// Update view related information used in use cases
|
||||
private void updateViewInfo() {
|
||||
if (mImageCapture != null) {
|
||||
mImageCapture.setTargetAspectRatioCustom(new Rational(getWidth(), getHeight()));
|
||||
mImageCapture.setCropAspectRatio(new Rational(getWidth(), getHeight()));
|
||||
mImageCapture.setTargetRotation(getDisplaySurfaceRotation());
|
||||
}
|
||||
|
||||
|
@ -642,29 +539,46 @@ final class CameraXModule {
|
|||
}
|
||||
|
||||
@RequiresPermission(permission.CAMERA)
|
||||
private Set<CameraX.LensFacing> getAvailableCameraLensFacing() {
|
||||
private Set<Integer> getAvailableCameraLensFacing() {
|
||||
// Start with all camera directions
|
||||
Set<CameraX.LensFacing> available = new LinkedHashSet<>(Arrays.asList(CameraX.LensFacing.values()));
|
||||
Set<Integer> available = new LinkedHashSet<>(Arrays.asList(LensFacingConverter.values()));
|
||||
|
||||
// If we're bound to a lifecycle, remove unavailable cameras
|
||||
if (mCurrentLifecycle != null) {
|
||||
if (!hasCameraWithLensFacing(CameraX.LensFacing.BACK)) {
|
||||
available.remove(CameraX.LensFacing.BACK);
|
||||
if (!hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK)) {
|
||||
available.remove(CameraSelector.LENS_FACING_BACK);
|
||||
}
|
||||
|
||||
if (!hasCameraWithLensFacing(CameraX.LensFacing.FRONT)) {
|
||||
available.remove(CameraX.LensFacing.FRONT);
|
||||
if (!hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT)) {
|
||||
available.remove(CameraSelector.LENS_FACING_FRONT);
|
||||
}
|
||||
}
|
||||
|
||||
return available;
|
||||
}
|
||||
|
||||
public FlashMode getFlash() {
|
||||
@ImageCapture.FlashMode
|
||||
public int getFlash() {
|
||||
return mFlash;
|
||||
}
|
||||
|
||||
public void setFlash(FlashMode flash) {
|
||||
// Begin Signal Custom Code Block
|
||||
public boolean hasFlash() {
|
||||
if (mImageCapture == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CameraInternal camera = mImageCapture.getBoundCamera();
|
||||
|
||||
if (camera == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return camera.getCameraInfoInternal().hasFlashUnit();
|
||||
}
|
||||
// End Signal Custom Code Block
|
||||
|
||||
public void setFlash(@ImageCapture.FlashMode int flash) {
|
||||
this.mFlash = flash;
|
||||
|
||||
if (mImageCapture == null) {
|
||||
|
@ -676,101 +590,69 @@ final class CameraXModule {
|
|||
}
|
||||
|
||||
public void enableTorch(boolean torch) {
|
||||
if (mPreview == null) {
|
||||
if (mCamera == null) {
|
||||
return;
|
||||
}
|
||||
mPreview.enableTorch(torch);
|
||||
ListenableFuture<Void> future = mCamera.getCameraControl().enableTorch(torch);
|
||||
Futures.addCallback(future, new FutureCallback<Void>() {
|
||||
@Override
|
||||
public void onSuccess(@Nullable Void result) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
// Throw the unexpected error.
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
}, CameraXExecutors.directExecutor());
|
||||
}
|
||||
|
||||
public boolean isTorchOn() {
|
||||
if (mPreview == null) {
|
||||
if (mCamera == null) {
|
||||
return false;
|
||||
}
|
||||
return mPreview.isTorchOn();
|
||||
return mCamera.getCameraInfo().getTorchState().getValue() == TorchState.ON;
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return mCameraView.getContext();
|
||||
return mCameraXView.getContext();
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return mCameraView.getWidth();
|
||||
return mCameraXView.getWidth();
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return mCameraView.getHeight();
|
||||
return mCameraXView.getHeight();
|
||||
}
|
||||
|
||||
public int getDisplayRotationDegrees() {
|
||||
return CameraOrientationUtil.surfaceRotationToDegrees(getDisplaySurfaceRotation());
|
||||
}
|
||||
|
||||
// Begin Signal Custom Code Block
|
||||
public boolean hasFlash() {
|
||||
try {
|
||||
LiveData<Boolean> isFlashAvailable = CameraX.getCameraInfo(getLensFacing()).isFlashAvailable();
|
||||
return isFlashAvailable.getValue() == Boolean.TRUE;
|
||||
} catch (CameraInfoUnavailableException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// End Signal Custom Code Block
|
||||
|
||||
protected int getDisplaySurfaceRotation() {
|
||||
return mCameraView.getDisplaySurfaceRotation();
|
||||
}
|
||||
|
||||
public void setSurfaceTexture(SurfaceTexture st) {
|
||||
mCameraView.setSurfaceTexture(st);
|
||||
}
|
||||
|
||||
private int getPreviewWidth() {
|
||||
return mCameraView.getPreviewWidth();
|
||||
}
|
||||
|
||||
private int getPreviewHeight() {
|
||||
return mCameraView.getPreviewHeight();
|
||||
return mCameraXView.getDisplaySurfaceRotation();
|
||||
}
|
||||
|
||||
private int getMeasuredWidth() {
|
||||
return mCameraView.getMeasuredWidth();
|
||||
return mCameraXView.getMeasuredWidth();
|
||||
}
|
||||
|
||||
private int getMeasuredHeight() {
|
||||
return mCameraView.getMeasuredHeight();
|
||||
return mCameraXView.getMeasuredHeight();
|
||||
}
|
||||
|
||||
void setTransform(final Matrix matrix) {
|
||||
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||
mCameraView.post(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setTransform(matrix);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mCameraView.setTransform(matrix);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the view that the source dimensions have changed.
|
||||
*
|
||||
* <p>This will allow the view to layout the preview to display the correct aspect ratio.
|
||||
*
|
||||
* @param width width of camera source buffers.
|
||||
* @param height height of camera source buffers.
|
||||
*/
|
||||
void onPreviewSourceDimensUpdated(int width, int height) {
|
||||
mCameraView.onPreviewSourceDimensUpdated(width, height);
|
||||
@Nullable
|
||||
public Camera getCamera() {
|
||||
return mCamera;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public CameraXView.CaptureMode getCaptureMode() {
|
||||
return mCaptureMode;
|
||||
}
|
||||
|
||||
public void setCaptureMode(CameraXView.CaptureMode captureMode) {
|
||||
public void setCaptureMode(@NonNull CameraXView.CaptureMode captureMode) {
|
||||
this.mCaptureMode = captureMode;
|
||||
rebindToLifecycle();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.thoughtcrime.securesms.mediasend.camerax;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
|
@ -19,14 +20,13 @@ import android.util.Size;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.camera.camera2.impl.compat.CameraManagerCompat;
|
||||
import androidx.camera.core.CameraX;
|
||||
import androidx.camera.camera2.internal.compat.CameraManagerCompat;
|
||||
import androidx.camera.core.CameraSelector;
|
||||
import androidx.camera.core.ImageCapture;
|
||||
import androidx.camera.core.ImageProxy;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediasend.LegacyCameraModels;
|
||||
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
@ -57,11 +57,12 @@ public class CameraXUtil {
|
|||
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
@RequiresApi(21)
|
||||
public static ImageResult toJpeg(@NonNull ImageProxy image, int rotation, boolean flip) throws IOException {
|
||||
public static ImageResult toJpeg(@NonNull ImageProxy image, boolean flip) throws IOException {
|
||||
ImageProxy.PlaneProxy[] planes = image.getPlanes();
|
||||
ByteBuffer buffer = planes[0].getBuffer();
|
||||
Rect cropRect = shouldCropImage(image) ? image.getCropRect() : null;
|
||||
byte[] data = new byte[buffer.capacity()];
|
||||
int rotation = image.getImageInfo().getRotationDegrees();
|
||||
|
||||
buffer.get(data);
|
||||
|
||||
|
@ -86,25 +87,25 @@ public class CameraXUtil {
|
|||
return Build.VERSION.SDK_INT >= 21 && !LegacyCameraModels.isLegacyCameraModel();
|
||||
}
|
||||
|
||||
public static int toCameraDirectionInt(@Nullable CameraX.LensFacing facing) {
|
||||
if (facing == CameraX.LensFacing.FRONT) {
|
||||
public static int toCameraDirectionInt(int facing) {
|
||||
if (facing == CameraSelector.LENS_FACING_FRONT) {
|
||||
return Camera.CameraInfo.CAMERA_FACING_FRONT;
|
||||
} else {
|
||||
return Camera.CameraInfo.CAMERA_FACING_BACK;
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull CameraX.LensFacing toLensFacing(int cameraDirectionInt) {
|
||||
public static int toLensFacing(@CameraSelector.LensFacing int cameraDirectionInt) {
|
||||
if (cameraDirectionInt == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
||||
return CameraX.LensFacing.FRONT;
|
||||
return CameraSelector.LENS_FACING_FRONT;
|
||||
} else {
|
||||
return CameraX.LensFacing.BACK;
|
||||
return CameraSelector.LENS_FACING_BACK;
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull ImageCapture.CaptureMode getOptimalCaptureMode() {
|
||||
return FastCameraModels.contains(Build.MODEL) ? ImageCapture.CaptureMode.MAX_QUALITY
|
||||
: ImageCapture.CaptureMode.MIN_LATENCY;
|
||||
public static @NonNull @ImageCapture.CaptureMode int getOptimalCaptureMode() {
|
||||
return FastCameraModels.contains(Build.MODEL) ? ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
|
||||
: ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY;
|
||||
}
|
||||
|
||||
public static int getIdealResolution(int displayWidth, int displayHeight) {
|
||||
|
@ -186,7 +187,7 @@ public class CameraXUtil {
|
|||
|
||||
@RequiresApi(21)
|
||||
public static int getLowestSupportedHardwareLevel(@NonNull Context context) {
|
||||
CameraManager cameraManager = CameraManagerCompat.from(context).unwrap();
|
||||
@SuppressLint("RestrictedApi") CameraManager cameraManager = CameraManagerCompat.from(context).unwrap();
|
||||
|
||||
try {
|
||||
int supported = maxHardwareLevel();
|
||||
|
|
|
@ -20,10 +20,6 @@ import android.Manifest.permission;
|
|||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.hardware.display.DisplayManager.DisplayListener;
|
||||
import android.os.Bundle;
|
||||
|
@ -33,17 +29,14 @@ import android.os.Parcelable;
|
|||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.Size;
|
||||
import android.view.Display;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
import android.view.Surface;
|
||||
import android.view.TextureView;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.BaseInterpolator;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
@ -51,33 +44,42 @@ import androidx.annotation.RequiresApi;
|
|||
import androidx.annotation.RequiresPermission;
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.annotation.RestrictTo.Scope;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.camera.core.CameraInfoUnavailableException;
|
||||
import androidx.camera.core.CameraX;
|
||||
import androidx.camera.core.FlashMode;
|
||||
import androidx.camera.core.Camera;
|
||||
import androidx.camera.core.CameraSelector;
|
||||
import androidx.camera.core.DisplayOrientedMeteringPointFactory;
|
||||
import androidx.camera.core.FocusMeteringAction;
|
||||
import androidx.camera.core.FocusMeteringResult;
|
||||
import androidx.camera.core.ImageCapture;
|
||||
import androidx.camera.core.ImageCapture.OnImageCapturedCallback;
|
||||
import androidx.camera.core.ImageProxy;
|
||||
import androidx.camera.core.MeteringPoint;
|
||||
import androidx.camera.core.impl.LensFacingConverter;
|
||||
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
||||
import androidx.camera.core.impl.utils.futures.FutureCallback;
|
||||
import androidx.camera.core.impl.utils.futures.Futures;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* A {@link View} that displays a preview of the camera with methods {@link
|
||||
* #takePicture(Executor, OnImageCapturedListener)},
|
||||
* {@link #takePicture(File, Executor, OnImageSavedListener)},
|
||||
* {@link #startRecording(File, Executor, OnVideoSavedListener)} and {@link #stopRecording()}.
|
||||
* #takePicture(Executor, OnImageCapturedCallback)},
|
||||
* {@link #startRecording(FileDescriptor, Executor, VideoCapture.OnVideoSavedCallback)} and {@link #stopRecording()}.
|
||||
*
|
||||
* <p>Because the Camera is a limited resource and consumes a high amount of power, CameraView must
|
||||
* be opened/closed. CameraView will handle opening/closing automatically through use of a {@link
|
||||
* LifecycleOwner}. Use {@link #bindToLifecycle(LifecycleOwner)} to start the camera.
|
||||
*/
|
||||
// Begin Signal Custom Code Block
|
||||
@RequiresApi(21)
|
||||
public final class CameraXView extends ViewGroup {
|
||||
@SuppressLint("RestrictedApi")
|
||||
// End Signal Custom Code Block
|
||||
public final class CameraXView extends FrameLayout {
|
||||
static final String TAG = CameraXView.class.getSimpleName();
|
||||
static final boolean DEBUG = false;
|
||||
|
||||
|
@ -85,7 +87,7 @@ public final class CameraXView extends ViewGroup {
|
|||
static final int INDEFINITE_VIDEO_SIZE = -1;
|
||||
|
||||
private static final String EXTRA_SUPER = "super";
|
||||
private static final String EXTRA_ZOOM_LEVEL = "zoom_level";
|
||||
private static final String EXTRA_ZOOM_RATIO = "zoom_ratio";
|
||||
private static final String EXTRA_PINCH_TO_ZOOM_ENABLED = "pinch_to_zoom_enabled";
|
||||
private static final String EXTRA_FLASH = "flash";
|
||||
private static final String EXTRA_MAX_VIDEO_DURATION = "max_video_duration";
|
||||
|
@ -121,51 +123,31 @@ public final class CameraXView extends ViewGroup {
|
|||
mCameraModule.invalidateView();
|
||||
}
|
||||
};
|
||||
private TextureView mCameraTextureView;
|
||||
private Size mPreviewSrcSize = new Size(0, 0);
|
||||
private PreviewView mPreviewView;
|
||||
private ScaleType mScaleType = ScaleType.CENTER_CROP;
|
||||
// For accessibility event
|
||||
private MotionEvent mUpEvent;
|
||||
private @Nullable Paint mLayerPaint;
|
||||
|
||||
public CameraXView(Context context) {
|
||||
public CameraXView(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public CameraXView(Context context, AttributeSet attrs) {
|
||||
public CameraXView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public CameraXView(Context context, AttributeSet attrs, int defStyle) {
|
||||
public CameraXView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
@RequiresApi(21)
|
||||
public CameraXView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
public CameraXView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
/** Debug logging that can be enabled. */
|
||||
private static void log(String msg) {
|
||||
if (DEBUG) {
|
||||
Log.i(TAG, msg);
|
||||
}
|
||||
}
|
||||
|
||||
/** Utility method for converting an displayRotation int into a human readable string. */
|
||||
private static String displayRotationToString(int displayRotation) {
|
||||
if (displayRotation == Surface.ROTATION_0 || displayRotation == Surface.ROTATION_180) {
|
||||
return "Portrait-" + (displayRotation * 90);
|
||||
} else if (displayRotation == Surface.ROTATION_90
|
||||
|| displayRotation == Surface.ROTATION_270) {
|
||||
return "Landscape-" + (displayRotation * 90);
|
||||
} else {
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds control of the camera used by this view to the given lifecycle.
|
||||
*
|
||||
|
@ -184,21 +166,16 @@ public final class CameraXView extends ViewGroup {
|
|||
* @throws IllegalStateException if camera permissions are not granted.
|
||||
*/
|
||||
@RequiresPermission(permission.CAMERA)
|
||||
public void bindToLifecycle(LifecycleOwner lifecycleOwner) {
|
||||
public void bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner) {
|
||||
mCameraModule.bindToLifecycle(lifecycleOwner);
|
||||
}
|
||||
|
||||
private void init(Context context, @Nullable AttributeSet attrs) {
|
||||
addView(mCameraTextureView = new TextureView(getContext()), 0 /* view position */);
|
||||
mCameraTextureView.setLayerPaint(mLayerPaint);
|
||||
addView(mPreviewView = new PreviewView(getContext()), 0 /* view position */);
|
||||
mCameraModule = new CameraXModule(this);
|
||||
|
||||
if (isInEditMode()) {
|
||||
onPreviewSourceDimensUpdated(640, 480);
|
||||
}
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView);
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraXView);
|
||||
setScaleType(
|
||||
ScaleType.fromId(
|
||||
a.getInteger(R.styleable.CameraXView_scaleType,
|
||||
|
@ -217,10 +194,10 @@ public final class CameraXView extends ViewGroup {
|
|||
setCameraLensFacing(null);
|
||||
break;
|
||||
case LENS_FACING_FRONT:
|
||||
setCameraLensFacing(CameraX.LensFacing.FRONT);
|
||||
setCameraLensFacing(CameraSelector.LENS_FACING_FRONT);
|
||||
break;
|
||||
case LENS_FACING_BACK:
|
||||
setCameraLensFacing(CameraX.LensFacing.BACK);
|
||||
setCameraLensFacing(CameraSelector.LENS_FACING_BACK);
|
||||
break;
|
||||
default:
|
||||
// Unhandled event.
|
||||
|
@ -229,13 +206,13 @@ public final class CameraXView extends ViewGroup {
|
|||
int flashMode = a.getInt(R.styleable.CameraXView_flash, 0);
|
||||
switch (flashMode) {
|
||||
case FLASH_MODE_AUTO:
|
||||
setFlash(FlashMode.AUTO);
|
||||
setFlash(ImageCapture.FLASH_MODE_AUTO);
|
||||
break;
|
||||
case FLASH_MODE_ON:
|
||||
setFlash(FlashMode.ON);
|
||||
setFlash(ImageCapture.FLASH_MODE_ON);
|
||||
break;
|
||||
case FLASH_MODE_OFF:
|
||||
setFlash(FlashMode.OFF);
|
||||
setFlash(ImageCapture.FLASH_MODE_OFF);
|
||||
break;
|
||||
default:
|
||||
// Unhandled event.
|
||||
|
@ -252,12 +229,14 @@ public final class CameraXView extends ViewGroup {
|
|||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
protected LayoutParams generateDefaultLayoutParams() {
|
||||
return new LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
protected Parcelable onSaveInstanceState() {
|
||||
// TODO(b/113884082): Decide what belongs here or what should be invalidated on
|
||||
// configuration
|
||||
|
@ -265,20 +244,21 @@ public final class CameraXView extends ViewGroup {
|
|||
Bundle state = new Bundle();
|
||||
state.putParcelable(EXTRA_SUPER, super.onSaveInstanceState());
|
||||
state.putInt(EXTRA_SCALE_TYPE, getScaleType().getId());
|
||||
state.putFloat(EXTRA_ZOOM_LEVEL, getZoomLevel());
|
||||
state.putFloat(EXTRA_ZOOM_RATIO, getZoomRatio());
|
||||
state.putBoolean(EXTRA_PINCH_TO_ZOOM_ENABLED, isPinchToZoomEnabled());
|
||||
state.putString(EXTRA_FLASH, getFlash().name());
|
||||
state.putString(EXTRA_FLASH, FlashModeConverter.nameOf(getFlash()));
|
||||
state.putLong(EXTRA_MAX_VIDEO_DURATION, getMaxVideoDuration());
|
||||
state.putLong(EXTRA_MAX_VIDEO_SIZE, getMaxVideoSize());
|
||||
if (getCameraLensFacing() != null) {
|
||||
state.putString(EXTRA_CAMERA_DIRECTION, getCameraLensFacing().name());
|
||||
state.putString(EXTRA_CAMERA_DIRECTION,
|
||||
LensFacingConverter.nameOf(getCameraLensFacing()));
|
||||
}
|
||||
state.putInt(EXTRA_CAPTURE_MODE, getCaptureMode().getId());
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Parcelable savedState) {
|
||||
protected void onRestoreInstanceState(@Nullable Parcelable savedState) {
|
||||
// TODO(b/113884082): Decide what belongs here or what should be invalidated on
|
||||
// configuration
|
||||
// change
|
||||
|
@ -286,39 +266,22 @@ public final class CameraXView extends ViewGroup {
|
|||
Bundle state = (Bundle) savedState;
|
||||
super.onRestoreInstanceState(state.getParcelable(EXTRA_SUPER));
|
||||
setScaleType(ScaleType.fromId(state.getInt(EXTRA_SCALE_TYPE)));
|
||||
setZoomLevel(state.getFloat(EXTRA_ZOOM_LEVEL));
|
||||
setZoomRatio(state.getFloat(EXTRA_ZOOM_RATIO));
|
||||
setPinchToZoomEnabled(state.getBoolean(EXTRA_PINCH_TO_ZOOM_ENABLED));
|
||||
setFlash(FlashMode.valueOf(state.getString(EXTRA_FLASH)));
|
||||
setFlash(FlashModeConverter.valueOf(state.getString(EXTRA_FLASH)));
|
||||
setMaxVideoDuration(state.getLong(EXTRA_MAX_VIDEO_DURATION));
|
||||
setMaxVideoSize(state.getLong(EXTRA_MAX_VIDEO_SIZE));
|
||||
String lensFacingString = state.getString(EXTRA_CAMERA_DIRECTION);
|
||||
setCameraLensFacing(
|
||||
TextUtils.isEmpty(lensFacingString)
|
||||
? null
|
||||
: CameraX.LensFacing.valueOf(lensFacingString));
|
||||
: LensFacingConverter.valueOf(lensFacingString));
|
||||
setCaptureMode(CaptureMode.fromId(state.getInt(EXTRA_CAPTURE_MODE)));
|
||||
} else {
|
||||
super.onRestoreInstanceState(savedState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the paint on the preview.
|
||||
*
|
||||
* <p>This only affects the preview, and does not affect captured images/video.
|
||||
*
|
||||
* @param paint The paint object to apply to the preview.
|
||||
* @hide This may not work once {@link android.view.SurfaceView} is supported along with {@link
|
||||
* TextureView}.
|
||||
*/
|
||||
@Override
|
||||
@RestrictTo(Scope.LIBRARY_GROUP)
|
||||
public void setLayerPaint(@Nullable Paint paint) {
|
||||
super.setLayerPaint(paint);
|
||||
mLayerPaint = paint;
|
||||
mCameraTextureView.setLayerPaint(paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
@ -335,33 +298,21 @@ public final class CameraXView extends ViewGroup {
|
|||
dpyMgr.unregisterDisplayListener(mDisplayListener);
|
||||
}
|
||||
|
||||
PreviewView getPreviewView() {
|
||||
return mPreviewView;
|
||||
}
|
||||
|
||||
// TODO(b/124269166): Rethink how we can handle permissions here.
|
||||
@SuppressLint("MissingPermission")
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
int viewWidth = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int viewHeight = MeasureSpec.getSize(heightMeasureSpec);
|
||||
|
||||
int displayRotation = getDisplay().getRotation();
|
||||
|
||||
if (mPreviewSrcSize.getHeight() == 0 || mPreviewSrcSize.getWidth() == 0) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
mCameraTextureView.measure(viewWidth, viewHeight);
|
||||
} else {
|
||||
Size scaled =
|
||||
calculatePreviewViewDimens(
|
||||
mPreviewSrcSize, viewWidth, viewHeight, displayRotation, mScaleType);
|
||||
super.setMeasuredDimension(
|
||||
Math.min(scaled.getWidth(), viewWidth),
|
||||
Math.min(scaled.getHeight(), viewHeight));
|
||||
mCameraTextureView.measure(scaled.getWidth(), scaled.getHeight());
|
||||
}
|
||||
|
||||
// Since bindToLifecycle will depend on the measured dimension, only call it when measured
|
||||
// dimension is not 0x0
|
||||
if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) {
|
||||
mCameraModule.bindToLifecycleAfterViewMeasured();
|
||||
}
|
||||
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
// TODO(b/124269166): Rethink how we can handle permissions here.
|
||||
|
@ -372,114 +323,8 @@ public final class CameraXView extends ViewGroup {
|
|||
// binding to lifecycle
|
||||
mCameraModule.bindToLifecycleAfterViewMeasured();
|
||||
|
||||
// If we don't know the src buffer size yet, set the preview to be the parent size
|
||||
if (mPreviewSrcSize.getWidth() == 0 || mPreviewSrcSize.getHeight() == 0) {
|
||||
mCameraTextureView.layout(left, top, right, bottom);
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute the preview ui size based on the available width, height, and ui orientation.
|
||||
int viewWidth = (right - left);
|
||||
int viewHeight = (bottom - top);
|
||||
int displayRotation = getDisplay().getRotation();
|
||||
Size scaled =
|
||||
calculatePreviewViewDimens(
|
||||
mPreviewSrcSize, viewWidth, viewHeight, displayRotation, mScaleType);
|
||||
|
||||
// Compute the center of the view.
|
||||
int centerX = viewWidth / 2;
|
||||
int centerY = viewHeight / 2;
|
||||
|
||||
// Compute the left / top / right / bottom values such that preview is centered.
|
||||
int layoutL = centerX - (scaled.getWidth() / 2);
|
||||
int layoutT = centerY - (scaled.getHeight() / 2);
|
||||
int layoutR = layoutL + scaled.getWidth();
|
||||
int layoutB = layoutT + scaled.getHeight();
|
||||
|
||||
// Layout debugging
|
||||
log("layout: viewWidth: " + viewWidth);
|
||||
log("layout: viewHeight: " + viewHeight);
|
||||
log("layout: viewRatio: " + (viewWidth / (float) viewHeight));
|
||||
log("layout: sizeWidth: " + mPreviewSrcSize.getWidth());
|
||||
log("layout: sizeHeight: " + mPreviewSrcSize.getHeight());
|
||||
log(
|
||||
"layout: sizeRatio: "
|
||||
+ (mPreviewSrcSize.getWidth() / (float) mPreviewSrcSize.getHeight()));
|
||||
log("layout: scaledWidth: " + scaled.getWidth());
|
||||
log("layout: scaledHeight: " + scaled.getHeight());
|
||||
log("layout: scaledRatio: " + (scaled.getWidth() / (float) scaled.getHeight()));
|
||||
log(
|
||||
"layout: size: "
|
||||
+ scaled
|
||||
+ " ("
|
||||
+ (scaled.getWidth() / (float) scaled.getHeight())
|
||||
+ " - "
|
||||
+ mScaleType
|
||||
+ "-"
|
||||
+ displayRotationToString(displayRotation)
|
||||
+ ")");
|
||||
log("layout: final " + layoutL + ", " + layoutT + ", " + layoutR + ", " + layoutB);
|
||||
|
||||
mCameraTextureView.layout(layoutL, layoutT, layoutR, layoutB);
|
||||
|
||||
mCameraModule.invalidateView();
|
||||
}
|
||||
|
||||
/** Records the size of the preview's buffers. */
|
||||
@UiThread
|
||||
void onPreviewSourceDimensUpdated(int srcWidth, int srcHeight) {
|
||||
if (srcWidth != mPreviewSrcSize.getWidth()
|
||||
|| srcHeight != mPreviewSrcSize.getHeight()) {
|
||||
mPreviewSrcSize = new Size(srcWidth, srcHeight);
|
||||
requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
private Size calculatePreviewViewDimens(
|
||||
Size srcSize,
|
||||
int parentWidth,
|
||||
int parentHeight,
|
||||
int displayRotation,
|
||||
ScaleType scaleType) {
|
||||
int inWidth = srcSize.getWidth();
|
||||
int inHeight = srcSize.getHeight();
|
||||
if (displayRotation == Surface.ROTATION_90 || displayRotation == Surface.ROTATION_270) {
|
||||
// Need to reverse the width and height since we're in landscape orientation.
|
||||
inWidth = srcSize.getHeight();
|
||||
inHeight = srcSize.getWidth();
|
||||
}
|
||||
|
||||
int outWidth = parentWidth;
|
||||
int outHeight = parentHeight;
|
||||
if (inWidth != 0 && inHeight != 0) {
|
||||
float vfRatio = inWidth / (float) inHeight;
|
||||
float parentRatio = parentWidth / (float) parentHeight;
|
||||
|
||||
switch (scaleType) {
|
||||
case CENTER_INSIDE:
|
||||
// Match longest sides together.
|
||||
if (vfRatio > parentRatio) {
|
||||
outWidth = parentWidth;
|
||||
outHeight = Math.round(parentWidth / vfRatio);
|
||||
} else {
|
||||
outWidth = Math.round(parentHeight * vfRatio);
|
||||
outHeight = parentHeight;
|
||||
}
|
||||
break;
|
||||
case CENTER_CROP:
|
||||
// Match shortest sides together.
|
||||
if (vfRatio < parentRatio) {
|
||||
outWidth = parentWidth;
|
||||
outHeight = Math.round(parentWidth / vfRatio);
|
||||
} else {
|
||||
outWidth = Math.round(parentHeight * vfRatio);
|
||||
outHeight = parentHeight;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new Size(outWidth, outHeight);
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -499,58 +344,12 @@ public final class CameraXView extends ViewGroup {
|
|||
return display.getRotation();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
SurfaceTexture getSurfaceTexture() {
|
||||
if (mCameraTextureView != null) {
|
||||
return mCameraTextureView.getSurfaceTexture();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
void setSurfaceTexture(SurfaceTexture surfaceTexture) {
|
||||
if (mCameraTextureView.getSurfaceTexture() != surfaceTexture) {
|
||||
if (mCameraTextureView.isAvailable()) {
|
||||
// Remove the old TextureView to properly detach the old SurfaceTexture from the GL
|
||||
// Context.
|
||||
removeView(mCameraTextureView);
|
||||
addView(mCameraTextureView = new TextureView(getContext()), 0);
|
||||
mCameraTextureView.setLayerPaint(mLayerPaint);
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
mCameraTextureView.setSurfaceTexture(surfaceTexture);
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
Matrix getTransform(Matrix matrix) {
|
||||
return mCameraTextureView.getTransform(matrix);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
int getPreviewWidth() {
|
||||
return mCameraTextureView.getWidth();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
int getPreviewHeight() {
|
||||
return mCameraTextureView.getHeight();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
void setTransform(final Matrix matrix) {
|
||||
if (mCameraTextureView != null) {
|
||||
mCameraTextureView.setTransform(matrix);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the scale type used to scale the preview.
|
||||
*
|
||||
* @return The current {@link ScaleType}.
|
||||
*/
|
||||
@NonNull
|
||||
public ScaleType getScaleType() {
|
||||
return mScaleType;
|
||||
}
|
||||
|
@ -562,7 +361,7 @@ public final class CameraXView extends ViewGroup {
|
|||
*
|
||||
* @param scaleType The desired {@link ScaleType}.
|
||||
*/
|
||||
public void setScaleType(ScaleType scaleType) {
|
||||
public void setScaleType(@NonNull ScaleType scaleType) {
|
||||
if (scaleType != mScaleType) {
|
||||
mScaleType = scaleType;
|
||||
requestLayout();
|
||||
|
@ -574,6 +373,7 @@ public final class CameraXView extends ViewGroup {
|
|||
*
|
||||
* @return The current {@link CaptureMode}.
|
||||
*/
|
||||
@NonNull
|
||||
public CaptureMode getCaptureMode() {
|
||||
return mCameraModule.getCaptureMode();
|
||||
}
|
||||
|
@ -585,7 +385,7 @@ public final class CameraXView extends ViewGroup {
|
|||
*
|
||||
* @param captureMode The desired {@link CaptureMode}.
|
||||
*/
|
||||
public void setCaptureMode(CaptureMode captureMode) {
|
||||
public void setCaptureMode(@NonNull CaptureMode captureMode) {
|
||||
mCameraModule.setCaptureMode(captureMode);
|
||||
}
|
||||
|
||||
|
@ -601,7 +401,7 @@ public final class CameraXView extends ViewGroup {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum video duration before {@link OnVideoSavedListener#onVideoSaved(File)} is
|
||||
* Sets the maximum video duration before {@link VideoCapture.OnVideoSavedCallback#onVideoSaved(FileDescriptor)} is
|
||||
* called automatically. Use {@link #INDEFINITE_VIDEO_DURATION} to disable the timeout.
|
||||
*/
|
||||
private void setMaxVideoDuration(long duration) {
|
||||
|
@ -617,7 +417,7 @@ public final class CameraXView extends ViewGroup {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum video size in bytes before {@link OnVideoSavedListener#onVideoSaved(File)}
|
||||
* Sets the maximum video size in bytes before {@link VideoCapture.OnVideoSavedCallback#onVideoSaved(FileDescriptor)}
|
||||
* is called automatically. Use {@link #INDEFINITE_VIDEO_SIZE} to disable the size restriction.
|
||||
*/
|
||||
private void setMaxVideoSize(long size) {
|
||||
|
@ -625,44 +425,32 @@ public final class CameraXView extends ViewGroup {
|
|||
}
|
||||
|
||||
/**
|
||||
* Takes a picture, and calls {@link OnImageCapturedListener#onCaptureSuccess(ImageProxy, int)}
|
||||
* Takes a picture, and calls {@link OnImageCapturedCallback#onCaptureSuccess(ImageProxy)}
|
||||
* once when done.
|
||||
*
|
||||
* @param executor The executor in which the listener callback methods will be run.
|
||||
* @param listener Listener which will receive success or failure callbacks.
|
||||
* @param executor The executor in which the callback methods will be run.
|
||||
* @param callback Callback which will receive success or failure callbacks.
|
||||
*/
|
||||
@SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901
|
||||
public void takePicture(@NonNull Executor executor, @NonNull ImageCapture.OnImageCapturedListener listener) {
|
||||
mCameraModule.takePicture(executor, listener);
|
||||
public void takePicture(@NonNull Executor executor, @NonNull OnImageCapturedCallback callback) {
|
||||
mCameraModule.takePicture(executor, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a picture and calls {@link OnImageSavedListener#onImageSaved(File)} when done.
|
||||
* Takes a video and calls the OnVideoSavedCallback when done.
|
||||
*
|
||||
* @param file The destination.
|
||||
* @param executor The executor in which the listener callback methods will be run.
|
||||
* @param listener Listener which will receive success or failure callbacks.
|
||||
*/
|
||||
@SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901
|
||||
public void takePicture(@NonNull File file, @NonNull Executor executor,
|
||||
@NonNull ImageCapture.OnImageSavedListener listener) {
|
||||
mCameraModule.takePicture(file, executor, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a video and calls the OnVideoSavedListener when done.
|
||||
*
|
||||
* @param file The destination.
|
||||
* @param executor The executor in which the listener callback methods will be run.
|
||||
* @param listener Listener which will receive success or failure callbacks.
|
||||
* @param executor The executor in which the callback methods will be run.
|
||||
* @param callback Callback which will receive success or failure.
|
||||
*/
|
||||
// Begin Signal Custom Code Block
|
||||
@RequiresApi(26)
|
||||
@SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901
|
||||
public void startRecording(@NonNull FileDescriptor file, @NonNull Executor executor,
|
||||
// End Signal Custom Code Block
|
||||
@NonNull VideoCapture.OnVideoSavedListener listener) {
|
||||
mCameraModule.startRecording(file, executor, listener);
|
||||
public void startRecording(// Begin Signal Custom Code Block
|
||||
@NonNull FileDescriptor file,
|
||||
// End Signal Custom Code Block
|
||||
@NonNull Executor executor,
|
||||
@NonNull VideoCapture.OnVideoSavedCallback callback) {
|
||||
mCameraModule.startRecording(file, executor, callback);
|
||||
}
|
||||
|
||||
/** Stops an in progress video. */
|
||||
|
@ -685,7 +473,7 @@ public final class CameraXView extends ViewGroup {
|
|||
* @throws IllegalStateException if the CAMERA permission is not currently granted.
|
||||
*/
|
||||
@RequiresPermission(permission.CAMERA)
|
||||
public boolean hasCameraWithLensFacing(CameraX.LensFacing lensFacing) {
|
||||
public boolean hasCameraWithLensFacing(@CameraSelector.LensFacing int lensFacing) {
|
||||
return mCameraModule.hasCameraWithLensFacing(lensFacing);
|
||||
}
|
||||
|
||||
|
@ -706,7 +494,7 @@ public final class CameraXView extends ViewGroup {
|
|||
*
|
||||
* <p>If called before {@link #bindToLifecycle(LifecycleOwner)}, this will set the camera to be
|
||||
* used when first bound to the lifecycle. If the specified lensFacing is not supported by the
|
||||
* device, as determined by {@link #hasCameraWithLensFacing(LensFacing)}, the first supported
|
||||
* device, as determined by {@link #hasCameraWithLensFacing(int)}, the first supported
|
||||
* lensFacing will be chosen when {@link #bindToLifecycle(LifecycleOwner)} is called.
|
||||
*
|
||||
* <p>If called with {@code null} AFTER binding to the lifecycle, the behavior would be
|
||||
|
@ -714,36 +502,33 @@ public final class CameraXView extends ViewGroup {
|
|||
*
|
||||
* @param lensFacing The desired camera lensFacing.
|
||||
*/
|
||||
public void setCameraLensFacing(@Nullable CameraX.LensFacing lensFacing) {
|
||||
public void setCameraLensFacing(@Nullable Integer lensFacing) {
|
||||
mCameraModule.setCameraLensFacing(lensFacing);
|
||||
}
|
||||
|
||||
/** Returns the currently selected {@link LensFacing}. */
|
||||
/** Returns the currently selected lensFacing. */
|
||||
@Nullable
|
||||
public CameraX.LensFacing getCameraLensFacing() {
|
||||
public Integer getCameraLensFacing() {
|
||||
return mCameraModule.getLensFacing();
|
||||
}
|
||||
|
||||
/** Gets the active flash strategy. */
|
||||
@ImageCapture.FlashMode
|
||||
public int getFlash() {
|
||||
return mCameraModule.getFlash();
|
||||
}
|
||||
|
||||
// Begin Signal Custom Code Block
|
||||
public boolean hasFlash() {
|
||||
return mCameraModule.hasFlash();
|
||||
}
|
||||
// End Signal Custom Code Block
|
||||
|
||||
/** Gets the active flash strategy. */
|
||||
public FlashMode getFlash() {
|
||||
return mCameraModule.getFlash();
|
||||
}
|
||||
|
||||
/** Sets the active flash strategy. */
|
||||
public void setFlash(@NonNull FlashMode flashMode) {
|
||||
public void setFlash(@ImageCapture.FlashMode int flashMode) {
|
||||
mCameraModule.setFlash(flashMode);
|
||||
}
|
||||
|
||||
private int getRelativeCameraOrientation(boolean compensateForMirroring) {
|
||||
return mCameraModule.getRelativeCameraOrientation(compensateForMirroring);
|
||||
}
|
||||
|
||||
private long delta() {
|
||||
return System.currentTimeMillis() - mDownEventTimestamp;
|
||||
}
|
||||
|
@ -793,42 +578,47 @@ public final class CameraXView extends ViewGroup {
|
|||
final float y = (mUpEvent != null) ? mUpEvent.getY() : getY() + getHeight() / 2f;
|
||||
mUpEvent = null;
|
||||
|
||||
TextureViewMeteringPointFactory pointFactory = new TextureViewMeteringPointFactory(
|
||||
mCameraTextureView);
|
||||
CameraSelector cameraSelector =
|
||||
new CameraSelector.Builder().requireLensFacing(
|
||||
mCameraModule.getLensFacing()).build();
|
||||
|
||||
DisplayOrientedMeteringPointFactory pointFactory = new DisplayOrientedMeteringPointFactory(
|
||||
getDisplay(), cameraSelector, mPreviewView.getWidth(), mPreviewView.getHeight());
|
||||
float afPointWidth = 1.0f / 6.0f; // 1/6 total area
|
||||
float aePointWidth = afPointWidth * 1.5f;
|
||||
MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth, 1.0f);
|
||||
MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth, 1.0f);
|
||||
MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth);
|
||||
MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth);
|
||||
|
||||
try {
|
||||
CameraX.getCameraControl(getCameraLensFacing()).startFocusAndMetering(
|
||||
FocusMeteringAction.Builder.from(afPoint, FocusMeteringAction.MeteringMode.AF_ONLY)
|
||||
.addPoint(aePoint, FocusMeteringAction.MeteringMode.AE_ONLY)
|
||||
.build());
|
||||
} catch (CameraInfoUnavailableException e) {
|
||||
Log.d(TAG, "cannot access camera", e);
|
||||
Camera camera = mCameraModule.getCamera();
|
||||
if (camera != null) {
|
||||
ListenableFuture<FocusMeteringResult> future =
|
||||
camera.getCameraControl().startFocusAndMetering(
|
||||
new FocusMeteringAction.Builder(afPoint,
|
||||
FocusMeteringAction.FLAG_AF).addPoint(aePoint,
|
||||
FocusMeteringAction.FLAG_AE).build());
|
||||
Futures.addCallback(future, new FutureCallback<FocusMeteringResult>() {
|
||||
@Override
|
||||
public void onSuccess(@Nullable FocusMeteringResult result) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
// Throw the unexpected error.
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
}, CameraXExecutors.directExecutor());
|
||||
|
||||
} else {
|
||||
Log.d(TAG, "cannot access camera");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Returns the width * height of the given rect */
|
||||
private int area(Rect rect) {
|
||||
return rect.width() * rect.height();
|
||||
}
|
||||
|
||||
private int rangeLimit(int val, int max, int min) {
|
||||
return Math.min(Math.max(val, min), max);
|
||||
}
|
||||
|
||||
float rangeLimit(float val, float max, float min) {
|
||||
return Math.min(Math.max(val, min), max);
|
||||
}
|
||||
|
||||
private int distance(int a, int b) {
|
||||
return Math.abs(a - b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the view allows pinch-to-zoom.
|
||||
*
|
||||
|
@ -851,47 +641,47 @@ public final class CameraXView extends ViewGroup {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the current zoom level.
|
||||
* Returns the current zoom ratio.
|
||||
*
|
||||
* @return The current zoom level.
|
||||
* @return The current zoom ratio.
|
||||
*/
|
||||
public float getZoomLevel() {
|
||||
return mCameraModule.getZoomLevel();
|
||||
public float getZoomRatio() {
|
||||
return mCameraModule.getZoomRatio();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current zoom level.
|
||||
* Sets the current zoom ratio.
|
||||
*
|
||||
* <p>Valid zoom values range from 1 to {@link #getMaxZoomLevel()}.
|
||||
* <p>Valid zoom values range from {@link #getMinZoomRatio()} to {@link #getMaxZoomRatio()}.
|
||||
*
|
||||
* @param zoomLevel The requested zoom level.
|
||||
* @param zoomRatio The requested zoom ratio.
|
||||
*/
|
||||
public void setZoomLevel(float zoomLevel) {
|
||||
mCameraModule.setZoomLevel(zoomLevel);
|
||||
public void setZoomRatio(float zoomRatio) {
|
||||
mCameraModule.setZoomRatio(zoomRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum zoom level.
|
||||
* Returns the minimum zoom ratio.
|
||||
*
|
||||
* <p>For most cameras this should return a zoom level of 1. A zoom level of 1 corresponds to a
|
||||
* <p>For most cameras this should return a zoom ratio of 1. A zoom ratio of 1 corresponds to a
|
||||
* non-zoomed image.
|
||||
*
|
||||
* @return The minimum zoom level.
|
||||
* @return The minimum zoom ratio.
|
||||
*/
|
||||
public float getMinZoomLevel() {
|
||||
return mCameraModule.getMinZoomLevel();
|
||||
public float getMinZoomRatio() {
|
||||
return mCameraModule.getMinZoomRatio();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum zoom level.
|
||||
* Returns the maximum zoom ratio.
|
||||
*
|
||||
* <p>The zoom level corresponds to the ratio between both the widths and heights of a
|
||||
* <p>The zoom ratio corresponds to the ratio between both the widths and heights of a
|
||||
* non-zoomed image and a maximally zoomed image for the selected camera.
|
||||
*
|
||||
* @return The maximum zoom level.
|
||||
* @return The maximum zoom ratio.
|
||||
*/
|
||||
public float getMaxZoomLevel() {
|
||||
return mCameraModule.getMaxZoomLevel();
|
||||
public float getMaxZoomRatio() {
|
||||
return mCameraModule.getMaxZoomRatio();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -935,7 +725,7 @@ public final class CameraXView extends ViewGroup {
|
|||
*/
|
||||
CENTER_INSIDE(1);
|
||||
|
||||
private int mId;
|
||||
private final int mId;
|
||||
|
||||
int getId() {
|
||||
return mId;
|
||||
|
@ -959,7 +749,7 @@ public final class CameraXView extends ViewGroup {
|
|||
* The capture mode used by CameraView.
|
||||
*
|
||||
* <p>This enum can be used to determine which capture mode will be enabled for {@link
|
||||
* CameraView}.
|
||||
* CameraXView}.
|
||||
*/
|
||||
public enum CaptureMode {
|
||||
/** A mode where image capture is enabled. */
|
||||
|
@ -972,7 +762,7 @@ public final class CameraXView extends ViewGroup {
|
|||
*/
|
||||
MIXED(2);
|
||||
|
||||
private int mId;
|
||||
private final int mId;
|
||||
|
||||
int getId() {
|
||||
return mId;
|
||||
|
@ -1007,10 +797,6 @@ public final class CameraXView extends ViewGroup {
|
|||
|
||||
private class PinchToZoomGestureDetector extends ScaleGestureDetector
|
||||
implements ScaleGestureDetector.OnScaleGestureListener {
|
||||
private static final float SCALE_MULTIPIER = 0.75f;
|
||||
private final BaseInterpolator mInterpolator = new DecelerateInterpolator(2f);
|
||||
private float mNormalizedScaleFactor = 0;
|
||||
|
||||
PinchToZoomGestureDetector(Context context) {
|
||||
this(context, new S());
|
||||
}
|
||||
|
@ -1022,34 +808,23 @@ public final class CameraXView extends ViewGroup {
|
|||
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector) {
|
||||
mNormalizedScaleFactor += (detector.getScaleFactor() - 1f) * SCALE_MULTIPIER;
|
||||
// Since the scale factor is normalized, it should always be in the range [0, 1]
|
||||
mNormalizedScaleFactor = rangeLimit(mNormalizedScaleFactor, 1f, 0);
|
||||
float scale = detector.getScaleFactor();
|
||||
|
||||
// Apply decelerate interpolation. This will cause the differences to seem less
|
||||
// pronounced
|
||||
// at higher zoom levels.
|
||||
float transformedScale = mInterpolator.getInterpolation(mNormalizedScaleFactor);
|
||||
// Speeding up the zoom by 2X.
|
||||
if (scale > 1f) {
|
||||
scale = 1.0f + (scale - 1.0f) * 2;
|
||||
} else {
|
||||
scale = 1.0f - (1.0f - scale) * 2;
|
||||
}
|
||||
|
||||
// Transform back from normalized coordinates to the zoom scale
|
||||
float zoomLevel =
|
||||
(getMaxZoomLevel() == getMinZoomLevel())
|
||||
? getMinZoomLevel()
|
||||
: getMinZoomLevel()
|
||||
+ transformedScale * (getMaxZoomLevel() - getMinZoomLevel());
|
||||
|
||||
setZoomLevel(rangeLimit(zoomLevel, getMaxZoomLevel(), getMinZoomLevel()));
|
||||
float newRatio = getZoomRatio() * scale;
|
||||
newRatio = rangeLimit(newRatio, getMaxZoomRatio(), getMinZoomRatio());
|
||||
setZoomRatio(newRatio);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScaleBegin(ScaleGestureDetector detector) {
|
||||
float initialZoomLevel = getZoomLevel();
|
||||
mNormalizedScaleFactor =
|
||||
(getMaxZoomLevel() == getMinZoomLevel())
|
||||
? 0
|
||||
: (initialZoomLevel - getMinZoomLevel())
|
||||
/ (getMaxZoomLevel() - getMinZoomLevel());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.mediasend.camerax;
|
||||
|
||||
import static androidx.camera.core.ImageCapture.FLASH_MODE_AUTO;
|
||||
import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
|
||||
import static androidx.camera.core.ImageCapture.FLASH_MODE_ON;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.camera.core.ImageCapture.FlashMode;
|
||||
|
||||
/**
|
||||
* Helper class that defines certain enum-like methods for {@link FlashMode}
|
||||
*/
|
||||
final class FlashModeConverter {
|
||||
|
||||
private FlashModeConverter() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link FlashMode} constant for the specified name
|
||||
*
|
||||
* @param name The name of the {@link FlashMode} to return
|
||||
* @return The {@link FlashMode} constant for the specified name
|
||||
*/
|
||||
@FlashMode
|
||||
public static int valueOf(@Nullable final String name) {
|
||||
if (name == null) {
|
||||
throw new NullPointerException("name cannot be null");
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
case "AUTO":
|
||||
return FLASH_MODE_AUTO;
|
||||
case "ON":
|
||||
return FLASH_MODE_ON;
|
||||
case "OFF":
|
||||
return FLASH_MODE_OFF;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown flash mode name " + name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the {@link FlashMode} constant, exactly as it is declared.
|
||||
*
|
||||
* @param flashMode A {@link FlashMode} constant
|
||||
* @return The name of the {@link FlashMode} constant.
|
||||
*/
|
||||
@NonNull
|
||||
public static String nameOf(@FlashMode final int flashMode) {
|
||||
switch (flashMode) {
|
||||
case FLASH_MODE_AUTO:
|
||||
return "AUTO";
|
||||
case FLASH_MODE_ON:
|
||||
return "ON";
|
||||
case FLASH_MODE_OFF:
|
||||
return "OFF";
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown flash mode " + flashMode);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,273 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.mediasend.camerax;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.camera.core.Preview;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Custom View that displays camera feed for CameraX's Preview use case.
|
||||
*
|
||||
* <p> This class manages the Surface lifecycle, as well as the preview aspect ratio and
|
||||
* orientation. Internally, it uses either a {@link android.view.TextureView} or
|
||||
* {@link android.view.SurfaceView} to display the camera feed.
|
||||
*/
|
||||
// Begin Signal Custom Code Block
|
||||
@RequiresApi(21)
|
||||
// End Signal Custom Code Block
|
||||
public class PreviewView extends FrameLayout {
|
||||
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
|
||||
Implementation mImplementation;
|
||||
|
||||
private ImplementationMode mImplementationMode;
|
||||
|
||||
private final DisplayManager.DisplayListener mDisplayListener =
|
||||
new DisplayManager.DisplayListener() {
|
||||
@Override
|
||||
public void onDisplayAdded(int displayId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayRemoved(int displayId) {
|
||||
}
|
||||
@Override
|
||||
public void onDisplayChanged(int displayId) {
|
||||
mImplementation.onDisplayChanged();
|
||||
}
|
||||
};
|
||||
|
||||
public PreviewView(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public PreviewView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public PreviewView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
this(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
public PreviewView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
|
||||
final TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs,
|
||||
R.styleable.PreviewView, defStyleAttr, defStyleRes);
|
||||
|
||||
try {
|
||||
final int implementationModeId = attributes.getInteger(
|
||||
R.styleable.PreviewView_implementationMode,
|
||||
ImplementationMode.TEXTURE_VIEW.getId());
|
||||
mImplementationMode = ImplementationMode.fromId(implementationModeId);
|
||||
} finally {
|
||||
attributes.recycle();
|
||||
}
|
||||
setUp();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
final DisplayManager displayManager =
|
||||
(DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
|
||||
if (displayManager != null) {
|
||||
displayManager.registerDisplayListener(mDisplayListener, getHandler());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
final DisplayManager displayManager =
|
||||
(DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
|
||||
if (displayManager != null) {
|
||||
displayManager.unregisterDisplayListener(mDisplayListener);
|
||||
}
|
||||
}
|
||||
|
||||
private void setUp() {
|
||||
removeAllViews();
|
||||
switch (mImplementationMode) {
|
||||
case SURFACE_VIEW:
|
||||
mImplementation = new SurfaceViewImplementation();
|
||||
break;
|
||||
case TEXTURE_VIEW:
|
||||
mImplementation = new TextureViewImplementation();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(
|
||||
"Unsupported implementation mode " + mImplementationMode);
|
||||
}
|
||||
mImplementation.init(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the {@link ImplementationMode} to use for the preview.
|
||||
*
|
||||
* @param implementationMode <code>SURFACE_VIEW</code> if a {@link android.view.SurfaceView}
|
||||
* should be used to display the camera feed, or
|
||||
* <code>TEXTURE_VIEW</code> to use a {@link android.view.TextureView}
|
||||
*/
|
||||
public void setImplementationMode(@NonNull final ImplementationMode implementationMode) {
|
||||
mImplementationMode = implementationMode;
|
||||
setUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the implementation mode of the {@link PreviewView}.
|
||||
*
|
||||
* @return <code>SURFACE_VIEW</code> if the {@link PreviewView} is internally using a
|
||||
* {@link android.view.SurfaceView} to display the camera feed, or <code>TEXTURE_VIEW</code>
|
||||
* if a {@link android.view.TextureView} is being used.
|
||||
*/
|
||||
@NonNull
|
||||
public ImplementationMode getImplementationMode() {
|
||||
return mImplementationMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link Preview.SurfaceProvider} to be used with
|
||||
* {@link Preview#setSurfaceProvider(Executor, Preview.SurfaceProvider)}.
|
||||
*/
|
||||
@NonNull
|
||||
public Preview.SurfaceProvider getPreviewSurfaceProvider() {
|
||||
return mImplementation.getSurfaceProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements this interface to create PreviewView implementation.
|
||||
*/
|
||||
interface Implementation {
|
||||
|
||||
/**
|
||||
* Initializes the parent view with sub views.
|
||||
*
|
||||
* @param parent the containing parent {@link FrameLayout}.
|
||||
*/
|
||||
void init(@NonNull FrameLayout parent);
|
||||
|
||||
/**
|
||||
* Gets the {@link Preview.SurfaceProvider} to be used with {@link Preview}.
|
||||
*/
|
||||
@NonNull
|
||||
Preview.SurfaceProvider getSurfaceProvider();
|
||||
|
||||
/**
|
||||
* Notifies that the display properties have changed.
|
||||
*
|
||||
* <p>Implementation might need to adjust transform by latest display properties such as
|
||||
* display orientation in order to show the preview correctly.
|
||||
*/
|
||||
void onDisplayChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* The implementation mode of a {@link PreviewView}
|
||||
*
|
||||
* <p>Specifies how the Preview surface will be implemented internally: Using a
|
||||
* {@link android.view.SurfaceView} or a {@link android.view.TextureView} (which is the default)
|
||||
* </p>
|
||||
*/
|
||||
public enum ImplementationMode {
|
||||
/** Use a {@link android.view.SurfaceView} for the preview */
|
||||
SURFACE_VIEW(0),
|
||||
|
||||
/** Use a {@link android.view.TextureView} for the preview */
|
||||
TEXTURE_VIEW(1);
|
||||
|
||||
private final int mId;
|
||||
|
||||
ImplementationMode(final int id) {
|
||||
mId = id;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
static ImplementationMode fromId(final int id) {
|
||||
for (final ImplementationMode mode : values()) {
|
||||
if (mode.mId == id) {
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unsupported implementation mode " + id);
|
||||
}
|
||||
}
|
||||
|
||||
/** Options for scaling the preview vis-à-vis its container {@link PreviewView}. */
|
||||
public enum ScaleType {
|
||||
/**
|
||||
* Scale the preview, maintaining the source aspect ratio, so it fills the entire
|
||||
* {@link PreviewView}, and align it to the top left corner of the view.
|
||||
* This may cause the preview to be cropped if the camera preview aspect ratio does not
|
||||
* match that of its container {@link PreviewView}.
|
||||
*/
|
||||
FILL_START,
|
||||
/**
|
||||
* Scale the preview, maintaining the source aspect ratio, so it fills the entire
|
||||
* {@link PreviewView}, and center it inside the view.
|
||||
* This may cause the preview to be cropped if the camera preview aspect ratio does not
|
||||
* match that of its container {@link PreviewView}.
|
||||
*/
|
||||
FILL_CENTER,
|
||||
/**
|
||||
* Scale the preview, maintaining the source aspect ratio, so it fills the entire
|
||||
* {@link PreviewView}, and align it to the bottom right corner of the view.
|
||||
* This may cause the preview to be cropped if the camera preview aspect ratio does not
|
||||
* match that of its container {@link PreviewView}.
|
||||
*/
|
||||
FILL_END,
|
||||
/**
|
||||
* Scale the preview, maintaining the source aspect ratio, so it is entirely contained
|
||||
* within the {@link PreviewView}, and align it to the top left corner of the view.
|
||||
* Both dimensions of the preview will be equal or less than the corresponding dimensions
|
||||
* of its container {@link PreviewView}.
|
||||
*/
|
||||
FIT_START,
|
||||
/**
|
||||
* Scale the preview, maintaining the source aspect ratio, so it is entirely contained
|
||||
* within the {@link PreviewView}, and center it inside the view.
|
||||
* Both dimensions of the preview will be equal or less than the corresponding dimensions
|
||||
* of its container {@link PreviewView}.
|
||||
*/
|
||||
FIT_CENTER,
|
||||
/**
|
||||
* Scale the preview, maintaining the source aspect ratio, so it is entirely contained
|
||||
* within the {@link PreviewView}, and align it to the bottom right corner of the view.
|
||||
* Both dimensions of the preview will be equal or less than the corresponding dimensions
|
||||
* of its container {@link PreviewView}.
|
||||
*/
|
||||
FIT_END
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.mediasend.camerax;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Point;
|
||||
import android.util.Pair;
|
||||
import android.util.Size;
|
||||
import android.view.Display;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
// Begin Signal Custom Code Block
|
||||
@RequiresApi(21)
|
||||
// End Signal Custom Code Block
|
||||
final class ScaleTypeTransform {
|
||||
|
||||
/**
|
||||
* Computes the scale by which a view has to scale in x and y in order to fill its parent
|
||||
* while maintaining the buffer's aspect ratio.
|
||||
*
|
||||
* @param container A parent {@link android.view.View} that wraps {@code view}.
|
||||
* @param view A child {@link android.view.View} of {@code container}.
|
||||
* @param bufferSize A {@link android.util.Size} whose aspect ratio must be maintained when
|
||||
* scaling {@code view} inside its parent {@code container}.
|
||||
* @return The scale by which {@code view} has to scale in x and y in order to fill its
|
||||
* parent while maintaining {@code bufferSize}'s aspect ratio.
|
||||
*/
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
static Pair<Float, Float> getFillScaleWithBufferAspectRatio(@NonNull final View container,
|
||||
@NonNull final View view, @NonNull final Size bufferSize) {
|
||||
// Scaling only makes sense when none of the dimensions are equal to zero. In the
|
||||
// opposite case, a default scale of 1 is returned,
|
||||
if (container.getWidth() == 0 || container.getHeight() == 0 || view.getWidth() == 0
|
||||
|| view.getHeight() == 0 || bufferSize.getWidth() == 0
|
||||
|| bufferSize.getHeight() == 0) {
|
||||
return new Pair<>(1F, 1F);
|
||||
}
|
||||
|
||||
final int viewRotationDegrees = getRotationDegrees(view);
|
||||
final boolean isNaturalPortrait = isNaturalPortrait(view.getContext(), viewRotationDegrees);
|
||||
|
||||
final int bufferWidth;
|
||||
final int bufferHeight;
|
||||
if (isNaturalPortrait) {
|
||||
bufferWidth = bufferSize.getHeight();
|
||||
bufferHeight = bufferSize.getWidth();
|
||||
} else {
|
||||
bufferWidth = bufferSize.getWidth();
|
||||
bufferHeight = bufferSize.getHeight();
|
||||
}
|
||||
|
||||
// Scale the buffers back to the original output size.
|
||||
float scaleX = bufferWidth / (float) view.getWidth();
|
||||
float scaleY = bufferHeight / (float) view.getHeight();
|
||||
|
||||
int bufferRotatedWidth;
|
||||
int bufferRotatedHeight;
|
||||
if (viewRotationDegrees == 0 || viewRotationDegrees == 180) {
|
||||
bufferRotatedWidth = bufferWidth;
|
||||
bufferRotatedHeight = bufferHeight;
|
||||
} else {
|
||||
bufferRotatedWidth = bufferHeight;
|
||||
bufferRotatedHeight = bufferWidth;
|
||||
}
|
||||
|
||||
// Scale the buffer so that it completely fills the container.
|
||||
final float scale = Math.max(container.getWidth() / (float) bufferRotatedWidth,
|
||||
container.getHeight() / (float) bufferRotatedHeight);
|
||||
scaleX *= scale;
|
||||
scaleY *= scale;
|
||||
|
||||
return new Pair<>(scaleX, scaleY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the top left coordinates for the view to be centered inside its parent.
|
||||
*
|
||||
* @param container A parent {@link android.view.View} that wraps {@code view}.
|
||||
* @param view A child {@link android.view.View} of {@code container}.
|
||||
* @return A {@link android.graphics.Point} whose coordinates represent the top left of
|
||||
* {@code view} when centered inside its parent.
|
||||
*/
|
||||
static Point getOriginOfCenteredView(@NonNull final View container,
|
||||
@NonNull final View view) {
|
||||
final int offsetX = (view.getWidth() - container.getWidth()) / 2;
|
||||
final int offsetY = (view.getHeight() - container.getHeight()) / 2;
|
||||
return new Point(-offsetX, -offsetY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the rotation of a {@link android.view.View} in degrees from its natural
|
||||
* orientation.
|
||||
*/
|
||||
static int getRotationDegrees(@NonNull final View view) {
|
||||
final WindowManager windowManager = (WindowManager) view.getContext().getSystemService(
|
||||
Context.WINDOW_SERVICE);
|
||||
if (windowManager == null) {
|
||||
return 0;
|
||||
}
|
||||
final int rotation = windowManager.getDefaultDisplay().getRotation();
|
||||
return SurfaceRotation.rotationDegreesFromSurfaceRotation(rotation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the current device is a natural portrait-oriented device
|
||||
*
|
||||
* <p>
|
||||
* Using the current app's window to determine whether the device is a natural
|
||||
* portrait-oriented device doesn't work in all scenarios, one example of this is multi-window
|
||||
* mode.
|
||||
* Taking a natural portrait-oriented device in multi-window mode, rotating it 90 degrees (so
|
||||
* that it's in landscape), with the app open, and its window's width being smaller than its
|
||||
* height. Using the app's width and height would determine that the device isn't
|
||||
* naturally portrait-oriented, where in fact it is, which is why it is important to use the
|
||||
* size of the device instead.
|
||||
* </p>
|
||||
*
|
||||
* @param context Current context. Can be an {@link android.app.Application} context
|
||||
* or an {@link android.app.Activity} context.
|
||||
* @param rotationDegrees The device's rotation in degrees from its natural orientation.
|
||||
* @return Whether the device is naturally portrait-oriented.
|
||||
*/
|
||||
private static boolean isNaturalPortrait(@NonNull final Context context,
|
||||
final int rotationDegrees) {
|
||||
final WindowManager windowManager = (WindowManager) context.getSystemService(
|
||||
Context.WINDOW_SERVICE);
|
||||
if (windowManager == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final Display display = windowManager.getDefaultDisplay();
|
||||
final Point deviceSize = new Point();
|
||||
display.getRealSize(deviceSize);
|
||||
|
||||
final int width = deviceSize.x;
|
||||
final int height = deviceSize.y;
|
||||
return ((rotationDegrees == 0 || rotationDegrees == 180) && width < height) || (
|
||||
(rotationDegrees == 90 || rotationDegrees == 270) && width >= height);
|
||||
}
|
||||
|
||||
// Prevent creating an instance
|
||||
private ScaleTypeTransform() {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.mediasend.camerax;
|
||||
|
||||
import android.view.Surface;
|
||||
|
||||
final class SurfaceRotation {
|
||||
/**
|
||||
* Get the int value degree of a rotation from the {@link Surface} constants.
|
||||
*
|
||||
* <p>Valid values for the relative rotation are {@link Surface#ROTATION_0}, {@link
|
||||
* * Surface#ROTATION_90}, {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
|
||||
*/
|
||||
static int rotationDegreesFromSurfaceRotation(int rotationConstant) {
|
||||
switch (rotationConstant) {
|
||||
case Surface.ROTATION_0:
|
||||
return 0;
|
||||
case Surface.ROTATION_90:
|
||||
return 90;
|
||||
case Surface.ROTATION_180:
|
||||
return 180;
|
||||
case Surface.ROTATION_270:
|
||||
return 270;
|
||||
default:
|
||||
throw new UnsupportedOperationException(
|
||||
"Unsupported surface rotation constant: " + rotationConstant);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prevents construction */
|
||||
private SurfaceRotation() {}
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.mediasend.camerax;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.Size;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.camera.core.Preview;
|
||||
import androidx.camera.core.SurfaceRequest;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
/**
|
||||
* The SurfaceView implementation for {@link PreviewView}.
|
||||
*/
|
||||
@RequiresApi(21)
|
||||
final class SurfaceViewImplementation implements PreviewView.Implementation {
|
||||
|
||||
private static final String TAG = "SurfaceViewPreviewView";
|
||||
|
||||
// Synthetic Accessor
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
TransformableSurfaceView mSurfaceView;
|
||||
|
||||
// Synthetic Accessor
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final SurfaceRequestCallback mSurfaceRequestCallback =
|
||||
new SurfaceRequestCallback();
|
||||
|
||||
private Preview.SurfaceProvider mSurfaceProvider =
|
||||
new Preview.SurfaceProvider() {
|
||||
@Override
|
||||
public void onSurfaceRequested(@NonNull SurfaceRequest surfaceRequest) {
|
||||
mSurfaceView.post(
|
||||
() -> mSurfaceRequestCallback.setSurfaceRequest(surfaceRequest));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void init(@NonNull FrameLayout parent) {
|
||||
mSurfaceView = new TransformableSurfaceView(parent.getContext());
|
||||
mSurfaceView.setLayoutParams(
|
||||
new FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT));
|
||||
parent.addView(mSurfaceView);
|
||||
mSurfaceView.getHolder().addCallback(mSurfaceRequestCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public Preview.SurfaceProvider getSurfaceProvider() {
|
||||
return mSurfaceProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayChanged() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link SurfaceHolder.Callback} on mSurfaceView.
|
||||
*
|
||||
* <p> SurfaceView creates Surface on its own before we can do anything. This class makes
|
||||
* sure only the Surface with correct size will be returned to Preview.
|
||||
*/
|
||||
class SurfaceRequestCallback implements SurfaceHolder.Callback {
|
||||
|
||||
// Target Surface size. Only complete the SurfaceRequest when the size of the Surface
|
||||
// matches this value.
|
||||
// Guarded by UI thread.
|
||||
@Nullable
|
||||
private Size mTargetSize;
|
||||
|
||||
// SurfaceRequest to set when the target size is met.
|
||||
// Guarded by UI thread.
|
||||
@Nullable
|
||||
private SurfaceRequest mSurfaceRequest;
|
||||
|
||||
// The cached size of the current Surface.
|
||||
// Guarded by UI thread.
|
||||
@Nullable
|
||||
private Size mCurrentSurfaceSize;
|
||||
|
||||
/**
|
||||
* Sets the completer and the size. The completer will only be set if the current size of
|
||||
* the Surface matches the target size.
|
||||
*/
|
||||
@UiThread
|
||||
void setSurfaceRequest(@NonNull SurfaceRequest surfaceRequest) {
|
||||
cancelPreviousRequest();
|
||||
mSurfaceRequest = surfaceRequest;
|
||||
Size targetSize = surfaceRequest.getResolution();
|
||||
mTargetSize = targetSize;
|
||||
if (!tryToComplete()) {
|
||||
// The current size is incorrect. Wait for it to change.
|
||||
Log.d(TAG, "Wait for new Surface creation.");
|
||||
mSurfaceView.getHolder().setFixedSize(targetSize.getWidth(),
|
||||
targetSize.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the completer if size matches.
|
||||
*
|
||||
* @return true if the completer is set.
|
||||
*/
|
||||
@UiThread
|
||||
private boolean tryToComplete() {
|
||||
Surface surface = mSurfaceView.getHolder().getSurface();
|
||||
if (mSurfaceRequest != null && mTargetSize != null && mTargetSize.equals(
|
||||
mCurrentSurfaceSize)) {
|
||||
Log.d(TAG, "Surface set on Preview.");
|
||||
mSurfaceRequest.provideSurface(surface,
|
||||
ContextCompat.getMainExecutor(mSurfaceView.getContext()),
|
||||
(result) -> Log.d(TAG, "Safe to release surface."));
|
||||
mSurfaceRequest = null;
|
||||
mTargetSize = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void cancelPreviousRequest() {
|
||||
if (mSurfaceRequest != null) {
|
||||
Log.d(TAG, "Request canceled: " + mSurfaceRequest);
|
||||
mSurfaceRequest.willNotProvideSurface();
|
||||
mSurfaceRequest = null;
|
||||
}
|
||||
mTargetSize = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder surfaceHolder) {
|
||||
Log.d(TAG, "Surface created.");
|
||||
// No-op. Handling surfaceChanged() is enough because it's always called afterwards.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
|
||||
Log.d(TAG, "Surface changed. Size: " + width + "x" + height);
|
||||
mCurrentSurfaceSize = new Size(width, height);
|
||||
tryToComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
|
||||
Log.d(TAG, "Surface destroyed.");
|
||||
mCurrentSurfaceSize = null;
|
||||
cancelPreviousRequest();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.mediasend.camerax;
|
||||
|
||||
import static androidx.camera.core.SurfaceRequest.Result;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.util.Size;
|
||||
import android.view.Surface;
|
||||
import android.view.TextureView;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.camera.core.Preview;
|
||||
import androidx.camera.core.SurfaceRequest;
|
||||
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
||||
import androidx.camera.core.impl.utils.futures.FutureCallback;
|
||||
import androidx.camera.core.impl.utils.futures.Futures;
|
||||
import androidx.concurrent.futures.CallbackToFutureAdapter;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.util.Preconditions;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
/**
|
||||
* The {@link TextureView} implementation for {@link PreviewView}
|
||||
*/
|
||||
// Begin Signal Custom Code Block
|
||||
@RequiresApi(21)
|
||||
@SuppressLint("RestrictedApi")
|
||||
// End Signal Custom Code Block
|
||||
public class TextureViewImplementation implements PreviewView.Implementation {
|
||||
|
||||
private static final String TAG = "TextureViewImpl";
|
||||
|
||||
private FrameLayout mParent;
|
||||
TextureView mTextureView;
|
||||
SurfaceTexture mSurfaceTexture;
|
||||
private Size mResolution;
|
||||
ListenableFuture<Result> mSurfaceReleaseFuture;
|
||||
SurfaceRequest mSurfaceRequest;
|
||||
|
||||
@Override
|
||||
public void init(@NonNull FrameLayout parent) {
|
||||
mParent = parent;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Preview.SurfaceProvider getSurfaceProvider() {
|
||||
return (surfaceRequest) -> {
|
||||
mResolution = surfaceRequest.getResolution();
|
||||
initInternal();
|
||||
if (mSurfaceRequest != null) {
|
||||
mSurfaceRequest.willNotProvideSurface();
|
||||
}
|
||||
|
||||
mSurfaceRequest = surfaceRequest;
|
||||
surfaceRequest.addRequestCancellationListener(
|
||||
ContextCompat.getMainExecutor(mTextureView.getContext()), () -> {
|
||||
if (mSurfaceRequest != null && mSurfaceRequest == surfaceRequest) {
|
||||
mSurfaceRequest = null;
|
||||
mSurfaceReleaseFuture = null;
|
||||
}
|
||||
});
|
||||
|
||||
tryToProvidePreviewSurface();
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayChanged() {
|
||||
if (mParent == null || mTextureView == null || mResolution == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
correctPreviewForCenterCrop(mParent, mTextureView, mResolution);
|
||||
}
|
||||
|
||||
private void initInternal() {
|
||||
mTextureView = new TextureView(mParent.getContext());
|
||||
mTextureView.setLayoutParams(
|
||||
new FrameLayout.LayoutParams(mResolution.getWidth(), mResolution.getHeight()));
|
||||
mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
|
||||
@Override
|
||||
public void onSurfaceTextureAvailable(final SurfaceTexture surfaceTexture,
|
||||
final int width, final int height) {
|
||||
mSurfaceTexture = surfaceTexture;
|
||||
tryToProvidePreviewSurface();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceTextureSizeChanged(final SurfaceTexture surfaceTexture,
|
||||
final int width, final int height) {
|
||||
Log.d(TAG, "onSurfaceTextureSizeChanged(width:" + width + ", height: " + height
|
||||
+ " )");
|
||||
}
|
||||
|
||||
/**
|
||||
* If a surface has been provided to the camera (meaning
|
||||
* {@link TextureViewImplementation#mSurfaceRequest} is null), but the camera
|
||||
* is still using it (meaning {@link TextureViewImplementation#mSurfaceReleaseFuture} is
|
||||
* not null), a listener must be added to
|
||||
* {@link TextureViewImplementation#mSurfaceReleaseFuture} to ensure the surface
|
||||
* is properly released after the camera is done using it.
|
||||
*
|
||||
* @param surfaceTexture The {@link SurfaceTexture} about to be destroyed.
|
||||
* @return false if the camera is not done with the surface, true otherwise.
|
||||
*/
|
||||
@Override
|
||||
public boolean onSurfaceTextureDestroyed(final SurfaceTexture surfaceTexture) {
|
||||
mSurfaceTexture = null;
|
||||
if (mSurfaceRequest == null && mSurfaceReleaseFuture != null) {
|
||||
Futures.addCallback(mSurfaceReleaseFuture,
|
||||
new FutureCallback<Result>() {
|
||||
@Override
|
||||
public void onSuccess(Result result) {
|
||||
Preconditions.checkState(result.getResultCode()
|
||||
!= Result.RESULT_SURFACE_ALREADY_PROVIDED,
|
||||
"Unexpected result from SurfaceRequest. Surface was "
|
||||
+ "provided twice.");
|
||||
surfaceTexture.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
throw new IllegalStateException("SurfaceReleaseFuture did not "
|
||||
+ "complete nicely.", t);
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(mTextureView.getContext()));
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceTextureUpdated(final SurfaceTexture surfaceTexture) {
|
||||
}
|
||||
});
|
||||
|
||||
// Even though PreviewView calls `removeAllViews()` before calling init(), it should be
|
||||
// called again here in case `getPreviewSurfaceProvider()` is called more than once on
|
||||
// the same TextureViewImplementation instance.
|
||||
mParent.removeAllViews();
|
||||
mParent.addView(mTextureView);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
void tryToProvidePreviewSurface() {
|
||||
/*
|
||||
Should only continue if:
|
||||
- The preview size has been specified.
|
||||
- The textureView's surfaceTexture is available (after TextureView
|
||||
.SurfaceTextureListener#onSurfaceTextureAvailable is invoked)
|
||||
- The surfaceCompleter has been set (after CallbackToFutureAdapter
|
||||
.Resolver#attachCompleter is invoked).
|
||||
*/
|
||||
if (mResolution == null || mSurfaceTexture == null || mSurfaceRequest == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mSurfaceTexture.setDefaultBufferSize(mResolution.getWidth(), mResolution.getHeight());
|
||||
|
||||
final Surface surface = new Surface(mSurfaceTexture);
|
||||
final ListenableFuture<Result> surfaceReleaseFuture =
|
||||
CallbackToFutureAdapter.getFuture(completer -> {
|
||||
mSurfaceRequest.provideSurface(surface,
|
||||
CameraXExecutors.directExecutor(), completer::set);
|
||||
return "provideSurface[request=" + mSurfaceRequest + " surface=" + surface
|
||||
+ "]";
|
||||
});
|
||||
mSurfaceReleaseFuture = surfaceReleaseFuture;
|
||||
mSurfaceReleaseFuture.addListener(() -> {
|
||||
surface.release();
|
||||
if (mSurfaceReleaseFuture == surfaceReleaseFuture) {
|
||||
mSurfaceReleaseFuture = null;
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(mTextureView.getContext()));
|
||||
|
||||
mSurfaceRequest = null;
|
||||
|
||||
correctPreviewForCenterCrop(mParent, mTextureView, mResolution);
|
||||
}
|
||||
|
||||
/**
|
||||
* Corrects the preview to match the UI orientation and completely fill the PreviewView.
|
||||
*
|
||||
* <p>
|
||||
* The camera produces a preview that depends on its sensor orientation and that has a
|
||||
* specific resolution. In order to display it correctly, this preview must be rotated to
|
||||
* match the UI orientation, and must be scaled up/down to fit inside the view that's
|
||||
* displaying it. This method takes care of doing so while keeping the preview centered.
|
||||
* </p>
|
||||
*
|
||||
* @param container The {@link PreviewView}'s root layout, which wraps the preview.
|
||||
* @param textureView The {@link android.view.TextureView} that displays the preview, its size
|
||||
* must match the camera sensor output size.
|
||||
* @param bufferSize The camera sensor output size.
|
||||
*/
|
||||
private void correctPreviewForCenterCrop(@NonNull final View container,
|
||||
@NonNull final TextureView textureView, @NonNull final Size bufferSize) {
|
||||
// Scale TextureView to fill PreviewView while respecting sensor output size aspect ratio
|
||||
final Pair<Float, Float> scale = ScaleTypeTransform.getFillScaleWithBufferAspectRatio(container, textureView,
|
||||
bufferSize);
|
||||
textureView.setScaleX(scale.first);
|
||||
textureView.setScaleY(scale.second);
|
||||
|
||||
// Center TextureView inside PreviewView
|
||||
final Point newOrigin = ScaleTypeTransform.getOriginOfCenteredView(container, textureView);
|
||||
textureView.setX(newOrigin.x);
|
||||
textureView.setY(newOrigin.y);
|
||||
|
||||
// Rotate TextureView to correct preview orientation
|
||||
final int rotation = ScaleTypeTransform.getRotationDegrees(textureView);
|
||||
textureView.setRotation(-rotation);
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ import android.graphics.SurfaceTexture;
|
|||
import android.view.TextureView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.camera.core.MeteringPoint;
|
||||
import androidx.camera.core.MeteringPointFactory;
|
||||
|
||||
|
@ -37,57 +38,60 @@ import androidx.camera.core.MeteringPointFactory;
|
|||
* to the lens face of current camera ouput.
|
||||
*/
|
||||
public class TextureViewMeteringPointFactory extends MeteringPointFactory {
|
||||
private final TextureView mTextureView;
|
||||
private final TextureView mTextureView;
|
||||
|
||||
public TextureViewMeteringPointFactory(@NonNull TextureView textureView) {
|
||||
mTextureView = textureView;
|
||||
}
|
||||
public TextureViewMeteringPointFactory(@NonNull TextureView textureView) {
|
||||
mTextureView = textureView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a (x,y) from TextureView.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
protected PointF translatePoint(float x, float y) {
|
||||
Matrix transform = new Matrix();
|
||||
mTextureView.getTransform(transform);
|
||||
/**
|
||||
* Translates a (x,y) from TextureView.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@NonNull
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
|
||||
@Override
|
||||
protected PointF convertPoint(float x, float y) {
|
||||
Matrix transform = new Matrix();
|
||||
mTextureView.getTransform(transform);
|
||||
|
||||
// applying reverse of TextureView#getTransform
|
||||
Matrix inverse = new Matrix();
|
||||
transform.invert(inverse);
|
||||
float[] pt = new float[]{x, y};
|
||||
inverse.mapPoints(pt);
|
||||
// applying reverse of TextureView#getTransform
|
||||
Matrix inverse = new Matrix();
|
||||
transform.invert(inverse);
|
||||
float[] pt = new float[]{x, y};
|
||||
inverse.mapPoints(pt);
|
||||
|
||||
// get SurfaceTexture#getTransformMatrix
|
||||
float[] surfaceTextureMat = new float[16];
|
||||
mTextureView.getSurfaceTexture().getTransformMatrix(surfaceTextureMat);
|
||||
// get SurfaceTexture#getTransformMatrix
|
||||
float[] surfaceTextureMat = new float[16];
|
||||
mTextureView.getSurfaceTexture().getTransformMatrix(surfaceTextureMat);
|
||||
|
||||
// convert SurfaceTexture#getTransformMatrix(4x4 column major 3D matrix) to
|
||||
// android.graphics.Matrix(3x3 row major 2D matrix)
|
||||
Matrix surfaceTextureTransform = glMatrixToGraphicsMatrix(surfaceTextureMat);
|
||||
// convert SurfaceTexture#getTransformMatrix(4x4 column major 3D matrix) to
|
||||
// android.graphics.Matrix(3x3 row major 2D matrix)
|
||||
Matrix surfaceTextureTransform = glMatrixToGraphicsMatrix(surfaceTextureMat);
|
||||
|
||||
float[] pt2 = new float[2];
|
||||
// convert to texture coordinates first.
|
||||
pt2[0] = pt[0] / mTextureView.getWidth();
|
||||
pt2[1] = (mTextureView.getHeight() - pt[1]) / mTextureView.getHeight();
|
||||
surfaceTextureTransform.mapPoints(pt2);
|
||||
float[] pt2 = new float[2];
|
||||
// convert to texture coordinates first.
|
||||
pt2[0] = pt[0] / mTextureView.getWidth();
|
||||
pt2[1] = (mTextureView.getHeight() - pt[1]) / mTextureView.getHeight();
|
||||
surfaceTextureTransform.mapPoints(pt2);
|
||||
|
||||
return new PointF(pt2[0], pt2[1]);
|
||||
}
|
||||
return new PointF(pt2[0], pt2[1]);
|
||||
}
|
||||
|
||||
private Matrix glMatrixToGraphicsMatrix(float[] glMatrix) {
|
||||
float[] convert = new float[9];
|
||||
convert[0] = glMatrix[0];
|
||||
convert[1] = glMatrix[4];
|
||||
convert[2] = glMatrix[12];
|
||||
convert[3] = glMatrix[1];
|
||||
convert[4] = glMatrix[5];
|
||||
convert[5] = glMatrix[13];
|
||||
convert[6] = glMatrix[3];
|
||||
convert[7] = glMatrix[7];
|
||||
convert[8] = glMatrix[15];
|
||||
Matrix graphicsMatrix = new Matrix();
|
||||
graphicsMatrix.setValues(convert);
|
||||
return graphicsMatrix;
|
||||
}
|
||||
private Matrix glMatrixToGraphicsMatrix(float[] glMatrix) {
|
||||
float[] convert = new float[9];
|
||||
convert[0] = glMatrix[0];
|
||||
convert[1] = glMatrix[4];
|
||||
convert[2] = glMatrix[12];
|
||||
convert[3] = glMatrix[1];
|
||||
convert[4] = glMatrix[5];
|
||||
convert[5] = glMatrix[13];
|
||||
convert[6] = glMatrix[3];
|
||||
convert[7] = glMatrix[7];
|
||||
convert[8] = glMatrix[15];
|
||||
Matrix graphicsMatrix = new Matrix();
|
||||
graphicsMatrix.setValues(convert);
|
||||
return graphicsMatrix;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.mediasend.camerax;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.RectF;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.SurfaceView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
/**
|
||||
* A subclass of {@link SurfaceView} that supports translation and scaling transformations.
|
||||
*/
|
||||
// Begin Signal Custom Code Block
|
||||
@RequiresApi(21)
|
||||
// End Signal Custom Code Block
|
||||
final class TransformableSurfaceView extends SurfaceView {
|
||||
|
||||
private RectF mOverriddenLayoutRect;
|
||||
|
||||
TransformableSurfaceView(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
TransformableSurfaceView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
TransformableSurfaceView(@NonNull Context context, @Nullable AttributeSet attrs,
|
||||
int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
TransformableSurfaceView(@NonNull Context context, @Nullable AttributeSet attrs,
|
||||
int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
if (mOverriddenLayoutRect == null) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
} else {
|
||||
setMeasuredDimension((int) mOverriddenLayoutRect.width(),
|
||||
(int) mOverriddenLayoutRect.height());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the transform to associate with this surface view. Only translation and scaling are
|
||||
* supported. If a rotated transformation is passed in, an exception is thrown.
|
||||
*
|
||||
* @param transform The transform to apply to the content of this view.
|
||||
*/
|
||||
void setTransform(final Matrix transform) {
|
||||
if (hasRotation(transform)) {
|
||||
throw new IllegalArgumentException("TransformableSurfaceView does not support "
|
||||
+ "rotation transformations.");
|
||||
}
|
||||
|
||||
final RectF rect = new RectF(getLeft(), getTop(), getRight(), getBottom());
|
||||
transform.mapRect(rect);
|
||||
overrideLayout(rect);
|
||||
}
|
||||
|
||||
private boolean hasRotation(final Matrix matrix) {
|
||||
final float[] values = new float[9];
|
||||
matrix.getValues(values);
|
||||
|
||||
/*
|
||||
A translation matrix can be represented as:
|
||||
(1 0 transX)
|
||||
(0 1 transX)
|
||||
(0 0 1)
|
||||
|
||||
A rotation Matrix of ψ degrees can be represented as:
|
||||
(cosψ -sinψ 0)
|
||||
(sinψ cosψ 0)
|
||||
(0 0 1)
|
||||
|
||||
A scale matrix can be represented as:
|
||||
(scaleX 0 0)
|
||||
(0 scaleY 0)
|
||||
(0 0 0)
|
||||
|
||||
Meaning a transformed matrix can be represented as:
|
||||
(scaleX * cosψ -scaleX * sinψ transX)
|
||||
(scaleY * sinψ scaleY * cosψ transY)
|
||||
(0 0 1)
|
||||
|
||||
Using the following 2 equalities:
|
||||
scaleX * cosψ = matrix[0][0]
|
||||
-scaleX * sinψ = matrix[0][1]
|
||||
|
||||
The following is deduced:
|
||||
-tanψ = matrix[0][1] / matrix[0][0]
|
||||
|
||||
Or:
|
||||
ψ = -arctan(matrix[0][1] / matrix[0][0])
|
||||
*/
|
||||
final double angle = -Math.atan2(values[Matrix.MSKEW_X], values[Matrix.MSCALE_X]);
|
||||
|
||||
return Math.round(angle * (180 / Math.PI)) != 0;
|
||||
}
|
||||
|
||||
private void overrideLayout(final RectF overriddenLayoutRect) {
|
||||
mOverriddenLayoutRect = overriddenLayoutRect;
|
||||
setX(overriddenLayoutRect.left);
|
||||
setY(overriddenLayoutRect.top);
|
||||
requestLayout();
|
||||
}
|
||||
}
|
|
@ -16,7 +16,22 @@
|
|||
|
||||
package org.thoughtcrime.securesms.mediasend.camerax;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import android.location.Location;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioRecord;
|
||||
|
@ -30,39 +45,42 @@ import android.media.MediaMuxer;
|
|||
import android.media.MediaRecorder.AudioSource;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.util.Size;
|
||||
import android.view.Display;
|
||||
import android.view.Surface;
|
||||
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.annotation.RestrictTo.Scope;
|
||||
import androidx.camera.core.CameraInfoInternal;
|
||||
import androidx.camera.core.CameraInfoUnavailableException;
|
||||
import androidx.camera.core.CameraInfo;
|
||||
import androidx.camera.core.CameraX;
|
||||
import androidx.camera.core.CameraX.LensFacing;
|
||||
import androidx.camera.core.CameraXThreads;
|
||||
import androidx.camera.core.ConfigProvider;
|
||||
import androidx.camera.core.DeferrableSurface;
|
||||
import androidx.camera.core.ImageOutputConfig;
|
||||
import androidx.camera.core.ImageOutputConfig.RotationValue;
|
||||
import androidx.camera.core.ImmediateSurface;
|
||||
import androidx.camera.core.SessionConfig;
|
||||
import androidx.camera.core.UseCase;
|
||||
import androidx.camera.core.UseCaseConfig;
|
||||
import androidx.camera.core.VideoCaptureConfig;
|
||||
import androidx.camera.core.impl.CameraInfoInternal;
|
||||
import androidx.camera.core.impl.CameraInternal;
|
||||
import androidx.camera.core.impl.ConfigProvider;
|
||||
import androidx.camera.core.impl.DeferrableSurface;
|
||||
import androidx.camera.core.impl.ImageOutputConfig;
|
||||
import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
|
||||
import androidx.camera.core.impl.ImmediateSurface;
|
||||
import androidx.camera.core.impl.SessionConfig;
|
||||
import androidx.camera.core.impl.UseCaseConfig;
|
||||
import androidx.camera.core.impl.VideoCaptureConfig;
|
||||
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
||||
import androidx.camera.core.internal.utils.UseCaseConfigUtil;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.video.VideoUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
|
@ -77,10 +95,31 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
*
|
||||
* @hide In the earlier stage, the VideoCapture is deprioritized.
|
||||
*/
|
||||
// Begin Signal Custom Code Block
|
||||
@RequiresApi(26)
|
||||
// End Signal Custom Code Block
|
||||
@RestrictTo(Scope.LIBRARY_GROUP)
|
||||
@SuppressWarnings("ClassCanBeStatic") // TODO(b/141958189): Suppressed during upgrade to AGP 3.6.
|
||||
public class VideoCapture extends UseCase {
|
||||
|
||||
/**
|
||||
* An unknown error occurred.
|
||||
*
|
||||
* <p>See message parameter in onError callback or log for more details.
|
||||
*/
|
||||
public static final int ERROR_UNKNOWN = 0;
|
||||
/**
|
||||
* An error occurred with encoder state, either when trying to change state or when an
|
||||
* unexpected state change occurred.
|
||||
*/
|
||||
public static final int ERROR_ENCODER = 1;
|
||||
/** An error with muxer state such as during creation or when stopping. */
|
||||
public static final int ERROR_MUXER = 2;
|
||||
/**
|
||||
* An error indicating start recording was called when video recording is still in progress.
|
||||
*/
|
||||
public static final int ERROR_RECORDING_IN_PROGRESS = 3;
|
||||
|
||||
/**
|
||||
* Provides a static configuration with implementation-agnostic options.
|
||||
*
|
||||
|
@ -131,7 +170,6 @@ public class VideoCapture extends UseCase {
|
|||
/** For record the first sample written time. */
|
||||
private final AtomicBoolean mIsFirstVideoSampleWrite = new AtomicBoolean(false);
|
||||
private final AtomicBoolean mIsFirstAudioSampleWrite = new AtomicBoolean(false);
|
||||
private final VideoCaptureConfig.Builder mUseCaseConfigBuilder;
|
||||
|
||||
@NonNull
|
||||
MediaCodec mVideoEncoder;
|
||||
|
@ -147,7 +185,9 @@ public class VideoCapture extends UseCase {
|
|||
private int mAudioTrackIndex;
|
||||
/** Surface the camera writes to, which the videoEncoder uses as input. */
|
||||
Surface mCameraSurface;
|
||||
|
||||
/** audio raw data */
|
||||
@NonNull
|
||||
private AudioRecord mAudioRecorder;
|
||||
private int mAudioBufferSize;
|
||||
private boolean mIsRecording = false;
|
||||
|
@ -163,7 +203,6 @@ public class VideoCapture extends UseCase {
|
|||
*/
|
||||
public VideoCapture(VideoCaptureConfig config) {
|
||||
super(config);
|
||||
mUseCaseConfigBuilder = VideoCaptureConfig.Builder.fromConfig(config);
|
||||
|
||||
// video thread start
|
||||
mVideoHandlerThread.start();
|
||||
|
@ -182,9 +221,6 @@ public class VideoCapture extends UseCase {
|
|||
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface);
|
||||
format.setInteger(MediaFormat.KEY_BIT_RATE, config.getBitRate());
|
||||
format.setInteger(MediaFormat.KEY_FRAME_RATE, config.getVideoFrameRate());
|
||||
// Begin Signal Custom Code Block
|
||||
format.setInteger(MediaFormat.KEY_CAPTURE_RATE, config.getVideoFrameRate());
|
||||
// End Signal Custom Code Block
|
||||
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, config.getIFrameInterval());
|
||||
|
||||
return format;
|
||||
|
@ -199,9 +235,9 @@ public class VideoCapture extends UseCase {
|
|||
@Override
|
||||
@Nullable
|
||||
@RestrictTo(Scope.LIBRARY_GROUP)
|
||||
protected UseCaseConfig.Builder<?, ?, ?> getDefaultBuilder(LensFacing lensFacing) {
|
||||
VideoCaptureConfig defaults = CameraX.getDefaultUseCaseConfig(
|
||||
VideoCaptureConfig.class, lensFacing);
|
||||
protected UseCaseConfig.Builder<?, ?, ?> getDefaultBuilder(@Nullable CameraInfo cameraInfo) {
|
||||
VideoCaptureConfig defaults = CameraX.getDefaultUseCaseConfig(VideoCaptureConfig.class,
|
||||
cameraInfo);
|
||||
if (defaults != null) {
|
||||
return VideoCaptureConfig.Builder.fromConfig(defaults);
|
||||
}
|
||||
|
@ -216,9 +252,9 @@ public class VideoCapture extends UseCase {
|
|||
*/
|
||||
@Override
|
||||
@RestrictTo(Scope.LIBRARY_GROUP)
|
||||
@NonNull
|
||||
protected Map<String, Size> onSuggestedResolutionUpdated(
|
||||
Map<String, Size> suggestedResolutionMap) {
|
||||
VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
|
||||
@NonNull Map<String, Size> suggestedResolutionMap) {
|
||||
if (mCameraSurface != null) {
|
||||
mVideoEncoder.stop();
|
||||
mVideoEncoder.release();
|
||||
|
@ -234,14 +270,14 @@ public class VideoCapture extends UseCase {
|
|||
throw new IllegalStateException("Unable to create MediaCodec due to: " + e.getCause());
|
||||
}
|
||||
|
||||
String cameraId = getCameraIdUnchecked(config);
|
||||
String cameraId = getBoundCameraId();
|
||||
Size resolution = suggestedResolutionMap.get(cameraId);
|
||||
if (resolution == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Suggested resolution map missing resolution for camera " + cameraId);
|
||||
}
|
||||
|
||||
setupEncoder(resolution);
|
||||
setupEncoder(cameraId, resolution);
|
||||
return suggestedResolutionMap;
|
||||
}
|
||||
|
||||
|
@ -250,20 +286,19 @@ public class VideoCapture extends UseCase {
|
|||
* called.
|
||||
*
|
||||
* <p>StartRecording() is asynchronous. User needs to check if any error occurs by setting the
|
||||
* {@link OnVideoSavedListener#onError(VideoCaptureError, String, Throwable)}.
|
||||
* {@link OnVideoSavedCallback#onError(int, String, Throwable)}.
|
||||
*
|
||||
* @param saveLocation Location to save the video capture
|
||||
* @param executor The executor in which the listener callback methods will be run.
|
||||
* @param listener Listener to call for the recorded video
|
||||
* @param executor The executor in which the callback methods will be run.
|
||||
* @param callback Callback for when the recorded video saving completion or failure.
|
||||
*/
|
||||
@SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901
|
||||
// Begin Signal Custom Code Block
|
||||
public void startRecording(@NonNull FileDescriptor saveLocation,
|
||||
@NonNull Executor executor, @NonNull OnVideoSavedListener listener) {
|
||||
// End Signal Custom Code Block
|
||||
@NonNull Executor executor, @NonNull OnVideoSavedCallback callback) {
|
||||
mIsFirstVideoSampleWrite.set(false);
|
||||
mIsFirstAudioSampleWrite.set(false);
|
||||
startRecording(saveLocation, EMPTY_METADATA, executor, listener);
|
||||
startRecording(saveLocation, EMPTY_METADATA, executor, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -271,26 +306,26 @@ public class VideoCapture extends UseCase {
|
|||
* called.
|
||||
*
|
||||
* <p>StartRecording() is asynchronous. User needs to check if any error occurs by setting the
|
||||
* {@link OnVideoSavedListener#onError(VideoCaptureError, String, Throwable)}.
|
||||
* {@link OnVideoSavedCallback#onError(int, String, Throwable)}.
|
||||
*
|
||||
* @param saveLocation Location to save the video capture
|
||||
* @param metadata Metadata to save with the recorded video
|
||||
* @param executor The executor in which the listener callback methods will be run.
|
||||
* @param listener Listener to call for the recorded video
|
||||
* @param executor The executor in which the callback methods will be run.
|
||||
* @param callback Callback for when the recorded video saving completion or failure.
|
||||
*/
|
||||
@SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901
|
||||
// Begin Signal Custom Code Block
|
||||
public void startRecording(
|
||||
@NonNull FileDescriptor saveLocation, @NonNull Metadata metadata,
|
||||
// Begin Signal Custom Code Block
|
||||
@NonNull FileDescriptor saveLocation,
|
||||
// End Signal Custom Code Block
|
||||
@NonNull Metadata metadata,
|
||||
@NonNull Executor executor,
|
||||
@NonNull OnVideoSavedListener listener) {
|
||||
// End Signal Custom Code Block
|
||||
@NonNull OnVideoSavedCallback callback) {
|
||||
Log.i(TAG, "startRecording");
|
||||
OnVideoSavedListener postListener = new VideoSavedListenerWrapper(executor, listener);
|
||||
OnVideoSavedCallback postListener = new VideoSavedListenerWrapper(executor, callback);
|
||||
|
||||
if (!mEndOfAudioVideoSignal.get()) {
|
||||
postListener.onError(
|
||||
VideoCaptureError.RECORDING_IN_PROGRESS, "It is still in video recording!",
|
||||
ERROR_RECORDING_IN_PROGRESS, "It is still in video recording!",
|
||||
null);
|
||||
return;
|
||||
}
|
||||
|
@ -305,12 +340,13 @@ public class VideoCapture extends UseCase {
|
|||
}
|
||||
// End Signal Custom Code Block
|
||||
} catch (IllegalStateException e) {
|
||||
postListener.onError(VideoCaptureError.ENCODER_ERROR, "AudioRecorder start fail", e);
|
||||
postListener.onError(ERROR_ENCODER, "AudioRecorder start fail", e);
|
||||
return;
|
||||
}
|
||||
|
||||
VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
|
||||
String cameraId = getCameraIdUnchecked(config);
|
||||
CameraInternal boundCamera = getBoundCamera();
|
||||
String cameraId = getBoundCameraId();
|
||||
Size resolution = getAttachedSurfaceResolution(cameraId);
|
||||
try {
|
||||
// video encoder start
|
||||
Log.i(TAG, "videoEncoder start");
|
||||
|
@ -320,23 +356,15 @@ public class VideoCapture extends UseCase {
|
|||
mAudioEncoder.start();
|
||||
|
||||
} catch (IllegalStateException e) {
|
||||
setupEncoder(getAttachedSurfaceResolution(cameraId));
|
||||
postListener.onError(VideoCaptureError.ENCODER_ERROR, "Audio/Video encoder start fail",
|
||||
setupEncoder(cameraId, resolution);
|
||||
postListener.onError(ERROR_ENCODER, "Audio/Video encoder start fail",
|
||||
e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the relative rotation or default to 0 if the camera info is unavailable
|
||||
int relativeRotation = 0;
|
||||
try {
|
||||
CameraInfoInternal cameraInfoInternal = CameraX.getCameraInfo(cameraId);
|
||||
relativeRotation =
|
||||
cameraInfoInternal.getSensorRotationDegrees(
|
||||
((ImageOutputConfig) getUseCaseConfig())
|
||||
.getTargetRotation(Surface.ROTATION_0));
|
||||
} catch (CameraInfoUnavailableException e) {
|
||||
Log.e(TAG, "Unable to retrieve camera sensor orientation.", e);
|
||||
}
|
||||
CameraInfoInternal cameraInfoInternal = boundCamera.getCameraInfoInternal();
|
||||
int relativeRotation = cameraInfoInternal.getSensorRotationDegrees(
|
||||
((ImageOutputConfig) getUseCaseConfig()).getTargetRotation(Surface.ROTATION_0));
|
||||
|
||||
try {
|
||||
synchronized (mMuxerLock) {
|
||||
|
@ -355,8 +383,8 @@ public class VideoCapture extends UseCase {
|
|||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
setupEncoder(getAttachedSurfaceResolution(cameraId));
|
||||
postListener.onError(VideoCaptureError.MUXER_ERROR, "MediaMuxer creation failed!", e);
|
||||
setupEncoder(cameraId, resolution);
|
||||
postListener.onError(ERROR_MUXER, "MediaMuxer creation failed!", e);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -378,7 +406,8 @@ public class VideoCapture extends UseCase {
|
|||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
boolean errorOccurred = VideoCapture.this.videoEncode(postListener);
|
||||
boolean errorOccurred = VideoCapture.this.videoEncode(postListener,
|
||||
cameraId, resolution);
|
||||
if (!errorOccurred) {
|
||||
postListener.onVideoSaved(saveLocation);
|
||||
}
|
||||
|
@ -388,11 +417,11 @@ public class VideoCapture extends UseCase {
|
|||
|
||||
/**
|
||||
* Stops recording video, this must be called after {@link
|
||||
* VideoCapture#startRecording(File, Metadata, Executor, OnVideoSavedListener)} is called.
|
||||
* VideoCapture#startRecording(File, Metadata, Executor, OnVideoSavedCallback)} is called.
|
||||
*
|
||||
* <p>stopRecording() is asynchronous API. User need to check if {@link
|
||||
* OnVideoSavedListener#onVideoSaved(File)} or
|
||||
* {@link OnVideoSavedListener#onError(VideoCaptureError, String, Throwable)} be called
|
||||
* OnVideoSavedCallback#onVideoSaved(File)} or
|
||||
* {@link OnVideoSavedCallback#onError(int, String, Throwable)} be called
|
||||
* before startRecording.
|
||||
*/
|
||||
public void stopRecording() {
|
||||
|
@ -438,23 +467,17 @@ public class VideoCapture extends UseCase {
|
|||
return;
|
||||
}
|
||||
|
||||
final Surface surface = mCameraSurface;
|
||||
final MediaCodec videoEncoder = mVideoEncoder;
|
||||
|
||||
mDeferrableSurface.setOnSurfaceDetachedListener(
|
||||
CameraXExecutors.mainThreadExecutor(),
|
||||
new DeferrableSurface.OnSurfaceDetachedListener() {
|
||||
@Override
|
||||
public void onSurfaceDetached() {
|
||||
if (releaseVideoEncoder && videoEncoder != null) {
|
||||
videoEncoder.release();
|
||||
}
|
||||
|
||||
if (surface != null) {
|
||||
surface.release();
|
||||
}
|
||||
// Calling close should allow termination future to complete and close the surface with
|
||||
// the listener that was added after constructing the DeferrableSurface.
|
||||
mDeferrableSurface.close();
|
||||
mDeferrableSurface.getTerminationFuture().addListener(
|
||||
() -> {
|
||||
if (releaseVideoEncoder && videoEncoder != null) {
|
||||
videoEncoder.release();
|
||||
}
|
||||
});
|
||||
}, CameraXExecutors.mainThreadExecutor());
|
||||
|
||||
if (releaseVideoEncoder) {
|
||||
mVideoEncoder = null;
|
||||
|
@ -473,11 +496,12 @@ public class VideoCapture extends UseCase {
|
|||
* @param rotation Desired rotation of the output video.
|
||||
*/
|
||||
public void setTargetRotation(@RotationValue int rotation) {
|
||||
ImageOutputConfig oldConfig = (ImageOutputConfig) getUseCaseConfig();
|
||||
VideoCaptureConfig oldConfig = (VideoCaptureConfig) getUseCaseConfig();
|
||||
VideoCaptureConfig.Builder builder = VideoCaptureConfig.Builder.fromConfig(oldConfig);
|
||||
int oldRotation = oldConfig.getTargetRotation(ImageOutputConfig.INVALID_ROTATION);
|
||||
if (oldRotation == ImageOutputConfig.INVALID_ROTATION || oldRotation != rotation) {
|
||||
mUseCaseConfigBuilder.setTargetRotation(rotation);
|
||||
updateUseCaseConfig(mUseCaseConfigBuilder.build());
|
||||
UseCaseConfigUtil.updateTargetRotationAndRelatedConfigs(builder, rotation);
|
||||
updateUseCaseConfig(builder.getUseCaseConfig());
|
||||
|
||||
// TODO(b/122846516): Update session configuration and possibly reconfigure session.
|
||||
}
|
||||
|
@ -488,7 +512,7 @@ public class VideoCapture extends UseCase {
|
|||
* audio from selected audio source.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
|
||||
void setupEncoder(Size resolution) {
|
||||
void setupEncoder(@NonNull String cameraId, @NonNull Size resolution) {
|
||||
VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
|
||||
|
||||
// video encoder setup
|
||||
|
@ -501,21 +525,32 @@ public class VideoCapture extends UseCase {
|
|||
if (mCameraSurface != null) {
|
||||
releaseCameraSurface(false);
|
||||
}
|
||||
mCameraSurface = mVideoEncoder.createInputSurface();
|
||||
Surface cameraSurface = mVideoEncoder.createInputSurface();
|
||||
mCameraSurface = cameraSurface;
|
||||
|
||||
SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
|
||||
|
||||
if (mDeferrableSurface != null) {
|
||||
mDeferrableSurface.close();
|
||||
}
|
||||
mDeferrableSurface = new ImmediateSurface(mCameraSurface);
|
||||
mDeferrableSurface.getTerminationFuture().addListener(
|
||||
cameraSurface::release, CameraXExecutors.mainThreadExecutor()
|
||||
);
|
||||
|
||||
sessionConfigBuilder.addSurface(mDeferrableSurface);
|
||||
|
||||
String cameraId = getCameraIdUnchecked(config);
|
||||
|
||||
sessionConfigBuilder.addErrorListener(new SessionConfig.ErrorListener() {
|
||||
@Override
|
||||
public void onError(@NonNull SessionConfig sessionConfig,
|
||||
@NonNull SessionConfig.SessionError error) {
|
||||
setupEncoder(resolution);
|
||||
// Ensure the bound camera has not changed before calling setupEncoder.
|
||||
// TODO(b/143915543): Ensure this never gets called by a camera that is not bound
|
||||
// to this use case so we don't need to do this check.
|
||||
if (isCurrentlyBoundCamera(cameraId)) {
|
||||
// Only reset the pipeline when the bound camera is the same.
|
||||
setupEncoder(cameraId, resolution);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -620,8 +655,8 @@ public class VideoCapture extends UseCase {
|
|||
*
|
||||
* @return returns {@code true} if an error condition occurred, otherwise returns {@code false}
|
||||
*/
|
||||
boolean videoEncode(OnVideoSavedListener videoSavedListener) {
|
||||
VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
|
||||
boolean videoEncode(@NonNull OnVideoSavedCallback videoSavedCallback, @NonNull String cameraId,
|
||||
@NonNull Size resolution) {
|
||||
// Main encoding loop. Exits on end of stream.
|
||||
boolean errorOccurred = false;
|
||||
boolean videoEos = false;
|
||||
|
@ -638,8 +673,8 @@ public class VideoCapture extends UseCase {
|
|||
switch (outputBufferId) {
|
||||
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
||||
if (mMuxerStarted) {
|
||||
videoSavedListener.onError(
|
||||
VideoCaptureError.ENCODER_ERROR,
|
||||
videoSavedCallback.onError(
|
||||
ERROR_ENCODER,
|
||||
"Unexpected change in video encoding format.",
|
||||
null);
|
||||
errorOccurred = true;
|
||||
|
@ -656,10 +691,6 @@ public class VideoCapture extends UseCase {
|
|||
break;
|
||||
case MediaCodec.INFO_TRY_AGAIN_LATER:
|
||||
// Timed out. Just wait until next attempt to deque.
|
||||
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
|
||||
// Ignore output buffers changed since we dequeue a single buffer instead of
|
||||
// multiple
|
||||
break;
|
||||
default:
|
||||
videoEos = writeVideoEncodedBuffer(outputBufferId);
|
||||
}
|
||||
|
@ -669,7 +700,7 @@ public class VideoCapture extends UseCase {
|
|||
Log.i(TAG, "videoEncoder stop");
|
||||
mVideoEncoder.stop();
|
||||
} catch (IllegalStateException e) {
|
||||
videoSavedListener.onError(VideoCaptureError.ENCODER_ERROR,
|
||||
videoSavedCallback.onError(ERROR_ENCODER,
|
||||
"Video encoder stop failed!", e);
|
||||
errorOccurred = true;
|
||||
}
|
||||
|
@ -686,16 +717,15 @@ public class VideoCapture extends UseCase {
|
|||
}
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
videoSavedListener.onError(VideoCaptureError.MUXER_ERROR, "Muxer stop failed!", e);
|
||||
videoSavedCallback.onError(ERROR_MUXER, "Muxer stop failed!", e);
|
||||
errorOccurred = true;
|
||||
}
|
||||
|
||||
mMuxerStarted = false;
|
||||
// Do the setup of the videoEncoder at the end of video recording instead of at the start of
|
||||
// recording because it requires attaching a new Surface. This causes a glitch so we don't
|
||||
// want
|
||||
// that to incur latency at the start of capture.
|
||||
setupEncoder(getAttachedSurfaceResolution(getCameraIdUnchecked(config)));
|
||||
// want that to incur latency at the start of capture.
|
||||
setupEncoder(cameraId, resolution);
|
||||
notifyReset();
|
||||
|
||||
// notify the UI thread that the video recording has finished
|
||||
|
@ -705,7 +735,7 @@ public class VideoCapture extends UseCase {
|
|||
return errorOccurred;
|
||||
}
|
||||
|
||||
boolean audioEncode(OnVideoSavedListener videoSavedListener) {
|
||||
boolean audioEncode(OnVideoSavedCallback videoSavedCallback) {
|
||||
// Audio encoding loop. Exits on end of stream.
|
||||
boolean audioEos = false;
|
||||
int outIndex;
|
||||
|
@ -766,14 +796,14 @@ public class VideoCapture extends UseCase {
|
|||
}
|
||||
// End Signal Custom Code Block
|
||||
} catch (IllegalStateException e) {
|
||||
videoSavedListener.onError(
|
||||
VideoCaptureError.ENCODER_ERROR, "Audio recorder stop failed!", e);
|
||||
videoSavedCallback.onError(
|
||||
ERROR_ENCODER, "Audio recorder stop failed!", e);
|
||||
}
|
||||
|
||||
try {
|
||||
mAudioEncoder.stop();
|
||||
} catch (IllegalStateException e) {
|
||||
videoSavedListener.onError(VideoCaptureError.ENCODER_ERROR,
|
||||
videoSavedCallback.onError(ERROR_ENCODER,
|
||||
"Audio encoder stop failed!", e);
|
||||
}
|
||||
|
||||
|
@ -889,39 +919,29 @@ public class VideoCapture extends UseCase {
|
|||
* Describes the error that occurred during video capture operations.
|
||||
*
|
||||
* <p>This is a parameter sent to the error callback functions set in listeners such as {@link
|
||||
* VideoCapture.OnVideoSavedListener#onError(VideoCaptureError, String, Throwable)}.
|
||||
* VideoCapture.OnVideoSavedCallback#onError(int, String, Throwable)}.
|
||||
*
|
||||
* <p>See message parameter in onError callback or log for more details.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public enum VideoCaptureError {
|
||||
/**
|
||||
* An unknown error occurred.
|
||||
*
|
||||
* <p>See message parameter in onError callback or log for more details.
|
||||
*/
|
||||
UNKNOWN_ERROR,
|
||||
/**
|
||||
* An error occurred with encoder state, either when trying to change state or when an
|
||||
* unexpected state change occurred.
|
||||
*/
|
||||
ENCODER_ERROR,
|
||||
/** An error with muxer state such as during creation or when stopping. */
|
||||
MUXER_ERROR,
|
||||
/**
|
||||
* An error indicating start recording was called when video recording is still in progress.
|
||||
*/
|
||||
RECORDING_IN_PROGRESS
|
||||
@IntDef({ERROR_UNKNOWN, ERROR_ENCODER, ERROR_MUXER, ERROR_RECORDING_IN_PROGRESS})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@RestrictTo(Scope.LIBRARY_GROUP)
|
||||
public @interface VideoCaptureError {
|
||||
}
|
||||
|
||||
/** Listener containing callbacks for video file I/O events. */
|
||||
public interface OnVideoSavedListener {
|
||||
public interface OnVideoSavedCallback {
|
||||
/** Called when the video has been successfully saved. */
|
||||
// TODO: Should remove file argument to match ImageCapture.OnImageSavedCallback
|
||||
// #onImageSaved()
|
||||
// Begin Signal Custom Code Block
|
||||
void onVideoSaved(@NonNull FileDescriptor file);
|
||||
// End Signal Custom Code Block
|
||||
|
||||
/** Called when an error occurs while attempting to save the video. */
|
||||
void onError(@NonNull VideoCaptureError videoCaptureError, @NonNull String message,
|
||||
void onError(@VideoCaptureError int videoCaptureError, @NonNull String message,
|
||||
@Nullable Throwable cause);
|
||||
}
|
||||
|
||||
|
@ -936,7 +956,6 @@ public class VideoCapture extends UseCase {
|
|||
@RestrictTo(Scope.LIBRARY_GROUP)
|
||||
public static final class Defaults
|
||||
implements ConfigProvider<VideoCaptureConfig> {
|
||||
private static final Handler DEFAULT_HANDLER = new Handler(Looper.getMainLooper());
|
||||
private static final int DEFAULT_VIDEO_FRAME_RATE = 30;
|
||||
/** 8Mb/s the recommend rate for 30fps 1080p */
|
||||
private static final int DEFAULT_BIT_RATE = 8 * 1024 * 1024;
|
||||
|
@ -973,11 +992,12 @@ public class VideoCapture extends UseCase {
|
|||
.setMaxResolution(DEFAULT_MAX_RESOLUTION)
|
||||
.setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
|
||||
|
||||
DEFAULT_CONFIG = builder.build();
|
||||
DEFAULT_CONFIG = builder.getUseCaseConfig();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public VideoCaptureConfig getConfig(LensFacing lensFacing) {
|
||||
public VideoCaptureConfig getConfig(@Nullable CameraInfo cameraInfo) {
|
||||
return DEFAULT_CONFIG;
|
||||
}
|
||||
}
|
||||
|
@ -989,15 +1009,17 @@ public class VideoCapture extends UseCase {
|
|||
public Location location;
|
||||
}
|
||||
|
||||
private final class VideoSavedListenerWrapper implements OnVideoSavedListener {
|
||||
private final class VideoSavedListenerWrapper implements OnVideoSavedCallback {
|
||||
|
||||
@NonNull Executor mExecutor;
|
||||
@NonNull OnVideoSavedListener mOnVideoSavedListener;
|
||||
@NonNull
|
||||
Executor mExecutor;
|
||||
@NonNull
|
||||
OnVideoSavedCallback mOnVideoSavedCallback;
|
||||
|
||||
VideoSavedListenerWrapper(@NonNull Executor executor,
|
||||
@NonNull OnVideoSavedListener onVideoSavedListener) {
|
||||
@NonNull OnVideoSavedCallback onVideoSavedCallback) {
|
||||
mExecutor = executor;
|
||||
mOnVideoSavedListener = onVideoSavedListener;
|
||||
mOnVideoSavedCallback = onVideoSavedCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1005,18 +1027,18 @@ public class VideoCapture extends UseCase {
|
|||
public void onVideoSaved(@NonNull FileDescriptor file) {
|
||||
// End Signal Custom Code Block
|
||||
try {
|
||||
mExecutor.execute(() -> mOnVideoSavedListener.onVideoSaved(file));
|
||||
mExecutor.execute(() -> mOnVideoSavedCallback.onVideoSaved(file));
|
||||
} catch (RejectedExecutionException e) {
|
||||
Log.e(TAG, "Unable to post to the supplied executor.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull VideoCaptureError videoCaptureError, @NonNull String message,
|
||||
public void onError(@VideoCaptureError int videoCaptureError, @NonNull String message,
|
||||
@Nullable Throwable cause) {
|
||||
try {
|
||||
mExecutor.execute(
|
||||
() -> mOnVideoSavedListener.onError(videoCaptureError, message, cause));
|
||||
() -> mOnVideoSavedCallback.onError(videoCaptureError, message, cause));
|
||||
} catch (RejectedExecutionException e) {
|
||||
Log.e(TAG, "Unable to post to the supplied executor.");
|
||||
}
|
||||
|
|
|
@ -56,9 +56,9 @@ public class BasicMegaphoneView extends FrameLayout {
|
|||
this.megaphone = megaphone;
|
||||
this.megaphoneListener = megaphoneListener;
|
||||
|
||||
if (megaphone.getImage() != 0) {
|
||||
if (megaphone.getImageRequest() != null) {
|
||||
image.setVisibility(VISIBLE);
|
||||
image.setImageResource(megaphone.getImage());
|
||||
megaphone.getImageRequest().into(image);
|
||||
} else {
|
||||
image.setVisibility(GONE);
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue