Merge branch 'dev' of https://github.com/loki-project/session-android into permission-refactoring

This commit is contained in:
Anton Chekulaev
2020-09-09 17:12:46 +10:00
85 changed files with 814 additions and 388 deletions

View File

@@ -23,9 +23,6 @@ import android.content.Intent;
import android.graphics.PorterDuff;
import android.os.Build;
import android.os.Bundle;
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
import androidx.core.os.CancellationSignal;
import androidx.appcompat.widget.Toolbar;
import android.text.Editable;
import android.text.InputType;
import android.text.SpannableString;
@@ -49,6 +46,9 @@ import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
import androidx.core.os.CancellationSignal;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.thoughtcrime.securesms.crypto.InvalidPassphraseException;
@@ -76,8 +76,8 @@ public class PassphrasePromptActivity extends PassphraseActivity {
private Button lockScreenButton;
private EditText passphraseText;
// private ImageButton showButton;
// private ImageButton hideButton;
private ImageButton showButton;
private ImageButton hideButton;
private AnimatingToggle visibilityToggle;
private FingerprintManagerCompat fingerprintManager;
@@ -91,8 +91,6 @@ public class PassphrasePromptActivity extends PassphraseActivity {
public void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate()");
dynamicLanguage.onCreate(this);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
super.onCreate(savedInstanceState);
setContentView(R.layout.prompt_passphrase_activity);
@@ -203,11 +201,10 @@ public class PassphrasePromptActivity extends PassphraseActivity {
private void initializeResources() {
// ImageButton okButton = findViewById(R.id.ok_button);
Toolbar toolbar = findViewById(R.id.toolbar);
ImageButton okButton = findViewById(R.id.ok_button);
// showButton = findViewById(R.id.passphrase_visibility);
// hideButton = findViewById(R.id.passphrase_visibility_off);
showButton = findViewById(R.id.passphrase_visibility);
hideButton = findViewById(R.id.passphrase_visibility_off);
visibilityToggle = findViewById(R.id.button_toggle);
passphraseText = findViewById(R.id.passphrase_edit);
passphraseAuthContainer = findViewById(R.id.password_auth_container);
@@ -217,20 +214,16 @@ public class PassphrasePromptActivity extends PassphraseActivity {
fingerprintCancellationSignal = new CancellationSignal();
fingerprintListener = new FingerprintListener();
// setSupportActionBar(toolbar);
// getSupportActionBar().setTitle("");
SpannableString hint = new SpannableString(" " + getString(R.string.PassphrasePromptActivity_enter_passphrase));
hint.setSpan(new RelativeSizeSpan(0.9f), 0, hint.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
hint.setSpan(new TypefaceSpan("sans-serif"), 0, hint.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
passphraseText.setHint(hint);
// okButton.setOnClickListener(new OkButtonClickListener());
// showButton.setOnClickListener(new ShowButtonOnClickListener());
// hideButton.setOnClickListener(new HideButtonOnClickListener());
okButton.setOnClickListener(new OkButtonClickListener());
showButton.setOnClickListener(new ShowButtonOnClickListener());
hideButton.setOnClickListener(new HideButtonOnClickListener());
passphraseText.setOnEditorActionListener(new PassphraseActionListener());
passphraseText.setImeActionLabel(getString(R.string.prompt_passphrase_activity__unlock),
EditorInfo.IME_ACTION_DONE);
passphraseText.setImeActionLabel(getString(R.string.prompt_passphrase_activity__unlock), EditorInfo.IME_ACTION_DONE);
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN);
@@ -316,7 +309,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
private class ShowButtonOnClickListener implements OnClickListener {
@Override
public void onClick(View v) {
// visibilityToggle.display(hideButton);
visibilityToggle.display(hideButton);
setPassphraseVisibility(true);
}
}
@@ -324,7 +317,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
private class HideButtonOnClickListener implements OnClickListener {
@Override
public void onClick(View v) {
// visibilityToggle.display(showButton);
visibilityToggle.display(showButton);
setPassphraseVisibility(false);
}
}

View File

@@ -160,6 +160,7 @@ import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol;
import org.thoughtcrime.securesms.loki.protocol.SessionManagementProtocol;
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities;
import org.thoughtcrime.securesms.loki.views.MentionCandidateSelectionView;
import org.thoughtcrime.securesms.loki.views.ProfilePictureView;
import org.thoughtcrime.securesms.loki.views.SessionRestoreBannerView;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
@@ -234,6 +235,7 @@ import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
@@ -300,6 +302,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private AnimatingToggle buttonToggle;
private SendButton sendButton;
private ImageButton attachButton;
private ProfilePictureView profilePictureView;
private TextView titleTextView;
private TextView charactersLeft;
private ConversationFragment fragment;
@@ -527,6 +530,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
composeText.setTransport(sendButton.getSelectedTransport());
updateTitleTextView(recipient);
updateProfilePicture();
updateSubtitleTextView();
setActionBarColor(recipient.getColor());
updateInputUI(recipient, isSecureText, isDefaultSms);
@@ -621,6 +625,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
recipient = Recipient.from(this, data.getParcelableExtra(GroupCreateActivity.GROUP_ADDRESS_EXTRA), true);
recipient.addListener(this);
updateTitleTextView(recipient);
updateProfilePicture();
updateSubtitleTextView();
NotificationChannels.updateContactChannelName(this, recipient);
updateInputUI(recipient, isSecureText, isDefaultSms);
@@ -1654,6 +1659,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void initializeViews() {
profilePictureView = findViewById(R.id.profilePictureView);
titleTextView = findViewById(R.id.titleTextView);
buttonToggle = ViewUtil.findById(this, R.id.button_toggle);
sendButton = ViewUtil.findById(this, R.id.send_button);
@@ -1872,6 +1878,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
Util.runOnMain(() -> {
Log.i(TAG, "onModifiedRun(): " + recipient.getRegistered());
updateTitleTextView(recipient);
updateProfilePicture();
updateSubtitleTextView();
// titleView.setVerified(identityRecords.isVerified());
updateInputUI(recipient, isSecureText, isDefaultSms);
@@ -3098,6 +3105,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
private void updateProfilePicture() {
profilePictureView.glide = GlideApp.with(this);
profilePictureView.update(recipient, threadId);
}
private void updateSubtitleTextView() {
muteIndicatorImageView.setVisibility(View.GONE);
subtitleTextView.setVisibility(View.VISIBLE);

View File

@@ -804,6 +804,7 @@ public class ConversationItem extends LinearLayout
bodyBubble.setLayoutParams(layoutParams);
if (profilePictureView == null) return;
profilePictureView.setPublicKey(recipient.getAddress().toString());
profilePictureView.setDisplayName(recipient.getName());
profilePictureView.setAdditionalPublicKey(null);
profilePictureView.setRSSFeed(false);
profilePictureView.setGlide(glideRequests);

View File

@@ -528,6 +528,7 @@ public class ThreadDatabase extends Database {
if (cursor != null && cursor.moveToFirst()) {
return cursor.getLong(cursor.getColumnIndexOrThrow(ID));
} else {
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true);
return createThreadForRecipient(recipient.getAddress(), recipient.isGroupRecipient(), distributionType);
}
} finally {

View File

@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.loki.activities
import android.app.AlertDialog
import androidx.lifecycle.Observer
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -11,10 +10,6 @@ import android.net.Uri
import android.os.AsyncTask
import android.os.Bundle
import android.os.Handler
import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.recyclerview.widget.LinearLayoutManager
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
@@ -22,6 +17,11 @@ import android.util.DisplayMetrics
import android.view.View
import android.widget.RelativeLayout
import android.widget.Toast
import androidx.lifecycle.Observer
import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_home.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.ApplicationContext
@@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob
import org.thoughtcrime.securesms.loki.dialogs.ConversationOptionsBottomSheet
import org.thoughtcrime.securesms.loki.dialogs.LightThemeFeatureIntroBottomSheet
import org.thoughtcrime.securesms.loki.dialogs.MultiDeviceRemovalBottomSheet
import org.thoughtcrime.securesms.loki.dialogs.UserDetailsBottomSheet
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation
import org.thoughtcrime.securesms.loki.utilities.*
@@ -42,14 +43,13 @@ import org.thoughtcrime.securesms.loki.views.NewConversationButtonSetViewDelegat
import org.thoughtcrime.securesms.loki.views.SeedReminderViewDelegate
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.GroupUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI
import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager
import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol
import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol
import org.whispersystems.signalservice.loki.protocol.sessionmanagement.SessionManagementProtocol
import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol
import org.whispersystems.signalservice.loki.protocol.shelved.syncmessages.SyncMessagesProtocol
import org.whispersystems.signalservice.loki.utilities.toHexString
@@ -98,8 +98,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
// Set up toolbar buttons
profileButton.glide = glide
profileButton.publicKey = publicKey
profileButton.displayName = TextSecurePreferences.getProfileName(this)
profileButton.update()
profileButton.setOnClickListener { openSettings() }
pathStatusViewContainer.disableClipping()
pathStatusViewContainer.setOnClickListener { showPath() }
// Set up seed reminder view
val isMasterDevice = (TextSecurePreferences.getMasterHexEncodedPublicKey(this) == null)
@@ -271,6 +273,14 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
val thread = view.thread ?: return
val bottomSheet = ConversationOptionsBottomSheet()
bottomSheet.recipient = thread.recipient
bottomSheet.onViewDetailsTapped = {
bottomSheet.dismiss()
val userDetailsBottomSheet = UserDetailsBottomSheet()
val bundle = Bundle()
bundle.putString("publicKey", thread.recipient.address.toPhoneString())
userDetailsBottomSheet.arguments = bundle
userDetailsBottomSheet.show(supportFragmentManager, userDetailsBottomSheet.tag)
}
bottomSheet.onBlockTapped = {
bottomSheet.dismiss()
if (!thread.recipient.isBlocked) {

View File

@@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.loki.activities
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.app.AlertDialog
import android.content.Intent
import android.graphics.drawable.TransitionDrawable
@@ -9,27 +11,36 @@ import androidx.annotation.DrawableRes
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.LinearLayout
import android.widget.Toast
import androidx.annotation.ColorRes
import kotlinx.android.synthetic.main.activity_display_name.registerButton
import kotlinx.android.synthetic.main.activity_pn_mode.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.loki.utilities.disableClipping
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
import org.thoughtcrime.securesms.loki.utilities.show
import org.thoughtcrime.securesms.loki.views.GlowViewUtilities
import org.thoughtcrime.securesms.loki.views.PNModeView
import org.thoughtcrime.securesms.util.TextSecurePreferences
class PNModeActivity : BaseActionBarActivity() {
private var selectedOptionView: LinearLayout? = null
private var selectedOptionView: PNModeView? = null
// region Lifecycle
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setUpActionBarSessionLogo()
setContentView(R.layout.activity_pn_mode)
contentView.disableClipping()
fcmOptionView.setOnClickListener { toggleFCM() }
fcmOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme)
fcmOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme)
backgroundPollingOptionView.setOnClickListener { toggleBackgroundPolling() }
backgroundPollingOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme)
backgroundPollingOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme)
registerButton.setOnClickListener { register() }
}
@@ -71,15 +82,23 @@ class PNModeActivity : BaseActionBarActivity() {
when (selectedOptionView) {
null -> {
performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView)
GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.transparent, R.color.accent)
animateStrokeColorChange(fcmOptionView, R.color.pn_option_border, R.color.accent)
selectedOptionView = fcmOptionView
}
fcmOptionView -> {
performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView)
GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.accent, R.color.transparent)
animateStrokeColorChange(fcmOptionView, R.color.accent, R.color.pn_option_border)
selectedOptionView = null
}
backgroundPollingOptionView -> {
performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView)
GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.transparent, R.color.accent)
animateStrokeColorChange(fcmOptionView, R.color.pn_option_border, R.color.accent)
performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView)
GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.accent, R.color.transparent)
animateStrokeColorChange(backgroundPollingOptionView, R.color.accent, R.color.pn_option_border)
selectedOptionView = fcmOptionView
}
}
@@ -89,20 +108,40 @@ class PNModeActivity : BaseActionBarActivity() {
when (selectedOptionView) {
null -> {
performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView)
GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.transparent, R.color.accent)
animateStrokeColorChange(backgroundPollingOptionView, R.color.pn_option_border, R.color.accent)
selectedOptionView = backgroundPollingOptionView
}
backgroundPollingOptionView -> {
performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView)
GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.accent, R.color.transparent)
animateStrokeColorChange(backgroundPollingOptionView, R.color.accent, R.color.pn_option_border)
selectedOptionView = null
}
fcmOptionView -> {
performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView)
GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.transparent, R.color.accent)
animateStrokeColorChange(backgroundPollingOptionView, R.color.pn_option_border, R.color.accent)
performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView)
GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.accent, R.color.transparent)
animateStrokeColorChange(fcmOptionView, R.color.accent, R.color.pn_option_border)
selectedOptionView = backgroundPollingOptionView
}
}
}
private fun animateStrokeColorChange(bubble: PNModeView, @ColorRes startColorID: Int, @ColorRes endColorID: Int) {
val startColor = resources.getColorWithID(startColorID, theme)
val endColor = resources.getColorWithID(endColorID, theme)
val animation = ValueAnimator.ofObject(ArgbEvaluator(), startColor, endColor)
animation.duration = 250
animation.addUpdateListener { animator ->
val color = animator.animatedValue as Int
bubble.strokeColor = color
}
animation.start()
}
private fun register() {
if (selectedOptionView == null) {
val dialog = AlertDialog.Builder(this)
@@ -111,7 +150,6 @@ class PNModeActivity : BaseActionBarActivity() {
dialog.create().show()
return
}
val displayName = TextSecurePreferences.getProfileName(this)
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
TextSecurePreferences.setPromptedPushRegistration(this, true)
TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == fcmOptionView))

