Compare commits

...

36 Commits

Author SHA1 Message Date
Greyson Parrelli
a3176bbb67 Bump version to 5.3.1 2021-01-21 22:52:31 -05:00
Greyson Parrelli
4203dde151 Updated language translations. 2021-01-21 22:52:08 -05:00
Greyson Parrelli
880661710f Fix possible NPE in group list item. 2021-01-21 22:47:30 -05:00
Greyson Parrelli
d844fa0fb5 Fix possible NPE on wallpaper change. 2021-01-21 22:47:30 -05:00
Greyson Parrelli
18ede2e900 Bump version to 5.3.0 2021-01-21 18:29:56 -05:00
Greyson Parrelli
06cc96bee7 Updated language translations. 2021-01-21 18:29:15 -05:00
Greyson Parrelli
43a12d2a81 Refine incognito keyboard setting string. 2021-01-21 18:22:05 -05:00
Michael Crenshaw
0a29ffcf4c Update incognito keyboard copy. 2021-01-21 18:17:35 -05:00
Thore Goebel
9c88532c21 Add ripple to review storage button. 2021-01-21 18:04:56 -05:00
Martin d'Allens
f3450b8f10 Center the terms link on the welcome fragment for long translations. 2021-01-21 18:02:20 -05:00
Alan Evans
a4d56e376f Allow clicking on typer avatar to bring up their bottom sheet details. 2021-01-21 18:02:20 -05:00
ascendingSun
24b5bac589 Fix camera crash when mic permission is granted.
Fixes #10642
2021-01-21 18:02:19 -05:00
Greyson Parrelli
762f17f1c1 Install a new animated sticker pack. 2021-01-21 18:02:19 -05:00
Greyson Parrelli
105c8c9745 Fix issue where forwarded link previews weren't marked uploaded. 2021-01-21 18:02:19 -05:00
Alan Evans
93d99287eb Wallpaper preview size respects device aspect ratio. 2021-01-21 18:02:19 -05:00
Jack Lloyd
ce156c3450 Bump libsignal-client to 0.2.2 2021-01-21 18:02:19 -05:00
Greyson Parrelli
7db16e6156 Add support for an 'About' field on your profile. 2021-01-21 18:02:19 -05:00
Alex Hart
e80033c287 Fix several issues with local expandable pip. 2021-01-21 18:02:19 -05:00
Alex Hart
1553f9b75d Upgrade libphonenumber to v8.12.16 2021-01-21 18:02:19 -05:00
Alex Hart
c244a98962 Finalize wallpaper UX.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
Co-authored-by: Alan Evans <alan@signal.org>
2021-01-21 18:02:19 -05:00
Alan Evans
a8ad1e718e Wallpaper image selection and cropping. 2021-01-21 18:02:19 -05:00
Alex Hart
b5712f4bd1 Improve wallpaper settings screen, conversation rendering.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2021-01-21 18:02:19 -05:00
Greyson Parrelli
6bcb0de43d Add support for persisting wallpaper selection. 2021-01-21 18:02:19 -05:00
Alex Hart
80651d2425 Initial wallpaper settings screens. 2021-01-21 18:02:19 -05:00
Martin d'Allens
46492b8238 Remove ~200 unused English strings. 2021-01-21 18:02:19 -05:00
Alan Evans
1be561543c Fix API19 drawable. 2021-01-21 18:02:19 -05:00
PockelHockel
e430a46e20 Prevent screen rotation during voice record.
Fixes #8276
2021-01-21 18:02:19 -05:00
Alex Hart
8d187c8ba1 Add the ability to forward content to multiple chats at once. 2021-01-21 18:02:19 -05:00
Sicco van Sas
eacf03768f Create FUNDING.yml
This will add a 'sponsor' button at the top of the repository which links to Signals donate page https://signal.org/donate/
2021-01-19 17:09:42 -04:00
Alan Evans
b077c9b4f3 Only schedule one job per constraint set. 2021-01-19 17:07:32 -04:00
Alan Evans
893749fcab Prevent stripping of leading zeros in national numbers. 2021-01-19 17:06:49 -04:00
Alan Evans
848ead5e78 Include an English filter line in the support email body. 2021-01-19 17:06:23 -04:00
Jim Gustafson
9c47acb004 Update to RingRTC v2.8.10 2021-01-19 16:03:37 -05:00
Greyson Parrelli
8ca54bcc7b Create a new manage profile screen. 2021-01-19 10:39:49 -05:00
Greyson Parrelli
7e64d57ba8 Bump version to 5.2.3 2021-01-17 00:17:47 -05:00
Greyson Parrelli
a517fc4e15 Fix NPE in RecipientDatabase. 2021-01-17 00:17:26 -05:00
299 changed files with 16907 additions and 18011 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
custom: https://signal.org/donate/

View File

@@ -61,8 +61,8 @@ protobuf {
}
}
def canonicalVersionCode = 771
def canonicalVersionName = "5.2.2"
def canonicalVersionCode = 774
def canonicalVersionName = "5.3.1"
def postFixSize = 100
def abiPostFix = ['universal' : 0,
@@ -340,11 +340,11 @@ dependencies {
implementation project(':video')
implementation 'org.signal:zkgroup-android:0.7.0'
implementation 'org.whispersystems:signal-client-android:0.1.5'
implementation 'org.whispersystems:signal-client-android:0.2.2'
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
implementation 'org.signal:argon2:13.1@aar'
implementation 'org.signal:ringrtc-android:2.8.9'
implementation 'org.signal:ringrtc-android:2.8.10'
implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'

View File

@@ -159,12 +159,15 @@
<activity android:name=".preferences.MmsPreferencesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".sharing.interstitial.ShareInterstitialActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:windowSoftInputMode="adjustResize" />
<activity android:name=".sharing.ShareActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:taskAffinity=""
android:noHistory="true"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter>
@@ -354,6 +357,16 @@
</intent-filter>
</activity>
<activity android:name=".wallpaper.ChatWallpaperActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:windowSoftInputMode="stateAlwaysHidden">
</activity>
<activity android:name=".wallpaper.ChatWallpaperPreviewActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:windowSoftInputMode="stateAlwaysHidden">
</activity>
<activity android:name=".registration.RegistrationNavigationActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightRegistrationTheme"
@@ -460,6 +473,10 @@
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="stateVisible|adjustResize" />
<activity android:name=".profiles.manage.ManageProfileActivity"
android:theme="@style/TextSecure.LightTheme"
android:windowSoftInputMode="stateVisible|adjustResize" />
<activity android:name=".lock.v2.CreateKbsPinActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="adjustResize"
@@ -535,6 +552,15 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:launchMode="singleTask" />
<activity android:name=".wallpaper.crop.WallpaperImageSelectionActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/TextSecure.FullScreenMedia" />
<activity android:name=".wallpaper.crop.WallpaperCropActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:screenOrientation="portrait"
android:theme="@style/Theme.Signal.WallpaperCropper" />
<service android:enabled="true" android:name=".service.WebRtcCallService"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>

View File

@@ -43,6 +43,7 @@ public final class AppInitialization {
SignalStore.onFirstEverAppLaunch();
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
}
@@ -55,6 +56,7 @@ public final class AppInitialization {
SignalStore.onboarding().clearAll();
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
}
@@ -76,6 +78,7 @@ public final class AppInitialization {
SignalStore.onFirstEverAppLaunch();
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
}

View File

@@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference;
import org.thoughtcrime.securesms.preferences.widgets.UsernamePreference;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.profiles.manage.ManageProfileActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.CachedInflater;
@@ -345,7 +346,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
private class ProfileClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
requireActivity().startActivity(EditProfileActivity.getIntentForUserProfileEdit(preference.getContext()));
requireActivity().startActivity(ManageProfileActivity.getIntent(requireActivity()));
return true;
}
}
@@ -353,7 +354,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
private class UsernameClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
requireActivity().startActivity(EditProfileActivity.getIntentForUsernameEdit(preference.getContext()));
requireActivity().startActivity(ManageProfileActivity.getIntentForUsernameEdit(preference.getContext()));
return true;
}
}

View File

@@ -36,7 +36,8 @@ public interface BindableConversationItem extends Unbindable {
@NonNull Set<ConversationMessage> batchSelected,
@NonNull Recipient recipients,
@Nullable String searchQuery,
boolean pulseMention);
boolean pulseMention,
boolean hasWallpaper);
ConversationMessage getConversationMessage();

View File

@@ -99,7 +99,6 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
private void initializeResources() {
contactsFragment = (ContactSelectionListFragment) getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
contactsFragment.setOnContactSelectedListener(this);
contactsFragment.setOnRefreshListener(this);
}

View File

@@ -110,22 +110,26 @@ public final class ContactSelectionListFragment extends LoggingFragment
public static final String SELECTION_LIMITS = "selection_limits";
public static final String CURRENT_SELECTION = "current_selection";
public static final String HIDE_COUNT = "hide_count";
public static final String CAN_SELECT_SELF = "can_select_self";
public static final String DISPLAY_CHIPS = "display_chips";
private ConstraintLayout constraintLayout;
private TextView emptyText;
private OnContactSelectedListener onContactSelectedListener;
private SwipeRefreshLayout swipeRefresh;
private View showContactsLayout;
private Button showContactsButton;
private TextView showContactsDescription;
private ProgressWheel showContactsProgress;
private String cursorFilter;
private RecyclerView recyclerView;
private RecyclerViewFastScroller fastScroller;
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
private ChipGroup chipGroup;
private HorizontalScrollView chipGroupScrollContainer;
private WarningTextView groupLimit;
private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
private ConstraintLayout constraintLayout;
private TextView emptyText;
private OnContactSelectedListener onContactSelectedListener;
private SwipeRefreshLayout swipeRefresh;
private View showContactsLayout;
private Button showContactsButton;
private TextView showContactsDescription;
private ProgressWheel showContactsProgress;
private String cursorFilter;
private RecyclerView recyclerView;
private RecyclerViewFastScroller fastScroller;
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
private ChipGroup chipGroup;
private HorizontalScrollView chipGroupScrollContainer;
private WarningTextView groupLimit;
@Nullable private FixedViewsAdapter headerAdapter;
@Nullable private FixedViewsAdapter footerAdapter;
@@ -136,6 +140,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
private Set<RecipientId> currentSelection;
private boolean isMulti;
private boolean hideCount;
private boolean canSelectSelf;
@Override
public void onAttach(@NonNull Context context) {
@@ -148,6 +153,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
if (context instanceof ScrollCallback) {
scrollCallback = (ScrollCallback) context;
}
if (context instanceof OnContactSelectedListener) {
onContactSelectedListener = (OnContactSelectedListener) context;
}
if (context instanceof OnSelectionLimitReachedListener) {
onSelectionLimitReachedListener = (OnSelectionLimitReachedListener) context;
}
}
@Override
@@ -217,6 +230,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
hideCount = intent.getBooleanExtra(HIDE_COUNT, false);
selectionLimit = intent.getParcelableExtra(SELECTION_LIMITS);
isMulti = selectionLimit != null;
canSelectSelf = intent.getBooleanExtra(CAN_SELECT_SELF, !isMulti);
if (!isMulti) {
selectionLimit = SelectionLimits.NO_LIMITS;
@@ -295,7 +309,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
recyclerView.setAdapter(concatenateAdapter);
recyclerView.addItemDecoration(new StickyHeaderDecoration(concatenateAdapter, true, true));
recyclerView.addItemDecoration(new StickyHeaderDecoration(concatenateAdapter, true, true, 0));
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
@@ -464,14 +478,18 @@ public final class ContactSelectionListFragment extends LoggingFragment
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber())
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
if (isMulti && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
if (!canSelectSelf && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
return;
}
if (!isMulti || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
if (selectionHardLimitReached()) {
GroupLimitDialog.showHardLimitMessage(requireContext());
if (onSelectionLimitReachedListener != null) {
onSelectionLimitReachedListener.onHardLimitReached(selectionLimit.getHardLimit());
} else {
GroupLimitDialog.showHardLimitMessage(requireContext());
}
return;
}
@@ -489,11 +507,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
if (onContactSelectedListener != null) {
if (onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null)) {
markContactSelected(selected);
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
} else {
markContactSelected(selected);
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
} else {
new AlertDialog.Builder(requireContext())
@@ -507,16 +525,16 @@ public final class ContactSelectionListFragment extends LoggingFragment
if (onContactSelectedListener != null) {
if (onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber())) {
markContactSelected(selectedContact);
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
} else {
markContactSelected(selectedContact);
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
}
} else {
markContactUnselected(selectedContact);
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber());
@@ -611,7 +629,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
chipGroup.addView(chip);
updateGroupLimit(getChipCount());
if (selectionWarningLimitReachedExactly()) {
GroupLimitDialog.showRecommendedLimitMessage(requireContext());
if (onSelectionLimitReachedListener != null) {
onSelectionLimitReachedListener.onSuggestedLimitReached(selectionLimit.getRecommendedLimit());
} else {
GroupLimitDialog.showRecommendedLimitMessage(requireContext());
}
}
}
@@ -633,6 +655,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
private void setChipGroupVisibility(int visibility) {
if (!requireActivity().getIntent().getBooleanExtra(DISPLAY_CHIPS, true)) {
return;
}
TransitionManager.beginDelayedTransition(constraintLayout, new AutoTransition().setDuration(CHIP_GROUP_REVEAL_DURATION_MS));
ConstraintSet constraintSet = new ConstraintSet();
@@ -641,10 +667,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
constraintSet.applyTo(constraintLayout);
}
public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) {
this.onContactSelectedListener = onContactSelectedListener;
}
public void setOnRefreshListener(SwipeRefreshLayout.OnRefreshListener onRefreshListener) {
this.swipeRefresh.setOnRefreshListener(onRefreshListener);
}
@@ -660,6 +682,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
void onContactDeselected(Optional<RecipientId> recipientId, String number);
}
public interface OnSelectionLimitReachedListener {
void onSuggestedLimitReached(int limit);
void onHardLimitReached(int limit);
}
public interface ListCallback {
void onInvite();
void onNewGroup(boolean forceV1);

View File

@@ -107,7 +107,6 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
inviteText.setText(getString(R.string.InviteActivity_lets_switch_to_signal, getString(R.string.install_url)));
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
contactsFragment.setOnContactSelectedListener(this);
smsCancelButton.setOnClickListener(new SmsCancelClickListener());
smsSendButton.setOnClickListener(new SmsSendClickListener());
contactFilter.setOnFilterChangedListener(new ContactFilterChangedListener());

View File

@@ -124,8 +124,6 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
ContactSelectionListFragment fragment = new ContactSelectionListFragment();
Intent intent = getIntent();
fragment.setOnContactSelectedListener(this);
intent.putExtra(ContactSelectionListFragment.REFRESHABLE, false);
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, 1);
intent.putExtra(ContactSelectionListFragment.HIDE_COUNT, true);

View File

@@ -12,6 +12,7 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -130,6 +131,20 @@ public class ConversationItemFooter extends LinearLayout {
presentDeliveryStatus(messageRecord);
}
public void enableBubbleBackground(@DrawableRes int drawableRes, @Nullable Integer tint) {
setBackgroundResource(drawableRes);
if (tint != null) {
getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
} else {
getBackground().clearColorFilter();
}
}
public void disableBubbleBackground() {
setBackground(null);
}
private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
dateView.forceLayout();
if (messageRecord.isFailed()) {

View File

@@ -44,7 +44,7 @@ public class ConversationTypingView extends LinearLayout {
bubble.getBackground().setColorFilter(typist.getColor().toConversationColor(getContext()), PorterDuff.Mode.MULTIPLY);
if (isGroupThread) {
avatar.setAvatar(glideRequests, typist, false);
avatar.setAvatar(glideRequests, typist, true);
avatar.setVisibility(VISIBLE);
} else {
avatar.setVisibility(GONE);

View File

@@ -0,0 +1,47 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.Guideline;
import org.thoughtcrime.securesms.R;
public class InsetAwareConstraintLayout extends ConstraintLayout {
public InsetAwareConstraintLayout(@NonNull Context context) {
super(context);
}
public InsetAwareConstraintLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public InsetAwareConstraintLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public InsetAwareConstraintLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected boolean fitSystemWindows(Rect insets) {
Guideline statusBarGuideline = findViewById(R.id.status_bar_guideline);
Guideline navigationBarGuideline = findViewById(R.id.navigation_bar_guideline);
if (statusBarGuideline != null) {
statusBarGuideline.setGuidelineBegin(insets.top);
}
if (navigationBarGuideline != null) {
navigationBarGuideline.setGuidelineEnd(insets.bottom);
}
return true;
}
}

View File

@@ -8,7 +8,6 @@ import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.TextView;
@@ -17,6 +16,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;
public class LabeledEditText extends FrameLayout implements View.OnFocusChangeListener {
@@ -94,16 +94,6 @@ public class LabeledEditText extends FrameLayout implements View.OnFocusChangeLi
}
public void focusAndMoveCursorToEndAndOpenKeyboard() {
input.requestFocus();
int numberLength = getText().length();
input.setSelection(numberLength, numberLength);
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(input, InputMethodManager.SHOW_IMPLICIT);
if (!imm.isAcceptingText()) {
imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.HIDE_IMPLICIT_ONLY);
}
ViewUtil.focusAndMoveCursorToEndAndOpenKeyboard(input);
}
}

View File

@@ -0,0 +1,45 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.util.AttributeSet;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
/**
* Selection aware {@link EmojiEditText}. This view allows the developer to provide an
* {@link OnSelectionChangedListener} that will be notified when the selection is changed.
*/
public class SelectionAwareEmojiEditText extends EmojiEditText {
private OnSelectionChangedListener onSelectionChangedListener;
public SelectionAwareEmojiEditText(Context context) {
super(context);
}
public SelectionAwareEmojiEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SelectionAwareEmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setOnSelectionChangedListener(@Nullable OnSelectionChangedListener onSelectionChangedListener) {
this.onSelectionChangedListener = onSelectionChangedListener;
}
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
if (onSelectionChangedListener != null) {
onSelectionChangedListener.onSelectionChanged(selStart, selEnd);
}
super.onSelectionChanged(selStart, selEnd);
}
public interface OnSelectionChangedListener {
void onSelectionChanged(int selStart, int selEnd);
}
}

View File

@@ -30,7 +30,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
{
private static final KeyEvent DELETE_KEY_EVENT = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
private static final String RECENT_STORAGE_KEY = "pref_recent_emoji2";
public static final String RECENT_STORAGE_KEY = "pref_recent_emoji2";
private final Context context;
private final List<EmojiPageModel> models;

View File

@@ -1,7 +1,14 @@
package org.thoughtcrime.securesms.components.emoji;
import androidx.annotation.NonNull;
import android.content.Context;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.util.StringUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.Pair;
import java.util.HashMap;
@@ -72,4 +79,15 @@ public final class EmojiUtil {
String canonical = VARIATION_MAP.get(emoji);
return canonical != null ? canonical : emoji;
}
/**
* Converts the provided emoji string into a single drawable, if possible.
*/
public static @Nullable Drawable convertToDrawable(@NonNull Context context, @Nullable String emoji) {
if (Util.isEmpty(emoji)) {
return null;
} else {
return EmojiProvider.getInstance(context).getEmojiDrawable(emoji);
}
}
}

View File