View File

@@ -16,10 +16,13 @@ import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.ColorRes
import kotlinx.android.synthetic.main.activity_path.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.loki.utilities.*
import org.thoughtcrime.securesms.loki.views.GlowViewUtilities
import org.thoughtcrime.securesms.loki.views.PathDotView
import org.whispersystems.signalservice.loki.api.Snode
import org.whispersystems.signalservice.loki.api.onionrequests.OnionRequestAPI
@@ -31,6 +34,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
super.onCreate(savedInstanceState, isReady)
setContentView(R.layout.activity_path)
supportActionBar!!.title = resources.getString(R.string.activity_path_title)
pathRowsContainer.disableClipping()
learnMoreButton.setOnClickListener { learnMore() }
update(false)
registerObservers()
@@ -111,6 +115,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
val mainContainer = LinearLayout(this)
mainContainer.orientation = LinearLayout.HORIZONTAL
mainContainer.gravity = Gravity.CENTER_VERTICAL
mainContainer.disableClipping()
val mainContainerLayoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
mainContainer.layoutParams = mainContainerLayoutParams
val lineView = LineView(this, location, dotAnimationStartDelay, dotAnimationRepeatInterval)
@@ -170,8 +175,9 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
private var dotAnimationRepeatInterval: Long = 0
private val dotView by lazy {
val result = View(context)
val result = PathDotView(context)
result.setBackgroundResource(R.drawable.accent_dot)
result.mainColor = resources.getColorWithID(R.color.accent, context.theme)
result
}
@@ -203,6 +209,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
}
private fun setUpViewHierarchy() {
disableClipping()
val lineView = View(context)
lineView.setBackgroundColor(resources.getColorWithID(R.color.text, context.theme))
val lineViewHeight = when (location) {
@@ -239,10 +246,14 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
private fun expand() {
dotView.animateSizeChange(R.dimen.path_row_dot_size, R.dimen.path_row_expanded_dot_size)
@ColorRes val startColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.transparent_black_30 else R.color.black
GlowViewUtilities.animateShadowColorChange(context, dotView, startColorID, R.color.accent)
}
private fun collapse() {
dotView.animateSizeChange(R.dimen.path_row_expanded_dot_size, R.dimen.path_row_dot_size)
@ColorRes val endColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.transparent_black_30 else R.color.black
GlowViewUtilities.animateShadowColorChange(context, dotView, R.color.accent, endColorID)
}
}
// endregion

View File

@@ -54,10 +54,8 @@ import java.security.SecureRandom
import java.util.*
class SettingsActivity : PassphraseRequiredActionBarActivity() {
private var displayNameEditActionMode: ActionMode? = null
set(value) { field = value; handleDisplayNameEditActionModeChanged() }
private lateinit var glide: GlideRequests
private var displayNameToBeUploaded: String? = null
private var profilePictureToBeUploaded: ByteArray? = null
@@ -73,17 +71,17 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
// region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady)
setContentView(R.layout.activity_settings)
val displayName = DatabaseFactory.getLokiUserDatabase(this).getDisplayName(hexEncodedPublicKey)
glide = GlideApp.with(this)
profilePictureView.glide = glide
profilePictureView.publicKey = hexEncodedPublicKey
profilePictureView.displayName = displayName
profilePictureView.isLarge = true
profilePictureView.update()
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
btnGroupNameDisplay.text = DatabaseFactory.getLokiUserDatabase(this).getDisplayName(hexEncodedPublicKey)
btnGroupNameDisplay.text = displayName
publicKeyTextView.text = hexEncodedPublicKey
copyButton.setOnClickListener { copyPublicKey() }
shareButton.setOnClickListener { sharePublicKey() }
@@ -105,12 +103,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.settings_general, menu)
// Update UI mode menu icon.
// It uses three-level selector where each level corresponds to the related UiMode ordinal value.
// Update UI mode menu icon
val uiMode = UiModeUtilities.getUserSelectedUiMode(this)
menu.findItem(R.id.action_change_theme).icon!!.level = uiMode.ordinal
return true
}