@@ -43,6 +43,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.google.android.material.button.MaterialButton;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.ResizeAnimation;
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
@@ -330,7 +331,6 @@ public class WebRtcCallView extends FrameLayout {
}
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant, @NonNull CallParticipant focusedParticipant) {
smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
largeLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
smallLocalRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
@@ -346,11 +346,12 @@ public class WebRtcCallView extends FrameLayout {
if (state == WebRtcLocalRenderState.EXPANDED) {
expandPip(localCallParticipant, focusedParticipant);
return;
} else if (state == WebRtcLocalRenderState.SMALL_RECTANGLE && pictureInPictureExpansionHelper.isExpandedOrExpanding()) {
} else if ((state == WebRtcLocalRenderState.SMALL_RECTANGLE || state == WebRtcLocalRenderState.GONE) && pictureInPictureExpansionHelper.isExpandedOrExpanding()) {
shrinkPip(localCallParticipant);
return;
} else {
smallLocalRender.setCallParticipant(localCallParticipant);
smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
}
switch (state) {
@@ -597,11 +598,14 @@ public class WebRtcCallView extends FrameLayout {
@Override
public void onPictureInPictureExpanded() {
largeLocalRenderFrame.setVisibility(View.VISIBLE);
largeLocalRenderNoVideo.setVisibility(View.GONE);
largeLocalRenderNoVideoAvatar.setVisibility(View.GONE);
}
@Override
public void onPictureInPictureNotVisible() {
smallLocalRender.setCallParticipant(focusedParticipant);
smallLocalRender.setMirror(false);
}
@Override
@@ -626,6 +630,11 @@ public class WebRtcCallView extends FrameLayout {
@Override
public void onPictureInPictureNotVisible() {
smallLocalRender.setCallParticipant(localCallParticipant);
smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
if (!localCallParticipant.isVideoEnabled()) {
smallLocalRenderFrame.setVisibility(View.GONE);
}
}
@Override

View File

@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.Pair;
@@ -42,6 +43,7 @@ public class ContactRepository {
static final String NUMBER_TYPE_COLUMN = "number_type";
static final String LABEL_COLUMN = "label";
static final String CONTACT_TYPE_COLUMN = "contact_type";
static final String ABOUT_COLUMN = "about";
static final int NORMAL_TYPE = 0;
static final int PUSH_TYPE = 1;
@@ -52,18 +54,18 @@ public class ContactRepository {
/** Maps the recipient results to the legacy contact column names */
private static final List<Pair<String, ValueMapper>> SEARCH_CURSOR_MAPPERS = new ArrayList<Pair<String, ValueMapper>>() {{
add(new Pair<>(ID_COLUMN, cursor -> cursor.getLong(cursor.getColumnIndexOrThrow(RecipientDatabase.ID))));
add(new Pair<>(ID_COLUMN, cursor -> CursorUtil.requireLong(cursor, RecipientDatabase.ID)));
add(new Pair<>(NAME_COLUMN, cursor -> {
String system = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.SYSTEM_DISPLAY_NAME));
String profile = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.SEARCH_PROFILE_NAME));
String system = CursorUtil.requireString(cursor, RecipientDatabase.SYSTEM_DISPLAY_NAME);
String profile = CursorUtil.requireString(cursor, RecipientDatabase.SEARCH_PROFILE_NAME);
return Util.getFirstNonEmpty(system, profile);
}));
add(new Pair<>(NUMBER_COLUMN, cursor -> {
String phone = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.PHONE));
String email = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.EMAIL));
String phone = CursorUtil.requireString(cursor, RecipientDatabase.PHONE);
String email = CursorUtil.requireString(cursor, RecipientDatabase.EMAIL);
if (phone != null) {
phone = PhoneNumberFormatter.prettyPrint(phone);
@@ -72,14 +74,31 @@ public class ContactRepository {
return Util.getFirstNonEmpty(phone, email);
}));
add(new Pair<>(NUMBER_TYPE_COLUMN, cursor -> cursor.getInt(cursor.getColumnIndexOrThrow(RecipientDatabase.SYSTEM_PHONE_TYPE))));
add(new Pair<>(NUMBER_TYPE_COLUMN, cursor -> CursorUtil.requireInt(cursor, RecipientDatabase.SYSTEM_PHONE_TYPE)));
add(new Pair<>(LABEL_COLUMN, cursor -> cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.SYSTEM_PHONE_LABEL))));
add(new Pair<>(LABEL_COLUMN, cursor -> CursorUtil.requireString(cursor, RecipientDatabase.SYSTEM_PHONE_LABEL)));
add(new Pair<>(CONTACT_TYPE_COLUMN, cursor -> {
int registered = cursor.getInt(cursor.getColumnIndexOrThrow(RecipientDatabase.REGISTERED));
int registered = CursorUtil.requireInt(cursor, RecipientDatabase.REGISTERED);
return registered == RecipientDatabase.RegisteredState.REGISTERED.getId() ? PUSH_TYPE : NORMAL_TYPE;
}));
add(new Pair<>(ABOUT_COLUMN, cursor -> {
String aboutEmoji = CursorUtil.requireString(cursor, RecipientDatabase.ABOUT_EMOJI);
String about = CursorUtil.requireString(cursor, RecipientDatabase.ABOUT);
if (aboutEmoji != null) {
if (about != null) {
return aboutEmoji + " " + about;
} else {
return aboutEmoji;
}
} else if (about != null) {
return about;
} else {
return "";
}
}));
}};
public ContactRepository(@NonNull Context context) {
@@ -106,7 +125,7 @@ public class ContactRepository {
if (shouldAdd) {
MatrixCursor selfCursor = new MatrixCursor(RecipientDatabase.SEARCH_PROJECTION_NAMES);
selfCursor.addRow(new Object[]{ self.getId().serialize(), noteToSelfTitle, null, self.getE164().or(""), self.getEmail().orNull(), null, -1, RecipientDatabase.RegisteredState.REGISTERED.getId(), noteToSelfTitle });
selfCursor.addRow(new Object[]{ self.getId().serialize(), noteToSelfTitle, self.getE164().or(""), self.getEmail().orNull(), null, -1, RecipientDatabase.RegisteredState.REGISTERED.getId(), self.getAbout(), self.getAboutEmoji(), noteToSelfTitle, noteToSelfTitle });
cursor = cursor == null ? selfCursor : new MergeCursor(new Cursor[]{ cursor, selfCursor });
}

View File

@@ -40,6 +40,7 @@ import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.ViewHolde
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration.StickyHeaderAdapter;
import org.thoughtcrime.securesms.util.Util;
@@ -97,7 +98,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
super(itemView);
}
public abstract void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, int color, boolean checkboxVisible);
public abstract void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, String about, int color, boolean checkboxVisible);
public abstract void unbind(@NonNull GlideRequests glideRequests);
public abstract void setChecked(boolean checked);
public abstract void setEnabled(boolean enabled);
@@ -117,8 +118,8 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
return (ContactSelectionListItem) itemView;
}
public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, int color, boolean checkBoxVisible) {
getView().set(glideRequests, recipientId, type, name, number, label, color, checkBoxVisible);
public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, String about, int color, boolean checkBoxVisible) {
getView().set(glideRequests, recipientId, type, name, number, label, about, color, checkBoxVisible);
}
@Override
@@ -147,7 +148,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
}
@Override
public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, int color, boolean checkboxVisible) {
public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, String about, int color, boolean checkboxVisible) {
this.label.setText(name);
}
@@ -204,13 +205,14 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
@Override
public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
String rawId = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.ID_COLUMN));
String rawId = CursorUtil.requireString(cursor, ContactRepository.ID_COLUMN);
RecipientId id = rawId != null ? RecipientId.from(rawId) : null;
int contactType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactRepository.CONTACT_TYPE_COLUMN));
String name = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NAME_COLUMN ));
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_COLUMN));
int numberType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_TYPE_COLUMN ));
String label = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.LABEL_COLUMN ));
int contactType = CursorUtil.requireInt(cursor, ContactRepository.CONTACT_TYPE_COLUMN);
String name = CursorUtil.requireString(cursor, ContactRepository.NAME_COLUMN);
String number = CursorUtil.requireString(cursor, ContactRepository.NUMBER_COLUMN);
int numberType = CursorUtil.requireInt(cursor, ContactRepository.NUMBER_TYPE_COLUMN);
String about = CursorUtil.requireString(cursor, ContactRepository.ABOUT_COLUMN);
String label = CursorUtil.requireString(cursor, ContactRepository.LABEL_COLUMN);
String labelText = ContactsContract.CommonDataKinds.Phone.getTypeLabel(getContext().getResources(),
numberType, label).toString();
@@ -220,7 +222,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
boolean currentContact = currentContacts.contains(id);
viewHolder.unbind(glideRequests);
viewHolder.bind(glideRequests, id, contactType, name, number, labelText, color, multiSelect || currentContact);
viewHolder.bind(glideRequests, id, contactType, name, number, labelText, about, color, multiSelect || currentContact);
viewHolder.setEnabled(true);
if (currentContact) {
@@ -239,10 +241,10 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
throw new AssertionError();
}
String rawId = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.ID_COLUMN));
String rawId = CursorUtil.requireString(cursor, ContactRepository.ID_COLUMN);
RecipientId id = rawId != null ? RecipientId.from(rawId) : null;
int numberType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_TYPE_COLUMN));
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_COLUMN));
int numberType = CursorUtil.requireInt(cursor, ContactRepository.NUMBER_TYPE_COLUMN);
String number = CursorUtil.requireString(cursor, ContactRepository.NUMBER_COLUMN);
viewHolder.setEnabled(true);
@@ -258,7 +260,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
@Override
public int getItemViewType(@NonNull Cursor cursor) {
if (cursor.getInt(cursor.getColumnIndexOrThrow(ContactRepository.CONTACT_TYPE_COLUMN)) == ContactRepository.DIVIDER_TYPE) {
if (CursorUtil.requireInt(cursor, ContactRepository.CONTACT_TYPE_COLUMN) == ContactRepository.DIVIDER_TYPE) {
return VIEW_TYPE_DIVIDER;
} else {
return VIEW_TYPE_CONTACT;
@@ -266,12 +268,12 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
}
@Override
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position) {
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position, int type) {
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.contact_selection_recyclerview_header, parent, false));
}
@Override
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) {
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position, int type) {
((TextView)viewHolder.itemView).setText(getSpannedHeaderString(position));
}
@@ -317,7 +319,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
}
Cursor cursor = getCursorAtPositionOrThrow(position);
String letter = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NAME_COLUMN));
String letter = CursorUtil.requireString(cursor, ContactRepository.NAME_COLUMN);
if (letter != null) {
letter = letter.trim();

View File

@@ -19,6 +19,7 @@ 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.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -65,6 +66,7 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
String name,
String number,
String label,
String about,
int color,
boolean checkboxVisible)
{
@@ -87,7 +89,7 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
this.numberView.setTextColor(color);
this.contactPhotoImage.setAvatar(glideRequests, recipientSnapshot, false);
setText(recipientSnapshot, type, name, number, label);
setText(recipientSnapshot, type, name, number, label, about);
this.checkBox.setVisibility(checkboxVisible ? View.VISIBLE : View.GONE);
}
@@ -110,7 +112,7 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
}
@SuppressLint("SetTextI18n")
private void setText(@Nullable Recipient recipient, int type, String name, String number, String label) {
private void setText(@Nullable Recipient recipient, int type, String name, String number, String label, @Nullable String about) {
if (number == null || number.isEmpty()) {
this.nameView.setEnabled(false);
this.numberView.setText("");
@@ -120,7 +122,7 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
this.numberView.setText(getGroupMemberCount(recipient));
this.labelView.setVisibility(View.GONE);
} else if (type == ContactRepository.PUSH_TYPE) {
this.numberView.setText(number);
this.numberView.setText(!Util.isEmpty(about) ? about : number);
this.nameView.setEnabled(true);
this.labelView.setVisibility(View.GONE);
} else if (type == ContactRepository.NEW_USERNAME_TYPE) {
@@ -129,7 +131,7 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
this.labelView.setText(label);
this.labelView.setVisibility(View.VISIBLE);
} else {
this.numberView.setText(number);
this.numberView.setText(!Util.isEmpty(about) ? about : number);
this.nameView.setEnabled(true);
this.labelView.setText(label != null && !label.equals("null") ? label : "");
this.labelView.setVisibility(View.VISIBLE);

View File

@@ -71,7 +71,8 @@ public class ContactsCursorLoader extends CursorLoader {
ContactRepository.NUMBER_COLUMN,
ContactRepository.NUMBER_TYPE_COLUMN,
ContactRepository.LABEL_COLUMN,
ContactRepository.CONTACT_TYPE_COLUMN};
ContactRepository.CONTACT_TYPE_COLUMN,
ContactRepository.ABOUT_COLUMN};
private static final int RECENT_CONVERSATION_MAX = 25;
@@ -212,7 +213,8 @@ public class ContactsCursorLoader extends CursorLoader {
"",
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"",
ContactRepository.DIVIDER_TYPE });
ContactRepository.DIVIDER_TYPE,
"" });
return recentsHeader;
}
@@ -223,7 +225,8 @@ public class ContactsCursorLoader extends CursorLoader {
"",
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"",
ContactRepository.DIVIDER_TYPE });
ContactRepository.DIVIDER_TYPE,
"" });
return contactsHeader;
}
@@ -234,7 +237,8 @@ public class ContactsCursorLoader extends CursorLoader {
"",
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"",
ContactRepository.DIVIDER_TYPE });
ContactRepository.DIVIDER_TYPE,
"" });
return groupHeader;
}
@@ -245,7 +249,8 @@ public class ContactsCursorLoader extends CursorLoader {
"",
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"",
ContactRepository.DIVIDER_TYPE });
ContactRepository.DIVIDER_TYPE,
"" });
return contactsHeader;
}
@@ -256,7 +261,8 @@ public class ContactsCursorLoader extends CursorLoader {
"",
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"",
ContactRepository.DIVIDER_TYPE });
ContactRepository.DIVIDER_TYPE,
"" });
return contactsHeader;
}
@@ -281,7 +287,8 @@ public class ContactsCursorLoader extends CursorLoader {
stringId,
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"",
ContactRepository.RECENT_TYPE });
ContactRepository.RECENT_TYPE,
recipient.getCombinedAboutAndEmoji() });
}
}
return recentConversations;
@@ -316,7 +323,8 @@ public class ContactsCursorLoader extends CursorLoader {
groupRecord.getId(),
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
"",
ContactRepository.NORMAL_TYPE });
ContactRepository.NORMAL_TYPE,
"" });
}
}
return groupContacts;
@@ -329,7 +337,8 @@ public class ContactsCursorLoader extends CursorLoader {
filter,
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
"\u21e2",
ContactRepository.NEW_PHONE_TYPE});
ContactRepository.NEW_PHONE_TYPE,
"" });
return newNumberCursor;
}
@@ -340,7 +349,8 @@ public class ContactsCursorLoader extends CursorLoader {
filter,
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
"\u21e2",
ContactRepository.NEW_USERNAME_TYPE});
ContactRepository.NEW_USERNAME_TYPE,
"" });
return cursor;
}
@@ -368,7 +378,8 @@ public class ContactsCursorLoader extends CursorLoader {
cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_COLUMN)),
cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_TYPE_COLUMN)),
cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.LABEL_COLUMN)),
ContactRepository.NORMAL_TYPE});
ContactRepository.NORMAL_TYPE,
"" });
}
}
Log.i(TAG, "filterNonPushContacts() -> " + (System.currentTimeMillis() - startMillis) + "ms");

View File