View File

@@ -20,6 +20,7 @@ public class ConversationOptionsBottomSheet : BottomSheetDialogFragment() {
// if we want to use dialog fragments properly.
lateinit var recipient: Recipient
var onViewDetailsTapped: (() -> Unit?)? = null
var onBlockTapped: (() -> Unit)? = null
var onUnblockTapped: (() -> Unit)? = null
var onDeleteTapped: (() -> Unit)? = null
@@ -30,18 +31,16 @@ public class ConversationOptionsBottomSheet : BottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (!this::recipient.isInitialized) {
dismiss()
return
}
if (!this::recipient.isInitialized) { return dismiss() }
if (!recipient.isGroupRecipient && !recipient.isLocalNumber) {
detailsTextView.visibility = View.VISIBLE
unblockTextView.visibility = if (recipient.isBlocked) View.VISIBLE else View.GONE
blockTextView.visibility = if (recipient.isBlocked) View.GONE else View.VISIBLE
detailsTextView.setOnClickListener { onViewDetailsTapped?.invoke() }
blockTextView.setOnClickListener { onBlockTapped?.invoke() }
unblockTextView.setOnClickListener { onUnblockTapped?.invoke() }
} else {
detailsTextView.visibility = View.GONE
}
deleteTextView.setOnClickListener { onDeleteTapped?.invoke() }
}

View File

@@ -0,0 +1,40 @@
package org.thoughtcrime.securesms.loki.dialogs
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import kotlinx.android.synthetic.main.fragment_user_details_bottom_sheet.*
import kotlinx.android.synthetic.main.view_conversation.view.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.mms.GlideApp
public class UserDetailsBottomSheet : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_user_details_bottom_sheet, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val publicKey = arguments?.getString("publicKey") ?: return dismiss()
profilePictureView.publicKey = publicKey
profilePictureView.glide = GlideApp.with(this)
profilePictureView.isLarge = true
profilePictureView.update()
nameTextView.text = DatabaseFactory.getLokiUserDatabase(requireContext()).getDisplayName(publicKey) ?: "Anonymous"
publicKeyTextView.text = publicKey
copyButton.setOnClickListener {
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Session ID", publicKey)
clipboard.setPrimaryClip(clip)
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
}
}
}

View File

@@ -0,0 +1,79 @@
package org.thoughtcrime.securesms.loki.todo
import android.content.Context
import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.text.TextPaint
import android.text.TextUtils
import androidx.annotation.ColorInt
import androidx.core.graphics.ColorUtils
import network.loki.messenger.R
import java.util.*
object AvatarPlaceholderGenerator {
private val tmpFloatArray = FloatArray(3)
private const val EMPTY_LABEL = "0";
fun generate(context: Context, pixelSize: Int, hashString: String, displayName: String?): BitmapDrawable {
//TODO That should be replaced with a proper hash extraction code.
val hash: Long
if (hashString.length >= 12 && hashString.matches(Regex("^[0-9A-Fa-f]+\$"))) {
hash = hashString.substring(0 until 12).toLong(16)
} else {
hash = 0
}
// Do not cache color array, it may be different depends on the current theme.
val colorArray = context.resources.getIntArray(R.array.profile_picture_placeholder_colors)
val colorPrimary = colorArray[(hash % colorArray.size).toInt()]
val colorSecondary = changeColorHueBy(colorPrimary, 12f)
val labelText = when {
!TextUtils.isEmpty(displayName) -> extractLabel(displayName!!)
!TextUtils.isEmpty(hashString) -> extractLabel(hashString)
else -> EMPTY_LABEL
}
val bitmap = Bitmap.createBitmap(pixelSize, pixelSize, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
// Draw background/frame
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.shader = LinearGradient(0f, 0f, 0f, pixelSize.toFloat(),
colorPrimary,
colorPrimary,
Shader.TileMode.REPEAT)
canvas.drawCircle(pixelSize.toFloat() / 2, pixelSize.toFloat() / 2, pixelSize.toFloat() / 2, paint)
// Draw text
val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
textPaint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
textPaint.textSize = pixelSize * 0.5f
textPaint.color = Color.WHITE
val areaRect = Rect(0, 0, pixelSize, pixelSize)
val textBounds = RectF(areaRect)
textBounds.right = textPaint.measureText(labelText)
textBounds.bottom = textPaint.descent() - textPaint.ascent()
textBounds.left += (areaRect.width() - textBounds.right) * 0.5f
textBounds.top += (areaRect.height() - textBounds.bottom) * 0.5f
canvas.drawText(labelText, textBounds.left, textBounds.top - textPaint.ascent(), textPaint)
return BitmapDrawable(context.resources, bitmap)
}
@ColorInt
private fun changeColorHueBy(@ColorInt color: Int, hueDelta: Float): Int {
val hslColor = tmpFloatArray
ColorUtils.colorToHSL(color, hslColor)
hslColor[0] = (hslColor[0] + hueDelta) % 360f
return ColorUtils.HSLToColor(hslColor)
}
private fun extractLabel(content: String): String {
var content = content.trim()
if (content.isEmpty()) return EMPTY_LABEL
return content.first().toString().toUpperCase(Locale.ROOT)
}
}

View File

@@ -2,12 +2,14 @@ package org.thoughtcrime.securesms.loki.views
import android.content.Context
import android.graphics.Typeface
import android.text.TextUtils
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import kotlinx.android.synthetic.main.view_conversation.view.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities.highlightMentions
@@ -55,30 +57,8 @@ class ConversationView : LinearLayout {
accentView.setBackgroundResource(R.color.accent)
accentView.visibility = if (thread.unreadCount > 0) View.VISIBLE else View.INVISIBLE
}
if (thread.recipient.isGroupRecipient) {
if ("Session Public Chat" == thread.recipient.name) {
profilePictureView.publicKey = ""
profilePictureView.additionalPublicKey = null
profilePictureView.isRSSFeed = true
} else {
val users = MentionsManager.shared.userPublicKeyCache[thread.threadId]?.toMutableList() ?: mutableListOf()
users.remove(TextSecurePreferences.getLocalNumber(context))
val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
if (masterPublicKey != null) {
users.remove(masterPublicKey)
}
val randomUsers = users.sorted() // Sort to provide a level of stability
profilePictureView.publicKey = randomUsers.getOrNull(0) ?: ""
profilePictureView.additionalPublicKey = randomUsers.getOrNull(1) ?: ""
profilePictureView.isRSSFeed = thread.recipient.name == "Loki News" || thread.recipient.name == "Session Updates"
}
} else {
profilePictureView.publicKey = thread.recipient.address.toString()
profilePictureView.additionalPublicKey = null
profilePictureView.isRSSFeed = false
}
profilePictureView.glide = glide
profilePictureView.update()
profilePictureView.update(thread.recipient, thread.threadId)
val senderDisplayName = if (thread.recipient.isLocalNumber) context.getString(R.string.note_to_self) else if (!thread.recipient.name.isNullOrEmpty()) thread.recipient.name else thread.recipient.address.toString()
btnGroupNameDisplay.text = senderDisplayName
timestampTextView.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), thread.date)
@@ -103,5 +83,10 @@ class ConversationView : LinearLayout {
else -> statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check)
}
}
private fun getUserDisplayName(publicKey: String?): String? {
if (TextUtils.isEmpty(publicKey)) return null
return DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey!!)
}
// endregion
}