@@ -24,6 +24,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.ShortcutManager;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
@@ -60,6 +61,7 @@ import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
@@ -200,7 +202,7 @@ import org.thoughtcrime.securesms.messagerequests.MessageRequestState;
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel;
import org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView;
import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType;
import org.thoughtcrime.securesms.mms.SlideFactory.MediaType;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GifSlide;
@@ -216,6 +218,7 @@ import org.thoughtcrime.securesms.mms.QuoteId;
import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.mms.SlideFactory;
import org.thoughtcrime.securesms.mms.StickerSlide;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
@@ -256,6 +259,7 @@ import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.MessageUtil;
@@ -265,6 +269,7 @@ import org.thoughtcrime.securesms.util.SmsUtil;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.TextSecurePreferences.MediaKeyboardMode;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.WindowUtil;
@@ -273,6 +278,8 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.views.Stub;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperDimLevelUtil;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -373,6 +380,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
private Stub<View> mentionsSuggestions;
private MaterialButton joinGroupCallButton;
private boolean callingTooltipShown;
private ImageView wallpaper;
private View wallpaperDim;
private LinkPreviewViewModel linkPreviewViewModel;
private ConversationSearchViewModel searchViewModel;
@@ -386,8 +395,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
private LiveRecipient recipient;
private long threadId;
private int distributionType;
private int reactWithAnyEmojiStartPage;
private boolean isSecureText;
private int reactWithAnyEmojiStartPage = -1;
private boolean isDefaultSms = true;
private boolean isMmsEnabled = true;
private boolean isSecurityInitialized = false;
@@ -412,18 +421,22 @@ public class ConversationActivity extends PassphraseRequiredActivity
return;
}
new FullscreenHelper(this).showSystemUI();
ConversationIntents.Args args = ConversationIntents.Args.from(getIntent());
reportShortcutLaunch(args.getRecipientId());
setContentView(R.layout.conversation_activity);
getWindow().getDecorView().setBackgroundResource(R.color.signal_background_primary);
WindowUtil.setLightNavigationBar(getWindow());
fragment = initFragment(R.id.fragment_content, new ConversationFragment(), dynamicLanguage.getCurrentLocale());
initializeReceivers();
initializeActionBar();
initializeViews();
updateWallpaper(args.getWallpaper());
initializeResources(args);
initializeLinkPreviewObserver();
initializeSearchObserver();
@@ -475,7 +488,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
return;
}
reactWithAnyEmojiStartPage = 0;
reactWithAnyEmojiStartPage = -1;
if (!Util.isEmpty(composeText) || attachmentManager.isAttachmentPresent() || inputPanel.getQuote().isPresent()) {
saveDraft();
attachmentManager.clear(glideRequests, false);
@@ -612,10 +625,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
switch (reqCode) {
case PICK_DOCUMENT:
setMedia(data.getData(), MediaType.DOCUMENT);
setMedia(data.getData(), SlideFactory.MediaType.DOCUMENT);
break;
case PICK_AUDIO:
setMedia(data.getData(), MediaType.AUDIO);
setMedia(data.getData(), SlideFactory.MediaType.AUDIO);
break;
case PICK_CONTACT:
if (isSecureText && !isSmsForced()) {
@@ -655,7 +668,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
break;
case PICK_GIF:
setMedia(data.getData(),
MediaType.GIF,
SlideFactory.MediaType.GIF,
data.getIntExtra(GiphyActivity.EXTRA_WIDTH, 0),
data.getIntExtra(GiphyActivity.EXTRA_HEIGHT, 0),
data.getBooleanExtra(GiphyActivity.EXTRA_BORDERLESS, false));
@@ -732,7 +745,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
reactWithAnyEmojiStartPage = savedInstanceState.getInt(STATE_REACT_WITH_ANY_PAGE, 0);
reactWithAnyEmojiStartPage = savedInstanceState.getInt(STATE_REACT_WITH_ANY_PAGE, -1);
}
private void setVisibleThread(long threadId) {
@@ -766,7 +779,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
getContentResolver().delete(attachmentManager.getCaptureUri(), null, null);
setMedia(mediaUri, MediaType.IMAGE);
setMedia(mediaUri, SlideFactory.MediaType.IMAGE);
} catch (IOException ioe) {
Log.w(TAG, "Could not handle public image", ioe);
}
@@ -1470,7 +1483,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
final CharSequence draftText = args.getDraftText();
final Uri draftMedia = getIntent().getData();
final String draftContentType = getIntent().getType();
final MediaType draftMediaType = MediaType.from(draftContentType);
final MediaType draftMediaType = SlideFactory.MediaType.from(draftContentType);
final List<Media> mediaList = args.getMedia();
final StickerLocator stickerLocator = args.getStickerLocator();
final boolean borderless = args.isBorderless();
@@ -1652,13 +1665,13 @@ public class ConversationActivity extends PassphraseRequiredActivity
attachmentManager.setLocation(SignalPlace.deserialize(draft.getValue()), getCurrentMediaConstraints()).addListener(listener);
break;
case Draft.IMAGE:
setMedia(Uri.parse(draft.getValue()), MediaType.IMAGE).addListener(listener);
setMedia(Uri.parse(draft.getValue()), SlideFactory.MediaType.IMAGE).addListener(listener);
break;
case Draft.AUDIO:
setMedia(Uri.parse(draft.getValue()), MediaType.AUDIO).addListener(listener);
setMedia(Uri.parse(draft.getValue()), SlideFactory.MediaType.AUDIO).addListener(listener);
break;
case Draft.VIDEO:
setMedia(Uri.parse(draft.getValue()), MediaType.VIDEO).addListener(listener);
setMedia(Uri.parse(draft.getValue()), SlideFactory.MediaType.VIDEO).addListener(listener);
break;
case Draft.QUOTE:
SettableFuture<Boolean> quoteResult = new SettableFuture<>();
@@ -1908,6 +1921,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
messageRequestBottomView = findViewById(R.id.conversation_activity_message_request_bottom_bar);
reactionOverlay = findViewById(R.id.conversation_reaction_scrubber);
mentionsSuggestions = ViewUtil.findStubById(this, R.id.conversation_mention_suggestions_stub);
wallpaper = findViewById(R.id.conversation_wallpaper);
wallpaperDim = findViewById(R.id.conversation_wallpaper_dim);
ImageButton quickCameraToggle = findViewById(R.id.quick_camera_toggle);
ImageButton inlineAttachmentButton = findViewById(R.id.inline_attachment_button);
@@ -1974,6 +1989,18 @@ public class ConversationActivity extends PassphraseRequiredActivity
joinGroupCallButton.setOnClickListener(v -> handleVideo(getRecipient()));
}
private void updateWallpaper(@Nullable ChatWallpaper chatWallpaper) {
Log.d(TAG, "Setting wallpaper.");
if (chatWallpaper != null) {
chatWallpaper.loadInto(wallpaper);
ChatWallpaperDimLevelUtil.applyDimLevelForNightMode(wallpaperDim, chatWallpaper);
} else {
wallpaper.setImageDrawable(null);
wallpaperDim.setVisibility(View.GONE);
}
fragment.onWallpaperChanged(chatWallpaper);
}
protected void initializeActionBar() {
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
@@ -2081,6 +2108,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
this.viewModel = ViewModelProviders.of(this, new ConversationViewModel.Factory()).get(ConversationViewModel.class);
this.viewModel.setArgs(args);
this.viewModel.getWallpaper().observe(this, this::updateWallpaper);
}
private void initializeGroupViewModel() {
@@ -2234,6 +2262,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
reactWithAnyEmojiStartPage = page;
}
@Override
public void onReactWithAnyEmojiSelected(@NonNull String emoji) {
}
@Override
public void onSearchMoveUpPressed() {
searchViewModel.onMoveUp();
@@ -2335,10 +2367,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
return new SettableFuture<>(false);
}
if (MediaType.VCARD.equals(mediaType) && isSecureText) {
if (SlideFactory.MediaType.VCARD.equals(mediaType) && isSecureText) {
openContactShareEditor(uri);
return new SettableFuture<>(false);
} else if (MediaType.IMAGE.equals(mediaType) || MediaType.GIF.equals(mediaType) || MediaType.VIDEO.equals(mediaType)) {
} else if (SlideFactory.MediaType.IMAGE.equals(mediaType) || SlideFactory.MediaType.GIF.equals(mediaType) || SlideFactory.MediaType.VIDEO.equals(mediaType)) {
String mimeType = MediaUtil.getMimeType(this, uri);
if (mimeType == null) {
mimeType = mediaType.toFallbackMimeType();
@@ -2910,6 +2942,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
vibrator.vibrate(20);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
audioRecorder.startRecording();
}
@@ -2926,6 +2959,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
vibrator.vibrate(20);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
ListenableFuture<Pair<Uri, Long>> future = audioRecorder.stopRecording();
future.addListener(new ListenableFuture.Listener<Pair<Uri, Long>>() {
@@ -3033,9 +3067,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
() -> getKeyboardImageDetails(uri),
details -> sendKeyboardImage(uri, contentType, details));
} else if (MediaUtil.isVideoType(contentType)) {
setMedia(uri, MediaType.VIDEO);
setMedia(uri, SlideFactory.MediaType.VIDEO);
} else if (MediaUtil.isAudioType(contentType)) {
setMedia(uri, MediaType.AUDIO);
setMedia(uri, SlideFactory.MediaType.AUDIO);
}
}
@@ -3536,7 +3570,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
private void sendKeyboardImage(@NonNull Uri uri, @NonNull String contentType, @Nullable KeyboardImageDetails details) {
if (details == null || !details.hasTransparency) {
setMedia(uri, Objects.requireNonNull(MediaType.from(contentType)));
setMedia(uri, Objects.requireNonNull(SlideFactory.MediaType.from(contentType)));
return;
}

View File

@@ -23,6 +23,7 @@ import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.AnyThread;
import androidx.annotation.DrawableRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
@@ -72,6 +73,10 @@ public class ConversationAdapter
private static final String TAG = Log.tag(ConversationAdapter.class);
public static final int HEADER_TYPE_POPOVER_DATE = 1;
public static final int HEADER_TYPE_INLINE_DATE = 2;
public static final int HEADER_TYPE_LAST_SEEN = 3;
private static final int MESSAGE_TYPE_OUTGOING_MULTIMEDIA = 0;
private static final int MESSAGE_TYPE_OUTGOING_TEXT = 1;
private static final int MESSAGE_TYPE_INCOMING_MULTIMEDIA = 2;
@@ -101,6 +106,7 @@ public class ConversationAdapter
private View headerView;
private View footerView;
private PagingController pagingController;
private boolean hasWallpaper;
ConversationAdapter(@NonNull LifecycleOwner lifecycleOwner,
@NonNull GlideRequests glideRequests,
@@ -131,6 +137,7 @@ public class ConversationAdapter
this.releasedFastRecords = new HashSet<>();
this.calendar = Calendar.getInstance();
this.digest = getMessageDigestOrThrow();
this.hasWallpaper = recipient.hasWallpaper();
setHasStableIds(true);
}
@@ -241,7 +248,8 @@ public class ConversationAdapter
selected,
recipient,
searchQuery,
conversationMessage == recordToPulse);
conversationMessage == recordToPulse,
hasWallpaper);
if (conversationMessage == recordToPulse) {
recordToPulse = null;
@@ -288,14 +296,28 @@ public class ConversationAdapter
}
@Override
public StickyHeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position) {
public StickyHeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position, int type) {
return new StickyHeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.conversation_item_header, parent, false));
}
@Override
public void onBindHeaderViewHolder(StickyHeaderViewHolder viewHolder, int position) {
public void onBindHeaderViewHolder(StickyHeaderViewHolder viewHolder, int position, int type) {
ConversationMessage conversationMessage = Objects.requireNonNull(getItem(position));
viewHolder.setText(DateUtils.getRelativeDate(viewHolder.itemView.getContext(), locale, conversationMessage.getMessageRecord().getDateReceived()));
if (type == HEADER_TYPE_POPOVER_DATE) {
if (hasWallpaper) {
viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_8);
} else {
viewHolder.setBackgroundRes(R.drawable.sticky_date_header_background);
}
} else if (type == HEADER_TYPE_INLINE_DATE) {
if (hasWallpaper) {
viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_8);
} else {
viewHolder.clearBackground();
}
}
}
public @Nullable ConversationMessage getItem(int position) {
@@ -325,6 +347,12 @@ public class ConversationAdapter
void onBindLastSeenViewHolder(StickyHeaderViewHolder viewHolder, int position) {
viewHolder.setText(viewHolder.itemView.getContext().getResources().getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, (position + 1), (position + 1)));
if (hasWallpaper) {
viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_8);
} else {
viewHolder.clearBackground();
}
}
boolean hasNoConversationMessages() {
@@ -421,6 +449,17 @@ public class ConversationAdapter
notifyDataSetChanged();
}
/**
* Lets the adapter know that the wallpaper state has changed.
*/
void onHasWallpaperChanged(boolean hasWallpaper) {
if (this.hasWallpaper != hasWallpaper) {
Log.d(TAG, "Resetting adapter due to wallpaper change.");
this.hasWallpaper = hasWallpaper;
notifyDataSetChanged();
}
}
/**
* Adds a record to a memory cache to allow it to be rendered immediately, as opposed to waiting
* for a database change.
@@ -563,6 +602,14 @@ public class ConversationAdapter
public void setText(CharSequence text) {
textView.setText(text);
}
public void setBackgroundRes(@DrawableRes int resId) {
textView.setBackgroundResource(resId);
}
public void clearBackground() {
textView.setBackground(null);
}
}
private static class HeaderFooterViewHolder extends RecyclerView.ViewHolder {

View File

@@ -21,6 +21,7 @@ public class ConversationBannerView extends ConstraintLayout {
private AvatarImageView contactAvatar;
private TextView contactTitle;
private TextView contactAbout;
private TextView contactSubtitle;
private TextView contactDescription;
@@ -39,6 +40,7 @@ public class ConversationBannerView extends ConstraintLayout {
contactAvatar = findViewById(R.id.message_request_avatar);
contactTitle = findViewById(R.id.message_request_title);
contactAbout = findViewById(R.id.message_request_about);
contactSubtitle = findViewById(R.id.message_request_subtitle);
contactDescription = findViewById(R.id.message_request_description);
@@ -53,6 +55,11 @@ public class ConversationBannerView extends ConstraintLayout {
contactTitle.setText(title);
}
public void setAbout(@Nullable String about) {
contactAbout.setText(about);
contactAbout.setVisibility(TextUtils.isEmpty(about) ? GONE : VISIBLE);
}
public void setSubtitle(@Nullable CharSequence subtitle) {
contactSubtitle.setText(subtitle);
contactSubtitle.setVisibility(TextUtils.isEmpty(subtitle) ? GONE : VISIBLE);
@@ -62,6 +69,14 @@ public class ConversationBannerView extends ConstraintLayout {
contactDescription.setText(description);
}
public void showBackgroundBubble(boolean enabled) {
if (enabled) {
setBackgroundResource(R.drawable.wallpaper_bubble_background_12);
} else {
setBackground(null);
}
}
public void hideSubtitle() {
contactSubtitle.setVisibility(View.GONE);
}

View File

@@ -126,6 +126,7 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.HtmlUtil;
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
@@ -141,6 +142,7 @@ import org.thoughtcrime.securesms.util.WindowUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.views.AdaptiveActionsToolbar;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
@@ -171,7 +173,7 @@ public class ConversationFragment extends LoggingFragment {
private Locale locale;
private RecyclerView list;
private RecyclerView.ItemDecoration lastSeenDecoration;
private RecyclerView.ItemDecoration stickyHeaderDecoration;
private RecyclerView.ItemDecoration popoverDateDecoration;
private ViewSwitcher topLoadMoreView;
private ViewSwitcher bottomLoadMoreView;
private ConversationTypingView typingView;
@@ -390,6 +392,17 @@ public class ConversationFragment extends LoggingFragment {
snapToTopDataObserver.requestScrollPosition(position);
}
public void onWallpaperChanged(@Nullable ChatWallpaper wallpaper) {
if (list != null) {
ConversationAdapter adapter = getListAdapter();
if (adapter != null) {
Log.d(TAG, "Notifying adapter that wallpaper state has changed.");
getListAdapter().onHasWallpaperChanged(wallpaper != null);
}
}
}
private int getStartPosition() {
return conversationViewModel.getArgs().getStartingPosition();
}
@@ -409,7 +422,6 @@ public class ConversationFragment extends LoggingFragment {
}
private static void presentMessageRequestProfileView(@NonNull Context context, @NonNull MessageRequestViewModel.RecipientInfo recipientInfo, @Nullable ConversationBannerView conversationBanner) {
if (conversationBanner == null) {
return;
}
@@ -422,9 +434,11 @@ public class ConversationFragment extends LoggingFragment {
if (recipient != null) {
conversationBanner.setAvatar(GlideApp.with(context), recipient);
conversationBanner.showBackgroundBubble(recipient.hasWallpaper());
String title = isSelf ? context.getString(R.string.note_to_self) : recipient.getDisplayNameOrUsername(context);
conversationBanner.setTitle(title);
conversationBanner.setAbout(recipient.getCombinedAboutAndEmoji());
if (recipient.isGroup()) {
if (pendingMemberCount > 0) {
@@ -487,7 +501,7 @@ public class ConversationFragment extends LoggingFragment {
this.threadId = conversationViewModel.getArgs().getThreadId();
this.markReadHelper = new MarkReadHelper(threadId, requireContext());
conversationViewModel.onConversationDataAvailable(threadId, startingPosition);
conversationViewModel.onConversationDataAvailable(recipient.getId(), threadId, startingPosition);
messageCountsViewModel.setThreadId(threadId);
messageCountsViewModel.getUnreadMessagesCount().observe(getViewLifecycleOwner(), scrollToBottomButton::setUnreadCount);
@@ -510,7 +524,7 @@ public class ConversationFragment extends LoggingFragment {
ConversationAdapter adapter = new ConversationAdapter(this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get());
adapter.setPagingController(conversationViewModel.getPagingController());
list.setAdapter(adapter);
setStickyHeaderDecoration(adapter);
setPopoverDateDecoration(adapter);
ConversationAdapter.initializePool(list.getRecycledViewPool());
adapter.registerAdapterDataObserver(snapToTopDataObserver);
@@ -614,7 +628,7 @@ public class ConversationFragment extends LoggingFragment {
menu.findItem(R.id.menu_context_copy).setVisible(menuState.shouldShowCopyAction());
}
private ConversationAdapter getListAdapter() {
private @Nullable ConversationAdapter getListAdapter() {
return (ConversationAdapter) list.getAdapter();
}
@@ -637,7 +651,7 @@ public class ConversationFragment extends LoggingFragment {
messageRequestViewModel.setConversationInfo(recipient.getId(), threadId);
snapToTopDataObserver.requestScrollPosition(0);
conversationViewModel.onConversationDataAvailable(threadId, -1);
conversationViewModel.onConversationDataAvailable(recipient.getId(), threadId, -1);
messageCountsViewModel.setThreadId(threadId);
initializeListAdapter();
}
@@ -651,13 +665,13 @@ public class ConversationFragment extends LoggingFragment {
}
}
public void setStickyHeaderDecoration(@NonNull ConversationAdapter adapter) {
if (stickyHeaderDecoration != null) {
list.removeItemDecoration(stickyHeaderDecoration);
public void setPopoverDateDecoration(@NonNull ConversationAdapter adapter) {
if (popoverDateDecoration != null) {
list.removeItemDecoration(popoverDateDecoration);
}
stickyHeaderDecoration = new StickyHeaderDecoration(adapter, false, false);
list.addItemDecoration(stickyHeaderDecoration);
popoverDateDecoration = new StickyHeaderDecoration(adapter, false, false, ConversationAdapter.HEADER_TYPE_INLINE_DATE);
list.addItemDecoration(popoverDateDecoration);
}
public void setLastSeen(long lastSeen) {
@@ -1179,7 +1193,7 @@ public class ConversationFragment extends LoggingFragment {
private void bindScrollHeader(StickyHeaderViewHolder headerViewHolder, int positionId) {
if (((ConversationAdapter)list.getAdapter()).getHeaderId(positionId) != -1) {
((ConversationAdapter) list.getAdapter()).onBindHeaderViewHolder(headerViewHolder, positionId);
((ConversationAdapter) list.getAdapter()).onBindHeaderViewHolder(headerViewHolder, positionId, ConversationAdapter.HEADER_TYPE_POPOVER_DATE);
}
}
}

View File

@@ -9,8 +9,10 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import java.util.ArrayList;
import java.util.Collection;
@@ -147,6 +149,11 @@ public class ConversationIntents {
public boolean isFirstTimeInSelfCreatedGroup() {
return firstTimeInSelfCreatedGroup;
}
public @Nullable ChatWallpaper getWallpaper() {
// TODO [greyson][wallpaper] Is it worth it to do this beforehand?
return Recipient.resolved(recipientId).getWallpaper();
}
}
public final static class Builder {

View File

@@ -155,7 +155,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private boolean groupThread;
private LiveRecipient recipient;
private GlideRequests glideRequests;
private ValueAnimator pulseOutlinerAlphaAnimator;
private ValueAnimator pulseOutlinerAlphaAnimator;
protected ConversationItemBodyBubble bodyBubble;
protected View reply;
@@ -165,7 +165,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private ConversationItemFooter footer;
private ConversationItemFooter stickerFooter;
@Nullable private TextView groupSender;
@Nullable private TextView groupSenderProfileName;
@Nullable private View groupSenderHolder;
private AvatarImageView contactPhoto;
private AlertView alertView;
@@ -223,7 +222,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
this.footer = findViewById(R.id.conversation_item_footer);
this.stickerFooter = findViewById(R.id.conversation_item_sticker_footer);
this.groupSender = findViewById(R.id.group_message_sender);
this.groupSenderProfileName = findViewById(R.id.group_message_sender_profile);
this.alertView = findViewById(R.id.indicators_parent);
this.contactPhoto = findViewById(R.id.contact_photo);
this.contactPhotoHolder = findViewById(R.id.contact_photo_container);
@@ -256,7 +254,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
@NonNull Set<ConversationMessage> batchSelected,
@NonNull Recipient conversationRecipient,
@Nullable String searchQuery,
boolean pulse)
boolean pulse,
boolean hasWallpaper)
{
if (this.recipient != null) this.recipient.removeForeverObserver(this);
if (this.conversationRecipient != null) this.conversationRecipient.removeForeverObserver(this);
@@ -279,17 +278,17 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, conversationRecipient, groupThread);
setBodyText(messageRecord, searchQuery);
setBubbleState(messageRecord);
setBubbleState(messageRecord, hasWallpaper);
setInteractionState(conversationMessage, pulse);
setStatusIcons(messageRecord);
setContactPhoto(recipient.get());
setGroupMessageStatus(messageRecord, recipient.get());
setGroupAuthorColor(messageRecord);
setAuthor(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
setGroupAuthorColor(messageRecord, hasWallpaper);
setAuthor(messageRecord, previousMessageRecord, nextMessageRecord, groupThread, hasWallpaper);
setQuote(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
setMessageSpacing(context, messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
setReactions(messageRecord);
setFooter(messageRecord, nextMessageRecord, locale, groupThread);
setFooter(messageRecord, nextMessageRecord, locale, groupThread, hasWallpaper);
}
@Override
@@ -344,12 +343,14 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
ConversationItemFooter activeFooter = getActiveFooter(messageRecord);
int availableWidth = getAvailableMessageBubbleWidth(footer);
if (!hasNoBubble(messageRecord)) {
ConversationItemFooter activeFooter = getActiveFooter(messageRecord);
int availableWidth = getAvailableMessageBubbleWidth(footer);
if (activeFooter.getVisibility() != GONE && activeFooter.getMeasuredWidth() != availableWidth) {
activeFooter.getLayoutParams().width = availableWidth;
needsMeasure = true;
if (activeFooter.getVisibility() != GONE && activeFooter.getMeasuredWidth() != availableWidth) {
activeFooter.getLayoutParams().width = availableWidth;
needsMeasure = true;
}
}
if (needsMeasure) {
@@ -366,7 +367,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
@Override
public void onRecipientChanged(@NonNull Recipient modified) {
setBubbleState(messageRecord);
setBubbleState(messageRecord, modified.hasWallpaper());
if (recipient.getId().equals(modified.getId())) {
setContactPhoto(modified);
setGroupMessageStatus(messageRecord, modified);
@@ -409,16 +410,20 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
/// MessageRecord Attribute Parsers
private void setBubbleState(MessageRecord messageRecord) {
private void setBubbleState(MessageRecord messageRecord, boolean hasWallpaper) {
if (messageRecord.isOutgoing() && !messageRecord.isRemoteDelete()) {
bodyBubble.getBackground().setColorFilter(defaultBubbleColor, PorterDuff.Mode.MULTIPLY);
footer.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
footer.setIconColor(ContextCompat.getColor(context, R.color.signal_icon_tint_secondary));
footer.setOnlyShowSendingStatus(false, messageRecord);
} else if (messageRecord.isRemoteDelete() || (isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord))) {
bodyBubble.getBackground().setColorFilter(ContextCompat.getColor(context, R.color.signal_background_primary), PorterDuff.Mode.MULTIPLY);
if (hasWallpaper) {
bodyBubble.getBackground().setColorFilter(ContextCompat.getColor(context, R.color.wallpaper_bubble_color), PorterDuff.Mode.SRC_IN);
} else {
bodyBubble.getBackground().setColorFilter(ContextCompat.getColor(context, R.color.signal_background_primary), PorterDuff.Mode.MULTIPLY);
footer.setIconColor(ContextCompat.getColor(context, R.color.signal_icon_tint_secondary));
}
footer.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
footer.setIconColor(ContextCompat.getColor(context, R.color.signal_icon_tint_secondary));
footer.setOnlyShowSendingStatus(messageRecord.isRemoteDelete(), messageRecord);
} else {
bodyBubble.getBackground().setColorFilter(messageRecord.getRecipient().getColor().toConversationColor(context), PorterDuff.Mode.MULTIPLY);
@@ -433,7 +438,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
pulseOutliner.setStrokeWidth(ViewUtil.dpToPx(4));
outliners.clear();
if (shouldDrawBodyBubbleOutline(messageRecord)) {
if (shouldDrawBodyBubbleOutline(messageRecord, hasWallpaper)) {
outliners.add(outliner);
}
outliners.add(pulseOutliner);
@@ -515,9 +520,13 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
pulseOutliner.setAlpha(0);
}
private boolean shouldDrawBodyBubbleOutline(MessageRecord messageRecord) {
boolean isIncomingViewedOnce = !messageRecord.isOutgoing() && isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord);
return isIncomingViewedOnce || messageRecord.isRemoteDelete();
private boolean shouldDrawBodyBubbleOutline(MessageRecord messageRecord, boolean hasWallpaper) {
if (hasWallpaper) {
return false;
} else {
boolean isIncomingViewedOnce = !messageRecord.isOutgoing() && isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord);
return isIncomingViewedOnce || messageRecord.isRemoteDelete();
}
}
private boolean isCaptionlessMms(MessageRecord messageRecord) {
@@ -543,6 +552,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
((MmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide().isBorderless();
}
private boolean hasNoBubble(MessageRecord messageRecord) {
return hasSticker(messageRecord) || isBorderless(messageRecord);
}
private boolean hasOnlyThumbnail(MessageRecord messageRecord) {
return hasThumbnail(messageRecord) &&
!hasAudio(messageRecord) &&
@@ -1074,7 +1087,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
});
}
private void setFooter(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> next, @NonNull Locale locale, boolean isGroupThread) {
private void setFooter(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> next, @NonNull Locale locale, boolean isGroupThread, boolean hasWallpaper) {
ViewUtil.updateLayoutParams(footer, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
footer.setVisibility(GONE);
@@ -1090,11 +1103,27 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
ConversationItemFooter activeFooter = getActiveFooter(current);
activeFooter.setVisibility(VISIBLE);
activeFooter.setMessageRecord(current, locale);
if (hasWallpaper && hasNoBubble((messageRecord))) {
if (messageRecord.isOutgoing()) {
activeFooter.enableBubbleBackground(R.drawable.wallpaper_bubble_background_tintable_11, defaultBubbleColor);
} else {
activeFooter.enableBubbleBackground(R.drawable.wallpaper_bubble_background_tintable_11, messageRecord.getRecipient().getColor().toConversationColor(context));
activeFooter.setTextColor(ContextCompat.getColor(context, R.color.conversation_item_received_text_secondary_color));
activeFooter.setIconColor(ContextCompat.getColor(context, R.color.conversation_item_received_text_secondary_color));
}
} else if (hasNoBubble(messageRecord)){
activeFooter.disableBubbleBackground();
activeFooter.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
activeFooter.setIconColor(ContextCompat.getColor(context, R.color.signal_icon_tint_secondary));
} else {
activeFooter.disableBubbleBackground();
}
}
}
private ConversationItemFooter getActiveFooter(@NonNull MessageRecord messageRecord) {
if (hasSticker(messageRecord) || isBorderless(messageRecord)) {
if (hasNoBubble(messageRecord)) {
return stickerFooter;
} else if (hasSharedContact(messageRecord) && TextUtils.isEmpty(messageRecord.getDisplayBody(getContext()))) {
return sharedContactStub.get().getFooter();
@@ -1118,30 +1147,27 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
@SuppressLint("SetTextI18n")
private void setGroupMessageStatus(MessageRecord messageRecord, Recipient recipient) {
if (groupThread && !messageRecord.isOutgoing() && groupSender != null && groupSenderProfileName != null) {
if (groupThread && !messageRecord.isOutgoing() && groupSender != null) {
groupSender.setText(recipient.getDisplayName(getContext()));
groupSenderProfileName.setVisibility(View.GONE);
}
}
private void setGroupAuthorColor(@NonNull MessageRecord messageRecord) {
if (groupSender != null && groupSenderProfileName != null) {
private void setGroupAuthorColor(@NonNull MessageRecord messageRecord, boolean hasWallpaper) {
if (groupSender != null) {
int stickerAuthorColor = ContextCompat.getColor(context, R.color.signal_text_primary);
if (shouldDrawBodyBubbleOutline(messageRecord)) {
if (shouldDrawBodyBubbleOutline(messageRecord, hasWallpaper)) {
groupSender.setTextColor(stickerAuthorColor);
groupSenderProfileName.setTextColor(stickerAuthorColor);
} else if (hasSticker(messageRecord) || isBorderless(messageRecord)) {
} else if (!hasWallpaper && hasNoBubble(messageRecord)) {
groupSender.setTextColor(stickerAuthorColor);
groupSenderProfileName.setTextColor(stickerAuthorColor);
} else {
groupSender.setTextColor(ContextCompat.getColor(context, R.color.conversation_item_received_text_primary_color));
groupSenderProfileName.setTextColor(ContextCompat.getColor(context, R.color.conversation_item_received_text_primary_color));
}
}
}
@SuppressWarnings("ConstantConditions")
private void setAuthor(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread) {
private void setAuthor(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread, boolean hasWallpaper) {
if (isGroupThread && !current.isOutgoing()) {
contactPhotoHolder.setVisibility(VISIBLE);
@@ -1149,6 +1175,13 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
!DateUtils.isSameDay(previous.get().getTimestamp(), current.getTimestamp()))
{
groupSenderHolder.setVisibility(VISIBLE);
if (hasWallpaper && hasNoBubble(current)) {
groupSenderHolder.setBackgroundResource(R.drawable.wallpaper_bubble_background_tintable_11);
groupSenderHolder.getBackground().setColorFilter(messageRecord.getRecipient().getColor().toConversationColor(context), PorterDuff.Mode.MULTIPLY);
} else {
groupSenderHolder.setBackground(null);
}
} else {
groupSenderHolder.setVisibility(GONE);
}

View File

@@ -142,7 +142,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
}
public void setListVerticalTranslation(float translationY) {
maskView.setTargetParentTranslationY(translationY);
maskView.setTargetParentTranslationY(translationY - ViewUtil.getStatusBarHeight(maskView));
}
public void show(@NonNull Activity activity,

View File

@@ -5,7 +5,7 @@ import android.text.Spannable;
import android.text.SpannableString;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
@@ -41,7 +41,7 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
public final class ConversationUpdateItem extends LinearLayout
public final class ConversationUpdateItem extends FrameLayout
implements BindableConversationItem
{
private static final String TAG = ConversationUpdateItem.class.getSimpleName();
@@ -50,6 +50,7 @@ public final class ConversationUpdateItem extends LinearLayout
private TextView body;
private TextView actionButton;
private View background;
private ConversationMessage conversationMessage;
private Recipient conversationRecipient;
private Optional<MessageRecord> nextMessageRecord;
@@ -76,6 +77,7 @@ public final class ConversationUpdateItem extends LinearLayout
super.onFinishInflate();
this.body = findViewById(R.id.conversation_update_body);
this.actionButton = findViewById(R.id.conversation_update_action);
this.background = findViewById(R.id.conversation_update_background);
this.setOnClickListener(new InternalClickListener(null));
}
@@ -90,11 +92,12 @@ public final class ConversationUpdateItem extends LinearLayout
@NonNull Set<ConversationMessage> batchSelected,
@NonNull Recipient conversationRecipient,
@Nullable String searchQuery,
boolean pulseMention)
boolean pulseMention,
boolean hasWallpaper)
{
this.batchSelected = batchSelected;
bind(lifecycleOwner, conversationMessage, nextMessageRecord, conversationRecipient);
bind(lifecycleOwner, conversationMessage, nextMessageRecord, conversationRecipient, hasWallpaper);
}
@Override
@@ -110,7 +113,8 @@ public final class ConversationUpdateItem extends LinearLayout
private void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ConversationMessage conversationMessage,
@NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull Recipient conversationRecipient)
@NonNull Recipient conversationRecipient,
boolean hasWallpaper)
{
this.conversationMessage = conversationMessage;
this.messageRecord = conversationMessage.getMessageRecord();
@@ -125,6 +129,12 @@ public final class ConversationUpdateItem extends LinearLayout
groupObserver.observe(lifecycleOwner, null);
}
if (hasWallpaper) {
background.setBackgroundResource(R.drawable.wallpaper_bubble_background_12);
} else {
background.setBackground(null);
}
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext()));
LiveData<Spannable> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(getContext(), updateDescription, ContextCompat.getColor(getContext(), R.color.conversation_item_update_text_color));
LiveData<Spannable> spannableMessage = loading(liveUpdateMessage);

View File

@@ -19,7 +19,11 @@ import org.thoughtcrime.securesms.database.DatabaseObserver;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.MediaRepository;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.whispersystems.libsignal.util.Pair;
import java.util.List;
@@ -41,6 +45,8 @@ class ConversationViewModel extends ViewModel {
private final LiveData<Boolean> canShowAsBubble;
private final ProxyPagingController pagingController;
private final DatabaseObserver.Observer messageObserver;
private final MutableLiveData<RecipientId> recipientId;
private final LiveData<ChatWallpaper> wallpaper;
private ConversationIntents.Args args;
private int jumpToPosition;
@@ -53,6 +59,7 @@ class ConversationViewModel extends ViewModel {
this.threadId = new MutableLiveData<>();
this.showScrollButtons = new MutableLiveData<>(false);
this.hasUnreadMentions = new MutableLiveData<>(false);
this.recipientId = new MutableLiveData<>();
this.pagingController = new ProxyPagingController();
this.messageObserver = pagingController::onDataInvalidated;
@@ -97,6 +104,9 @@ class ConversationViewModel extends ViewModel {
conversationMetadata = Transformations.switchMap(messages, m -> metadata);
canShowAsBubble = LiveDataUtil.mapAsync(threadId, conversationRepository::canShowAsBubble);
wallpaper = Transformations.distinctUntilChanged(Transformations.map(Transformations.switchMap(recipientId,
id -> Recipient.live(id).getLiveData()),
Recipient::getWallpaper));
}
void onAttachmentKeyboardOpen() {
@@ -104,11 +114,12 @@ class ConversationViewModel extends ViewModel {
}
@MainThread
void onConversationDataAvailable(long threadId, int startingPosition) {
void onConversationDataAvailable(@NonNull RecipientId recipientId, long threadId, int startingPosition) {
Log.d(TAG, "[onConversationDataAvailable] threadId: " + threadId + ", startingPosition: " + startingPosition);
this.jumpToPosition = startingPosition;
this.threadId.setValue(threadId);
this.recipientId.setValue(recipientId);
}
void clearThreadId() {
@@ -128,6 +139,10 @@ class ConversationViewModel extends ViewModel {
return Transformations.distinctUntilChanged(LiveDataUtil.combineLatest(showScrollButtons, hasUnreadMentions, (a, b) -> a && b));
}
@NonNull LiveData<ChatWallpaper> getWallpaper() {
return wallpaper;
}
void setHasUnreadMentions(boolean hasUnreadMentions) {
this.hasUnreadMentions.setValue(hasUnreadMentions);
}

View File

@@ -17,7 +17,7 @@ class LastSeenHeader extends StickyHeaderDecoration {
private final long lastSeenTimestamp;
LastSeenHeader(ConversationAdapter adapter, long lastSeenTimestamp) {
super(adapter, false, false);
super(adapter, false, false, ConversationAdapter.HEADER_TYPE_LAST_SEEN);
this.adapter = adapter;
this.lastSeenTimestamp = lastSeenTimestamp;
}

View File

@@ -481,7 +481,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
private void initializeListAdapters() {
defaultAdapter = new ConversationListAdapter(GlideApp.with(this), this);
searchAdapter = new ConversationListSearchAdapter(GlideApp.with(this), this, Locale.getDefault());
searchAdapterDecoration = new StickyHeaderDecoration(searchAdapter, false, false);
searchAdapterDecoration = new StickyHeaderDecoration(searchAdapter, false, false, 0);
setAdapter(defaultAdapter);

View File

@@ -94,13 +94,13 @@ class ConversationListSearchAdapter extends RecyclerView.Adapter<Conversation
}
@Override
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position) {
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position, int type) {
return new HeaderViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.search_result_list_divider, parent, false));
}
@Override
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) {
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position, int type) {
viewHolder.bind((int) getHeaderId(position));
}

View File

@@ -60,8 +60,8 @@ public class TextSecureSessionStore implements SessionStore {
SessionRecord sessionRecord = DatabaseFactory.getSessionDatabase(context).load(recipientId, address.getDeviceId());
return sessionRecord != null &&
sessionRecord.getSessionState().hasSenderChain() &&
sessionRecord.getSessionState().getSessionVersion() == CiphertextMessage.CURRENT_VERSION;
sessionRecord.hasSenderChain() &&
sessionRecord.getSessionVersion() == CiphertextMessage.CURRENT_VERSION;
} else {
return false;
}

View File

@@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.database.model.databaseprotos.DeviceLastResetTime;
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileKeyCredentialColumnData;
import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
@@ -38,6 +39,7 @@ import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -53,6 +55,9 @@ import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.thoughtcrime.securesms.util.StringUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperFactory;
import org.thoughtcrime.securesms.wallpaper.WallpaperStorage;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.Pair;
@@ -131,6 +136,10 @@ public class RecipientDatabase extends Database {
private static final String STORAGE_PROTO = "storage_proto";
private static final String LAST_GV1_MIGRATE_REMINDER = "last_gv1_migrate_reminder";
private static final String LAST_SESSION_RESET = "last_session_reset";
private static final String WALLPAPER = "wallpaper";
private static final String WALLPAPER_URI = "wallpaper_file";
public static final String ABOUT = "about";
public static final String ABOUT_EMOJI = "about_emoji";
public static final String SEARCH_PROFILE_NAME = "search_signal_profile";
private static final String SORT_NAME = "sort_name";
@@ -155,12 +164,14 @@ public class RecipientDatabase extends Database {
FORCE_SMS_SELECTION,
CAPABILITIES,
STORAGE_SERVICE_ID, DIRTY,
MENTION_SETTING
MENTION_SETTING, WALLPAPER, WALLPAPER_URI,
MENTION_SETTING,
ABOUT, ABOUT_EMOJI
};
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(" + 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};
private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, ABOUT, ABOUT_EMOJI, "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, ABOUT, ABOUT_EMOJI, SEARCH_PROFILE_NAME, SORT_NAME};
private static final String[] TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
.map(columnName -> TABLE_NAME + "." + columnName)
.toList().toArray(new String[0]);
@@ -350,7 +361,11 @@ public class RecipientDatabase extends Database {
STORAGE_PROTO + " TEXT DEFAULT NULL, " +
CAPABILITIES + " INTEGER DEFAULT 0, " +
LAST_GV1_MIGRATE_REMINDER + " INTEGER DEFAULT 0, " +
LAST_SESSION_RESET + " BLOB DEFAULT NULL);";
LAST_SESSION_RESET + " BLOB DEFAULT NULL, " +
WALLPAPER + " BLOB DEFAULT NULL, " +
WALLPAPER_URI + " TEXT DEFAULT NULL, " +
ABOUT + " TEXT DEFAULT NULL, " +
ABOUT_EMOJI + " TEXT DEFAULT NULL);";
private static final String INSIGHTS_INVITEE_LIST = "SELECT " + TABLE_NAME + "." + ID +
" FROM " + TABLE_NAME +
@@ -1264,6 +1279,9 @@ public class RecipientDatabase extends Database {
long capabilities = CursorUtil.requireLong(cursor, CAPABILITIES);
String storageKeyRaw = CursorUtil.requireString(cursor, STORAGE_SERVICE_ID);
int mentionSettingId = CursorUtil.requireInt(cursor, MENTION_SETTING);
byte[] wallpaper = CursorUtil.requireBlob(cursor, WALLPAPER);
String about = CursorUtil.requireString(cursor, ABOUT);
String aboutEmoji = CursorUtil.requireString(cursor, ABOUT_EMOJI);
MaterialColor color;
byte[] profileKey = null;
@@ -1303,6 +1321,16 @@ public class RecipientDatabase extends Database {
byte[] storageKey = storageKeyRaw != null ? Base64.decodeOrThrow(storageKeyRaw) : null;
ChatWallpaper chatWallpaper = null;
if (wallpaper != null) {
try {
chatWallpaper = ChatWallpaperFactory.create(Wallpaper.parseFrom(wallpaper));
} catch (InvalidProtocolBufferException e) {
Log.w(TAG, "Failed to parse wallpaper.", e);
}
}
return new RecipientSettings(RecipientId.from(id),
uuid,
username,
@@ -1338,6 +1366,9 @@ public class RecipientDatabase extends Database {
InsightsBannerTier.fromId(insightsBannerTier),
storageKey,
MentionSetting.fromId(mentionSettingId),
chatWallpaper,
about,
aboutEmoji,
getSyncExtras(cursor));
}
@@ -1529,7 +1560,12 @@ public class RecipientDatabase extends Database {
try (Cursor cursor = db.query(TABLE_NAME, new String[] {LAST_SESSION_RESET}, ID_WHERE, SqlUtil.buildArgs(id), null, null, null)) {
if (cursor.moveToFirst()) {
try {
return DeviceLastResetTime.parseFrom(CursorUtil.requireBlob(cursor, LAST_SESSION_RESET));
byte[] serialized = CursorUtil.requireBlob(cursor, LAST_SESSION_RESET);
if (serialized != null) {
return DeviceLastResetTime.parseFrom(serialized);
} else {
return DeviceLastResetTime.newBuilder().build();
}
} catch (InvalidProtocolBufferException | SQLException e) {
Log.w(TAG, e);
return DeviceLastResetTime.newBuilder().build();
@@ -1751,6 +1787,16 @@ public class RecipientDatabase extends Database {
}
}
public void setAbout(@NonNull RecipientId id, @Nullable String about, @Nullable String emoji) {
ContentValues contentValues = new ContentValues();
contentValues.put(ABOUT, about);
contentValues.put(ABOUT_EMOJI, emoji);
if (update(id, contentValues)) {
Recipient.live(id).refresh();
}
}
public void setProfileSharing(@NonNull RecipientId id, @SuppressWarnings("SameParameterValue") boolean enabled) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(PROFILE_SHARING, enabled ? 1 : 0);
@@ -1773,6 +1819,134 @@ public class RecipientDatabase extends Database {
}
}
public void resetAllWallpaper() {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
String[] selection = SqlUtil.buildArgs(ID, WALLPAPER_URI);
String where = WALLPAPER + " IS NOT NULL";
List<Pair<RecipientId, String>> idWithWallpaper = new LinkedList<>();
database.beginTransaction();
try {
try (Cursor cursor = database.query(TABLE_NAME, selection, where, null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
idWithWallpaper.add(new Pair<>(RecipientId.from(CursorUtil.requireInt(cursor, ID)),
CursorUtil.getString(cursor, WALLPAPER_URI).orNull()));
}
}
if (idWithWallpaper.isEmpty()) {
return;
}
ContentValues values = new ContentValues(2);
values.put(WALLPAPER_URI, (String) null);
values.put(WALLPAPER, (byte[]) null);
int rowsUpdated = database.update(TABLE_NAME, values, where, null);
if (rowsUpdated == idWithWallpaper.size()) {
for (Pair<RecipientId, String> pair : idWithWallpaper) {
Recipient.live(pair.first()).refresh();
if (pair.second() != null) {
WallpaperStorage.onWallpaperDeselected(context, Uri.parse(pair.second()));
}
}
} else {
throw new AssertionError("expected " + idWithWallpaper.size() + " but got " + rowsUpdated);
}
} finally {
database.setTransactionSuccessful();
database.endTransaction();
}
}
public void setWallpaper(@NonNull RecipientId id, @Nullable ChatWallpaper chatWallpaper) {
setWallpaper(id, chatWallpaper != null ? chatWallpaper.serialize() : null);
}
private void setWallpaper(@NonNull RecipientId id, @Nullable Wallpaper wallpaper) {
Uri existingWallpaperUri = getWallpaperUri(id);
ContentValues values = new ContentValues();
values.put(WALLPAPER, wallpaper != null ? wallpaper.toByteArray() : null);
if (wallpaper != null && wallpaper.hasFile()) {
values.put(WALLPAPER_URI, wallpaper.getFile().getUri());
} else {
values.putNull(WALLPAPER_URI);
}
if (update(id, values)) {
Recipient.live(id).refresh();
}
if (existingWallpaperUri != null) {
WallpaperStorage.onWallpaperDeselected(context, existingWallpaperUri);
}
}
public void setDimWallpaperInDarkTheme(@NonNull RecipientId id, boolean enabled) {
Wallpaper wallpaper = getWallpaper(id);
if (wallpaper == null) {
throw new IllegalStateException("No wallpaper set for " + id);
}
Wallpaper updated = wallpaper.toBuilder()
.setDimLevelInDarkTheme(enabled ? ChatWallpaper.FIXED_DIM_LEVEL_FOR_DARK_THEME : 0)
.build();
setWallpaper(id, updated);
}
private @Nullable Wallpaper getWallpaper(@NonNull RecipientId id) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
try (Cursor cursor = db.query(TABLE_NAME, new String[] {WALLPAPER}, ID_WHERE, SqlUtil.buildArgs(id), null, null, null)) {
if (cursor.moveToFirst()) {
byte[] raw = CursorUtil.requireBlob(cursor, WALLPAPER);
if (raw != null) {
try {
return Wallpaper.parseFrom(raw);
} catch (InvalidProtocolBufferException e) {
return null;
}
} else {
return null;
}
}
}
return null;
}
private @Nullable Uri getWallpaperUri(@NonNull RecipientId id) {
Wallpaper wallpaper = getWallpaper(id);
if (wallpaper != null && wallpaper.hasFile()) {
return Uri.parse(wallpaper.getFile().getUri());
} else {
return null;
}
}
public int getWallpaperUriUsageCount(@NonNull Uri uri) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String query = WALLPAPER_URI + " = ?";
String[] args = SqlUtil.buildArgs(uri);
try (Cursor cursor = db.query(TABLE_NAME, new String[] { "COUNT(*)" }, query, args, null, null, null)) {
if (cursor.moveToFirst()) {
return cursor.getInt(0);
}
}
return 0;
}
/**
* @return True if setting the phone number resulted in changed recipientId, otherwise false.
*/
@@ -2783,6 +2957,9 @@ public class RecipientDatabase extends Database {
private final InsightsBannerTier insightsBannerTier;
private final byte[] storageId;
private final MentionSetting mentionSetting;
private final ChatWallpaper wallpaper;
private final String about;
private final String aboutEmoji;
private final SyncExtras syncExtras;
RecipientSettings(@NonNull RecipientId id,
@@ -2820,6 +2997,9 @@ public class RecipientDatabase extends Database {
@NonNull InsightsBannerTier insightsBannerTier,
@Nullable byte[] storageId,
@NonNull MentionSetting mentionSetting,
@Nullable ChatWallpaper wallpaper,
@Nullable String about,
@Nullable String aboutEmoji,
@NonNull SyncExtras syncExtras)
{
this.id = id;
@@ -2859,6 +3039,9 @@ public class RecipientDatabase extends Database {
this.insightsBannerTier = insightsBannerTier;
this.storageId = storageId;
this.mentionSetting = mentionSetting;
this.wallpaper = wallpaper;
this.about = about;
this.aboutEmoji = aboutEmoji;
this.syncExtras = syncExtras;
}
@@ -3006,6 +3189,18 @@ public class RecipientDatabase extends Database {
return mentionSetting;
}
public @Nullable ChatWallpaper getWallpaper() {
return wallpaper;
}
public @Nullable String getAbout() {
return about;
}
public @Nullable String getAboutEmoji() {
return aboutEmoji;
}
public @NonNull SyncExtras getSyncExtras() {
return syncExtras;
}

View File

@@ -65,6 +65,7 @@ import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -962,6 +963,19 @@ public class ThreadDatabase extends Database {
}
}
public Map<RecipientId, Long> getThreadIdsIfExistsFor(@NonNull RecipientId ... recipientIds) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
SqlUtil.Query query = SqlUtil.buildCollectionQuery(RECIPIENT_ID, Arrays.asList(recipientIds));
Map<RecipientId, Long> results = new HashMap<>();
try (Cursor cursor = db.query(TABLE_NAME, new String[]{ ID, RECIPIENT_ID }, query.getWhere(), query.getWhereArgs(), null, null, null, "1")) {
while (cursor != null && cursor.moveToNext()) {
results.put(RecipientId.from(CursorUtil.requireString(cursor, RECIPIENT_ID)), CursorUtil.requireLong(cursor, ID));
}
}
return results;
}
public long getOrCreateValidThreadId(@NonNull Recipient recipient, long candidateId) {
return getOrCreateValidThreadId(recipient, candidateId, DistributionTypes.DEFAULT);
}

View File

@@ -169,8 +169,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
private static final int GV1_MIGRATION_REFACTOR = 85;
private static final int CLEAR_PROFILE_KEY_CREDENTIALS = 86;
private static final int LAST_RESET_SESSION_TIME = 87;
private static final int WALLPAPER = 88;
private static final int ABOUT = 89;
private static final int DATABASE_VERSION = 87;
private static final int DATABASE_VERSION = 89;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -1246,6 +1248,16 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
db.execSQL("ALTER TABLE recipient ADD COLUMN last_session_reset BLOB DEFAULT NULL");
}
if (oldVersion < WALLPAPER) {
db.execSQL("ALTER TABLE recipient ADD COLUMN wallpaper BLOB DEFAULT NULL");
db.execSQL("ALTER TABLE recipient ADD COLUMN wallpaper_file TEXT DEFAULT NULL");
}
if (oldVersion < ABOUT) {
db.execSQL("ALTER TABLE recipient ADD COLUMN about TEXT DEFAULT NULL");
db.execSQL("ALTER TABLE recipient ADD COLUMN about_emoji TEXT DEFAULT NULL");
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@@ -10,7 +10,6 @@ import org.signal.core.util.Conversions;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.SessionDatabase;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SessionState;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.File;
@@ -64,9 +63,7 @@ class SessionStoreMigrationHelper {
if (versionMarker == SINGLE_STATE_VERSION) {
Log.i(TAG, "Migrating single state version: " + sessionFile.getAbsolutePath());
SessionState sessionState = new SessionState(serialized);
sessionRecord = new SessionRecord(sessionState);
sessionRecord = SessionRecord.fromSingleSessionState(serialized);
} else if (versionMarker >= ARCHIVE_STATES_VERSION) {
Log.i(TAG, "Migrating session: " + sessionFile.getAbsolutePath());
sessionRecord = new SessionRecord(serialized);

View File

@@ -15,9 +15,12 @@ import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.LifecycleRecyclerAdapter;
import org.thoughtcrime.securesms.util.LifecycleViewHolder;
import org.thoughtcrime.securesms.util.Util;
import java.util.ArrayList;
import java.util.HashSet;
@@ -166,6 +169,7 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
final Context context;
final AvatarImageView avatar;
final TextView recipient;
final EmojiTextView about;
final CheckBox selected;
final PopupMenuView popupMenu;
final View popupMenuContainer;
@@ -187,6 +191,7 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
this.context = itemView.getContext();
this.avatar = itemView.findViewById(R.id.recipient_avatar);
this.recipient = itemView.findViewById(R.id.recipient_name);
this.about = itemView.findViewById(R.id.recipient_about);
this.selected = itemView.findViewById(R.id.recipient_selected);
this.popupMenu = itemView.findViewById(R.id.popupMenu);
this.popupMenuContainer = itemView.findViewById(R.id.popupMenuProgressContainer);
@@ -201,12 +206,17 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
void bindRecipient(@NonNull Recipient recipient) {
String displayName = recipient.isSelf() ? context.getString(R.string.GroupMembersDialog_you)
: recipient.getDisplayName(itemView.getContext());
bindImageAndText(recipient, displayName);
bindImageAndText(recipient, displayName, recipient.getCombinedAboutAndEmoji());
}
void bindImageAndText(@NonNull Recipient recipient, @NonNull String displayText) {
void bindImageAndText(@NonNull Recipient recipient, @NonNull String displayText, @Nullable String about) {
this.recipient.setText(displayText);
this.avatar.setRecipient(recipient);
if (this.about != null) {
this.about.setText(about);
this.about.setVisibility(Util.isEmpty(about) ? View.GONE : View.VISIBLE);
}
}
void bindRecipientClick(@NonNull Recipient recipient) {
@@ -333,7 +343,7 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
GroupMemberEntry.PendingMember pendingMember = (GroupMemberEntry.PendingMember) memberEntry;
bindImageAndText(pendingMember.getInvitee(), pendingMember.getInvitee().getDisplayNameOrUsername(context));
bindImageAndText(pendingMember.getInvitee(), pendingMember.getInvitee().getDisplayNameOrUsername(context), pendingMember.getInvitee().getCombinedAboutAndEmoji());
bindRecipientClick(pendingMember.getInvitee());
if (pendingMember.isCancellable() && adminActionsListener != null) {
@@ -370,7 +380,7 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
pendingMembers.getInviteCount(),
displayName, pendingMembers.getInviteCount());
bindImageAndText(inviter, displayText);
bindImageAndText(inviter, displayText, inviter.getAbout());
if (pendingMembers.isCancellable() && adminActionsListener != null) {
popupMenu.setMenu(R.menu.others_invite_pending_menu,

View File

@@ -62,6 +62,7 @@ import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.LifecycleCursorWrapper;
import org.thoughtcrime.securesms.util.views.LearnMoreTextView;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity;
import java.util.List;
import java.util.Locale;
@@ -114,6 +115,7 @@ public class ManageGroupFragment extends LoggingFragment {
private View toggleAllMembers;
private View groupLinkRow;
private TextView groupLinkButton;
private View wallpaperButton;
private final Recipient.FallbackPhotoProvider fallbackPhotoProvider = new Recipient.FallbackPhotoProvider() {
@Override
@@ -175,6 +177,7 @@ public class ManageGroupFragment extends LoggingFragment {
toggleAllMembers = view.findViewById(R.id.toggle_all_members);
groupLinkRow = view.findViewById(R.id.group_link_row);
groupLinkButton = view.findViewById(R.id.group_link_button);
wallpaperButton = view.findViewById(R.id.chat_wallpaper);
return view;
}
@@ -240,6 +243,7 @@ public class ManageGroupFragment extends LoggingFragment {
});
customNotificationsRow.setOnClickListener(v -> CustomNotificationsDialogFragment.create(groupRecipient.getId())
.show(requireFragmentManager(), DIALOG_TAG));
wallpaperButton.setOnClickListener(v -> startActivity(ChatWallpaperActivity.createIntent(requireContext(), groupRecipient.getId())));
});
if (groupId.isV2()) {

View File

@@ -172,7 +172,7 @@ public class HelpFragment extends LoggingFragment {
}
return SupportEmailUtil.generateSupportEmailBody(requireContext(),
getString(R.string.HelpFragment__signal_android_support_request),
R.string.HelpFragment__signal_android_support_request,
problem.getText().toString() + "\n\n",
suffix.toString());
}

View File

@@ -61,6 +61,9 @@ public final class ImageEditorView extends FrameLayout {
@Nullable
private DrawingChangedListener drawingChangedListener;
@Nullable
private SizeChangedListener sizeChangedListener;
@Nullable
private UndoRedoStackListener undoRedoStackListener;
@@ -93,7 +96,7 @@ public final class ImageEditorView extends FrameLayout {
private void init() {
setWillNotDraw(false);
setModel(new EditorModel());
setModel(EditorModel.create());
editText = createAHiddenTextEntryField();
@@ -170,6 +173,9 @@ public final class ImageEditorView extends FrameLayout {
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
updateViewMatrix();
if (sizeChangedListener != null) {
sizeChangedListener.onSizeChanged(w, h);
}
}
private void updateViewMatrix() {
@@ -393,6 +399,10 @@ public final class ImageEditorView extends FrameLayout {
this.drawingChangedListener = drawingChangedListener;
}
public void setSizeChangedListener(@Nullable SizeChangedListener sizeChangedListener) {
this.sizeChangedListener = sizeChangedListener;
}
public void setUndoRedoStackListener(@Nullable UndoRedoStackListener undoRedoStackListener) {
this.undoRedoStackListener = undoRedoStackListener;
}
@@ -463,6 +473,10 @@ public final class ImageEditorView extends FrameLayout {
void onDrawingChanged();
}
public interface SizeChangedListener {
void onSizeChanged(int newWidth, int newHeight);
}
public interface TapListener {
void onEntityDown(@Nullable EditorElement editorElement);

View File

@@ -45,11 +45,15 @@ import org.thoughtcrime.securesms.imageeditor.renderers.OvalGuideRenderer;
final class EditorElementHierarchy {
static @NonNull EditorElementHierarchy create() {
return new EditorElementHierarchy(createRoot(false));
return new EditorElementHierarchy(createRoot(CropStyle.RECTANGLE));
}
static @NonNull EditorElementHierarchy createForCircleEditing() {
return new EditorElementHierarchy(createRoot(true));
return new EditorElementHierarchy(createRoot(CropStyle.CIRCLE));
}
static @NonNull EditorElementHierarchy createForPinchAndPanCropping() {
return new EditorElementHierarchy(createRoot(CropStyle.PINCH_AND_PAN));
}
static @NonNull EditorElementHierarchy create(@NonNull EditorElement root) {
@@ -78,7 +82,24 @@ final class EditorElementHierarchy {
this.thumbs = this.cropEditorElement.getChild(1);
}
private static @NonNull EditorElement createRoot(boolean circleEdit) {
private enum CropStyle {
/**
* A rectangular overlay with 8 thumbs, corners and edges.
*/
RECTANGLE,
/**
* Cropping with a circular template overlay with Corner thumbs only.
*/
CIRCLE,
/**
* No overlay and no thumbs. Cropping achieved through pinching and panning.
*/
PINCH_AND_PAN
}
private static @NonNull EditorElement createRoot(@NonNull CropStyle cropStyle) {
EditorElement root = new EditorElement(null);
EditorElement imageRoot = new EditorElement(null);
@@ -96,7 +117,8 @@ final class EditorElementHierarchy {
EditorElement imageCrop = new EditorElement(null);
overlay.addElement(imageCrop);
EditorElement cropEditorElement = new EditorElement(new CropAreaRenderer(R.color.crop_area_renderer_outer_color, !circleEdit));
boolean renderCenterThumbs = cropStyle == CropStyle.RECTANGLE;
EditorElement cropEditorElement = new EditorElement(new CropAreaRenderer(R.color.crop_area_renderer_outer_color, renderCenterThumbs));
cropEditorElement.getFlags()
.setRotateLocked(true)
@@ -116,14 +138,18 @@ final class EditorElementHierarchy {
cropEditorElement.addElement(blackout);
cropEditorElement.addElement(createThumbs(cropEditorElement, !circleEdit));
if (cropStyle == CropStyle.PINCH_AND_PAN) {
cropEditorElement.addElement(new EditorElement(null));
} else {
cropEditorElement.addElement(createThumbs(cropEditorElement, renderCenterThumbs));
if (circleEdit) {
EditorElement circle = new EditorElement(new OvalGuideRenderer(R.color.crop_circle_guide_color));
circle.getFlags().setSelectable(false)
.persist();
if (cropStyle == CropStyle.CIRCLE) {
EditorElement circle = new EditorElement(new OvalGuideRenderer(R.color.crop_circle_guide_color));
circle.getFlags().setSelectable(false)
.persist();
cropEditorElement.addElement(circle);
cropEditorElement.addElement(circle);
}
}
return root;
@@ -197,11 +223,14 @@ final class EditorElementHierarchy {
return flipRotate;
}
void startCrop(@NonNull Runnable invalidate) {
Matrix editor = new Matrix();
float scaleInForCrop = 0.8f;
/**
* @param scaleIn Use 1 for no scale in, use less than 1 and it will zoom the image out
* so user can see more of the surrounding image while cropping.
*/
void startCrop(@NonNull Runnable invalidate, float scaleIn) {
Matrix editor = new Matrix();
editor.postScale(scaleInForCrop, scaleInForCrop);
editor.postScale(scaleIn, scaleIn);
root.animateEditorTo(editor, invalidate);
cropEditorElement.getFlags()

View File

@@ -60,17 +60,21 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
private EditorElementHierarchy editorElementHierarchy;
private final RectF visibleViewPort = new RectF();
private final Point size;
private final boolean circleEditing;
private final RectF visibleViewPort = new RectF();
private final Point size;
private final EditingPurpose editingPurpose;
private float fixedRatio;
public EditorModel() {
this(false, EditorElementHierarchy.create());
private enum EditingPurpose {
IMAGE,
AVATAR_CIRCLE,
WALLPAPER
}
private EditorModel(@NonNull Parcel in) {
ClassLoader classLoader = getClass().getClassLoader();
this.circleEditing = in.readByte() == 1;
this.editingPurpose = EditingPurpose.values()[in.readInt()];
this.fixedRatio = in.readFloat();
this.size = new Point(in.readInt(), in.readInt());
//noinspection ConstantConditions
this.editorElementHierarchy = EditorElementHierarchy.create(in.readParcelable(classLoader));
@@ -78,8 +82,9 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
this.cropUndoRedoStacks = in.readParcelable(classLoader);
}
public EditorModel(boolean circleEditing, @NonNull EditorElementHierarchy editorElementHierarchy) {
this.circleEditing = circleEditing;
public EditorModel(@NonNull EditingPurpose editingPurpose, float fixedRatio, @NonNull EditorElementHierarchy editorElementHierarchy) {
this.editingPurpose = editingPurpose;
this.fixedRatio = fixedRatio;
this.size = new Point(1024, 1024);
this.editorElementHierarchy = editorElementHierarchy;
this.undoRedoStacks = new UndoRedoStacks(50);
@@ -87,11 +92,17 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
}
public static EditorModel create() {
return new EditorModel(false, EditorElementHierarchy.create());
return new EditorModel(EditingPurpose.IMAGE, 0, EditorElementHierarchy.create());
}
public static EditorModel createForCircleEditing() {
EditorModel editorModel = new EditorModel(true, EditorElementHierarchy.createForCircleEditing());
EditorModel editorModel = new EditorModel(EditingPurpose.AVATAR_CIRCLE, 1, EditorElementHierarchy.createForCircleEditing());
editorModel.setCropAspectLock(true);
return editorModel;
}
public static EditorModel createForWallpaperEditing(float fixedRatio) {
EditorModel editorModel = new EditorModel(EditingPurpose.WALLPAPER, fixedRatio, EditorElementHierarchy.createForPinchAndPanCropping());
editorModel.setCropAspectLock(true);
return editorModel;
}
@@ -260,9 +271,11 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
}
public void startCrop() {
float scaleIn = editingPurpose == EditingPurpose.WALLPAPER ? 1 : 0.8f;
pushUndoPoint();
cropUndoRedoStacks.clear(editorElementHierarchy.getRoot());
editorElementHierarchy.startCrop(invalidate);
editorElementHierarchy.startCrop(invalidate, scaleIn);
inBoundsMemory.push(editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement());
updateUndoRedoAvailableState(cropUndoRedoStacks);
}
@@ -538,7 +551,8 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeByte((byte) (circleEditing ? 1 : 0));
dest.writeInt(editingPurpose.ordinal());
dest.writeFloat(fixedRatio);
dest.writeInt(size.x);
dest.writeInt(size.y);
dest.writeParcelable(editorElementHierarchy.getRoot(), flags);
@@ -628,10 +642,10 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
if (imageCropMatrix.isIdentity()) {
imageCropMatrix.set(cropMatrix);
if (circleEditing) {
if (editingPurpose == EditingPurpose.AVATAR_CIRCLE || editingPurpose == EditingPurpose.WALLPAPER) {
Matrix userCropMatrix = editorElementHierarchy.getCropEditorElement().getLocalMatrix();
if (size.x > size.y) {
userCropMatrix.setScale(size.y / (float) size.x, 1f);
userCropMatrix.setScale(fixedRatio * size.y / (float) size.x, 1f);
} else {
userCropMatrix.setScale(1f, size.x / (float) size.y);
}
@@ -643,13 +657,37 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
undoRedoStacks.clear(editorElementHierarchy.getRoot());
}
if (circleEditing) {
startCrop();
switch (editingPurpose) {
case AVATAR_CIRCLE: {
startCrop();
break;
}
case WALLPAPER: {
setFixedRatio(fixedRatio);
startCrop();
break;
}
}
}
}
}
public void setFixedRatio(float r) {
fixedRatio = r;
Matrix userCropMatrix = editorElementHierarchy.getCropEditorElement().getLocalMatrix();
float w = size.x;
float h = size.y;
float imageRatio = w / h;
if (imageRatio > r) {
userCropMatrix.setScale(r / imageRatio, 1f);
} else {
userCropMatrix.setScale(1f, imageRatio / r);
}
editorElementHierarchy.doneCrop(visibleViewPort, null);
startCrop();
}
private boolean isRendererOfMainImage(@NonNull Renderer renderer) {
EditorElement mainImage = editorElementHierarchy.getMainImage();
Renderer mainImageRenderer = mainImage != null ? mainImage.getRenderer() : null;
@@ -790,7 +828,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
return editorElementHierarchy.getRoot();
}
public EditorElement getMainImage() {
public @Nullable EditorElement getMainImage() {
return editorElementHierarchy.getMainImage();
}

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.jobmanager;
import android.app.job.JobInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
public interface Constraint {
@@ -14,6 +15,15 @@ public interface Constraint {
@RequiresApi(26)
void applyToJobInfo(@NonNull JobInfo.Builder jobInfoBuilder);
/**
* If you do something in {@link #applyToJobInfo} you should return something here.
* <p>
* It is sorted and concatenated with other constraints key parts to form a unique job id.
*/
default @Nullable String getJobSchedulerKeyPart() {
return null;
}
interface Factory<T extends Constraint> {
T create();
}

View File

@@ -6,26 +6,23 @@ import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.content.ComponentName;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import java.util.List;
import java.util.Locale;
@RequiresApi(26)
public class JobSchedulerScheduler implements Scheduler {
public final class JobSchedulerScheduler implements Scheduler {
private static final String TAG = JobSchedulerScheduler.class.getSimpleName();
private static final String PREF_NAME = "JobSchedulerScheduler_prefs";
private static final String PREF_NEXT_ID = "pref_next_id";
private static final int MAX_ID = 20;
private static final String TAG = Log.tag(JobSchedulerScheduler.class);
private final Application application;
@@ -37,14 +34,23 @@ public class JobSchedulerScheduler implements Scheduler {
@Override
public void schedule(long delay, @NonNull List<Constraint> constraints) {
JobScheduler jobScheduler = application.getSystemService(JobScheduler.class);
int currentId = getCurrentId();
if (constraints.isEmpty() && jobScheduler.getPendingJob(currentId) != null) {
Log.d(TAG, "Skipping JobScheduler enqueue because we have no constraints and there's already one pending.");
String constraintNames = constraints.isEmpty() ? ""
: Stream.of(constraints)
.map(Constraint::getJobSchedulerKeyPart)
.withoutNulls()
.sorted()
.collect(Collectors.joining("-"));
int jobId = constraintNames.hashCode();
if (jobScheduler.getPendingJob(jobId) != null) {
return;
}
JobInfo.Builder jobInfoBuilder = new JobInfo.Builder(getAndUpdateNextId(), new ComponentName(application, SystemService.class))
Log.i(TAG, String.format(Locale.US, "JobScheduler enqueue of %s (%d)", constraintNames, jobId));
JobInfo.Builder jobInfoBuilder = new JobInfo.Builder(jobId, new ComponentName(application, SystemService.class))
.setMinimumLatency(delay)
.setPersisted(true);
@@ -55,21 +61,6 @@ public class JobSchedulerScheduler implements Scheduler {
jobScheduler.schedule(jobInfoBuilder.build());
}
private int getCurrentId() {
SharedPreferences prefs = application.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
return prefs.getInt(PREF_NEXT_ID, 0);
}
private int getAndUpdateNextId() {
SharedPreferences prefs = application.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
int returnedId = prefs.getInt(PREF_NEXT_ID, 0);
int nextId = returnedId + 1 > MAX_ID ? 0 : returnedId + 1;
prefs.edit().putInt(PREF_NEXT_ID, nextId).apply();
return returnedId;
}
@RequiresApi(api = 26)
public static class SystemService extends JobService {
@@ -77,6 +68,8 @@ public class JobSchedulerScheduler implements Scheduler {
public boolean onStartJob(JobParameters params) {
JobManager jobManager = ApplicationDependencies.getJobManager();
Log.i(TAG, "Waking due to job: " + params.getJobId());
jobManager.addOnEmptyQueueListener(new JobManager.EmptyQueueListener() {
@Override
public void onQueueEmpty() {

View File

@@ -33,6 +33,11 @@ public class ChargingConstraint implements Constraint {
jobInfoBuilder.setRequiresCharging(true);
}
@Override
public String getJobSchedulerKeyPart() {
return "CHARGING";
}
public static final class Factory implements Constraint.Factory<ChargingConstraint> {
@Override

View File

@@ -7,6 +7,7 @@ import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import org.thoughtcrime.securesms.jobmanager.Constraint;
@@ -37,6 +38,11 @@ public class NetworkConstraint implements Constraint {
jobInfoBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
}
@Override
public String getJobSchedulerKeyPart() {
return "NETWORK";
}
public static boolean isMet(@NonNull Context context) {
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();

View File

@@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.migrations.ProfileMigrationJob;
import org.thoughtcrime.securesms.migrations.RecipientSearchMigrationJob;
import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob;
import org.thoughtcrime.securesms.migrations.StickerAdditionMigrationJob;
import org.thoughtcrime.securesms.migrations.StickerDayByDayMigrationJob;
import org.thoughtcrime.securesms.migrations.StickerLaunchMigrationJob;
import org.thoughtcrime.securesms.migrations.StorageCapabilityMigrationJob;
import org.thoughtcrime.securesms.migrations.StorageServiceMigrationJob;
@@ -161,6 +162,7 @@ public final class JobManagerFactories {
put(RegistrationPinV2MigrationJob.KEY, new RegistrationPinV2MigrationJob.Factory());
put(StickerLaunchMigrationJob.KEY, new StickerLaunchMigrationJob.Factory());
put(StickerAdditionMigrationJob.KEY, new StickerAdditionMigrationJob.Factory());
put(StickerDayByDayMigrationJob.KEY, new StickerDayByDayMigrationJob.Factory());
put(StorageCapabilityMigrationJob.KEY, new StorageCapabilityMigrationJob.Factory());
put(StorageServiceMigrationJob.KEY, new StorageServiceMigrationJob.Factory());
put(TrimByLengthSettingsMigrationJob.KEY, new TrimByLengthSettingsMigrationJob.Factory());

View File

@@ -141,7 +141,7 @@ public final class MmsSendJob extends SendJob {
final MmsSendResult result = getSendResult(sendConf, pdu);
database.markAsSent(messageId, false);
markAttachmentsUploaded(messageId, message.getAttachments());
markAttachmentsUploaded(messageId, message);
Log.i(TAG, "Sent message: " + messageId);
} catch (UndeliverableMessageException | IOException e) {

View File

@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.util.StreamDetails;
@@ -58,10 +59,12 @@ public final class ProfileUploadJob extends BaseJob {
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
ProfileName profileName = Recipient.self().getProfileName();
String about = Optional.fromNullable(Recipient.self().getAbout()).or("");
String aboutEmoji = Optional.fromNullable(Recipient.self().getAboutEmoji()).or("");
String avatarPath;
try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) {
avatarPath = accountManager.setVersionedProfile(Recipient.self().getUuid().get(), profileKey, profileName.serialize(), avatar).orNull();
avatarPath = accountManager.setVersionedProfile(Recipient.self().getUuid().get(), profileKey, profileName.serialize(), about, aboutEmoji, avatar).orNull();
}
DatabaseFactory.getRecipientDatabase(context).setProfileAvatar(Recipient.self().getId(), avatarPath);

View File

@@ -232,7 +232,7 @@ public final class PushGroupSendJob extends PushSendJob {
if (existingNetworkFailures.isEmpty() && networkFailures.isEmpty() && identityMismatches.isEmpty() && existingIdentityMismatches.isEmpty()) {
database.markAsSent(messageId, true);
markAttachmentsUploaded(messageId, message.getAttachments());
markAttachmentsUploaded(messageId, message);
if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) {
database.markExpireStarted(messageId);

View File

@@ -129,7 +129,7 @@ public class PushMediaSendJob extends PushSendJob {
boolean unidentified = deliver(message);
database.markAsSent(messageId, true);
markAttachmentsUploaded(messageId, message.getAttachments());
markAttachmentsUploaded(messageId, message);
database.markUnidentified(messageId, unidentified);
if (recipient.isSelf()) {

View File

@@ -75,6 +75,7 @@ public class RefreshOwnProfileJob extends BaseJob {
SignalServiceProfile profile = profileAndCredential.getProfile();
setProfileName(profile.getName());
setProfileAbout(profile.getAbout(), profile.getAboutEmoji());
setProfileAvatar(profile.getAvatar());
setProfileCapabilities(profile.getCapabilities());
Optional<ProfileKeyCredential> profileKeyCredential = profileAndCredential.getProfileKeyCredential();
@@ -117,6 +118,18 @@ public class RefreshOwnProfileJob extends BaseJob {
}
}
private void setProfileAbout(@Nullable String encryptedAbout, @Nullable String encryptedEmoji) {
try {
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
String plaintextAbout = ProfileUtil.decryptName(profileKey, encryptedAbout);
String plaintextEmoji = ProfileUtil.decryptName(profileKey, encryptedEmoji);
DatabaseFactory.getRecipientDatabase(context).setAbout(Recipient.self().getId(), plaintextAbout, plaintextEmoji);
} catch (InvalidCiphertextException | IOException e) {
Log.w(TAG, e);
}
}
private static void setProfileAvatar(@Nullable String avatar) {
ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(Recipient.self(), avatar));
}

View File

@@ -328,6 +328,7 @@ public class RetrieveProfileJob extends BaseJob {
ProfileKey recipientProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
setProfileName(recipient, profile.getName());
setProfileAbout(recipient, profile.getAbout(), profile.getAboutEmoji());
setProfileAvatar(recipient, profile.getAvatar());
clearUsername(recipient);
setProfileCapabilities(recipient, profile.getCapabilities());
@@ -454,6 +455,20 @@ public class RetrieveProfileJob extends BaseJob {
}
}
private void setProfileAbout(@NonNull Recipient recipient, @Nullable String encryptedAbout, @Nullable String encryptedEmoji) {
try {
ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
if (profileKey == null) return;
String plaintextAbout = ProfileUtil.decryptName(profileKey, encryptedAbout);
String plaintextEmoji = ProfileUtil.decryptName(profileKey, encryptedEmoji);
DatabaseFactory.getRecipientDatabase(context).setAbout(recipient.getId(), plaintextAbout, plaintextEmoji);
} catch (InvalidCiphertextException | IOException e) {
Log.w(TAG, e);
}
}
private static void setProfileAvatar(Recipient recipient, String profileAvatar) {
if (recipient.getProfileKey() == null) return;

View File

@@ -2,15 +2,23 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.TextSecureExpiredException;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.util.Util;
import java.lang.reflect.Array;
import java.util.LinkedList;
import java.util.List;
public abstract class SendJob extends BaseJob {
@@ -37,7 +45,17 @@ public abstract class SendJob extends BaseJob {
protected abstract void onSend() throws Exception;
protected void markAttachmentsUploaded(long messageId, @NonNull List<Attachment> attachments) {
protected void markAttachmentsUploaded(long messageId, @NonNull OutgoingMediaMessage message) {
List<Attachment> attachments = new LinkedList<>();
attachments.addAll(message.getAttachments());
attachments.addAll(Stream.of(message.getLinkPreviews()).map(lp -> lp.getThumbnail().orNull()).withoutNulls().toList());
attachments.addAll(Stream.of(message.getSharedContacts()).map(Contact::getAvatarAttachment).withoutNulls().toList());
if (message.getOutgoingQuote() != null) {
attachments.addAll(message.getOutgoingQuote().getAttachments());
}
AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
for (Attachment attachment : attachments) {

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.keyvalue;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceDataStore;
import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
@@ -28,6 +29,7 @@ public final class SignalStore {
private final CertificateValues certificateValues;
private final PhoneNumberPrivacyValues phoneNumberPrivacyValues;
private final OnboardingValues onboardingValues;
private final WallpaperValues wallpaperValues;
private SignalStore() {
this.store = new KeyValueStore(ApplicationDependencies.getApplication());
@@ -45,6 +47,7 @@ public final class SignalStore {
this.certificateValues = new CertificateValues(store);
this.phoneNumberPrivacyValues = new PhoneNumberPrivacyValues(store);
this.onboardingValues = new OnboardingValues(store);
this.wallpaperValues = new WallpaperValues(store);
}
public static void onFirstEverAppLaunch() {
@@ -61,6 +64,7 @@ public final class SignalStore {
certificateValues().onFirstEverAppLaunch();
phoneNumberPrivacy().onFirstEverAppLaunch();
onboarding().onFirstEverAppLaunch();
wallpaper().onFirstEverAppLaunch();
}
public static @NonNull KbsValues kbsValues() {
@@ -119,6 +123,10 @@ public final class SignalStore {
return INSTANCE.onboardingValues;
}
public static @NonNull WallpaperValues wallpaper() {
return INSTANCE.wallpaperValues;
}
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() {
return new GroupsV2AuthorizationSignalStoreCache(getStore());
}

View File

@@ -0,0 +1,108 @@
package org.thoughtcrime.securesms.keyvalue;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.protobuf.InvalidProtocolBufferException;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperFactory;
import org.thoughtcrime.securesms.wallpaper.WallpaperStorage;
public final class WallpaperValues extends SignalStoreValues {
private static final String TAG = Log.tag(WallpaperValues.class);
private static final String KEY_WALLPAPER = "wallpaper.wallpaper";
WallpaperValues(@NonNull KeyValueStore store) {
super(store);
}
@Override
void onFirstEverAppLaunch() {
}
public void setWallpaper(@NonNull Context context, @Nullable ChatWallpaper wallpaper) {
Wallpaper currentWallpaper = getCurrentWallpaper();
Uri currentUri = null;
if (currentWallpaper != null && currentWallpaper.hasFile()) {
currentUri = Uri.parse(currentWallpaper.getFile().getUri());
}
if (wallpaper != null) {
putBlob(KEY_WALLPAPER, wallpaper.serialize().toByteArray());
} else {
getStore().beginWrite().remove(KEY_WALLPAPER).apply();
}
if (currentUri != null) {
WallpaperStorage.onWallpaperDeselected(context, currentUri);
}
}
public @Nullable ChatWallpaper getWallpaper() {
Wallpaper currentWallpaper = getCurrentWallpaper();
if (currentWallpaper != null) {
return ChatWallpaperFactory.create(currentWallpaper);
} else {
return null;
}
}
public boolean hasWallpaperSet() {
return getStore().getBlob(KEY_WALLPAPER, null) != null;
}
public void setDimInDarkTheme(boolean enabled) {
Wallpaper currentWallpaper = getCurrentWallpaper();
if (currentWallpaper != null) {
putBlob(KEY_WALLPAPER,
currentWallpaper.toBuilder()
.setDimLevelInDarkTheme(enabled ? 0.2f : 0)
.build()
.toByteArray());
} else {
throw new IllegalStateException("No wallpaper currently set!");
}
}
/**
* Retrieves the URI of the current wallpaper. Note that this will only return a value if the
* wallpaper is both set *and* it's an image.
*/
public @Nullable Uri getWallpaperUri() {
Wallpaper currentWallpaper = getCurrentWallpaper();
if (currentWallpaper != null && currentWallpaper.hasFile()) {
return Uri.parse(currentWallpaper.getFile().getUri());
} else {
return null;
}
}
private @Nullable Wallpaper getCurrentWallpaper() {
byte[] serialized = getBlob(KEY_WALLPAPER, null);
if (serialized != null) {
try {
return Wallpaper.parseFrom(serialized);
} catch (InvalidProtocolBufferException e) {
Log.w(TAG, "Invalid proto stored for wallpaper!");
return null;
}
} else {
return null;
}
}
}

View File

@@ -130,6 +130,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
public void onResume() {
super.onResume();
camera.bindToLifecycle(getViewLifecycleOwner());
viewModel.onCameraStarted();
requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);

View File

@@ -29,13 +29,19 @@ import org.thoughtcrime.securesms.recipients.Recipient;
public class MediaPickerFolderFragment extends Fragment implements MediaPickerFolderAdapter.EventListener {
private static final String KEY_TOOLBAR_TITLE = "toolbar_title";
private static final String KEY_HIDE_CAMERA = "hide_camera";
private String toolbarTitle;
private boolean showCamera;
private MediaSendViewModel viewModel;
private Controller controller;
private GridLayoutManager layoutManager;
public static @NonNull MediaPickerFolderFragment newInstance(@NonNull Context context, @Nullable Recipient recipient) {
return newInstance(context, recipient, false);
}
public static @NonNull MediaPickerFolderFragment newInstance(@NonNull Context context, @Nullable Recipient recipient, boolean hideCamera) {
String toolbarTitle;
if (recipient != null) {
@@ -45,8 +51,13 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
toolbarTitle = "";
}
return newInstance(toolbarTitle, hideCamera);
}
public static @NonNull MediaPickerFolderFragment newInstance(@NonNull String toolbarTitle, boolean hideCamera) {
Bundle args = new Bundle();
args.putString(KEY_TOOLBAR_TITLE, toolbarTitle);
args.putBoolean(KEY_HIDE_CAMERA, hideCamera);
MediaPickerFolderFragment fragment = new MediaPickerFolderFragment();
fragment.setArguments(args);
@@ -60,6 +71,7 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
setHasOptionsMenu(true);
toolbarTitle = getArguments().getString(KEY_TOOLBAR_TITLE);
showCamera = !getArguments().getBoolean(KEY_HIDE_CAMERA);
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
}
@@ -105,16 +117,14 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
@Override
public void onPrepareOptionsMenu(@NonNull Menu menu) {
requireActivity().getMenuInflater().inflate(R.menu.mediapicker_default, menu);
if (showCamera) {
requireActivity().getMenuInflater().inflate(R.menu.mediapicker_default, menu);
}
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.mediapicker_menu_camera:
controller.onCameraSelected();
return true;
}
if (item.getItemId() == R.id.mediapicker_menu_camera) { controller.onCameraSelected(); return true; }
return false;
}

View File

@@ -22,9 +22,7 @@ import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.util.Util;
import java.util.ArrayList;
import java.util.List;
/**
@@ -36,10 +34,12 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
private static final String KEY_FOLDER_TITLE = "folder_title";
private static final String KEY_MAX_SELECTION = "max_selection";
private static final String KEY_FORCE_MULTI_SELECT = "force_multi_select";
private static final String KEY_HIDE_CAMERA = "hide_camera";
private String bucketId;
private String folderTitle;
private int maxSelection;
private boolean showCamera;
private MediaSendViewModel viewModel;
private MediaPickerItemAdapter adapter;
private Controller controller;
@@ -50,11 +50,16 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
}
public static MediaPickerItemFragment newInstance(@NonNull String bucketId, @NonNull String folderTitle, int maxSelection, boolean forceMultiSelect) {
return newInstance(bucketId, folderTitle, maxSelection, forceMultiSelect, false);
}
public static MediaPickerItemFragment newInstance(@NonNull String bucketId, @NonNull String folderTitle, int maxSelection, boolean forceMultiSelect, boolean hideCamera) {
Bundle args = new Bundle();
args.putString(KEY_BUCKET_ID, bucketId);
args.putString(KEY_FOLDER_TITLE, folderTitle);
args.putInt(KEY_MAX_SELECTION, maxSelection);
args.putBoolean(KEY_FORCE_MULTI_SELECT, forceMultiSelect);
args.putBoolean(KEY_HIDE_CAMERA, hideCamera);
MediaPickerItemFragment fragment = new MediaPickerItemFragment();
fragment.setArguments(args);
@@ -70,6 +75,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
bucketId = getArguments().getString(KEY_BUCKET_ID);
folderTitle = getArguments().getString(KEY_FOLDER_TITLE);
maxSelection = getArguments().getInt(KEY_MAX_SELECTION);
showCamera = !getArguments().getBoolean(KEY_HIDE_CAMERA);
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
}
@@ -120,16 +126,14 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
@Override
public void onPrepareOptionsMenu(@NonNull Menu menu) {
requireActivity().getMenuInflater().inflate(R.menu.mediapicker_default, menu);
if (showCamera) {
requireActivity().getMenuInflater().inflate(R.menu.mediapicker_default, menu);
}
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.mediapicker_menu_camera:
controller.onCameraSelected();
return true;
}
if (item.getItemId() == R.id.mediapicker_menu_camera) { controller.onCameraSelected(); return true; }
return false;
}

View File

@@ -111,11 +111,12 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
public static final String EXTRA_RESULT = "result";
private static final String KEY_RECIPIENT = "recipient_id";
private static final String KEY_BODY = "body";
private static final String KEY_MEDIA = "media";
private static final String KEY_TRANSPORT = "transport";
private static final String KEY_IS_CAMERA = "is_camera";
private static final String KEY_RECIPIENT = "recipient_id";
private static final String KEY_RECIPIENTS = "recipient_ids";
private static final String KEY_BODY = "body";
private static final String KEY_MEDIA = "media";
private static final String KEY_TRANSPORT = "transport";
private static final String KEY_IS_CAMERA = "is_camera";
private static final String TAG_FOLDER_PICKER = "folder_picker";
private static final String TAG_ITEM_PICKER = "item_picker";
@@ -195,6 +196,20 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
return intent;
}
public static Intent buildShareIntent(@NonNull Context context,
@NonNull List<Media> media,
@NonNull List<RecipientId> recipientIds,
@NonNull CharSequence body,
@NonNull TransportOption transportOption)
{
Intent intent = new Intent(context, MediaSendActivity.class);
intent.putParcelableArrayListExtra(KEY_MEDIA, new ArrayList<>(media));
intent.putExtra(KEY_TRANSPORT, transportOption);
intent.putExtra(KEY_BODY, body == null ? "" : body);
intent.putParcelableArrayListExtra(KEY_RECIPIENTS, new ArrayList<>(recipientIds));
return intent;
}
@Override
protected void attachBaseContext(@NonNull Context newBase) {
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
@@ -332,7 +347,18 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
initViewModel();
revealButton.setOnClickListener(v -> viewModel.onRevealButtonToggled());
continueButton.setOnClickListener(v -> navigateToContactSelect());
List<RecipientId> recipientIds = getIntent().getParcelableArrayListExtra(KEY_RECIPIENTS);
continueButton.setOnClickListener(v -> {
continueButton.setEnabled(false);
if (recipientIds == null || recipientIds.isEmpty()) {
navigateToContactSelect();
} else {
SimpleTask.run(getLifecycle(),
() -> Stream.of(recipientIds).map(Recipient::resolved).toList(),
this::onCameraContactsSendClicked);
}
});
}
@Override
@@ -551,7 +577,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
SimpleProgressDialog.DismissibleDialog dialog = SimpleProgressDialog.showDelayed(this, 300, 0);
viewModel.onSendClicked(buildModelsToTransform(fragment), recipients, composeText.getMentions()).observe(this, result -> {
dialog.dismiss();
finish();
setActivityResultAndFinish(result);
});
} else {
throw new AssertionError("No editor fragment available!");

View File

@@ -88,7 +88,7 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder {
conversationItem = (ConversationItem) receivedStub.inflate();
}
}
conversationItem.bind(lifecycleOwner, conversationMessage, Optional.absent(), Optional.absent(), glideRequests, Locale.getDefault(), new HashSet<>(), conversationMessage.getMessageRecord().getRecipient(), null, false);
conversationItem.bind(lifecycleOwner, conversationMessage, Optional.absent(), Optional.absent(), glideRequests, Locale.getDefault(), new HashSet<>(), conversationMessage.getMessageRecord().getRecipient(), null, false, false);
}
private void bindErrorState(MessageRecord messageRecord) {

View File

@@ -40,7 +40,7 @@ public class ApplicationMigrations {
private static final int LEGACY_CANONICAL_VERSION = 455;
public static final int CURRENT_VERSION = 25;
public static final int CURRENT_VERSION = 26;
private static final class Version {
static final int LEGACY = 1;
@@ -68,6 +68,7 @@ public class ApplicationMigrations {
static final int BACKUP_NOTIFICATION = 23;
static final int GV1_MIGRATION = 24;
static final int USER_NOTIFICATION = 25;
static final int DAY_BY_DAY_STICKERS = 26;
}
/**
@@ -286,6 +287,10 @@ public class ApplicationMigrations {
jobs.put(Version.USER_NOTIFICATION, new UserNotificationMigrationJob());
}
if (lastSeenVersion < Version.DAY_BY_DAY_STICKERS) {
jobs.put(Version.DAY_BY_DAY_STICKERS, new StickerDayByDayMigrationJob());
}
return jobs;
}

View File

@@ -0,0 +1,52 @@
package org.thoughtcrime.securesms.migrations;
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.jobs.StickerPackDownloadJob;
import org.thoughtcrime.securesms.stickers.BlessedPacks;
/**
* Installs Day by Day blessed pack.
*/
public class StickerDayByDayMigrationJob extends MigrationJob {
public static final String KEY = "StickerDayByDayMigrationJob";
StickerDayByDayMigrationJob() {
this(new Parameters.Builder().build());
}
private StickerDayByDayMigrationJob(@NonNull Parameters parameters) {
super(parameters);
}
@Override
public boolean isUiBlocking() {
return false;
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void performMigration() {
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
}
@Override
boolean shouldRetry(@NonNull Exception e) {
return false;
}
public static class Factory implements Job.Factory<StickerDayByDayMigrationJob> {
@Override
public @NonNull StickerDayByDayMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new StickerDayByDayMigrationJob(parameters);
}
}
}

View File

@@ -29,7 +29,6 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.provider.ContactsContract;
import android.provider.OpenableColumns;
import android.text.TextUtils;
import android.util.Pair;
import android.view.View;
import android.widget.Toast;
@@ -44,7 +43,6 @@ import org.thoughtcrime.securesms.MediaPreviewActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.RemovableEditableMediaView;
@@ -223,7 +221,7 @@ public class AttachmentManager {
@SuppressLint("StaticFieldLeak")
public ListenableFuture<Boolean> setMedia(@NonNull final GlideRequests glideRequests,
@NonNull final Uri uri,
@NonNull final MediaType mediaType,
@NonNull final SlideFactory.MediaType mediaType,
@NonNull final MediaConstraints constraints,
final int width,
final int height)
@@ -286,7 +284,7 @@ public class AttachmentManager {
} else {
Attachment attachment = slide.asAttachment();
result.deferTo(thumbnail.setImageResource(glideRequests, slide, false, true, attachment.getWidth(), attachment.getHeight()));
removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE);
removableMediaView.display(thumbnail, mediaType == SlideFactory.MediaType.IMAGE);
}
attachmentListener.onAttachmentChanged();
@@ -480,58 +478,4 @@ public class AttachmentManager {
void onAttachmentChanged();
}
public enum MediaType {
IMAGE(MediaUtil.IMAGE_JPEG),
GIF(MediaUtil.IMAGE_GIF),
AUDIO(MediaUtil.AUDIO_AAC),
VIDEO(MediaUtil.VIDEO_MP4),
DOCUMENT(MediaUtil.UNKNOWN),
VCARD(MediaUtil.VCARD);
private final String fallbackMimeType;
MediaType(String fallbackMimeType) {
this.fallbackMimeType = fallbackMimeType;
}
public @NonNull Slide createSlide(@NonNull Context context,
@NonNull Uri uri,
@Nullable String fileName,
@Nullable String mimeType,
@Nullable BlurHash blurHash,
long dataSize,
int width,
int height)
{
if (mimeType == null) {
mimeType = "application/octet-stream";
}
switch (this) {
case IMAGE: return new ImageSlide(context, uri, dataSize, width, height, blurHash);
case GIF: return new GifSlide(context, uri, dataSize, width, height);
case AUDIO: return new AudioSlide(context, uri, dataSize, false);
case VIDEO: return new VideoSlide(context, uri, dataSize);
case VCARD:
case DOCUMENT: return new DocumentSlide(context, uri, mimeType, dataSize, fileName);
default: throw new AssertionError("unrecognized enum");
}
}
public static @Nullable MediaType from(final @Nullable String mimeType) {
if (TextUtils.isEmpty(mimeType)) return null;
if (MediaUtil.isGif(mimeType)) return GIF;
if (MediaUtil.isImageType(mimeType)) return IMAGE;
if (MediaUtil.isAudioType(mimeType)) return AUDIO;
if (MediaUtil.isVideoType(mimeType)) return VIDEO;
if (MediaUtil.isVcard(mimeType)) return VCARD;
return DOCUMENT;
}
public String toFallbackMimeType() {
return fallbackMimeType;
}
}
}

View File

@@ -15,22 +15,26 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.providers.DeprecatedPersistentBlobProvider;
import org.thoughtcrime.securesms.providers.PartProvider;
import org.thoughtcrime.securesms.wallpaper.WallpaperStorage;
import java.io.IOException;
import java.io.InputStream;
public class PartAuthority {
private static final String AUTHORITY = BuildConfig.APPLICATION_ID;
private static final String PART_URI_STRING = "content://" + AUTHORITY + "/part";
private static final String STICKER_URI_STRING = "content://" + AUTHORITY + "/sticker";
private static final Uri PART_CONTENT_URI = Uri.parse(PART_URI_STRING);
private static final Uri STICKER_CONTENT_URI = Uri.parse(STICKER_URI_STRING);
private static final String AUTHORITY = BuildConfig.APPLICATION_ID;
private static final String PART_URI_STRING = "content://" + AUTHORITY + "/part";
private static final String STICKER_URI_STRING = "content://" + AUTHORITY + "/sticker";
private static final String WALLPAPER_URI_STRING = "content://" + AUTHORITY + "/wallpaper";
private static final Uri PART_CONTENT_URI = Uri.parse(PART_URI_STRING);
private static final Uri STICKER_CONTENT_URI = Uri.parse(STICKER_URI_STRING);
private static final Uri WALLPAPER_CONTENT_URI = Uri.parse(WALLPAPER_URI_STRING);
private static final int PART_ROW = 1;
private static final int PERSISTENT_ROW = 2;
private static final int BLOB_ROW = 3;
private static final int STICKER_ROW = 4;
private static final int WALLPAPER_ROW = 5;
private static final UriMatcher uriMatcher;
@@ -38,6 +42,7 @@ public class PartAuthority {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, "part/*/#", PART_ROW);
uriMatcher.addURI(AUTHORITY, "sticker/#", STICKER_ROW);
uriMatcher.addURI(AUTHORITY, "wallpaper/*", WALLPAPER_ROW);
uriMatcher.addURI(DeprecatedPersistentBlobProvider.AUTHORITY, DeprecatedPersistentBlobProvider.EXPECTED_PATH_OLD, PERSISTENT_ROW);
uriMatcher.addURI(DeprecatedPersistentBlobProvider.AUTHORITY, DeprecatedPersistentBlobProvider.EXPECTED_PATH_NEW, PERSISTENT_ROW);
uriMatcher.addURI(BlobProvider.AUTHORITY, BlobProvider.PATH, BLOB_ROW);
@@ -59,6 +64,7 @@ public class PartAuthority {
case STICKER_ROW: return DatabaseFactory.getStickerDatabase(context).getStickerStream(ContentUris.parseId(uri));
case PERSISTENT_ROW: return DeprecatedPersistentBlobProvider.getInstance(context).getStream(context, ContentUris.parseId(uri));
case BLOB_ROW: return BlobProvider.getInstance().getStream(context, uri);
case WALLPAPER_ROW: return WallpaperStorage.read(context, getWallpaperFilename(uri));
default: return context.getContentResolver().openInputStream(uri);
}
} catch (SecurityException se) {
@@ -138,6 +144,14 @@ public class PartAuthority {
return ContentUris.withAppendedId(STICKER_CONTENT_URI, id);
}
public static Uri getWallpaperUri(String filename) {
return Uri.withAppendedPath(WALLPAPER_CONTENT_URI, filename);
}
public static String getWallpaperFilename(Uri uri) {
return uri.getPathSegments().get(1);
}
public static boolean isLocalUri(final @NonNull Uri uri) {
int match = uriMatcher.match(uri);
switch (match) {

View File

@@ -0,0 +1,174 @@
package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.text.TextUtils;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.IOException;
/**
* SlideFactory encapsulates logic related to constructing slides from a set of paramaeters as defined
* by {@link SlideFactory#getSlide}.
*/
public final class SlideFactory {
private static final String TAG = Log.tag(SlideFactory.class);
private SlideFactory() {
}
/**
* Generates a slide from the given parameters.
*
* @param context Application context
* @param contentType The contentType of the given Uri
* @param uri The Uri pointing to the resource to create a slide out of
* @param width (Optional) width, can be 0.
* @param height (Optional) height, can be 0.
*
* @return A Slide with all the information we can gather about it.
*/
@WorkerThread
public static @Nullable Slide getSlide(@NonNull Context context, @Nullable String contentType, @NonNull Uri uri, int width, int height) {
MediaType mediaType = MediaType.from(contentType);
try {
if (PartAuthority.isLocalUri(uri)) {
return getManuallyCalculatedSlideInfo(context, mediaType, uri, width, height);
} else {
Slide result = getContentResolverSlideInfo(context, mediaType, uri, width, height);
if (result == null) return getManuallyCalculatedSlideInfo(context, mediaType, uri, width, height);
else return result;
}
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
}
private static @Nullable Slide getContentResolverSlideInfo(@NonNull Context context, @NonNull MediaType mediaType, @NonNull Uri uri, int width, int height) {
Cursor cursor = null;
long start = System.currentTimeMillis();
try {
cursor = context.getContentResolver().query(uri, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
String fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
long fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
String mimeType = context.getContentResolver().getType(uri);
if (width == 0 || height == 0) {
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, mimeType, uri);
width = dimens.first;
height = dimens.second;
}
Log.d(TAG, "remote slide with size " + fileSize + " took " + (System.currentTimeMillis() - start) + "ms");
return mediaType.createSlide(context, uri, fileName, mimeType, null, fileSize, width, height);
}
} finally {
if (cursor != null) cursor.close();
}
return null;
}
private static @NonNull Slide getManuallyCalculatedSlideInfo(@NonNull Context context, @NonNull MediaType mediaType, @NonNull Uri uri, int width, int height) throws IOException {
long start = System.currentTimeMillis();
Long mediaSize = null;
String fileName = null;
String mimeType = null;
if (PartAuthority.isLocalUri(uri)) {
mediaSize = PartAuthority.getAttachmentSize(context, uri);
fileName = PartAuthority.getAttachmentFileName(context, uri);
mimeType = PartAuthority.getAttachmentContentType(context, uri);
}
if (mediaSize == null) {
mediaSize = MediaUtil.getMediaSize(context, uri);
}
if (mimeType == null) {
mimeType = MediaUtil.getMimeType(context, uri);
}
if (width == 0 || height == 0) {
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, mimeType, uri);
width = dimens.first;
height = dimens.second;
}
Log.d(TAG, "local slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms");
return mediaType.createSlide(context, uri, fileName, mimeType, null, mediaSize, width, height);
}
public enum MediaType {
IMAGE(MediaUtil.IMAGE_JPEG),
GIF(MediaUtil.IMAGE_GIF),
AUDIO(MediaUtil.AUDIO_AAC),
VIDEO(MediaUtil.VIDEO_MP4),
DOCUMENT(MediaUtil.UNKNOWN),
VCARD(MediaUtil.VCARD);
private final String fallbackMimeType;
MediaType(String fallbackMimeType) {
this.fallbackMimeType = fallbackMimeType;
}
public @NonNull Slide createSlide(@NonNull Context context,
@NonNull Uri uri,
@Nullable String fileName,
@Nullable String mimeType,
@Nullable BlurHash blurHash,
long dataSize,
int width,
int height)
{
if (mimeType == null) {
mimeType = "application/octet-stream";
}
switch (this) {
case IMAGE: return new ImageSlide(context, uri, dataSize, width, height, blurHash);
case GIF: return new GifSlide(context, uri, dataSize, width, height);
case AUDIO: return new AudioSlide(context, uri, dataSize, false);
case VIDEO: return new VideoSlide(context, uri, dataSize);
case VCARD:
case DOCUMENT: return new DocumentSlide(context, uri, mimeType, dataSize, fileName);
default: throw new AssertionError("unrecognized enum");
}
}
public static @Nullable MediaType from(final @Nullable String mimeType) {
if (TextUtils.isEmpty(mimeType)) return null;
if (MediaUtil.isGif(mimeType)) return GIF;
if (MediaUtil.isImageType(mimeType)) return IMAGE;
if (MediaUtil.isAudioType(mimeType)) return AUDIO;
if (MediaUtil.isVideoType(mimeType)) return VIDEO;
if (MediaUtil.isVcard(mimeType)) return VCARD;
return DOCUMENT;
}
public String toFallbackMimeType() {
return fallbackMimeType;
}
}
}

View File

@@ -198,7 +198,7 @@ public class PinRestoreEntryFragment extends LoggingFragment {
}))
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> {
String body = SupportEmailUtil.generateSupportEmailBody(requireContext(),
getString(R.string.PinRestoreEntryFragment_signal_registration_need_help_with_pin),
R.string.PinRestoreEntryFragment_signal_registration_need_help_with_pin,
null,
null);
CommunicationActions.openEmail(requireContext(),

View File

@@ -77,6 +77,7 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
private static final String PREFERENCE_CATEGORY_BLOCKED = "preference_category_blocked";
private static final String PREFERENCE_UNIDENTIFIED_LEARN_MORE = "pref_unidentified_learn_more";
private static final String PREFERENCE_INCOGNITO_LEARN_MORE = "pref_incognito_learn_more";
private static final String PREFERENCE_WHO_CAN_SEE_PHONE_NUMBER = "pref_who_can_see_phone_number";
private static final String PREFERENCE_WHO_CAN_FIND_BY_PHONE_NUMBER = "pref_who_can_find_by_phone_number";
@@ -107,6 +108,7 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
this.findPreference(TextSecurePreferences.SHOW_UNIDENTIFIED_DELIVERY_INDICATORS).setOnPreferenceChangeListener(new ShowUnidentifiedDeliveryIndicatorsChangedListener());
this.findPreference(TextSecurePreferences.UNIVERSAL_UNIDENTIFIED_ACCESS).setOnPreferenceChangeListener(new UniversalUnidentifiedAccessChangedListener());
this.findPreference(PREFERENCE_UNIDENTIFIED_LEARN_MORE).setOnPreferenceClickListener(new UnidentifiedLearnMoreClickListener());
this.findPreference(PREFERENCE_INCOGNITO_LEARN_MORE).setOnPreferenceClickListener(new IncognitoLearnMoreClickListener());
disablePassphrase.setOnPreferenceChangeListener(new DisablePassphraseClickListener());
if (FeatureFlags.phoneNumberPrivacy()) {
@@ -463,6 +465,14 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
}
}
private class IncognitoLearnMoreClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
CommunicationActions.openBrowserLink(preference.getContext(), "https://support.signal.org/hc/en-us/articles/360055276112");
return true;
}
}
private class RegistrationLockV2ChangedListener implements Preference.OnPreferenceChangeListener {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {

View File

@@ -8,18 +8,27 @@ import androidx.preference.ListPreference;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ActivityTransitionUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity;
import java.util.Arrays;
public class AppearancePreferenceFragment extends ListSummaryPreferenceFragment {
private static final String WALLPAPER_PREF = "pref_wallpaper";
@Override
public void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
this.findPreference(TextSecurePreferences.THEME_PREF).setOnPreferenceChangeListener(new ListSummaryListener());
this.findPreference(TextSecurePreferences.LANGUAGE_PREF).setOnPreferenceChangeListener(new ListSummaryListener());
this.findPreference(WALLPAPER_PREF).setOnPreferenceClickListener(preference -> {
startActivity(ChatWallpaperActivity.createIntent(requireContext()));
ActivityTransitionUtil.setSlideInTransition(requireActivity());
return true;
});
initializeListSummary((ListPreference)findPreference(TextSecurePreferences.THEME_PREF));
initializeListSummary((ListPreference)findPreference(TextSecurePreferences.LANGUAGE_PREF));
}

View File

@@ -18,7 +18,7 @@ import java.util.Objects;
public final class ProfileName implements Parcelable {
public static final ProfileName EMPTY = new ProfileName("", "");
public static final int MAX_PART_LENGTH = (ProfileCipher.NAME_PADDED_LENGTH - 1) / 2;
public static final int MAX_PART_LENGTH = (ProfileCipher.MAX_POSSIBLE_NAME_LENGTH - 1) / 2;
private final String givenName;
private final String familyName;

View File

@@ -17,16 +17,17 @@ import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
/**
* Shows editing screen for your profile during registration. Also handles group name editing.
*/
@SuppressLint("StaticFieldLeak")
public class EditProfileActivity extends BaseActivity implements EditProfileFragment.Controller {
public static final String NEXT_INTENT = "next_intent";
public static final String EXCLUDE_SYSTEM = "exclude_system";
public static final String DISPLAY_USERNAME = "display_username";
public static final String NEXT_BUTTON_TEXT = "next_button_text";
public static final String SHOW_TOOLBAR = "show_back_arrow";
public static final String GROUP_ID = "group_id";
public static final String START_AT_USERNAME = "start_at_username";
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
@@ -39,7 +40,6 @@ public class EditProfileActivity extends BaseActivity implements EditProfileFrag
public static @NonNull Intent getIntentForUserProfileEdit(@NonNull Context context) {
Intent intent = new Intent(context, EditProfileActivity.class);
intent.putExtra(EditProfileActivity.EXCLUDE_SYSTEM, true);
intent.putExtra(EditProfileActivity.DISPLAY_USERNAME, true);
intent.putExtra(EditProfileActivity.NEXT_BUTTON_TEXT, R.string.save);
return intent;
}
@@ -52,14 +52,6 @@ public class EditProfileActivity extends BaseActivity implements EditProfileFrag
return intent;
}
public static @NonNull Intent getIntentForUsernameEdit(@NonNull Context context) {
Intent intent = new Intent(context, EditProfileActivity.class);
intent.putExtra(EditProfileActivity.SHOW_TOOLBAR, true);
intent.putExtra(EditProfileActivity.DISPLAY_USERNAME, true);
intent.putExtra(EditProfileActivity.START_AT_USERNAME, true);
return intent;
}
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
@@ -73,13 +65,6 @@ public class EditProfileActivity extends BaseActivity implements EditProfileFrag
NavGraph graph = Navigation.findNavController(this, R.id.nav_host_fragment).getGraph();
Navigation.findNavController(this, R.id.nav_host_fragment).setGraph(graph, extras != null ? extras : new Bundle());
if (extras != null &&
extras.getBoolean(DISPLAY_USERNAME, false) &&
extras.getBoolean(START_AT_USERNAME, false)) {
NavDirections action = EditProfileFragmentDirections.actionEditUsername();
Navigation.findNavController(this, R.id.nav_host_fragment).navigate(action);
}
}
}

View File

@@ -18,11 +18,8 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.Toolbar;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.dd.CircularProgressButton;
@@ -39,6 +36,7 @@ import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFrag
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.profiles.manage.EditProfileNameFragment;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.util.CommunicationActions;
@@ -53,7 +51,6 @@ import java.io.IOException;
import java.io.InputStream;
import static android.app.Activity.RESULT_OK;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.DISPLAY_USERNAME;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.EXCLUDE_SYSTEM;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.GROUP_ID;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.NEXT_BUTTON_TEXT;
@@ -73,9 +70,6 @@ public class EditProfileFragment extends LoggingFragment {
private EditText familyName;
private View reveal;
private TextView preview;
private View usernameLabel;
private View usernameEditButton;
private TextView username;
private Intent nextIntent;
@@ -94,26 +88,8 @@ public class EditProfileFragment extends LoggingFragment {
}
}
public static EditProfileFragment create(boolean excludeSystem,
Intent nextIntent,
boolean displayUsernameField,
@StringRes int nextButtonText) {
EditProfileFragment fragment = new EditProfileFragment();
Bundle args = new Bundle();
args.putBoolean(EXCLUDE_SYSTEM, excludeSystem);
args.putParcelable(NEXT_INTENT, nextIntent);
args.putBoolean(DISPLAY_USERNAME, displayUsernameField);
args.putInt(NEXT_BUTTON_TEXT, nextButtonText);
fragment.setArguments(args);
return fragment;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.profile_create_fragment, container, false);
}
@@ -125,13 +101,6 @@ public class EditProfileFragment extends LoggingFragment {
initializeViewModel(requireArguments().getBoolean(EXCLUDE_SYSTEM, false), groupId, savedInstanceState != null);
initializeProfileAvatar();
initializeProfileName();
initializeUsername();
}
@Override
public void onResume() {
super.onResume();
viewModel.refreshUsername();
}
@Override
@@ -200,17 +169,8 @@ public class EditProfileFragment extends LoggingFragment {
this.finishButton = view.findViewById(R.id.finish_button);
this.reveal = view.findViewById(R.id.reveal);
this.preview = view.findViewById(R.id.name_preview);
this.username = view.findViewById(R.id.profile_overview_username);
this.usernameEditButton = view.findViewById(R.id.profile_overview_username_edit_button);
this.usernameLabel = view.findViewById(R.id.profile_overview_username_label);
this.nextIntent = arguments.getParcelable(NEXT_INTENT);
if (FeatureFlags.usernames() && arguments.getBoolean(DISPLAY_USERNAME, false)) {
username.setVisibility(View.VISIBLE);
usernameEditButton.setVisibility(View.VISIBLE);
usernameLabel.setVisibility(View.VISIBLE);
}
this.avatar.setOnClickListener(v -> startAvatarSelection());
view.findViewById(R.id.mms_group_hint)
@@ -228,12 +188,14 @@ public class EditProfileFragment extends LoggingFragment {
view.findViewById(R.id.description_text).setVisibility(View.GONE);
view.<ImageView>findViewById(R.id.avatar_placeholder).setImageResource(R.drawable.ic_group_outline_40);
} else {
EditTextUtil.addGraphemeClusterLimitFilter(givenName, EditProfileNameFragment.NAME_MAX_GLYPHS);
EditTextUtil.addGraphemeClusterLimitFilter(familyName, EditProfileNameFragment.NAME_MAX_GLYPHS);
this.givenName.addTextChangedListener(new AfterTextChanged(s -> {
trimInPlace(s);
EditProfileNameFragment.trimFieldToMaxByteLength(s);
viewModel.setGivenName(s.toString());
}));
this.familyName.addTextChangedListener(new AfterTextChanged(s -> {
trimInPlace(s);
EditProfileNameFragment.trimFieldToMaxByteLength(s);
viewModel.setFamilyName(s.toString());
}));
LearnMoreTextView descriptionText = view.findViewById(R.id.description_text);
@@ -249,11 +211,6 @@ public class EditProfileFragment extends LoggingFragment {
this.finishButton.setText(arguments.getInt(NEXT_BUTTON_TEXT, R.string.CreateProfileActivity_next));
this.usernameEditButton.setOnClickListener(v -> {
NavDirections action = EditProfileFragmentDirections.actionEditUsername();
Navigation.findNavController(v).navigate(action);
});
if (arguments.getBoolean(SHOW_TOOLBAR, true)) {
this.toolbar.setVisibility(View.VISIBLE);
this.toolbar.setNavigationOnClickListener(v -> requireActivity().finish());
@@ -285,10 +242,6 @@ public class EditProfileFragment extends LoggingFragment {
});
}
private void initializeUsername() {
viewModel.username().observe(getViewLifecycleOwner(), this::onUsernameChanged);
}
private static void updateFieldIfNeeded(@NonNull EditText field, @NonNull String value) {
String fieldTrimmed = field.getText().toString().trim();
String valueTrimmed = value.trim();
@@ -304,10 +257,6 @@ public class EditProfileFragment extends LoggingFragment {
}
}
private void onUsernameChanged(@NonNull Optional<String> username) {
this.username.setText(username.transform(s -> "@" + s).or(""));
}
private void startAvatarSelection() {
AvatarSelectionBottomSheetDialogFragment.create(viewModel.canRemoveProfilePhoto(),
true,
@@ -375,14 +324,6 @@ public class EditProfileFragment extends LoggingFragment {
animation.start();
}
private static void trimInPlace(Editable s) {
int trimmedLength = StringUtil.trimToFit(s.toString(), ProfileName.MAX_PART_LENGTH).length();
if (s.length() > trimmedLength) {
s.delete(trimmedLength, s.length());
}
}
public interface Controller {
void onProfileNameUploadCompleted();
}

View File

@@ -27,7 +27,6 @@ class EditProfileViewModel extends ViewModel {
private final LiveData<ProfileName> internalProfileName = LiveDataUtil.combineLatest(trimmedGivenName, trimmedFamilyName, ProfileName::fromParts);
private final MutableLiveData<byte[]> internalAvatar = new MutableLiveData<>();
private final MutableLiveData<byte[]> originalAvatar = new MutableLiveData<>();
private final MutableLiveData<Optional<String>> internalUsername = new MutableLiveData<>();
private final MutableLiveData<String> originalDisplayName = new MutableLiveData<>();
private final LiveData<Boolean> isFormValid;
private final EditProfileRepository repository;
@@ -77,10 +76,6 @@ class EditProfileViewModel extends ViewModel {
return Transformations.distinctUntilChanged(internalAvatar);
}
public LiveData<Optional<String>> username() {
return internalUsername;
}
public boolean hasAvatar() {
return internalAvatar.getValue() != null;
}
@@ -105,10 +100,6 @@ class EditProfileViewModel extends ViewModel {
internalAvatar.setValue(avatar);
}
public void refreshUsername() {
repository.getCurrentUsername(internalUsername::postValue);
}
public void submitProfile(Consumer<EditProfileRepository.UploadResult> uploadResultConsumer) {
ProfileName profileName = isGroup() ? ProfileName.EMPTY : internalProfileName.getValue();
String displayName = isGroup() ? givenName.getValue() : "";

View File

@@ -29,7 +29,7 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
class EditSelfProfileRepository implements EditProfileRepository {
public class EditSelfProfileRepository implements EditProfileRepository {
private static final String TAG = Log.tag(EditSelfProfileRepository.class);

View File

@@ -0,0 +1,132 @@
package org.thoughtcrime.securesms.profiles.manage;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.Editable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import org.signal.core.util.BreakIteratorCompat;
import org.signal.core.util.EditTextUtil;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.StringUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
/**
* Let's you edit the 'About' section of your profile.
*/
public class EditAboutFragment extends Fragment implements ManageProfileActivity.EmojiController {
public static final int ABOUT_MAX_GLYPHS = 100;
public static final int ABOUT_LIMIT_DISPLAY_THRESHOLD = 75;
private static final String KEY_SELECTED_EMOJI = "selected_emoji";
private ImageView emojiView;
private EditText bodyView;
private TextView countView;
private String selectedEmoji;
@Override
public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.edit_about_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
this.emojiView = view.findViewById(R.id.edit_about_emoji);
this.bodyView = view.findViewById(R.id.edit_about_body);
this.countView = view.findViewById(R.id.edit_about_count);
view.<Toolbar>findViewById(R.id.toolbar)
.setNavigationOnClickListener(v -> Navigation.findNavController(view)
.popBackStack());
EditTextUtil.addGraphemeClusterLimitFilter(bodyView, ABOUT_MAX_GLYPHS);
this.bodyView.addTextChangedListener(new AfterTextChanged(editable -> {
trimFieldToMaxByteLength(editable);
presentCount(editable.toString());
}));
this.emojiView.setOnClickListener(v -> {
ReactWithAnyEmojiBottomSheetDialogFragment.createForAboutSelection()
.show(requireFragmentManager(), "BOTTOM");
});
view.findViewById(R.id.edit_about_save).setOnClickListener(this::onSaveClicked);
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SELECTED_EMOJI)) {
onEmojiSelected(savedInstanceState.getString(KEY_SELECTED_EMOJI, ""));
} else {
this.bodyView.setText(Recipient.self().getAbout());
onEmojiSelected(Optional.fromNullable(Recipient.self().getAboutEmoji()).or(""));
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
outState.putString(KEY_SELECTED_EMOJI, selectedEmoji);
}
@Override
public void onEmojiSelected(@NonNull String emoji) {
Drawable drawable = EmojiUtil.convertToDrawable(requireContext(), emoji);
if (drawable != null) {
this.emojiView.setImageDrawable(drawable);
this.selectedEmoji = emoji;
}
}
private void presentCount(@NonNull String aboutBody) {
BreakIteratorCompat breakIterator = BreakIteratorCompat.getInstance();
breakIterator.setText(aboutBody);
int glyphCount = breakIterator.countBreaks();
if (glyphCount >= ABOUT_LIMIT_DISPLAY_THRESHOLD) {
this.countView.setVisibility(View.VISIBLE);
this.countView.setText(getResources().getString(R.string.EditAboutFragment_count, glyphCount, ABOUT_MAX_GLYPHS));
} else {
this.countView.setVisibility(View.GONE);
}
}
private void onSaveClicked(View view) {
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
DatabaseFactory.getRecipientDatabase(requireContext()).setAbout(Recipient.self().getId(), bodyView.getText().toString(), selectedEmoji);
ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
return null;
}, (nothing) -> {
Navigation.findNavController(view).popBackStack();
});
}
public static void trimFieldToMaxByteLength(Editable s) {
int trimmedLength = StringUtil.trimToFit(s.toString(), ProfileCipher.MAX_POSSIBLE_ABOUT_LENGTH).length();
if (s.length() > trimmedLength) {
s.delete(trimmedLength, s.length());
}
}
}

View File

@@ -0,0 +1,82 @@
package org.thoughtcrime.securesms.profiles.manage;
import android.os.Bundle;
import android.text.Editable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import org.signal.core.util.EditTextUtil;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.StringUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
/**
* Simple fragment to edit your profile name.
*/
public class EditProfileNameFragment extends Fragment {
public static final int NAME_MAX_GLYPHS = 26;
private EditText givenName;
private EditText familyName;
@Override
public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.edit_profile_name_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
this.givenName = view.findViewById(R.id.edit_profile_name_given_name);
this.familyName = view.findViewById(R.id.edit_profile_name_family_name);
this.givenName.setText(Recipient.self().getProfileName().getGivenName());
this.familyName.setText(Recipient.self().getProfileName().getFamilyName());
view.<Toolbar>findViewById(R.id.toolbar)
.setNavigationOnClickListener(v -> Navigation.findNavController(view)
.popBackStack());
EditTextUtil.addGraphemeClusterLimitFilter(givenName, NAME_MAX_GLYPHS);
EditTextUtil.addGraphemeClusterLimitFilter(familyName, NAME_MAX_GLYPHS);
this.givenName.addTextChangedListener(new AfterTextChanged(EditProfileNameFragment::trimFieldToMaxByteLength));
this.familyName.addTextChangedListener(new AfterTextChanged(EditProfileNameFragment::trimFieldToMaxByteLength));
view.findViewById(R.id.edit_profile_name_save).setOnClickListener(this::onSaveClicked);
}
private void onSaveClicked(View view) {
ProfileName profileName = ProfileName.fromParts(givenName.getText().toString(), familyName.getText().toString());
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
DatabaseFactory.getRecipientDatabase(requireContext()).setProfileName(Recipient.self().getId(), profileName);
ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
return null;
}, (nothing) -> {
Navigation.findNavController(view).popBackStack();
});
}
public static void trimFieldToMaxByteLength(Editable s) {
int trimmedLength = StringUtil.trimToFit(s.toString(), ProfileName.MAX_PART_LENGTH).length();
if (s.length() > trimmedLength) {
s.delete(trimmedLength, s.length());
}
}
}

View File

@@ -0,0 +1,89 @@
package org.thoughtcrime.securesms.profiles.manage;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.navigation.NavDirections;
import androidx.navigation.NavGraph;
import androidx.navigation.Navigation;
import androidx.navigation.fragment.NavHostFragment;
import org.thoughtcrime.securesms.BaseActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.profiles.edit.EditProfileFragmentDirections;
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
/**
* Activity that manages the local user's profile, as accessed via the settings.
*/
public class ManageProfileActivity extends BaseActivity implements ReactWithAnyEmojiBottomSheetDialogFragment.Callback {
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
public static final String START_AT_USERNAME = "start_at_username";
public static @NonNull Intent getIntent(@NonNull Context context) {
return new Intent(context, ManageProfileActivity.class);
}
public static @NonNull Intent getIntentForUsernameEdit(@NonNull Context context) {
Intent intent = new Intent(context, ManageProfileActivity.class);
intent.putExtra(START_AT_USERNAME, true);
return intent;
}
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
dynamicTheme.onCreate(this);
setContentView(R.layout.manage_profile_activity);
if (bundle == null) {
Bundle extras = getIntent().getExtras();
NavGraph graph = Navigation.findNavController(this, R.id.nav_host_fragment).getGraph();
Navigation.findNavController(this, R.id.nav_host_fragment).setGraph(graph, extras != null ? extras : new Bundle());
if (extras != null && extras.getBoolean(START_AT_USERNAME, false)) {
NavDirections action = ManageProfileFragmentDirections.actionManageUsername();
Navigation.findNavController(this, R.id.nav_host_fragment).navigate(action);
}
}
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
}
@Override
public void onReactWithAnyEmojiDialogDismissed() {
}
@Override
public void onReactWithAnyEmojiPageChanged(int page) {
}
@Override
public void onReactWithAnyEmojiSelected(@NonNull String emoji) {
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().getPrimaryNavigationFragment();
Fragment activeFragment = navHostFragment.getChildFragmentManager().getPrimaryNavigationFragment();
if (activeFragment instanceof EmojiController) {
((EmojiController) activeFragment).onEmojiSelected(emoji);
}
}
interface EmojiController {
void onEmojiSelected(@NonNull String emoji);
}
}

View File

@@ -0,0 +1,197 @@
package org.thoughtcrime.securesms.profiles.manage;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.res.ResourcesCompat;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.Navigation;
import com.bumptech.glide.Glide;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.profiles.manage.ManageProfileViewModel.AvatarState;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import static android.app.Activity.RESULT_OK;
public class ManageProfileFragment extends LoggingFragment {
private static final String TAG = Log.tag(ManageProfileFragment.class);
private static final short REQUEST_CODE_SELECT_AVATAR = 31726;
private Toolbar toolbar;
private ImageView avatarView;
private View avatarPlaceholderView;
private TextView profileNameView;
private View profileNameContainer;
private TextView usernameView;
private View usernameContainer;
private TextView aboutView;
private View aboutContainer;
private ImageView aboutEmojiView;
private AlertDialog avatarProgress;
private ManageProfileViewModel viewModel;
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.manage_profile_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
this.toolbar = view.findViewById(R.id.toolbar);
this.avatarView = view.findViewById(R.id.manage_profile_avatar);
this.avatarPlaceholderView = view.findViewById(R.id.manage_profile_avatar_placeholder);
this.profileNameView = view.findViewById(R.id.manage_profile_name);
this.profileNameContainer = view.findViewById(R.id.manage_profile_name_container);
this.usernameView = view.findViewById(R.id.manage_profile_username);
this.usernameContainer = view.findViewById(R.id.manage_profile_username_container);
this.aboutView = view.findViewById(R.id.manage_profile_about);
this.aboutContainer = view.findViewById(R.id.manage_profile_about_container);
this.aboutEmojiView = view.findViewById(R.id.manage_profile_about_icon);
initializeViewModel();
this.toolbar.setNavigationOnClickListener(v -> requireActivity().finish());
this.avatarView.setOnClickListener(v -> onAvatarClicked());
this.profileNameContainer.setOnClickListener(v -> {
Navigation.findNavController(v).navigate(ManageProfileFragmentDirections.actionManageProfileName());
});
this.usernameContainer.setOnClickListener(v -> {
Navigation.findNavController(v).navigate(ManageProfileFragmentDirections.actionManageUsername());
});
this.aboutContainer.setOnClickListener(v -> {
Navigation.findNavController(v).navigate(ManageProfileFragmentDirections.actionManageAbout());
});
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_SELECT_AVATAR && resultCode == RESULT_OK) {
if (data != null && data.getBooleanExtra("delete", false)) {
viewModel.onAvatarSelected(requireContext(), null);
return;
}
Media result = data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA);
viewModel.onAvatarSelected(requireContext(), result);
}
}
private void initializeViewModel() {
viewModel = ViewModelProviders.of(this, new ManageProfileViewModel.Factory()).get(ManageProfileViewModel.class);
viewModel.getAvatar().observe(getViewLifecycleOwner(), this::presentAvatar);
viewModel.getProfileName().observe(getViewLifecycleOwner(), this::presentProfileName);
viewModel.getEvents().observe(getViewLifecycleOwner(), this::presentEvent);
viewModel.getAbout().observe(getViewLifecycleOwner(), this::presentAbout);
viewModel.getAboutEmoji().observe(getViewLifecycleOwner(), this::presentAboutEmoji);
if (viewModel.shouldShowUsername()) {
viewModel.getUsername().observe(getViewLifecycleOwner(), this::presentUsername);
} else {
usernameContainer.setVisibility(View.GONE);
}
}
private void presentAvatar(@NonNull AvatarState avatarState) {
if (avatarState.getAvatar() == null) {
avatarView.setImageDrawable(null);
avatarPlaceholderView.setVisibility(View.VISIBLE);
} else {
avatarPlaceholderView.setVisibility(View.GONE);
Glide.with(this)
.load(avatarState.getAvatar())
.circleCrop()
.into(avatarView);
}
if (avatarProgress == null && avatarState.getLoadingState() == ManageProfileViewModel.LoadingState.LOADING) {
avatarProgress = SimpleProgressDialog.show(requireContext());
} else if (avatarProgress != null && avatarState.getLoadingState() == ManageProfileViewModel.LoadingState.LOADED) {
avatarProgress.dismiss();
}
}
private void presentProfileName(@Nullable ProfileName profileName) {
if (profileName == null || profileName.isEmpty()) {
profileNameView.setText(R.string.ManageProfileFragment_profile_name);
} else {
profileNameView.setText(profileName.toString());
}
}
private void presentUsername(@Nullable String username) {
if (username == null || username.isEmpty()) {
usernameView.setText(R.string.ManageProfileFragment_username);
usernameView.setTextColor(requireContext().getResources().getColor(R.color.signal_text_secondary));
} else {
usernameView.setText(username);
usernameView.setTextColor(requireContext().getResources().getColor(R.color.signal_text_primary));
}
}
private void presentAbout(@Nullable String about) {
if (about == null || about.isEmpty()) {
aboutView.setText(R.string.ManageProfileFragment_about);
aboutView.setTextColor(requireContext().getResources().getColor(R.color.signal_text_secondary));
} else {
aboutView.setText(about);
aboutView.setTextColor(requireContext().getResources().getColor(R.color.signal_text_primary));
}
}
private void presentAboutEmoji(@NonNull String aboutEmoji) {
if (aboutEmoji == null || aboutEmoji.isEmpty()) {
aboutEmojiView.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_compose_24, null));
} else {
Drawable emoji = EmojiUtil.convertToDrawable(requireContext(), aboutEmoji);
if (emoji != null) {
aboutEmojiView.setImageDrawable(emoji);
} else {
aboutEmojiView.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_compose_24, null));
}
}
}
private void presentEvent(@NonNull ManageProfileViewModel.Event event) {
if (event == ManageProfileViewModel.Event.AVATAR_FAILURE) {
Toast.makeText(requireContext(), R.string.ManageProfileFragment_failed_to_set_avatar, Toast.LENGTH_LONG).show();
}
}
private void onAvatarClicked() {
AvatarSelectionBottomSheetDialogFragment.create(viewModel.canRemoveAvatar(),
true,
REQUEST_CODE_SELECT_AVATAR,
false)
.show(getChildFragmentManager(), null);
}
}

View File

@@ -0,0 +1,189 @@
package org.thoughtcrime.securesms.profiles.manage;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.whispersystems.signalservice.api.util.StreamDetails;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
class ManageProfileViewModel extends ViewModel {
private static final String TAG = Log.tag(ManageProfileViewModel.class);
private final MutableLiveData<AvatarState> avatar;
private final MutableLiveData<ProfileName> profileName;
private final MutableLiveData<String> username;
private final MutableLiveData<String> about;
private final MutableLiveData<String> aboutEmoji;
private final SingleLiveEvent<Event> events;
private final RecipientForeverObserver observer;
public ManageProfileViewModel() {
this.avatar = new MutableLiveData<>();
this.profileName = new MutableLiveData<>();
this.username = new MutableLiveData<>();
this.about = new MutableLiveData<>();
this.aboutEmoji = new MutableLiveData<>();
this.events = new SingleLiveEvent<>();
this.observer = this::onRecipientChanged;
SignalExecutors.BOUNDED.execute(() -> {
onRecipientChanged(Recipient.self().fresh());
StreamDetails details = AvatarHelper.getSelfProfileAvatarStream(ApplicationDependencies.getApplication());
if (details != null) {
try {
avatar.postValue(AvatarState.loaded(StreamUtil.readFully(details.getStream())));
} catch (IOException e) {
Log.w(TAG, "Failed to read avatar!");
avatar.postValue(AvatarState.none());
}
} else {
avatar.postValue(AvatarState.none());
}
ApplicationDependencies.getJobManager().add(RetrieveProfileJob.forRecipient(Recipient.self().getId()));
});
Recipient.self().live().observeForever(observer);
}
public @NonNull LiveData<AvatarState> getAvatar() {
return avatar;
}
public @NonNull LiveData<ProfileName> getProfileName() {
return profileName;
}
public @NonNull LiveData<String> getUsername() {
return username;
}
public @NonNull LiveData<String> getAbout() {
return about;
}
public @NonNull LiveData<String> getAboutEmoji() {
return aboutEmoji;
}
public @NonNull LiveData<Event> getEvents() {
return events;
}
public boolean shouldShowUsername() {
return FeatureFlags.usernames();
}
public void onAvatarSelected(@NonNull Context context, @Nullable Media media) {
if (media == null) {
SignalExecutors.BOUNDED.execute(() -> {
AvatarHelper.delete(context, Recipient.self().getId());
avatar.postValue(AvatarState.none());
ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
});
} else {
SignalExecutors.BOUNDED.execute(() -> {
try {
InputStream stream = BlobProvider.getInstance().getStream(context, media.getUri());
byte[] data = StreamUtil.readFully(stream);
AvatarHelper.setAvatar(context, Recipient.self().getId(), new ByteArrayInputStream(data));
avatar.postValue(AvatarState.loaded(data));
ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
} catch (IOException e) {
Log.w(TAG, "Failed to save avatar!", e);
events.postValue(Event.AVATAR_FAILURE);
}
});
}
}
public boolean canRemoveAvatar() {
return avatar.getValue() != null;
}
private void onRecipientChanged(@NonNull Recipient recipient) {
profileName.postValue(recipient.getProfileName());
username.postValue(recipient.getUsername().orNull());
about.postValue(recipient.getAbout());
aboutEmoji.postValue(recipient.getAboutEmoji());
}
@Override
protected void onCleared() {
Recipient.self().live().removeForeverObserver(observer);
}
public static class AvatarState {
private final byte[] avatar;
private final LoadingState loadingState;
public AvatarState(@Nullable byte[] avatar, @NonNull LoadingState loadingState) {
this.avatar = avatar;
this.loadingState = loadingState;
}
private static @NonNull AvatarState none() {
return new AvatarState(null, LoadingState.LOADED);
}
private static @NonNull AvatarState loaded(@Nullable byte[] avatar) {
return new AvatarState(avatar, LoadingState.LOADED);
}
private static @NonNull AvatarState loading(@Nullable byte[] avatar) {
return new AvatarState(avatar, LoadingState.LOADING);
}
public @Nullable byte[] getAvatar() {
return avatar;
}
public LoadingState getLoadingState() {
return loadingState;
}
}
public enum LoadingState {
LOADING, LOADED
}
enum Event {
AVATAR_FAILURE
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
@Override
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return Objects.requireNonNull(modelClass.cast(new ManageProfileViewModel()));
}
}
}

View File

@@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.usernames.username;
package org.thoughtcrime.securesms.profiles.manage;
import android.os.Bundle;
import android.view.LayoutInflater;

View File

@@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.usernames.username;
package org.thoughtcrime.securesms.profiles.manage;
import android.app.Application;

View File

@@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.usernames.username;
package org.thoughtcrime.securesms.profiles.manage;
import android.app.Application;
import android.text.TextUtils;

View File

@@ -45,9 +45,14 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
EmojiPageViewGridAdapter.VariationSelectorListener
{
private static final String REACTION_STORAGE_KEY = "reactions_recent_emoji";
private static final String ABOUT_STORAGE_KEY = EmojiKeyboardProvider.RECENT_STORAGE_KEY;
private static final String ARG_MESSAGE_ID = "arg_message_id";
private static final String ARG_IS_MMS = "arg_is_mms";
private static final String ARG_START_PAGE = "arg_start_page";
private static final String ARG_SHADOWS = "arg_shadows";
private static final String ARG_RECENT_KEY = "arg_recent_key";
private ReactWithAnyEmojiViewModel viewModel;
private TextSwitcher categoryLabel;
@@ -65,6 +70,22 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
args.putLong(ARG_MESSAGE_ID, messageRecord.getId());
args.putBoolean(ARG_IS_MMS, messageRecord.isMms());
args.putInt(ARG_START_PAGE, startingPage);
args.putBoolean(ARG_SHADOWS, false);
args.putString(ARG_RECENT_KEY, REACTION_STORAGE_KEY);
fragment.setArguments(args);
return fragment;
}
public static DialogFragment createForAboutSelection() {
DialogFragment fragment = new ReactWithAnyEmojiBottomSheetDialogFragment();
Bundle args = new Bundle();
args.putLong(ARG_MESSAGE_ID, -1);
args.putBoolean(ARG_IS_MMS, false);
args.putInt(ARG_START_PAGE, -1);
args.putBoolean(ARG_SHADOWS, true);
args.putString(ARG_RECENT_KEY, ABOUT_STORAGE_KEY);
fragment.setArguments(args);
return fragment;
@@ -79,10 +100,13 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
boolean shadows = requireArguments().getBoolean(ARG_SHADOWS);
if (ThemeUtil.isDarkTheme(requireContext())) {
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_BottomSheetDialog_Fixed_ReactWithAny);
setStyle(DialogFragment.STYLE_NORMAL, shadows ? R.style.Theme_Signal_BottomSheetDialog_Fixed_ReactWithAny
: R.style.Theme_Signal_BottomSheetDialog_Fixed_ReactWithAny_Shadowless);
} else {
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_Light_BottomSheetDialog_Fixed_ReactWithAny);
setStyle(DialogFragment.STYLE_NORMAL, shadows ? R.style.Theme_Signal_Light_BottomSheetDialog_Fixed_ReactWithAny
: R.style.Theme_Signal_Light_BottomSheetDialog_Fixed_ReactWithAny_Shadowless);
}
super.onCreate(savedInstanceState);
@@ -168,15 +192,18 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
if (savedInstanceState == null) {
FrameLayout container = requireDialog().findViewById(R.id.container);
LayoutInflater layoutInflater = LayoutInflater.from(requireContext());
View statusBarShader = layoutInflater.inflate(R.layout.react_with_any_emoji_status_fade, container, false);
TabLayout categoryTabs = (TabLayout) layoutInflater.inflate(R.layout.react_with_any_emoji_tabs, container, false);
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtil.getStatusBarHeight(container));
statusBarShader.setLayoutParams(params);
container.addView(statusBarShader, 0);
if (!requireArguments().getBoolean(ARG_SHADOWS)) {
View statusBarShader = layoutInflater.inflate(R.layout.react_with_any_emoji_status_fade, container, false);
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtil.getStatusBarHeight(container));
statusBarShader.setLayoutParams(params);
container.addView(statusBarShader, 0);
}
container.addView(categoryTabs);
ViewCompat.setOnApplyWindowInsetsListener(container, (v, insets) -> insets.consumeSystemWindowInsets());
new TabLayoutMediator(categoryTabs, categoryPager, (tab, position) -> {
@@ -203,7 +230,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
private void initializeViewModel() {
Bundle args = requireArguments();
ReactWithAnyEmojiRepository repository = new ReactWithAnyEmojiRepository(requireContext());
ReactWithAnyEmojiRepository repository = new ReactWithAnyEmojiRepository(requireContext(), args.getString(ARG_RECENT_KEY));
ReactWithAnyEmojiViewModel.Factory factory = new ReactWithAnyEmojiViewModel.Factory(reactionsLoader, repository, args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS));
viewModel = ViewModelProviders.of(this, factory).get(ReactWithAnyEmojiViewModel.class);
@@ -212,6 +239,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
@Override
public void onEmojiSelected(String emoji) {
viewModel.onEmojiSelected(emoji);
callback.onReactWithAnyEmojiSelected(emoji);
dismiss();
}
@@ -239,7 +267,8 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
}
private int getStartingPage(boolean firstPageHasContent) {
return requireArguments().getInt(ARG_START_PAGE, firstPageHasContent ? 0 : 1);
int startPage = requireArguments().getInt(ARG_START_PAGE);
return startPage >= 0 ? startPage : (firstPageHasContent ? 0 : 1);
}
private class OnPageChanged extends ViewPager2.OnPageChangeCallback {
@@ -253,5 +282,6 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
public interface Callback {
void onReactWithAnyEmojiDialogDismissed();
void onReactWithAnyEmojiPageChanged(int page);
void onReactWithAnyEmojiSelected(@NonNull String emoji);
}
}

View File

@@ -32,15 +32,13 @@ final class ReactWithAnyEmojiRepository {
private static final String TAG = Log.tag(ReactWithAnyEmojiRepository.class);
private static final String RECENT_STORAGE_KEY = "reactions_recent_emoji";
private final Context context;
private final RecentEmojiPageModel recentEmojiPageModel;
private final Context context;
private final RecentEmojiPageModel recentEmojiPageModel;
private final List<ReactWithAnyEmojiPage> emojiPages;
ReactWithAnyEmojiRepository(@NonNull Context context) {
ReactWithAnyEmojiRepository(@NonNull Context context, @NonNull String storageKey) {
this.context = context;
this.recentEmojiPageModel = new RecentEmojiPageModel(context, RECENT_STORAGE_KEY);
this.recentEmojiPageModel = new RecentEmojiPageModel(context, storageKey);
this.emojiPages = new LinkedList<>();
emojiPages.addAll(Stream.of(EmojiUtil.getDisplayPages())

View File

@@ -36,8 +36,10 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel {
}
void onEmojiSelected(@NonNull String emoji) {
SignalStore.emojiValues().setPreferredVariation(emoji);
repository.addEmojiToMessage(emoji, messageId, isMms);
if (messageId > 0) {
SignalStore.emojiValues().setPreferredVariation(emoji);
repository.addEmojiToMessage(emoji, messageId, isMms);
}
}
static class Factory implements ViewModelProvider.Factory {

View File

@@ -43,6 +43,7 @@ import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.StringUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.libsignal.util.guava.Preconditions;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@@ -106,6 +107,9 @@ public class Recipient {
private final InsightsBannerTier insightsBannerTier;
private final byte[] storageId;
private final MentionSetting mentionSetting;
private final ChatWallpaper wallpaper;
private final String about;
private final String aboutEmoji;
/**
@@ -339,6 +343,9 @@ public class Recipient {
this.groupsV1MigrationCapability = Capability.UNKNOWN;
this.storageId = null;
this.mentionSetting = MentionSetting.ALWAYS_NOTIFY;
this.wallpaper = null;
this.about = null;
this.aboutEmoji = null;
}
public Recipient(@NonNull RecipientId id, @NonNull RecipientDetails details, boolean resolved) {
@@ -381,6 +388,9 @@ public class Recipient {
this.groupsV1MigrationCapability = details.groupsV1MigrationCapability;
this.storageId = details.storageId;
this.mentionSetting = details.mentionSetting;
this.wallpaper = details.wallpaper;
this.about = details.about;
this.aboutEmoji = details.aboutEmoji;
}
public @NonNull RecipientId getId() {
@@ -499,6 +509,15 @@ public class Recipient {
return StringUtil.isolateBidi(name);
}
public @NonNull String getShortDisplayNameIncludingUsername(@NonNull Context context) {
String name = Util.getFirstNonEmpty(getName(context),
getProfileName().getGivenName(),
getDisplayName(context),
getUsername().orNull());
return StringUtil.isolateBidi(name);
}
public @NonNull MaterialColor getColor() {
if (isGroupInternal()) {
return MaterialColor.GROUP;
@@ -834,10 +853,51 @@ public class Recipient {
return unidentifiedAccessMode;
}
public @Nullable ChatWallpaper getWallpaper() {
if (wallpaper != null) {
return wallpaper;
} else {
return SignalStore.wallpaper().getWallpaper();
}
}
public boolean hasOwnWallpaper() {
return wallpaper != null;
}
/**
* A cheap way to check if wallpaper is set without doing any unnecessary proto parsing.
*/
public boolean hasWallpaper() {
return wallpaper != null || SignalStore.wallpaper().hasWallpaperSet();
}
public boolean isSystemContact() {
return contactUri != null;
}
public @Nullable String getAbout() {
return about;
}
public @Nullable String getAboutEmoji() {
return aboutEmoji;
}
public @Nullable String getCombinedAboutAndEmoji() {
if (aboutEmoji != null) {
if (about != null) {
return aboutEmoji + " " + about;
} else {
return aboutEmoji;
}
} else if (about != null) {
return about;
} else {
return null;
}
}
/**
* If this recipient is missing crucial data, this will return a populated copy. Otherwise it
* returns itself.
@@ -952,7 +1012,10 @@ public class Recipient {
groupsV1MigrationCapability == other.groupsV1MigrationCapability &&
insightsBannerTier == other.insightsBannerTier &&
Arrays.equals(storageId, other.storageId) &&
mentionSetting == other.mentionSetting;
mentionSetting == other.mentionSetting &&
Objects.equals(wallpaper, other.wallpaper) &&
Objects.equals(about, other.about) &&
Objects.equals(aboutEmoji, other.aboutEmoji);
}
private static boolean allContentsAreTheSame(@NonNull List<Recipient> a, @NonNull List<Recipient> b) {
@@ -990,7 +1053,6 @@ public class Recipient {
public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
return new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_profile_outline_48);
}
}
private static class MissingAddressError extends AssertionError {

View File

@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.LinkedList;
@@ -65,6 +66,9 @@ public class RecipientDetails {
final InsightsBannerTier insightsBannerTier;
final byte[] storageId;
final MentionSetting mentionSetting;
final ChatWallpaper wallpaper;
final String about;
final String aboutEmoji;
public RecipientDetails(@Nullable String name,
@NonNull Optional<Long> groupAvatarId,
@@ -110,6 +114,9 @@ public class RecipientDetails {
this.insightsBannerTier = settings.getInsightsBannerTier();
this.storageId = settings.getStorageId();
this.mentionSetting = settings.getMentionSetting();
this.wallpaper = settings.getWallpaper();
this.about = settings.getAbout();
this.aboutEmoji = settings.getAboutEmoji();
if (name == null) this.name = settings.getSystemDisplayName();
else this.name = name;
@@ -157,6 +164,9 @@ public class RecipientDetails {
this.groupsV1MigrationCapability = Recipient.Capability.UNKNOWN;
this.storageId = null;
this.mentionSetting = MentionSetting.ALWAYS_NOTIFY;
this.wallpaper = null;
this.about = null;
this.aboutEmoji = null;
}
public static @NonNull RecipientDetails forIndividual(@NonNull Context context, @NonNull RecipientSettings settings) {

View File

@@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.recipients.RecipientExporter;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
@@ -55,6 +56,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
private RecipientDialogViewModel viewModel;
private AvatarImageView avatar;
private TextView fullName;
private TextView about;
private TextView usernameNumber;
private Button messageButton;
private Button secureCallButton;
@@ -102,6 +104,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
avatar = view.findViewById(R.id.rbs_recipient_avatar);
fullName = view.findViewById(R.id.rbs_full_name);
about = view.findViewById(R.id.rbs_about);
usernameNumber = view.findViewById(R.id.rbs_username_number);
messageButton = view.findViewById(R.id.rbs_message_button);
secureCallButton = view.findViewById(R.id.rbs_secure_call_button);
@@ -158,6 +161,14 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
TextViewCompat.setCompoundDrawableTintList(fullName, ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.signal_text_primary)));
}
String aboutText = recipient.getCombinedAboutAndEmoji();
if (!Util.isEmpty(aboutText)) {
about.setText(aboutText);
about.setVisibility(View.VISIBLE);
} else {
about.setVisibility(View.GONE);
}
String usernameNumberString = recipient.hasAUserSetDisplayName(requireContext()) && !recipient.isSelf()
? recipient.getSmsAddress().transform(PhoneNumberFormatter::prettyPrint).or("").trim()
: "";

View File

@@ -51,9 +51,11 @@ import org.thoughtcrime.securesms.recipients.RecipientExporter;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.ui.notifications.CustomNotificationsDialogFragment;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.LifecycleCursorWrapper;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity;
import java.util.Locale;
import java.util.Objects;
@@ -70,6 +72,7 @@ public class ManageRecipientFragment extends LoggingFragment {
private GroupMemberListView sharedGroupList;
private Toolbar toolbar;
private TextView title;
private TextView about;
private TextView subtitle;
private ViewGroup internalDetails;
private TextView internalDetailsText;
@@ -105,6 +108,7 @@ public class ManageRecipientFragment extends LoggingFragment {
private View secureCallButton;
private View insecureCallButton;
private View secureVideoCallButton;
private View chatWallpaperButton;
static ManageRecipientFragment newInstance(@NonNull RecipientId recipientId, boolean fromConversation) {
ManageRecipientFragment fragment = new ManageRecipientFragment();
@@ -130,6 +134,7 @@ public class ManageRecipientFragment extends LoggingFragment {
contactText = view.findViewById(R.id.recipient_contact_text);
contactIcon = view.findViewById(R.id.recipient_contact_icon);
title = view.findViewById(R.id.name);
about = view.findViewById(R.id.about);
subtitle = view.findViewById(R.id.username_number);
internalDetails = view.findViewById(R.id.recipient_internal_details);
internalDetailsText = view.findViewById(R.id.recipient_internal_details_text);
@@ -161,6 +166,7 @@ public class ManageRecipientFragment extends LoggingFragment {
secureCallButton = view.findViewById(R.id.recipient_voice_call);
insecureCallButton = view.findViewById(R.id.recipient_insecure_voice_call);
secureVideoCallButton = view.findViewById(R.id.recipient_video_call);
chatWallpaperButton = view.findViewById(R.id.chat_wallpaper);
return view;
}
@@ -270,6 +276,7 @@ public class ManageRecipientFragment extends LoggingFragment {
secureCallButton.setOnClickListener(v -> viewModel.onSecureCall(requireActivity()));
insecureCallButton.setOnClickListener(v -> viewModel.onInsecureCall(requireActivity()));
secureVideoCallButton.setOnClickListener(v -> viewModel.onSecureVideoCall(requireActivity()));
chatWallpaperButton.setOnClickListener(v -> startActivity(ChatWallpaperActivity.createIntent(requireContext(), recipientId)));
}
@Override
@@ -299,6 +306,10 @@ public class ManageRecipientFragment extends LoggingFragment {
});
}
String aboutText = recipient.getCombinedAboutAndEmoji();
about.setText(aboutText);
about.setVisibility(Util.isEmpty(aboutText) ? View.GONE : View.VISIBLE);
disappearingMessagesCard.setVisibility(recipient.isRegistered() ? View.VISIBLE : View.GONE);
addToAGroup.setVisibility(recipient.isRegistered() ? View.VISIBLE : View.GONE);

View File

@@ -335,7 +335,7 @@ public final class EnterCodeFragment extends BaseRegistrationFragment
private void sendEmailToSupport() {
String body = SupportEmailUtil.generateSupportEmailBody(requireContext(),
getString(R.string.RegistrationActivity_code_support_subject),
R.string.RegistrationActivity_code_support_subject,
null,
null);
CommunicationActions.openEmail(requireContext(),

View File

@@ -293,7 +293,7 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
private void initNumber(@NonNull NumberViewState numberViewState) {
int countryCode = numberViewState.getCountryCode();
long number = numberViewState.getNationalNumber();
String number = numberViewState.getNationalNumber();
String regionDisplayName = numberViewState.getCountryDisplayName();
this.countryCode.setText(String.valueOf(countryCode));
@@ -303,7 +303,7 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
String regionCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(countryCode);
setCountryFormatter(regionCode);
if (number != 0) {
if (!TextUtils.isEmpty(number)) {
this.number.setText(String.valueOf(number));
}
}
@@ -357,7 +357,7 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
@Override
public void afterTextChanged(Editable s) {
Long number = reformatText(s);
String number = reformatText(s);
if (number == null) return;
@@ -378,7 +378,7 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
}
}
private Long reformatText(Editable s) {
private String reformatText(Editable s) {
if (countryFormatter == null) {
return null;
}
@@ -408,7 +408,7 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
return null;
}
return Long.parseLong(justDigits.toString());
return justDigits.toString();
}
private void setCountryFormatter(@Nullable String regionCode) {

View File

@@ -34,6 +34,9 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Arrays;
import java.util.Locale;
public final class WelcomeFragment extends BaseRegistrationFragment {
private static final String TAG = Log.tag(WelcomeFragment.class);
@@ -186,15 +189,29 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
if (Permissions.hasAll(requireContext(), Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {
localNumber = Util.getDeviceNumber(requireContext());
} else {
Log.i(TAG, "No phone permission");
}
if (localNumber.isPresent()) {
getModel().onNumberDetected(localNumber.get().getCountryCode(), localNumber.get().getNationalNumber());
Log.i(TAG, "Phone number detected");
Phonenumber.PhoneNumber phoneNumber = localNumber.get();
String nationalNumber = String.valueOf(phoneNumber.getNationalNumber());
if (phoneNumber.getNumberOfLeadingZeros() != 0) {
char[] value = new char[phoneNumber.getNumberOfLeadingZeros()];
Arrays.fill(value, '0');
nationalNumber = new String(value) + nationalNumber;
Log.i(TAG, String.format(Locale.US, "Padded national number with %d zeros", phoneNumber.getNumberOfLeadingZeros()));
}
getModel().onNumberDetected(phoneNumber.getCountryCode(), nationalNumber);
} else {
Log.i(TAG, "No number detected");
Optional<String> simCountryIso = Util.getSimCountryIso(requireContext());
if (simCountryIso.isPresent() && !TextUtils.isEmpty(simCountryIso.get())) {
getModel().onNumberDetected(PhoneNumberUtil.getInstance().getCountryCodeForRegion(simCountryIso.get()), 0);
getModel().onNumberDetected(PhoneNumberUtil.getInstance().getCountryCodeForRegion(simCountryIso.get()), "");
}
}
}

View File

@@ -20,7 +20,7 @@ public final class NumberViewState implements Parcelable {
private final String selectedCountryName;
private final int countryCode;
private final long nationalNumber;
private final String nationalNumber;
private NumberViewState(Builder builder) {
this.selectedCountryName = builder.countryDisplayName;
@@ -38,7 +38,7 @@ public final class NumberViewState implements Parcelable {
return countryCode;
}
public long getNationalNumber() {
public String getNationalNumber() {
return nationalNumber;
}
@@ -87,7 +87,7 @@ public final class NumberViewState implements Parcelable {
public int hashCode() {
int hash = countryCode;
hash *= 31;
hash += (int) (nationalNumber ^ (nationalNumber >>> 32));
hash += nationalNumber != null ? nationalNumber.hashCode() : 0;
hash *= 31;
hash += selectedCountryName != null ? selectedCountryName.hashCode() : 0;
return hash;
@@ -101,7 +101,7 @@ public final class NumberViewState implements Parcelable {
NumberViewState other = (NumberViewState) obj;
return other.countryCode == countryCode &&
other.nationalNumber == nationalNumber &&
Objects.equals(other.nationalNumber, nationalNumber) &&
Objects.equals(other.selectedCountryName, selectedCountryName);
}
@@ -122,8 +122,8 @@ public final class NumberViewState implements Parcelable {
}
}
private static String getConfiguredE164Number(int countryCode, long number) {
return PhoneNumberFormatter.formatE164(String.valueOf(countryCode), String.valueOf(number));
private static String getConfiguredE164Number(int countryCode, String number) {
return PhoneNumberFormatter.formatE164(String.valueOf(countryCode), number);
}
private static Phonenumber.PhoneNumber getPhoneNumber(@NonNull PhoneNumberUtil util, @NonNull String e164Number)
@@ -135,7 +135,7 @@ public final class NumberViewState implements Parcelable {
public static class Builder {
private String countryDisplayName;
private int countryCode;
private long nationalNumber;
private String nationalNumber;
public Builder countryCode(int countryCode) {
this.countryCode = countryCode;
@@ -147,7 +147,7 @@ public final class NumberViewState implements Parcelable {
return this;
}
public Builder nationalNumber(long nationalNumber) {
public Builder nationalNumber(String nationalNumber) {
this.nationalNumber = nationalNumber;
return this;
}
@@ -166,7 +166,7 @@ public final class NumberViewState implements Parcelable {
public void writeToParcel(Parcel parcel, int i) {
parcel.writeString(selectedCountryName);
parcel.writeInt(countryCode);
parcel.writeLong(nationalNumber);
parcel.writeString(nationalNumber);
}
public static final Creator<NumberViewState> CREATOR = new Creator<NumberViewState>() {
@@ -174,7 +174,7 @@ public final class NumberViewState implements Parcelable {
public NumberViewState createFromParcel(Parcel in) {
return new Builder().selectedCountryDisplayName(in.readString())
.countryCode(in.readInt())
.nationalNumber(in.readLong())
.nationalNumber(in.readString())
.build();
}

View File

@@ -95,7 +95,7 @@ public final class RegistrationViewModel extends ViewModel {
.countryCode(countryCode).build());
}
public void setNationalNumber(long number) {
public void setNationalNumber(String number) {
NumberViewState numberViewState = getNumber().toBuilder().nationalNumber(number).build();
setViewState(numberViewState);
}
@@ -111,7 +111,7 @@ public final class RegistrationViewModel extends ViewModel {
textCodeEntered.setValue(code);
}
public void onNumberDetected(int countryCode, long nationalNumber) {
public void onNumberDetected(int countryCode, String nationalNumber) {
setViewState(getNumber().toBuilder()
.countryCode(countryCode)
.nationalNumber(nationalNumber)

View File

@@ -41,13 +41,16 @@ import java.util.concurrent.ExecutionException;
*
* The image can be encrypted.
*/
final class UriGlideRenderer implements Renderer {
public final class UriGlideRenderer implements Renderer {
private static final String TAG = Log.tag(UriGlideRenderer.class);
private static final int PREVIEW_DIMENSION_LIMIT = 2048;
private static final int MAX_BLUR_DIMENSION = 300;
public static final float WEAK_BLUR = 3f;
public static final float STRONG_BLUR = 25f;
private final Uri imageUri;
private final Paint paint = new Paint();
private final Matrix imageProjectionMatrix = new Matrix();
@@ -56,16 +59,22 @@ final class UriGlideRenderer implements Renderer {
private final boolean decryptable;
private final int maxWidth;
private final int maxHeight;
private final float blurRadius;
@Nullable private Bitmap bitmap;
@Nullable private Bitmap blurredBitmap;
@Nullable private Paint blurPaint;
@Nullable private Bitmap bitmap;
@Nullable private Bitmap blurredBitmap;
@Nullable private Paint blurPaint;
UriGlideRenderer(@NonNull Uri imageUri, boolean decryptable, int maxWidth, int maxHeight) {
public UriGlideRenderer(@NonNull Uri imageUri, boolean decryptable, int maxWidth, int maxHeight) {
this(imageUri, decryptable, maxWidth, maxHeight, STRONG_BLUR);
}
public UriGlideRenderer(@NonNull Uri imageUri, boolean decryptable, int maxWidth, int maxHeight, float blurRadius) {
this.imageUri = imageUri;
this.decryptable = decryptable;
this.maxWidth = maxWidth;
this.maxHeight = maxHeight;
this.blurRadius = blurRadius;
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
paint.setDither(true);
@@ -148,7 +157,7 @@ final class UriGlideRenderer implements Renderer {
blurPaint.setMaskFilter(null);
if (blurredBitmap == null) {
blurredBitmap = blur(bitmap, rendererContext.context);
blurredBitmap = blur(bitmap, rendererContext.context, blurRadius);
blurScaleMatrix.setRectToRect(new RectF(0, 0, blurredBitmap.getWidth(), blurredBitmap.getHeight()),
new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()),
@@ -235,7 +244,7 @@ final class UriGlideRenderer implements Renderer {
return matrix;
}
private static @NonNull Bitmap blur(Bitmap bitmap, Context context) {
private static @NonNull Bitmap blur(Bitmap bitmap, Context context, float blurRadius) {
Point previewSize = scaleKeepingAspectRatio(new Point(bitmap.getWidth(), bitmap.getHeight()), PREVIEW_DIMENSION_LIMIT);
Point blurSize = scaleKeepingAspectRatio(new Point(previewSize.x / 2, previewSize.y / 2 ), MAX_BLUR_DIMENSION);
Bitmap small = BitmapUtil.createScaledBitmap(bitmap, blurSize.x, blurSize.y);
@@ -247,7 +256,7 @@ final class UriGlideRenderer implements Renderer {
Allocation output = Allocation.createTyped(rs, input.getType());
ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
script.setRadius(25f);
script.setRadius(blurRadius);
script.setInput(input);
script.forEach(output);
@@ -283,7 +292,8 @@ final class UriGlideRenderer implements Renderer {
return new UriGlideRenderer(Uri.parse(in.readString()),
in.readInt() == 1,
in.readInt(),
in.readInt()
in.readInt(),
in.readFloat()
);
}
@@ -304,5 +314,6 @@ final class UriGlideRenderer implements Renderer {
dest.writeInt(decryptable ? 1 : 0);
dest.writeInt(maxWidth);
dest.writeInt(maxHeight);
dest.writeFloat(blurRadius);
}
}

View File

@@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.sharing;
public enum InterstitialContentType {
MEDIA,
TEXT,
NONE
}

View File

@@ -0,0 +1,235 @@
package org.thoughtcrime.securesms.sharing;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
public final class MultiShareArgs implements Parcelable {
private static final String ARGS = "ShareInterstitialArgs";
private final Set<ShareContactAndThread> shareContactAndThreads;
private final ArrayList<Media> media;
private final String draftText;
private final StickerLocator stickerLocator;
private final boolean borderless;
private final Uri dataUri;
private final String dataType;
private final boolean viewOnce;
private final LinkPreview linkPreview;
private MultiShareArgs(@NonNull Builder builder) {
shareContactAndThreads = builder.shareContactAndThreads;
media = builder.media == null ? new ArrayList<>() : builder.media;
draftText = builder.draftText;
stickerLocator = builder.stickerLocator;
borderless = builder.borderless;
dataUri = builder.dataUri;
dataType = builder.dataType;
viewOnce = builder.viewOnce;
linkPreview = builder.linkPreview;
}
protected MultiShareArgs(Parcel in) {
shareContactAndThreads = new HashSet<>(in.createTypedArrayList(ShareContactAndThread.CREATOR));
media = in.createTypedArrayList(Media.CREATOR);
draftText = in.readString();
stickerLocator = in.readParcelable(StickerLocator.class.getClassLoader());
borderless = in.readByte() != 0;
dataUri = in.readParcelable(Uri.class.getClassLoader());
dataType = in.readString();
viewOnce = in.readByte() != 0;
LinkPreview preview;
try {
preview = LinkPreview.deserialize(in.readString());
} catch (IOException e) {
preview = null;
}
linkPreview = preview;
}
public Set<ShareContactAndThread> getShareContactAndThreads() {
return shareContactAndThreads;
}
public ArrayList<Media> getMedia() {
return media;
}
public StickerLocator getStickerLocator() {
return stickerLocator;
}
public String getDataType() {
return dataType;
}
public String getDraftText() {
return draftText;
}
public Uri getDataUri() {
return dataUri;
}
public boolean isBorderless() {
return borderless;
}
public boolean isViewOnce() {
return viewOnce;
}
public @Nullable LinkPreview getLinkPreview() {
return linkPreview;
}
public @NonNull InterstitialContentType getInterstitialContentType() {
if (!requiresInterstitial()) {
return InterstitialContentType.NONE;
} else if (!this.getMedia().isEmpty() ||
(this.getDataUri() != null && this.getDataUri() != Uri.EMPTY && this.getDataType() != null))
{
return InterstitialContentType.MEDIA;
} else if (!TextUtils.isEmpty(this.getDraftText())) {
return InterstitialContentType.TEXT;
} else {
return InterstitialContentType.NONE;
}
}
public static final Creator<MultiShareArgs> CREATOR = new Creator<MultiShareArgs>() {
@Override
public MultiShareArgs createFromParcel(Parcel in) {
return new MultiShareArgs(in);
}
@Override
public MultiShareArgs[] newArray(int size) {
return new MultiShareArgs[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeTypedList(Stream.of(shareContactAndThreads).toList());
dest.writeTypedList(media);
dest.writeString(draftText);
dest.writeParcelable(stickerLocator, flags);
dest.writeByte((byte) (borderless ? 1 : 0));
dest.writeParcelable(dataUri, flags);
dest.writeString(dataType);
dest.writeByte((byte) (viewOnce ? 1 : 0));
if (linkPreview != null) {
try {
dest.writeString(linkPreview.serialize());
} catch (IOException e) {
dest.writeString("");
}
} else {
dest.writeString("");
}
}
public Builder buildUpon() {
return new Builder(shareContactAndThreads).asBorderless(borderless)
.asViewOnce(viewOnce)
.withDataType(dataType)
.withDataUri(dataUri)
.withDraftText(draftText)
.withLinkPreview(linkPreview)
.withMedia(media)
.withStickerLocator(stickerLocator);
}
private boolean requiresInterstitial() {
return !media.isEmpty() || !TextUtils.isEmpty(draftText) || MediaUtil.isImageOrVideoType(dataType);
}
public static final class Builder {
private final Set<ShareContactAndThread> shareContactAndThreads;
private ArrayList<Media> media;
private String draftText;
private StickerLocator stickerLocator;
private boolean borderless;
private Uri dataUri;
private String dataType;
private LinkPreview linkPreview;
private boolean viewOnce;
public Builder(@NonNull Set<ShareContactAndThread> shareContactAndThreads) {
this.shareContactAndThreads = shareContactAndThreads;
}
public @NonNull Builder withMedia(@Nullable ArrayList<Media> media) {
this.media = media;
return this;
}
public @NonNull Builder withDraftText(@Nullable String draftText) {
this.draftText = draftText;
return this;
}
public @NonNull Builder withStickerLocator(@Nullable StickerLocator stickerLocator) {
this.stickerLocator = stickerLocator;
return this;
}
public @NonNull Builder asBorderless(boolean borderless) {
this.borderless = borderless;
return this;
}
public @NonNull Builder withDataUri(@Nullable Uri dataUri) {
this.dataUri = dataUri;
return this;
}
public @NonNull Builder withDataType(@Nullable String dataType) {
this.dataType = dataType;
return this;
}
public @NonNull Builder withLinkPreview(@Nullable LinkPreview linkPreview) {
this.linkPreview = linkPreview;
return this;
}
public @NonNull Builder asViewOnce(boolean viewOnce) {
this.viewOnce = viewOnce;
return this;
}
public @NonNull MultiShareArgs build() {
return new MultiShareArgs(this);
}
}
}

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