View File

@@ -1,14 +1,18 @@
package org.thoughtcrime.securesms.loki.views
import android.animation.FloatEvaluator
import android.animation.ValueAnimator
import android.content.Context
import android.content.Context.LAYOUT_INFLATER_SERVICE
import android.os.Handler
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import android.widget.ScrollView
import kotlinx.android.synthetic.main.view_fake_chat.view.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.loki.utilities.disableClipping
class FakeChatView : ScrollView {
@@ -38,7 +42,8 @@ class FakeChatView : ScrollView {
private fun setUpViewHierarchy() {
val inflater = context.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater
val contentView = inflater.inflate(R.layout.view_fake_chat, null)
val contentView = inflater.inflate(R.layout.view_fake_chat, null) as LinearLayout
contentView.disableClipping()
addView(contentView)
isVerticalScrollBarEnabled = false
}
@@ -47,8 +52,13 @@ class FakeChatView : ScrollView {
// region Animation
fun startAnimating() {
listOf( bubble1, bubble2, bubble3, bubble4, bubble5 ).forEach { it.alpha = 0.0f }
fun show(view: View) {
view.animate().alpha(1.0f).setDuration(animationDuration).start()
fun show(bubble: View) {
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
animation.duration = animationDuration
animation.addUpdateListener { animator ->
bubble.alpha = animator.animatedValue as Float
}
animation.start()
}
Handler().postDelayed({
show(bubble1)

View File

@@ -0,0 +1,158 @@
package org.thoughtcrime.securesms.loki.views
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import android.view.ViewOutlineProvider
import android.widget.LinearLayout
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import network.loki.messenger.R
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
import org.thoughtcrime.securesms.loki.utilities.toPx
interface GlowView {
var mainColor: Int
var sessionShadowColor: Int
}
object GlowViewUtilities {
fun animateColorChange(context: Context, view: GlowView, @ColorRes startColorID: Int, @ColorRes endColorID: Int) {
val startColor = context.resources.getColorWithID(startColorID, context.theme)
val endColor = context.resources.getColorWithID(endColorID, context.theme)
val animation = ValueAnimator.ofObject(ArgbEvaluator(), startColor, endColor)
animation.duration = 250
animation.addUpdateListener { animator ->
val color = animator.animatedValue as Int
view.mainColor = color
}
animation.start()
}
fun animateShadowColorChange(context: Context, view: GlowView, @ColorRes startColorID: Int, @ColorRes endColorID: Int) {
val startColor = context.resources.getColorWithID(startColorID, context.theme)
val endColor = context.resources.getColorWithID(endColorID, context.theme)
val animation = ValueAnimator.ofObject(ArgbEvaluator(), startColor, endColor)
animation.duration = 250
animation.addUpdateListener { animator ->
val color = animator.animatedValue as Int
view.sessionShadowColor = color
}
animation.start()
}
}
class PNModeView : LinearLayout, GlowView {
@ColorInt override var mainColor: Int = 0
set(newValue) { field = newValue; paint.color = newValue }
@ColorInt var strokeColor: Int = 0
set(newValue) { field = newValue; strokePaint.color = newValue }
@ColorInt override var sessionShadowColor: Int = 0
set(newValue) { field = newValue; paint.setShadowLayer(toPx(4, resources).toFloat(), 0.0f, 0.0f, newValue) }
private val paint: Paint by lazy {
val result = Paint()
result.style = Paint.Style.FILL
result.isAntiAlias = true
result
}
private val strokePaint: Paint by lazy {
val result = Paint()
result.style = Paint.Style.STROKE
result.isAntiAlias = true
result.strokeWidth = toPx(1, resources).toFloat()
result
}
// region Lifecycle
constructor(context: Context) : super(context) { }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { }
init {
setWillNotDraw(false)
}
// endregion
// region Updating
override fun onDraw(c: Canvas) {
val w = width.toFloat()
val h = height.toFloat()
val r = resources.getDimension(R.dimen.pn_option_corner_radius)
c.drawRoundRect(0.0f, 0.0f, w, h, r, r, paint)
c.drawRoundRect(0.0f, 0.0f, w, h, r, r, strokePaint)
super.onDraw(c)
}
// endregion
}
class NewConversationButtonImageView : androidx.appcompat.widget.AppCompatImageView, GlowView {
@ColorInt override var mainColor: Int = 0
set(newValue) { field = newValue; paint.color = newValue }
@ColorInt override var sessionShadowColor: Int = 0
set(newValue) { field = newValue; paint.setShadowLayer(toPx(6, resources).toFloat(), 0.0f, 0.0f, newValue) }
private val paint: Paint by lazy {
val result = Paint()
result.style = Paint.Style.FILL
result.isAntiAlias = true
result
}
// region Lifecycle
constructor(context: Context) : super(context) { }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { }
init {
setWillNotDraw(false)
}
// endregion
// region Updating
override fun onDraw(c: Canvas) {
val w = width.toFloat()
val h = height.toFloat()
c.drawCircle(w / 2, h / 2, w / 2, paint)
super.onDraw(c)
}
// endregion
}
class PathDotView : View, GlowView {
@ColorInt override var mainColor: Int = 0
set(newValue) { field = newValue; paint.color = newValue }
@ColorInt override var sessionShadowColor: Int = 0
set(newValue) { field = newValue; paint.setShadowLayer(toPx(4, resources).toFloat(), 0.0f, 0.0f, newValue) }
private val paint: Paint by lazy {
val result = Paint()
result.style = Paint.Style.FILL
result.isAntiAlias = true
result
}
// region Lifecycle
constructor(context: Context) : super(context) { }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { }
init {
setWillNotDraw(false)
}
// endregion
// region Updating
override fun onDraw(c: Canvas) {
val w = width.toFloat()
val h = height.toFloat()
c.drawCircle(w / 2, h / 2, w / 2, paint)
super.onDraw(c)
}
// endregion
}

View File

@@ -16,7 +16,7 @@ class LabeledSeparatorView : RelativeLayout {
private val path = Path()
private val paint: Paint by lazy{
private val paint: Paint by lazy {
val result = Paint()
result.style = Paint.Style.STROKE
result.color = ThemeUtil.getThemedColor(context, R.attr.dividerHorizontal)

View File

@@ -32,6 +32,7 @@ class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr:
private fun update() {
btnGroupNameDisplay.text = mentionCandidate.displayName
profilePictureView.publicKey = mentionCandidate.publicKey
profilePictureView.displayName = mentionCandidate.displayName
profilePictureView.additionalPublicKey = null
profilePictureView.isRSSFeed = false
profilePictureView.glide = glide!!

View File

@@ -7,9 +7,7 @@ import android.animation.ValueAnimator
import android.content.Context
import android.content.Context.VIBRATOR_SERVICE
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.PointF
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.VibrationEffect
import android.os.VibrationEffect.DEFAULT_AMPLITUDE
@@ -66,24 +64,25 @@ class NewConversationButtonSetView : RelativeLayout {
private val collapsedImageViewPosition by lazy { PointF((expandedSize - collapsedSize) / 2, (expandedSize - collapsedSize) / 2) }
private val imageView by lazy {
val result = ImageView(context)
val result = NewConversationButtonImageView(context)
val size = collapsedSize.toInt()
result.layoutParams = LayoutParams(size, size)
result.setBackgroundResource(R.drawable.new_conversation_button_background)
val background = result.background as GradientDrawable
@ColorRes val backgroundColorID = if (isMain)
R.color.accent else
R.color.new_conversation_button_collapsed_background
background.color = ColorStateList.valueOf(resources.getColorWithID(backgroundColorID, context.theme))
@ColorRes val backgroundColorID = if (isMain) R.color.accent else R.color.new_conversation_button_collapsed_background
@ColorRes val shadowColorID = if (isMain) {
R.color.new_conversation_button_shadow
} else {
if (UiModeUtilities.isDayUiMode(context)) R.color.transparent_black_30 else R.color.black
}
result.mainColor = resources.getColorWithID(backgroundColorID, context.theme)
result.sessionShadowColor = resources.getColorWithID(shadowColorID, context.theme)
result.scaleType = ImageView.ScaleType.CENTER
result.setImageResource(iconID)
result.imageTintList = if (isMain)
// Always use white icon for the main button.
result.imageTintList = if (isMain) {
ColorStateList.valueOf(resources.getColorWithID(android.R.color.white, context.theme))
else
} else {
ColorStateList.valueOf(resources.getColorWithID(R.color.text, context.theme))
}
result
}
@@ -106,30 +105,21 @@ class NewConversationButtonSetView : RelativeLayout {
}
fun expand() {
animateImageViewColorChange(R.color.new_conversation_button_collapsed_background, R.color.accent)
GlowViewUtilities.animateColorChange(context, imageView, R.color.new_conversation_button_collapsed_background, R.color.accent)
@ColorRes val startShadowColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.transparent_black_30 else R.color.black
GlowViewUtilities.animateShadowColorChange(context, imageView, startShadowColorID, R.color.new_conversation_button_shadow)
imageView.animateSizeChange(R.dimen.new_conversation_button_collapsed_size, R.dimen.new_conversation_button_expanded_size, animationDuration)
animateImageViewPositionChange(collapsedImageViewPosition, expandedImageViewPosition)
}
fun collapse() {
animateImageViewColorChange(R.color.accent, R.color.new_conversation_button_collapsed_background)
GlowViewUtilities.animateColorChange(context, imageView, R.color.accent, R.color.new_conversation_button_collapsed_background)
@ColorRes val endShadowColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.transparent_black_30 else R.color.black
GlowViewUtilities.animateShadowColorChange(context, imageView, R.color.new_conversation_button_shadow, endShadowColorID)
imageView.animateSizeChange(R.dimen.new_conversation_button_expanded_size, R.dimen.new_conversation_button_collapsed_size, animationDuration)
animateImageViewPositionChange(expandedImageViewPosition, collapsedImageViewPosition)
}
private fun animateImageViewColorChange(@ColorRes startColorID: Int, @ColorRes endColorID: Int) {
val drawable = imageView.background as GradientDrawable
val startColor = resources.getColorWithID(startColorID, context.theme)
val endColor = resources.getColorWithID(endColorID, context.theme)
val animation = ValueAnimator.ofObject(ArgbEvaluator(), startColor, endColor)
animation.duration = animationDuration
animation.addUpdateListener { animator ->
val color = animator.animatedValue as Int
drawable.color = ColorStateList.valueOf(color)
}
animation.start()
}
private fun animateImageViewPositionChange(startPosition: PointF, endPosition: PointF) {
val animation = ValueAnimator.ofObject(PointFEvaluator(), startPosition, endPosition)
animation.duration = animationDuration
@@ -170,6 +160,7 @@ class NewConversationButtonSetView : RelativeLayout {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { setUpViewHierarchy() }
private fun setUpViewHierarchy() {
disableClipping()
// Set up session button
addView(sessionButton)
sessionButton.alpha = 0.0f

View File

@@ -4,14 +4,30 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Canvas
import android.graphics.Paint
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import android.util.AttributeSet
import android.view.View
import androidx.annotation.ColorInt
import network.loki.messenger.R
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
import org.thoughtcrime.securesms.loki.utilities.toPx
import org.whispersystems.signalservice.loki.api.onionrequests.OnionRequestAPI
class PathStatusView : View {
private val broadcastReceivers = mutableListOf<BroadcastReceiver>()
@ColorInt var mainColor: Int = 0
set(newValue) { field = newValue; paint.color = newValue }
@ColorInt var sessionShadowColor: Int = 0
set(newValue) { field = newValue; paint.setShadowLayer(toPx(8, resources).toFloat(), 0.0f, 0.0f, newValue) }
private val paint: Paint by lazy {
val result = Paint()
result.style = Paint.Style.FILL
result.isAntiAlias = true
result
}
constructor(context: Context) : super(context) {
initialize()
@@ -31,6 +47,7 @@ class PathStatusView : View {
private fun initialize() {
update()
setWillNotDraw(false)
}
override fun onAttachedToWindow() {
@@ -70,8 +87,19 @@ class PathStatusView : View {
private fun update() {
if (OnionRequestAPI.paths.count() >= OnionRequestAPI.pathCount) {
setBackgroundResource(R.drawable.accent_dot)
mainColor = resources.getColorWithID(R.color.accent, context.theme)
sessionShadowColor = resources.getColorWithID(R.color.accent, context.theme)
} else {
setBackgroundResource(R.drawable.paths_building_dot)
mainColor = resources.getColorWithID(R.color.paths_building, context.theme)
sessionShadowColor = resources.getColorWithID(R.color.paths_building, context.theme)
}
}
override fun onDraw(c: Canvas) {
val w = width.toFloat()
val h = height.toFloat()
c.drawCircle(w / 2, h / 2, w / 2, paint)
super.onDraw(c)
}
}

View File

@@ -1,28 +1,32 @@
package org.thoughtcrime.securesms.loki.views
import android.content.Context
import androidx.annotation.DimenRes
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.RelativeLayout
import androidx.annotation.DimenRes
import com.bumptech.glide.load.engine.DiskCacheStrategy
import kotlinx.android.synthetic.main.view_profile_picture.view.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.loki.todo.JazzIdenticonDrawable
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.todo.AvatarPlaceholderGenerator
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager
// TODO: Look into a better way of handling different sizes. Maybe an enum (with associated values) encapsulating the different modes?
class ProfilePictureView : RelativeLayout {
lateinit var glide: GlideRequests
var publicKey: String? = null
var displayName: String? = null
var additionalPublicKey: String? = null
var additionalDisplayName: String? = null
var isRSSFeed = false
var isLarge = false
@@ -51,6 +55,45 @@ class ProfilePictureView : RelativeLayout {
// endregion
// region Updating
fun update(recipient: Recipient, threadID: Long) {
fun getUserDisplayName(publicKey: String?): String? {
if (publicKey == null || publicKey.isBlank()) {
return null
} else {
return DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey!!)
}
}
if (recipient.isGroupRecipient) {
if ("Session Public Chat" == recipient.name) {
publicKey = ""
displayName = ""
additionalPublicKey = null
isRSSFeed = true
} else {
val users = MentionsManager.shared.userPublicKeyCache[threadID]?.toMutableList() ?: mutableListOf()
users.remove(TextSecurePreferences.getLocalNumber(context))
val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
if (masterPublicKey != null) {
users.remove(masterPublicKey)
}
val randomUsers = users.sorted() // Sort to provide a level of stability
val pk = randomUsers.getOrNull(0) ?: ""
publicKey = pk
displayName = getUserDisplayName(pk)
val apk = randomUsers.getOrNull(1) ?: ""
additionalPublicKey = apk
additionalDisplayName = getUserDisplayName(apk)
isRSSFeed = recipient.name == "Loki News" || recipient.name == "Session Updates"
}
} else {
publicKey = recipient.address.toString()
displayName = recipient.name
additionalPublicKey = null
isRSSFeed = false
}
update()
}
fun update() {
val publicKey = publicKey ?: return
val additionalPublicKey = additionalPublicKey
@@ -58,28 +101,50 @@ class ProfilePictureView : RelativeLayout {
singleModeImageViewContainer.visibility = if (additionalPublicKey == null && !isRSSFeed && !isLarge) View.VISIBLE else View.INVISIBLE
largeSingleModeImageViewContainer.visibility = if (additionalPublicKey == null && !isRSSFeed && isLarge) View.VISIBLE else View.INVISIBLE
rssImageView.visibility = if (isRSSFeed) View.VISIBLE else View.INVISIBLE
fun setProfilePictureIfNeeded(imageView: ImageView, hexEncodedPublicKey: String, @DimenRes sizeID: Int) {
glide.clear(imageView)
if (hexEncodedPublicKey.isNotEmpty()) {
val recipient = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false);
val signalProfilePicture = recipient.contactPhoto
if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0" && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "") {
glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
} else {
val size = resources.getDimensionPixelSize(sizeID)
val masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
val hepk = if (recipient.isLocalNumber && masterHexEncodedPublicKey != null) masterHexEncodedPublicKey else hexEncodedPublicKey
val jazzIcon = JazzIdenticonDrawable(size, size, hepk)
glide.load(jazzIcon).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
}
setProfilePictureIfNeeded(
doubleModeImageView1,
publicKey,
displayName,
R.dimen.small_profile_picture_size)
setProfilePictureIfNeeded(
doubleModeImageView2,
additionalPublicKey ?: "",
additionalDisplayName,
R.dimen.small_profile_picture_size)
setProfilePictureIfNeeded(
singleModeImageView,
publicKey,
displayName,
R.dimen.medium_profile_picture_size)
setProfilePictureIfNeeded(
largeSingleModeImageView,
publicKey,
displayName,
R.dimen.large_profile_picture_size)
}
private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) {
glide.clear(imageView)
if (publicKey.isNotEmpty()) {
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false);
val signalProfilePicture = recipient.contactPhoto
if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0"
&& (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "") {
glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
} else {
imageView.setImageDrawable(null)
val sizeInPX = resources.getDimensionPixelSize(sizeResId)
val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
val hepk = if (recipient.isLocalNumber && masterPublicKey != null) masterPublicKey else publicKey
glide.load(AvatarPlaceholderGenerator.generate(
context,
sizeInPX,
hepk,
displayName
)).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
}
} else {
imageView.setImageDrawable(null)
}
setProfilePictureIfNeeded(doubleModeImageView1, publicKey, R.dimen.small_profile_picture_size)
setProfilePictureIfNeeded(doubleModeImageView2, additionalPublicKey ?: "", R.dimen.small_profile_picture_size)
setProfilePictureIfNeeded(singleModeImageView, publicKey, R.dimen.medium_profile_picture_size)
setProfilePictureIfNeeded(largeSingleModeImageView, publicKey, R.dimen.large_profile_picture_size)
}
// endregion
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.loki.views
import android.content.Context
import android.text.TextUtils
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
@@ -8,6 +9,7 @@ import android.widget.LinearLayout
import kotlinx.android.synthetic.main.view_conversation.view.profilePictureView
import kotlinx.android.synthetic.main.view_user.view.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient
@@ -47,22 +49,35 @@ class UserView : LinearLayout {
// region Updating
fun bind(user: Recipient, glide: GlideRequests, actionIndicator: ActionIndicator, isSelected: Boolean = false) {
fun getUserDisplayName(publicKey: String?): String? {
if (publicKey == null || publicKey.isBlank()) {
return null
} else {
return DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey!!)
}
}
val address = user.address.serialize()
if (user.isGroupRecipient) {
if ("Session Public Chat" == user.name || user.address.isRSSFeed) {
profilePictureView.publicKey = ""
profilePictureView.displayName = null
profilePictureView.additionalPublicKey = null
profilePictureView.isRSSFeed = true
} else {
val threadID = GroupManager.getThreadIDFromGroupID(address, context)
val users = MentionsManager.shared.userPublicKeyCache[threadID]?.toList() ?: listOf()
val randomUsers = users.sorted() // Sort to provide a level of stability
profilePictureView.publicKey = randomUsers.getOrNull(0) ?: ""
profilePictureView.additionalPublicKey = randomUsers.getOrNull(1) ?: ""
val pk = randomUsers.getOrNull(0) ?: ""
profilePictureView.publicKey = pk
profilePictureView.displayName = getUserDisplayName(pk)
val apk = randomUsers.getOrNull(1) ?: ""
profilePictureView.additionalPublicKey = apk
profilePictureView.additionalDisplayName = getUserDisplayName(apk)
profilePictureView.isRSSFeed = false
}
} else {
profilePictureView.publicKey = address
profilePictureView.displayName = getUserDisplayName(address)
profilePictureView.additionalPublicKey = null
profilePictureView.isRSSFeed = false
}

View File

@@ -14,21 +14,22 @@ import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.text.SpannableStringBuilder;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationCompat.Action;
import androidx.core.app.RemoteInput;
import android.text.SpannableStringBuilder;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.todo.AvatarPlaceholderGenerator;
import org.thoughtcrime.securesms.loki.utilities.NotificationUtilities;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
@@ -80,9 +81,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
addPerson(recipient.getContactUri().toString());
}
ContactPhoto contactPhoto = recipient.getContactPhoto();
FallbackContactPhoto fallbackContactPhoto = recipient.getFallbackContactPhoto();
ContactPhoto contactPhoto = recipient.getContactPhoto();
if (contactPhoto != null) {
try {
setLargeIcon(GlideApp.with(context.getApplicationContext())
@@ -94,10 +93,10 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
.get());
} catch (InterruptedException | ExecutionException e) {
Log.w(TAG, e);
setLargeIcon(fallbackContactPhoto.asDrawable(context, recipient.getColor().toConversationColor(context)));
setLargeIcon(getPlaceholderDrawable(context, recipient));
}
} else {
setLargeIcon(fallbackContactPhoto.asDrawable(context, recipient.getColor().toConversationColor(context)));
setLargeIcon(getPlaceholderDrawable(context, recipient));
}
} else {
@@ -321,4 +320,12 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
return content;
}
private static Drawable getPlaceholderDrawable(Context context, Recipient recipient) {
String publicKey = recipient.getAddress().serialize();
String hepk = (recipient.isLocalNumber() && publicKey != null)
? TextSecurePreferences.getMasterHexEncodedPublicKey(context)
: publicKey;
String displayName = recipient.getName();
return AvatarPlaceholderGenerator.INSTANCE.generate(context, 128, hepk, displayName);
}
}