Merge branch 'dev' into comp
commit
9a84f6c67b
@ -0,0 +1,3 @@
|
||||
[submodule "libsession-util/libsession-util"]
|
||||
path = libsession-util/libsession-util
|
||||
url = https://github.com/oxen-io/libsession-util.git
|
@ -0,0 +1,102 @@
|
||||
package network.loki.messenger
|
||||
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import network.loki.messenger.libsession_util.ConfigBase
|
||||
import network.loki.messenger.libsession_util.Contacts
|
||||
import network.loki.messenger.libsession_util.util.Contact
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.argThat
|
||||
import org.mockito.kotlin.eq
|
||||
import org.mockito.kotlin.spy
|
||||
import org.mockito.kotlin.verify
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.KeyHelper
|
||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
||||
import kotlin.random.Random
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@SmallTest
|
||||
class LibSessionTests {
|
||||
|
||||
private fun randomSeedBytes() = (0 until 16).map { Random.nextInt(UByte.MAX_VALUE.toInt()).toByte() }
|
||||
private fun randomKeyPair() = KeyPairUtilities.generate(randomSeedBytes().toByteArray())
|
||||
private fun randomSessionId() = randomKeyPair().x25519KeyPair.hexEncodedPublicKey
|
||||
|
||||
private var fakeHashI = 0
|
||||
private val nextFakeHash: String
|
||||
get() = "fakehash${fakeHashI++}"
|
||||
|
||||
private fun maybeGetUserInfo(): Pair<ByteArray, String>? {
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
|
||||
val prefs = appContext.prefs
|
||||
val localUserPublicKey = prefs.getLocalNumber()
|
||||
val secretKey = with(appContext) {
|
||||
val edKey = KeyPairUtilities.getUserED25519KeyPair(this) ?: return null
|
||||
edKey.secretKey.asBytes
|
||||
}
|
||||
return if (localUserPublicKey == null || secretKey == null) null
|
||||
else secretKey to localUserPublicKey
|
||||
}
|
||||
|
||||
private fun buildContactMessage(contactList: List<Contact>): ByteArray {
|
||||
val (key,_) = maybeGetUserInfo()!!
|
||||
val contacts = Contacts.Companion.newInstance(key)
|
||||
contactList.forEach { contact ->
|
||||
contacts.set(contact)
|
||||
}
|
||||
return contacts.push().config
|
||||
}
|
||||
|
||||
private fun fakePollNewConfig(configBase: ConfigBase, toMerge: ByteArray) {
|
||||
configBase.merge(nextFakeHash to toMerge)
|
||||
MessagingModuleConfiguration.shared.configFactory.persist(configBase, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setupUser() {
|
||||
PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().targetContext.applicationContext).edit {
|
||||
putBoolean(TextSecurePreferences.HAS_FORCED_NEW_CONFIG, true).apply()
|
||||
}
|
||||
val newBytes = randomSeedBytes().toByteArray()
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
|
||||
val kp = KeyPairUtilities.generate(newBytes)
|
||||
KeyPairUtilities.store(context, kp.seed, kp.ed25519KeyPair, kp.x25519KeyPair)
|
||||
val registrationID = KeyHelper.generateRegistrationId(false)
|
||||
TextSecurePreferences.setLocalRegistrationId(context, registrationID)
|
||||
TextSecurePreferences.setLocalNumber(context, kp.x25519KeyPair.hexEncodedPublicKey)
|
||||
TextSecurePreferences.setRestorationTime(context, 0)
|
||||
TextSecurePreferences.setHasViewedSeed(context, false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migration_one_to_ones() {
|
||||
val app = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
|
||||
val storageSpy = spy(app.storage)
|
||||
app.storage = storageSpy
|
||||
|
||||
val newContactId = randomSessionId()
|
||||
val singleContact = Contact(
|
||||
id = newContactId,
|
||||
approved = true,
|
||||
expiryMode = ExpiryMode.NONE
|
||||
)
|
||||
val newContactMerge = buildContactMessage(listOf(singleContact))
|
||||
val contacts = MessagingModuleConfiguration.shared.configFactory.contacts!!
|
||||
fakePollNewConfig(contacts, newContactMerge)
|
||||
verify(storageSpy).addLibSessionContacts(argThat {
|
||||
first().let { it.id == newContactId && it.approved } && size == 1
|
||||
})
|
||||
verify(storageSpy).setRecipientApproved(argThat { address.serialize() == newContactId }, eq(true))
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package org.thoughtcrime.securesms
|
||||
|
||||
import android.content.Context
|
||||
import network.loki.messenger.R
|
||||
|
||||
class DeleteMediaDialog {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun show(context: Context, recordCount: Int, doDelete: Runnable) = context.showSessionDialog {
|
||||
iconAttribute(R.attr.dialog_alert_icon)
|
||||
title(
|
||||
context.resources.getQuantityString(
|
||||
R.plurals.MediaOverviewActivity_Media_delete_confirm_title,
|
||||
recordCount,
|
||||
recordCount
|
||||
)
|
||||
)
|
||||
text(
|
||||
context.resources.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_message,
|
||||
recordCount,
|
||||
recordCount
|
||||
)
|
||||
)
|
||||
button(R.string.delete) { doDelete.run() }
|
||||
cancelButton()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package org.thoughtcrime.securesms
|
||||
|
||||
import android.content.Context
|
||||
import network.loki.messenger.R
|
||||
|
||||
class DeleteMediaPreviewDialog {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun show(context: Context, doDelete: Runnable) {
|
||||
context.showSessionDialog {
|
||||
iconAttribute(R.attr.dialog_alert_icon)
|
||||
title(R.string.MediaPreviewActivity_media_delete_confirmation_title)
|
||||
text(R.string.MediaPreviewActivity_media_delete_confirmation_message)
|
||||
button(R.string.delete) { doDelete.run() }
|
||||
cancelButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.session.libsession.utilities.ExpirationUtil;
|
||||
|
||||
import cn.carbswang.android.numberpickerview.library.NumberPickerView;
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class ExpirationDialog extends AlertDialog {
|
||||
|
||||
protected ExpirationDialog(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
protected ExpirationDialog(Context context, int theme) {
|
||||
super(context, theme);
|
||||
}
|
||||
|
||||
protected ExpirationDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
|
||||
super(context, cancelable, cancelListener);
|
||||
}
|
||||
|
||||
public static void show(final Context context,
|
||||
final int currentExpiration,
|
||||
final @NonNull OnClickListener listener)
|
||||
{
|
||||
final View view = createNumberPickerView(context, currentExpiration);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(context.getString(R.string.ExpirationDialog_disappearing_messages));
|
||||
builder.setView(view);
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
int selected = ((NumberPickerView)view.findViewById(R.id.expiration_number_picker)).getValue();
|
||||
listener.onClick(context.getResources().getIntArray(R.array.expiration_times)[selected]);
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private static View createNumberPickerView(final Context context, final int currentExpiration) {
|
||||
final LayoutInflater inflater = LayoutInflater.from(context);
|
||||
final View view = inflater.inflate(R.layout.expiration_dialog, null);
|
||||
final NumberPickerView numberPickerView = view.findViewById(R.id.expiration_number_picker);
|
||||
final TextView textView = view.findViewById(R.id.expiration_details);
|
||||
final int[] expirationTimes = context.getResources().getIntArray(R.array.expiration_times);
|
||||
final String[] expirationDisplayValues = new String[expirationTimes.length];
|
||||
|
||||
int selectedIndex = expirationTimes.length - 1;
|
||||
|
||||
for (int i=0;i<expirationTimes.length;i++) {
|
||||
expirationDisplayValues[i] = ExpirationUtil.getExpirationDisplayValue(context, expirationTimes[i]);
|
||||
|
||||
if ((currentExpiration >= expirationTimes[i]) &&
|
||||
(i == expirationTimes.length -1 || currentExpiration < expirationTimes[i+1])) {
|
||||
selectedIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
numberPickerView.setDisplayedValues(expirationDisplayValues);
|
||||
numberPickerView.setMinValue(0);
|
||||
numberPickerView.setMaxValue(expirationTimes.length-1);
|
||||
|
||||
NumberPickerView.OnValueChangeListener listener = (picker, oldVal, newVal) -> {
|
||||
if (newVal == 0) {
|
||||
textView.setText(R.string.ExpirationDialog_your_messages_will_not_expire);
|
||||
} else {
|
||||
textView.setText(context.getString(R.string.ExpirationDialog_your_messages_will_disappear_s_after_they_have_been_seen, picker.getDisplayedValues()[newVal]));
|
||||
}
|
||||
};
|
||||
|
||||
numberPickerView.setOnValueChangedListener(listener);
|
||||
numberPickerView.setValue(selectedIndex);
|
||||
listener.onValueChange(numberPickerView, selectedIndex, selectedIndex);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
public interface OnClickListener {
|
||||
public void onClick(int expirationTime);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package org.thoughtcrime.securesms
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import cn.carbswang.android.numberpickerview.library.NumberPickerView
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.ExpirationUtil
|
||||
|
||||
fun Context.showExpirationDialog(
|
||||
expiration: Int,
|
||||
onExpirationTime: (Int) -> Unit
|
||||
): AlertDialog {
|
||||
val view = LayoutInflater.from(this).inflate(R.layout.expiration_dialog, null)
|
||||
val numberPickerView = view.findViewById<NumberPickerView>(R.id.expiration_number_picker)
|
||||
|
||||
fun updateText(index: Int) {
|
||||
view.findViewById<TextView>(R.id.expiration_details).text = when (index) {
|
||||
0 -> getString(R.string.ExpirationDialog_your_messages_will_not_expire)
|
||||
else -> getString(
|
||||
R.string.ExpirationDialog_your_messages_will_disappear_s_after_they_have_been_seen,
|
||||
numberPickerView.displayedValues[index]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val expirationTimes = resources.getIntArray(R.array.expiration_times)
|
||||
val expirationDisplayValues = expirationTimes
|
||||
.map { ExpirationUtil.getExpirationDisplayValue(this, it) }
|
||||
.toTypedArray()
|
||||
|
||||
val selectedIndex = expirationTimes.run { indexOfFirst { it >= expiration }.coerceIn(indices) }
|
||||
|
||||
numberPickerView.apply {
|
||||
displayedValues = expirationDisplayValues
|
||||
minValue = 0
|
||||
maxValue = expirationTimes.lastIndex
|
||||
setOnValueChangedListener { _, _, index -> updateText(index) }
|
||||
value = selectedIndex
|
||||
}
|
||||
|
||||
updateText(selectedIndex)
|
||||
|
||||
return showSessionDialog {
|
||||
title(getString(R.string.ExpirationDialog_disappearing_messages))
|
||||
view(view)
|
||||
okButton { onExpirationTime(numberPickerView.let { expirationTimes[it.value] }) }
|
||||
cancelButton()
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class MuteDialog extends AlertDialog {
|
||||
|
||||
|
||||
protected MuteDialog(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
protected MuteDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
|
||||
super(context, cancelable, cancelListener);
|
||||
}
|
||||
|
||||
protected MuteDialog(Context context, int theme) {
|
||||
super(context, theme);
|
||||
}
|
||||
|
||||
public static void show(final Context context, final @NonNull MuteSelectionListener listener) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.MuteDialog_mute_notifications);
|
||||
builder.setItems(R.array.mute_durations, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, final int which) {
|
||||
final long muteUntil;
|
||||
|
||||
switch (which) {
|
||||
case 1: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(2); break;
|
||||
case 2: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); break;
|
||||
case 3: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7); break;
|
||||
case 4: muteUntil = Long.MAX_VALUE; break;
|
||||
default: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
|
||||
}
|
||||
|
||||
listener.onMuted(muteUntil);
|
||||
}
|
||||
});
|
||||
|
||||
builder.show();
|
||||
|
||||
}
|
||||
|
||||
public interface MuteSelectionListener {
|
||||
public void onMuted(long until);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package org.thoughtcrime.securesms
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import network.loki.messenger.R
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
fun showMuteDialog(
|
||||
context: Context,
|
||||
onMuteDuration: (Long) -> Unit
|
||||
): AlertDialog = context.showSessionDialog {
|
||||
title(R.string.MuteDialog_mute_notifications)
|
||||
items(Option.values().map { it.stringRes }.map(context::getString).toTypedArray()) {
|
||||
onMuteDuration(Option.values()[it].getTime())
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Option(@StringRes val stringRes: Int, val getTime: () -> Long) {
|
||||
ONE_HOUR(R.string.arrays__mute_for_one_hour, duration = TimeUnit.HOURS.toMillis(1)),
|
||||
TWO_HOURS(R.string.arrays__mute_for_two_hours, duration = TimeUnit.DAYS.toMillis(2)),
|
||||
ONE_DAY(R.string.arrays__mute_for_one_day, duration = TimeUnit.DAYS.toMillis(1)),
|
||||
SEVEN_DAYS(R.string.arrays__mute_for_seven_days, duration = TimeUnit.DAYS.toMillis(7)),
|
||||
FOREVER(R.string.arrays__mute_forever, getTime = { Long.MAX_VALUE });
|
||||
|
||||
constructor(@StringRes stringRes: Int, duration: Long): this(stringRes, { System.currentTimeMillis() + duration })
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
package org.thoughtcrime.securesms
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.LinearLayout.VERTICAL
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.setMargins
|
||||
import androidx.core.view.setPadding
|
||||
import androidx.core.view.updateMargins
|
||||
import androidx.fragment.app.Fragment
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
|
||||
|
||||
@DslMarker
|
||||
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
|
||||
annotation class DialogDsl
|
||||
|
||||
@DialogDsl
|
||||
class SessionDialogBuilder(val context: Context) {
|
||||
|
||||
private val dp20 = toPx(20, context.resources)
|
||||
private val dp40 = toPx(40, context.resources)
|
||||
|
||||
private val dialogBuilder: AlertDialog.Builder = AlertDialog.Builder(context)
|
||||
|
||||
private var dialog: AlertDialog? = null
|
||||
private fun dismiss() = dialog?.dismiss()
|
||||
|
||||
private val topView = LinearLayout(context).apply { orientation = VERTICAL }
|
||||
.also(dialogBuilder::setCustomTitle)
|
||||
private val contentView = LinearLayout(context).apply { orientation = VERTICAL }
|
||||
private val buttonLayout = LinearLayout(context)
|
||||
|
||||
private val root = LinearLayout(context).apply { orientation = VERTICAL }
|
||||
.also(dialogBuilder::setView)
|
||||
.apply {
|
||||
addView(contentView)
|
||||
addView(buttonLayout)
|
||||
}
|
||||
|
||||
fun title(@StringRes id: Int) = title(context.getString(id))
|
||||
|
||||
fun title(text: CharSequence?) = title(text?.toString())
|
||||
fun title(text: String?) {
|
||||
text(text, R.style.TextAppearance_AppCompat_Title) { setPadding(dp20) }
|
||||
}
|
||||
|
||||
fun text(@StringRes id: Int, style: Int = 0) = text(context.getString(id), style)
|
||||
fun text(text: CharSequence?, @StyleRes style: Int = 0) {
|
||||
text(text, style) {
|
||||
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
.apply { updateMargins(dp40, 0, dp40, dp20) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun text(text: CharSequence?, @StyleRes style: Int, modify: TextView.() -> Unit) {
|
||||
text ?: return
|
||||
TextView(context, null, 0, style)
|
||||
.apply {
|
||||
setText(text)
|
||||
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||
modify()
|
||||
}.let(topView::addView)
|
||||
}
|
||||
|
||||
fun view(view: View) = contentView.addView(view)
|
||||
|
||||
fun view(@LayoutRes layout: Int): View = LayoutInflater.from(context).inflate(layout, contentView)
|
||||
|
||||
fun iconAttribute(@AttrRes icon: Int): AlertDialog.Builder = dialogBuilder.setIconAttribute(icon)
|
||||
|
||||
fun singleChoiceItems(
|
||||
options: Collection<String>,
|
||||
currentSelected: Int = 0,
|
||||
onSelect: (Int) -> Unit
|
||||
) = singleChoiceItems(options.toTypedArray(), currentSelected, onSelect)
|
||||
|
||||
fun singleChoiceItems(
|
||||
options: Array<String>,
|
||||
currentSelected: Int = 0,
|
||||
onSelect: (Int) -> Unit
|
||||
): AlertDialog.Builder = dialogBuilder.setSingleChoiceItems(
|
||||
options,
|
||||
currentSelected
|
||||
) { dialog, it -> onSelect(it); dialog.dismiss() }
|
||||
|
||||
fun items(
|
||||
options: Array<String>,
|
||||
onSelect: (Int) -> Unit
|
||||
): AlertDialog.Builder = dialogBuilder.setItems(
|
||||
options,
|
||||
) { dialog, it -> onSelect(it); dialog.dismiss() }
|
||||
|
||||
fun destructiveButton(
|
||||
@StringRes text: Int,
|
||||
@StringRes contentDescription: Int,
|
||||
listener: () -> Unit = {}
|
||||
) = button(
|
||||
text,
|
||||
contentDescription,
|
||||
R.style.Widget_Session_Button_Dialog_DestructiveText,
|
||||
listener
|
||||
)
|
||||
|
||||
fun okButton(listener: (() -> Unit) = {}) = button(android.R.string.ok, listener = listener)
|
||||
fun cancelButton(listener: (() -> Unit) = {}) = button(android.R.string.cancel, R.string.AccessibilityId_cancel_button, listener = listener)
|
||||
|
||||
fun button(
|
||||
@StringRes text: Int,
|
||||
@StringRes contentDescriptionRes: Int = text,
|
||||
@StyleRes style: Int = R.style.Widget_Session_Button_Dialog_UnimportantText,
|
||||
listener: (() -> Unit) = {}
|
||||
) = Button(context, null, 0, style).apply {
|
||||
setText(text)
|
||||
contentDescription = resources.getString(contentDescriptionRes)
|
||||
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1f)
|
||||
.apply { setMargins(toPx(20, resources)) }
|
||||
setOnClickListener {
|
||||
listener.invoke()
|
||||
dismiss()
|
||||
}
|
||||
}.let(buttonLayout::addView)
|
||||
|
||||
fun create(): AlertDialog = dialogBuilder.create().also { dialog = it }
|
||||
fun show(): AlertDialog = dialogBuilder.show().also { dialog = it }
|
||||
}
|
||||
|
||||
fun Context.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
||||
SessionDialogBuilder(this).apply { build() }.show()
|
||||
|
||||
fun Fragment.showSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
||||
SessionDialogBuilder(requireContext()).apply { build() }.show()
|
||||
fun Fragment.createSessionDialog(build: SessionDialogBuilder.() -> Unit): AlertDialog =
|
||||
SessionDialogBuilder(requireContext()).apply { build() }.create()
|
@ -1,51 +1,42 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.DialogBlockedBinding
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
import org.thoughtcrime.securesms.createSessionDialog
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
|
||||
/** Shown upon sending a message to a user that's blocked. */
|
||||
class BlockedDialog(private val recipient: Recipient, private val context: Context) : BaseDialog() {
|
||||
class BlockedDialog(private val recipient: Recipient, private val context: Context) : DialogFragment() {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val binding = DialogBlockedBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
||||
val contactDB = DatabaseComponent.get(requireContext()).sessionContactDatabase()
|
||||
val sessionID = recipient.address.toString()
|
||||
val contact = contactDB.getContactWithSessionID(sessionID)
|
||||
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
|
||||
val title = resources.getString(R.string.dialog_blocked_title, name)
|
||||
binding.blockedTitleTextView.text = title
|
||||
|
||||
val explanation = resources.getString(R.string.dialog_blocked_explanation, name)
|
||||
val spannable = SpannableStringBuilder(explanation)
|
||||
val startIndex = explanation.indexOf(name)
|
||||
spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
binding.blockedExplanationTextView.text = spannable
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
binding.unblockButton.setOnClickListener { unblock() }
|
||||
builder.setView(binding.root)
|
||||
|
||||
title(resources.getString(R.string.dialog_blocked_title, name))
|
||||
text(spannable)
|
||||
button(R.string.ConversationActivity_unblock) { unblock() }
|
||||
cancelButton { dismiss() }
|
||||
}
|
||||
|
||||
private fun unblock() {
|
||||
DatabaseComponent.get(requireContext()).recipientDatabase().setBlocked(recipient, false)
|
||||
MessagingModuleConfiguration.shared.storage.setBlocked(listOf(recipient), false)
|
||||
dismiss()
|
||||
|
||||
// TODO: Remove in UserConfig branch
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,23 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.dialogs
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import network.loki.messenger.databinding.DialogSendSeedBinding
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.createSessionDialog
|
||||
|
||||
/** Shown if the user is about to send their recovery phrase to someone. */
|
||||
class SendSeedDialog(private val proceed: (() -> Unit)? = null) : BaseDialog() {
|
||||
class SendSeedDialog(private val proceed: (() -> Unit)? = null) : DialogFragment() {
|
||||
|
||||
override fun setContentView(builder: AlertDialog.Builder) {
|
||||
val binding = DialogSendSeedBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
binding.sendSeedButton.setOnClickListener { send() }
|
||||
builder.setView(binding.root)
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
|
||||
title(R.string.dialog_send_seed_title)
|
||||
text(R.string.dialog_send_seed_explanation)
|
||||
button(R.string.dialog_send_seed_send_button_title) { send() }
|
||||
cancelButton()
|
||||
}
|
||||
|
||||
private fun send() {
|
||||
proceed?.invoke()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||
|
||||
open class BaseDialog : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val builder = AlertDialog.Builder(requireContext())
|
||||
setContentView(builder)
|
||||
val result = builder.create()
|
||||
result.window?.setDimAmount(0.6f)
|
||||
return result
|
||||
}
|
||||
|
||||
open fun setContentView(builder: AlertDialog.Builder) {
|
||||
// To be overridden by subclasses
|
||||
}
|
||||
}
|
@ -1,21 +1,18 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.showSessionDialog
|
||||
|
||||
object NotificationUtils {
|
||||
fun showNotifyDialog(context: Context, thread: Recipient, notifyTypeHandler: (Int)->Unit) {
|
||||
val notifyTypes = context.resources.getStringArray(R.array.notify_types)
|
||||
val currentSelected = thread.notifyType
|
||||
|
||||
AlertDialog.Builder(context)
|
||||
.setSingleChoiceItems(notifyTypes,currentSelected) { d, newSelection ->
|
||||
notifyTypeHandler(newSelection)
|
||||
d.dismiss()
|
||||
}
|
||||
.setTitle(R.string.RecipientPreferenceActivity_notification_settings)
|
||||
.show()
|
||||
context.showSessionDialog {
|
||||
title(R.string.RecipientPreferenceActivity_notification_settings)
|
||||
singleChoiceItems(
|
||||
context.resources.getStringArray(R.array.notify_types),
|
||||
thread.notifyType
|
||||
) { notifyTypeHandler(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,53 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.contentValuesOf
|
||||
import androidx.core.database.getBlobOrNull
|
||||
import androidx.core.database.getLongOrNull
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
|
||||
class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(context, helper) {
|
||||
|
||||
companion object {
|
||||
private const val VARIANT = "variant"
|
||||
private const val PUBKEY = "publicKey"
|
||||
private const val DATA = "data"
|
||||
private const val TIMESTAMP = "timestamp" // Milliseconds
|
||||
|
||||
private const val TABLE_NAME = "configs_table"
|
||||
|
||||
const val CREATE_CONFIG_TABLE_COMMAND =
|
||||
"CREATE TABLE $TABLE_NAME ($VARIANT TEXT NOT NULL, $PUBKEY TEXT NOT NULL, $DATA BLOB, $TIMESTAMP INTEGER NOT NULL DEFAULT 0, PRIMARY KEY($VARIANT, $PUBKEY));"
|
||||
|
||||
private const val VARIANT_AND_PUBKEY_WHERE = "$VARIANT = ? AND $PUBKEY = ?"
|
||||
}
|
||||
|
||||
fun storeConfig(variant: String, publicKey: String, data: ByteArray, timestamp: Long) {
|
||||
val db = writableDatabase
|
||||
val contentValues = contentValuesOf(
|
||||
VARIANT to variant,
|
||||
PUBKEY to publicKey,
|
||||
DATA to data,
|
||||
TIMESTAMP to timestamp
|
||||
)
|
||||
db.insertOrUpdate(TABLE_NAME, contentValues, VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey))
|
||||
}
|
||||
|
||||
fun retrieveConfigAndHashes(variant: String, publicKey: String): ByteArray? {
|
||||
val db = readableDatabase
|
||||
val query = db.query(TABLE_NAME, arrayOf(DATA), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null)
|
||||
return query?.use { cursor ->
|
||||
if (!cursor.moveToFirst()) return@use null
|
||||
val bytes = cursor.getBlobOrNull(cursor.getColumnIndex(DATA)) ?: return@use null
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
fun retrieveConfigLastUpdateTimestamp(variant: String, publicKey: String): Long {
|
||||
val db = readableDatabase
|
||||
val cursor = db.query(TABLE_NAME, arrayOf(TIMESTAMP), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null)
|
||||
if (cursor == null) return 0
|
||||
if (!cursor.moveToFirst()) return 0
|
||||
return (cursor.getLongOrNull(cursor.getColumnIndex(TIMESTAMP)) ?: 0)
|
||||
}
|
||||
}
|
@ -0,0 +1,251 @@
|
||||
package org.thoughtcrime.securesms.dependencies
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Trace
|
||||
import network.loki.messenger.libsession_util.ConfigBase
|
||||
import network.loki.messenger.libsession_util.Contacts
|
||||
import network.loki.messenger.libsession_util.ConversationVolatileConfig
|
||||
import network.loki.messenger.libsession_util.UserGroupsConfig
|
||||
import network.loki.messenger.libsession_util.UserProfile
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.ConfigFactoryProtocol
|
||||
import org.session.libsession.utilities.ConfigFactoryUpdateListener
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.database.ConfigDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
|
||||
class ConfigFactory(
|
||||
private val context: Context,
|
||||
private val configDatabase: ConfigDatabase,
|
||||
private val maybeGetUserInfo: () -> Pair<ByteArray, String>?
|
||||
) :
|
||||
ConfigFactoryProtocol {
|
||||
companion object {
|
||||
// This is a buffer period within which we will process messages which would result in a
|
||||
// config change, any message which would normally result in a config change which was sent
|
||||
// before `lastConfigMessage.timestamp - configChangeBufferPeriod` will not actually have
|
||||
// it's changes applied (control text will still be added though)
|
||||
val configChangeBufferPeriod: Long = (2 * 60 * 1000)
|
||||
}
|
||||
|
||||
fun keyPairChanged() { // this should only happen restoring or clearing data
|
||||
_userConfig?.free()
|
||||
_contacts?.free()
|
||||
_convoVolatileConfig?.free()
|
||||
_userConfig = null
|
||||
_contacts = null
|
||||
_convoVolatileConfig = null
|
||||
}
|
||||
|
||||
private val userLock = Object()
|
||||
private var _userConfig: UserProfile? = null
|
||||
private val contactsLock = Object()
|
||||
private var _contacts: Contacts? = null
|
||||
private val convoVolatileLock = Object()
|
||||
private var _convoVolatileConfig: ConversationVolatileConfig? = null
|
||||
private val userGroupsLock = Object()
|
||||
private var _userGroups: UserGroupsConfig? = null
|
||||
|
||||
private val isConfigForcedOn by lazy { TextSecurePreferences.hasForcedNewConfig(context) }
|
||||
|
||||
private val listeners: MutableList<ConfigFactoryUpdateListener> = mutableListOf()
|
||||
fun registerListener(listener: ConfigFactoryUpdateListener) {
|
||||
listeners += listener
|
||||
}
|
||||
|
||||
fun unregisterListener(listener: ConfigFactoryUpdateListener) {
|
||||
listeners -= listener
|
||||
}
|
||||
|
||||
private inline fun <T> synchronizedWithLog(lock: Any, body: ()->T): T {
|
||||
Trace.beginSection("synchronizedWithLog")
|
||||
val result = synchronized(lock) {
|
||||
body()
|
||||
}
|
||||
Trace.endSection()
|
||||
return result
|
||||
}
|
||||
|
||||
override val user: UserProfile?
|
||||
get() = synchronizedWithLog(userLock) {
|
||||
if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return null
|
||||
if (_userConfig == null) {
|
||||
val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
|
||||
val userDump = configDatabase.retrieveConfigAndHashes(
|
||||
SharedConfigMessage.Kind.USER_PROFILE.name,
|
||||
publicKey
|
||||
)
|
||||
_userConfig = if (userDump != null) {
|
||||
UserProfile.newInstance(secretKey, userDump)
|
||||
} else {
|
||||
ConfigurationMessageUtilities.generateUserProfileConfigDump()?.let { dump ->
|
||||
UserProfile.newInstance(secretKey, dump)
|
||||
} ?: UserProfile.newInstance(secretKey)
|
||||
}
|
||||
}
|
||||
_userConfig
|
||||
}
|
||||
|
||||
override val contacts: Contacts?
|
||||
get() = synchronizedWithLog(contactsLock) {
|
||||
if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return null
|
||||
if (_contacts == null) {
|
||||
val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
|
||||
val contactsDump = configDatabase.retrieveConfigAndHashes(
|
||||
SharedConfigMessage.Kind.CONTACTS.name,
|
||||
publicKey
|
||||
)
|
||||
_contacts = if (contactsDump != null) {
|
||||
Contacts.newInstance(secretKey, contactsDump)
|
||||
} else {
|
||||
ConfigurationMessageUtilities.generateContactConfigDump()?.let { dump ->
|
||||
Contacts.newInstance(secretKey, dump)
|
||||
} ?: Contacts.newInstance(secretKey)
|
||||
}
|
||||
}
|
||||
_contacts
|
||||
}
|
||||
|
||||
override val convoVolatile: ConversationVolatileConfig?
|
||||
get() = synchronizedWithLog(convoVolatileLock) {
|
||||
if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return null
|
||||
if (_convoVolatileConfig == null) {
|
||||
val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
|
||||
val convoDump = configDatabase.retrieveConfigAndHashes(
|
||||
SharedConfigMessage.Kind.CONVO_INFO_VOLATILE.name,
|
||||
publicKey
|
||||
)
|
||||
_convoVolatileConfig = if (convoDump != null) {
|
||||
ConversationVolatileConfig.newInstance(secretKey, convoDump)
|
||||
} else {
|
||||
ConfigurationMessageUtilities.generateConversationVolatileDump(context)
|
||||
?.let { dump ->
|
||||
ConversationVolatileConfig.newInstance(secretKey, dump)
|
||||
} ?: ConversationVolatileConfig.newInstance(secretKey)
|
||||
}
|
||||
}
|
||||
_convoVolatileConfig
|
||||
}
|
||||
|
||||
override val userGroups: UserGroupsConfig?
|
||||
get() = synchronizedWithLog(userGroupsLock) {
|
||||
if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return null
|
||||
if (_userGroups == null) {
|
||||
val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
|
||||
val userGroupsDump = configDatabase.retrieveConfigAndHashes(
|
||||
SharedConfigMessage.Kind.GROUPS.name,
|
||||
publicKey
|
||||
)
|
||||
_userGroups = if (userGroupsDump != null) {
|
||||
UserGroupsConfig.Companion.newInstance(secretKey, userGroupsDump)
|
||||
} else {
|
||||
ConfigurationMessageUtilities.generateUserGroupDump(context)?.let { dump ->
|
||||
UserGroupsConfig.Companion.newInstance(secretKey, dump)
|
||||
} ?: UserGroupsConfig.newInstance(secretKey)
|
||||
}
|
||||
}
|
||||
_userGroups
|
||||
}
|
||||
|
||||
override fun getUserConfigs(): List<ConfigBase> =
|
||||
listOfNotNull(user, contacts, convoVolatile, userGroups)
|
||||
|
||||
|
||||
private fun persistUserConfigDump(timestamp: Long) = synchronized(userLock) {
|
||||
val dumped = user?.dump() ?: return
|
||||
val (_, publicKey) = maybeGetUserInfo() ?: return
|
||||
configDatabase.storeConfig(SharedConfigMessage.Kind.USER_PROFILE.name, publicKey, dumped, timestamp)
|
||||
}
|
||||
|
||||
private fun persistContactsConfigDump(timestamp: Long) = synchronized(contactsLock) {
|
||||
val dumped = contacts?.dump() ?: return
|
||||
val (_, publicKey) = maybeGetUserInfo() ?: return
|
||||
configDatabase.storeConfig(SharedConfigMessage.Kind.CONTACTS.name, publicKey, dumped, timestamp)
|
||||
}
|
||||
|
||||
private fun persistConvoVolatileConfigDump(timestamp: Long) = synchronized(convoVolatileLock) {
|
||||
val dumped = convoVolatile?.dump() ?: return
|
||||
val (_, publicKey) = maybeGetUserInfo() ?: return
|
||||
configDatabase.storeConfig(
|
||||
SharedConfigMessage.Kind.CONVO_INFO_VOLATILE.name,
|
||||
publicKey,
|
||||
dumped,
|
||||
timestamp
|
||||
)
|
||||
}
|
||||
|
||||
private fun persistUserGroupsConfigDump(timestamp: Long) = synchronized(userGroupsLock) {
|
||||
val dumped = userGroups?.dump() ?: return
|
||||
val (_, publicKey) = maybeGetUserInfo() ?: return
|
||||
configDatabase.storeConfig(SharedConfigMessage.Kind.GROUPS.name, publicKey, dumped, timestamp)
|
||||
}
|
||||
|
||||
override fun persist(forConfigObject: ConfigBase, timestamp: Long) {
|
||||
try {
|
||||
listeners.forEach { listener ->
|
||||
listener.notifyUpdates(forConfigObject)
|
||||
}
|
||||
when (forConfigObject) {
|
||||
is UserProfile -> persistUserConfigDump(timestamp)
|
||||
is Contacts -> persistContactsConfigDump(timestamp)
|
||||
is ConversationVolatileConfig -> persistConvoVolatileConfigDump(timestamp)
|
||||
is UserGroupsConfig -> persistUserGroupsConfigDump(timestamp)
|
||||
else -> throw UnsupportedOperationException("Can't support type of ${forConfigObject::class.simpleName} yet")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "failed to persist ${forConfigObject.javaClass.simpleName}", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun conversationInConfig(
|
||||
publicKey: String?,
|
||||
groupPublicKey: String?,
|
||||
openGroupId: String?,
|
||||
visibleOnly: Boolean
|
||||
): Boolean {
|
||||
if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return true
|
||||
|
||||
val (_, userPublicKey) = maybeGetUserInfo() ?: return true
|
||||
|
||||
if (openGroupId != null) {
|
||||
val userGroups = userGroups ?: return false
|
||||
val threadId = GroupManager.getOpenGroupThreadID(openGroupId, context)
|
||||
val openGroup = get(context).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return false
|
||||
|
||||
// Not handling the `hidden` behaviour for communities so just indicate the existence
|
||||
return (userGroups.getCommunityInfo(openGroup.server, openGroup.room) != null)
|
||||
}
|
||||
else if (groupPublicKey != null) {
|
||||
val userGroups = userGroups ?: return false
|
||||
|
||||
// Not handling the `hidden` behaviour for legacy groups so just indicate the existence
|
||||
return (userGroups.getLegacyGroupInfo(groupPublicKey) != null)
|
||||
}
|
||||
else if (publicKey == userPublicKey) {
|
||||
val user = user ?: return false
|
||||
|
||||
return (!visibleOnly || user.getNtsPriority() != ConfigBase.PRIORITY_HIDDEN)
|
||||
}
|
||||
else if (publicKey != null) {
|
||||
val contacts = contacts ?: return false
|
||||
val targetContact = contacts.get(publicKey) ?: return false
|
||||
|
||||
return (!visibleOnly || targetContact.priority != ConfigBase.PRIORITY_HIDDEN)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override fun canPerformChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean {
|
||||
if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return true
|
||||
|
||||
val lastUpdateTimestampMs = configDatabase.retrieveConfigLastUpdateTimestamp(variant, publicKey)
|
||||
|
||||
// Ensure the change occurred after the last config message was handled (minus the buffer period)
|
||||
return (changeTimestampMs >= (lastUpdateTimestampMs - ConfigFactory.configChangeBufferPeriod))
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
package org.thoughtcrime.securesms.dependencies;
|
||||
|
||||
public interface InjectableType {
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package org.thoughtcrime.securesms.dependencies
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.session.libsession.utilities.ConfigFactoryUpdateListener
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
||||
import org.thoughtcrime.securesms.database.ConfigDatabase
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object SessionUtilModule {
|
||||
|
||||
private fun maybeUserEdSecretKey(context: Context): ByteArray? {
|
||||
val edKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return null
|
||||
return edKey.secretKey.asBytes
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideConfigFactory(@ApplicationContext context: Context, configDatabase: ConfigDatabase): ConfigFactory =
|
||||
ConfigFactory(context, configDatabase) {
|
||||
val localUserPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val secretKey = maybeUserEdSecretKey(context)
|
||||
if (localUserPublicKey == null || secretKey == null) null
|
||||
else secretKey to localUserPublicKey
|
||||
}.apply {
|
||||
registerListener(context as ConfigFactoryUpdateListener)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import android.content.Context
|
||||
import network.loki.messenger.libsession_util.ConfigBase
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupRecord
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||
|
||||
object ClosedGroupManager {
|
||||
|
||||
fun silentlyRemoveGroup(context: Context, threadId: Long, groupPublicKey: String, groupID: String, userPublicKey: String, delete: Boolean = true) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
// Mark the group as inactive
|
||||
storage.setActive(groupID, false)
|
||||
storage.removeClosedGroupPublicKey(groupPublicKey)
|
||||
// Remove the key pairs
|
||||
storage.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||
// Stop polling
|
||||
ClosedGroupPollerV2.shared.stopPolling(groupPublicKey)
|
||||
storage.cancelPendingMessageSendJobs(threadId)
|
||||
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
|
||||
if (delete) {
|
||||
storage.deleteConversation(threadId)
|
||||
}
|
||||
}
|
||||
|
||||
fun ConfigFactory.removeLegacyGroup(group: GroupRecord): Boolean {
|
||||
val groups = userGroups ?: return false
|
||||
if (!group.isClosedGroup) return false
|
||||
val groupPublicKey = GroupUtil.doubleEncodeGroupID(group.getId())
|
||||
return groups.eraseLegacyGroup(groupPublicKey)
|
||||
}
|
||||
|
||||
fun ConfigFactory.updateLegacyGroup(groupRecipientSettings: Recipient.RecipientSettings, group: GroupRecord) {
|
||||
val groups = userGroups ?: return
|
||||
if (!group.isClosedGroup) return
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val threadId = storage.getThreadId(group.encodedId) ?: return
|
||||
val groupPublicKey = GroupUtil.doubleEncodeGroupID(group.getId())
|
||||
val latestKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return
|
||||
val legacyInfo = groups.getOrConstructLegacyGroupInfo(groupPublicKey)
|
||||
val latestMemberMap = GroupUtil.createConfigMemberMap(group.members.map(Address::serialize), group.admins.map(Address::serialize))
|
||||
val toSet = legacyInfo.copy(
|
||||
members = latestMemberMap,
|
||||
name = group.title,
|
||||
disappearingTimer = groupRecipientSettings.expireMessages.toLong(),
|
||||
priority = if (storage.isPinned(threadId)) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE,
|
||||
encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
|
||||
encSecKey = latestKeyPair.privateKey.serialize()
|
||||
)
|
||||
groups.set(toSet)
|
||||
}
|
||||
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package org.thoughtcrime.securesms.permissions;
|
||||
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout.LayoutParams;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.utilities.ViewUtil;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class RationaleDialog {
|
||||
|
||||
public static AlertDialog.Builder createFor(@NonNull Context context, @NonNull String message, @DrawableRes int... drawables) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.permissions_rationale_dialog, null);
|
||||
view.setClipToOutline(true);
|
||||
ViewGroup header = view.findViewById(R.id.header_container);
|
||||
TextView text = view.findViewById(R.id.message);
|
||||
|
||||
for (int i=0;i<drawables.length;i++) {
|
||||
ImageView imageView = new ImageView(context);
|
||||
imageView.setImageDrawable(context.getResources().getDrawable(drawables[i]));
|
||||
imageView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
|
||||
|
||||
header.addView(imageView);
|
||||
|
||||
if (i != drawables.length - 1) {
|
||||
TextView plus = new TextView(context);
|
||||
plus.setText("+");
|
||||
plus.setTextSize(TypedValue.COMPLEX_UNIT_SP, 40);
|
||||
plus.setTextColor(Color.WHITE);
|
||||
|
||||
LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
|
||||
layoutParams.setMargins(ViewUtil.dpToPx(context, 20), 0, ViewUtil.dpToPx(context, 20), 0);
|
||||
|
||||
plus.setLayoutParams(layoutParams);
|
||||
header.addView(plus);
|
||||
}
|
||||
}
|
||||
|
||||
text.setText(message);
|
||||
|
||||
return new AlertDialog.Builder(context, R.style.ThemeOverlay_Session_AlertDialog).setView(view);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package org.thoughtcrime.securesms.permissions
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.ViewUtil
|
||||
import org.thoughtcrime.securesms.showSessionDialog
|
||||
|
||||
object RationaleDialog {
|
||||
@JvmStatic
|
||||
fun show(
|
||||
context: Context,
|
||||
message: String,
|
||||
onPositive: Runnable,
|
||||
onNegative: Runnable,
|
||||
@DrawableRes vararg drawables: Int
|
||||
): AlertDialog {
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.permissions_rationale_dialog, null)
|
||||
.apply { clipToOutline = true }
|
||||
val header = view.findViewById<ViewGroup>(R.id.header_container)
|
||||
view.findViewById<TextView>(R.id.message).text = message
|
||||
|
||||
fun addIcon(id: Int) {
|
||||
ImageView(context).apply {
|
||||
setImageDrawable(ResourcesCompat.getDrawable(context.resources, id, context.theme))
|
||||
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
|
||||
}.also(header::addView)
|
||||
}
|
||||
|
||||
fun addPlus() {
|
||||
TextView(context).apply {
|
||||
text = "+"
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 40f)
|
||||
setTextColor(Color.WHITE)
|
||||
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
||||
ViewUtil.dpToPx(context, 20).let { setMargins(it, 0, it, 0) }
|
||||
}
|
||||
}.also(header::addView)
|
||||
}
|
||||
|
||||
drawables.firstOrNull()?.let(::addIcon)
|
||||
drawables.drop(1).forEach { addPlus(); addIcon(it) }
|
||||
|
||||
return context.showSessionDialog {
|
||||
view(view)
|
||||
button(R.string.Permissions_continue) { onPositive.run() }
|
||||
button(R.string.Permissions_not_now) { onNegative.run() }
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package org.thoughtcrime.securesms.permissions
|
||||
|
||||
import android.content.Context
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.showSessionDialog
|
||||
|
||||
class SettingsDialog {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun show(context: Context, message: String) {
|
||||
context.showSessionDialog {
|
||||
title(R.string.Permissions_permission_required)
|
||||
text(message)
|
||||
button(R.string.Permissions_continue, R.string.AccessibilityId_continue) {
|
||||
context.startActivity(Permissions.getApplicationSettingsIntent(context))
|
||||
}
|
||||
cancelButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package org.thoughtcrime.securesms.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
|
||||
class BlockedContactsLayout @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : FrameLayout(context, attrs)
|
@ -0,0 +1,45 @@
|
||||
package org.thoughtcrime.securesms.preferences
|
||||
|
||||
import android.Manifest
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.preference.Preference
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.setBooleanPreference
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.showSessionDialog
|
||||
|
||||
internal class CallToggleListener(
|
||||
private val context: Fragment,
|
||||
private val setCallback: (Boolean) -> Unit
|
||||
) : Preference.OnPreferenceChangeListener {
|
||||
|
||||
override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
|
||||
if (newValue == false) return true
|
||||
|
||||
// check if we've shown the info dialog and check for microphone permissions
|
||||
context.showSessionDialog {
|
||||
title(R.string.dialog_voice_video_title)
|
||||
text(R.string.dialog_voice_video_message)
|
||||
button(R.string.dialog_link_preview_enable_button_title, R.string.AccessibilityId_enable) { requestMicrophonePermission() }
|
||||
cancelButton()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun requestMicrophonePermission() {
|
||||
Permissions.with(context)
|
||||
.request(Manifest.permission.RECORD_AUDIO)
|
||||
.onAllGranted {
|
||||
setBooleanPreference(
|
||||
context.requireContext(),
|
||||
TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED,
|
||||
true
|
||||
)
|
||||
setCallback(true)
|
||||
}
|
||||
.onAnyDenied { setCallback(false) }
|
||||
.execute()
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package org.thoughtcrime.securesms.preferences
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
|
||||
class ChangeUiModeDialog : DialogFragment() {
|
||||
|
||||
companion object {
|
||||
const val TAG = "ChangeUiModeDialog"
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val context = requireContext()
|
||||
return android.app.AlertDialog.Builder(context)
|
||||
.setTitle("TODO: remove this")
|
||||
.show()
|
||||
}
|
||||
}
|
@ -1,41 +1,24 @@
|
||||
package org.thoughtcrime.securesms.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.ListPreference
|
||||
import network.loki.messenger.databinding.DialogListPreferenceBinding
|
||||
import org.thoughtcrime.securesms.showSessionDialog
|
||||
|
||||
fun listPreferenceDialog(
|
||||
context: Context,
|
||||
listPreference: ListPreference,
|
||||
dialogListener: () -> Unit
|
||||
) : AlertDialog {
|
||||
|
||||
val builder = AlertDialog.Builder(context)
|
||||
|
||||
val binding = DialogListPreferenceBinding.inflate(LayoutInflater.from(context))
|
||||
binding.titleTextView.text = listPreference.dialogTitle
|
||||
binding.messageTextView.text = listPreference.dialogMessage
|
||||
|
||||
builder.setView(binding.root)
|
||||
|
||||
val dialog = builder.show()
|
||||
|
||||
val valueIndex = listPreference.findIndexOfValue(listPreference.value)
|
||||
RadioOptionAdapter(valueIndex) {
|
||||
listPreference.value = it.value
|
||||
dialog.dismiss()
|
||||
dialogListener()
|
||||
}
|
||||
.apply {
|
||||
listPreference.entryValues.zip(listPreference.entries) { value, title ->
|
||||
RadioOption(value.toString(), title.toString())
|
||||
}.let(this::submitList)
|
||||
onChange: () -> Unit
|
||||
) : AlertDialog = listPreference.run {
|
||||
context.showSessionDialog {
|
||||
val index = entryValues.indexOf(value)
|
||||
val options = entries.map(CharSequence::toString).toTypedArray()
|
||||
|
||||
title(dialogTitle)
|
||||
text(dialogMessage)
|
||||
singleChoiceItems(options, index) {
|
||||
listPreference.setValueIndex(it)
|
||||
onChange()
|
||||
}
|
||||
.let { binding.recyclerView.adapter = it }
|
||||
|
||||
binding.closeButton.setOnClickListener { dialog.dismiss() }
|
||||
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
|
@ -1,203 +0,0 @@
|
||||
package org.thoughtcrime.securesms.preferences;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.KeyguardManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.view.ContextThemeWrapper;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.CallNotificationBuilder;
|
||||
import org.thoughtcrime.securesms.util.IntentUtils;
|
||||
|
||||
import kotlin.jvm.functions.Function1;
|
||||
import network.loki.messenger.BuildConfig;
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class PrivacySettingsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle paramBundle) {
|
||||
super.onCreate(paramBundle);
|
||||
|
||||
this.findPreference(TextSecurePreferences.SCREEN_LOCK).setOnPreferenceChangeListener(new ScreenLockListener());
|
||||
|
||||
this.findPreference(TextSecurePreferences.READ_RECEIPTS_PREF).setOnPreferenceChangeListener(new ReadReceiptToggleListener());
|
||||
this.findPreference(TextSecurePreferences.TYPING_INDICATORS).setOnPreferenceChangeListener(new TypingIndicatorsToggleListener());
|
||||
this.findPreference(TextSecurePreferences.LINK_PREVIEWS).setOnPreferenceChangeListener(new LinkPreviewToggleListener());
|
||||
this.findPreference(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED).setOnPreferenceChangeListener(new CallToggleListener(this, this::setCall));
|
||||
|
||||
initializeVisibility();
|
||||
}
|
||||
|
||||
private Void setCall(boolean isEnabled) {
|
||||
((SwitchPreferenceCompat)findPreference(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED)).setChecked(isEnabled);
|
||||
if (isEnabled && !CallNotificationBuilder.areNotificationsEnabled(requireActivity())) {
|
||||
// show a dialog saying that calls won't work properly if you don't have notifications on at a system level
|
||||
new AlertDialog.Builder(new ContextThemeWrapper(requireActivity(), R.style.ThemeOverlay_Session_AlertDialog))
|
||||
.setTitle(R.string.CallNotificationBuilder_system_notification_title)
|
||||
.setMessage(R.string.CallNotificationBuilder_system_notification_message)
|
||||
.setPositiveButton(R.string.activity_notification_settings_title, (d, w) -> {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
Intent settingsIntent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(Settings.EXTRA_APP_PACKAGE, BuildConfig.APPLICATION_ID);
|
||||
if (IntentUtils.isResolvable(requireContext(), settingsIntent)) {
|
||||
startActivity(settingsIntent);
|
||||
}
|
||||
} else {
|
||||
Intent settingsIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.setData(Uri.parse("package:"+BuildConfig.APPLICATION_ID));
|
||||
if (IntentUtils.isResolvable(requireContext(), settingsIntent)) {
|
||||
startActivity(settingsIntent);
|
||||
}
|
||||
}
|
||||
d.dismiss();
|
||||
})
|
||||
.setNeutralButton(R.string.dismiss, (d, w) -> {
|
||||
// do nothing, user might have broken notifications
|
||||
d.dismiss();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.preferences_app_protection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
private void initializeVisibility() {
|
||||
if (TextSecurePreferences.isPasswordDisabled(getContext())) {
|
||||
KeyguardManager keyguardManager = (KeyguardManager)getContext().getSystemService(Context.KEYGUARD_SERVICE);
|
||||
if (!keyguardManager.isKeyguardSecure()) {
|
||||
((SwitchPreferenceCompat)findPreference(TextSecurePreferences.SCREEN_LOCK)).setChecked(false);
|
||||
findPreference(TextSecurePreferences.SCREEN_LOCK).setEnabled(false);
|
||||
}
|
||||
} else {
|
||||
findPreference(TextSecurePreferences.SCREEN_LOCK).setVisible(false);
|
||||
findPreference(TextSecurePreferences.SCREEN_LOCK_TIMEOUT).setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
private class ScreenLockListener implements Preference.OnPreferenceChangeListener {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
boolean enabled = (Boolean)newValue;
|
||||
|
||||
TextSecurePreferences.setScreenLockEnabled(getContext(), enabled);
|
||||
|
||||
Intent intent = new Intent(getContext(), KeyCachingService.class);
|
||||
intent.setAction(KeyCachingService.LOCK_TOGGLED_EVENT);
|
||||
getContext().startService(intent);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class ReadReceiptToggleListener implements Preference.OnPreferenceChangeListener {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class TypingIndicatorsToggleListener implements Preference.OnPreferenceChangeListener {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
boolean enabled = (boolean)newValue;
|
||||
|
||||
if (!enabled) {
|
||||
ApplicationContext.getInstance(requireContext()).getTypingStatusRepository().clear();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class LinkPreviewToggleListener implements Preference.OnPreferenceChangeListener {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class CallToggleListener implements Preference.OnPreferenceChangeListener {
|
||||
|
||||
private final Fragment context;
|
||||
private final Function1<Boolean, Void> setCallback;
|
||||
|
||||
private CallToggleListener(Fragment context, Function1<Boolean,Void> setCallback) {
|
||||
this.context = context;
|
||||
this.setCallback = setCallback;
|
||||
}
|
||||
|
||||
private void requestMicrophonePermission() {
|
||||
Permissions.with(context)
|
||||
.request(Manifest.permission.RECORD_AUDIO)
|
||||
.onAllGranted(() -> {
|
||||
TextSecurePreferences.setBooleanPreference(context.requireContext(), TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED, true);
|
||||
setCallback.invoke(true);
|
||||
})
|
||||
.onAnyDenied(() -> setCallback.invoke(false))
|
||||
.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
boolean val = (boolean) newValue;
|
||||
if (val) {
|
||||
// check if we've shown the info dialog and check for microphone permissions
|
||||
AlertDialog dialog = new AlertDialog.Builder(new ContextThemeWrapper(context.requireContext(), R.style.ThemeOverlay_Session_AlertDialog))
|
||||
.setTitle(R.string.dialog_voice_video_title)
|
||||
.setMessage(R.string.dialog_voice_video_message)
|
||||
.setPositiveButton(R.string.dialog_link_preview_enable_button_title, (d, w) -> {
|
||||
requestMicrophonePermission();
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (d, w) -> {
|
||||
|
||||
})
|
||||
.show();
|
||||
Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
|
||||
positiveButton.setContentDescription("Enable");
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
package org.thoughtcrime.securesms.preferences
|
||||
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.preference.Preference
|
||||
import network.loki.messenger.BuildConfig
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.isPasswordDisabled
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.setScreenLockEnabled
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||
import org.thoughtcrime.securesms.showSessionDialog
|
||||
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.areNotificationsEnabled
|
||||
import org.thoughtcrime.securesms.util.IntentUtils
|
||||
|
||||
class PrivacySettingsPreferenceFragment : ListSummaryPreferenceFragment() {
|
||||
override fun onCreate(paramBundle: Bundle?) {
|
||||
super.onCreate(paramBundle)
|
||||
findPreference<Preference>(TextSecurePreferences.SCREEN_LOCK)!!
|
||||
.onPreferenceChangeListener = ScreenLockListener()
|
||||
findPreference<Preference>(TextSecurePreferences.TYPING_INDICATORS)!!
|
||||
.onPreferenceChangeListener = TypingIndicatorsToggleListener()
|
||||
findPreference<Preference>(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED)!!
|
||||
.onPreferenceChangeListener = CallToggleListener(this) { setCall(it) }
|
||||
initializeVisibility()
|
||||
}
|
||||
|
||||
private fun setCall(isEnabled: Boolean) {
|
||||
(findPreference<Preference>(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED) as SwitchPreferenceCompat?)!!.isChecked =
|
||||
isEnabled
|
||||
if (isEnabled && !areNotificationsEnabled(requireActivity())) {
|
||||
// show a dialog saying that calls won't work properly if you don't have notifications on at a system level
|
||||
showSessionDialog {
|
||||
title(R.string.CallNotificationBuilder_system_notification_title)
|
||||
text(R.string.CallNotificationBuilder_system_notification_message)
|
||||
button(R.string.activity_notification_settings_title) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
|
||||
.putExtra(Settings.EXTRA_APP_PACKAGE, BuildConfig.APPLICATION_ID)
|
||||
} else {
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
.setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID))
|
||||
}
|
||||
.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
||||
.takeIf { IntentUtils.isResolvable(requireContext(), it) }.let {
|
||||
startActivity(it)
|
||||
}
|
||||
}
|
||||
button(R.string.dismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.preferences_app_protection)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
private fun initializeVisibility() {
|
||||
if (isPasswordDisabled(requireContext())) {
|
||||
val keyguardManager =
|
||||
requireContext().getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
if (!keyguardManager.isKeyguardSecure) {
|
||||
findPreference<SwitchPreferenceCompat>(TextSecurePreferences.SCREEN_LOCK)!!.isChecked = false
|
||||
findPreference<Preference>(TextSecurePreferences.SCREEN_LOCK)!!.isEnabled = false
|
||||
}
|
||||
} else {
|
||||
findPreference<Preference>(TextSecurePreferences.SCREEN_LOCK)!!.isVisible = false
|
||||
findPreference<Preference>(TextSecurePreferences.SCREEN_LOCK_TIMEOUT)!!.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ScreenLockListener : Preference.OnPreferenceChangeListener {
|
||||
override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
|
||||
val enabled = newValue as Boolean
|
||||
setScreenLockEnabled(context!!, enabled)
|
||||
val intent = Intent(context, KeyCachingService::class.java)
|
||||
intent.action = KeyCachingService.LOCK_TOGGLED_EVENT
|
||||
context!!.startService(intent)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private inner class TypingIndicatorsToggleListener : Preference.OnPreferenceChangeListener {
|
||||
override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
|
||||
val enabled = newValue as Boolean
|
||||
if (!enabled) {
|
||||
ApplicationContext.getInstance(requireContext()).typingStatusRepository.clear()
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,251 +0,0 @@
|
||||
package org.thoughtcrime.securesms.preferences.widgets;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.res.TypedArrayUtils;
|
||||
import androidx.preference.DialogPreference;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.takisoft.colorpicker.ColorPickerDialog;
|
||||
import com.takisoft.colorpicker.ColorPickerDialog.Size;
|
||||
import com.takisoft.colorpicker.ColorStateDrawable;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class ColorPickerPreference extends DialogPreference {
|
||||
|
||||
private static final String TAG = ColorPickerPreference.class.getSimpleName();
|
||||
|
||||
private int[] colors;
|
||||
private CharSequence[] colorDescriptions;
|
||||
private int color;
|
||||
private int columns;
|
||||
private int size;
|
||||
private boolean sortColors;
|
||||
|
||||
private ImageView colorWidget;
|
||||
private OnPreferenceChangeListener listener;
|
||||
|
||||
public ColorPickerPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ColorPickerPreference, defStyleAttr, 0);
|
||||
|
||||
int colorsId = a.getResourceId(R.styleable.ColorPickerPreference_colors, R.array.color_picker_default_colors);
|
||||
|
||||
if (colorsId != 0) {
|
||||
colors = context.getResources().getIntArray(colorsId);
|
||||
}
|
||||
|
||||
colorDescriptions = a.getTextArray(R.styleable.ColorPickerPreference_colorDescriptions);
|
||||
color = a.getColor(R.styleable.ColorPickerPreference_currentColor, 0);
|
||||
columns = a.getInt(R.styleable.ColorPickerPreference_columns, 3);
|
||||
size = a.getInt(R.styleable.ColorPickerPreference_colorSize, 2);
|
||||
sortColors = a.getBoolean(R.styleable.ColorPickerPreference_sortColors, false);
|
||||
|
||||
a.recycle();
|
||||
|
||||
setWidgetLayoutResource(R.layout.preference_widget_color_swatch);
|
||||
}
|
||||
|
||||
public ColorPickerPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
this(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
public ColorPickerPreference(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, TypedArrayUtils.getAttr(context, R.attr.dialogPreferenceStyle,
|
||||
android.R.attr.dialogPreferenceStyle));
|
||||
}
|
||||
|
||||
public ColorPickerPreference(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnPreferenceChangeListener(OnPreferenceChangeListener listener) {
|
||||
super.setOnPreferenceChangeListener(listener);
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
|
||||
colorWidget = (ImageView) holder.findViewById(R.id.color_picker_widget);
|
||||
setColorOnWidget(color);
|
||||
}
|
||||
|
||||
private void setColorOnWidget(int color) {
|
||||
if (colorWidget == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Drawable[] colorDrawable = new Drawable[]
|
||||
{ContextCompat.getDrawable(getContext(), R.drawable.colorpickerpreference_pref_swatch)};
|
||||
colorWidget.setImageDrawable(new ColorStateDrawable(colorDrawable, color));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current color.
|
||||
*
|
||||
* @return The current color.
|
||||
*/
|
||||
public int getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current color.
|
||||
*
|
||||
* @param color The current color.
|
||||
*/
|
||||
public void setColor(int color) {
|
||||
setInternalColor(color, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all of the available colors.
|
||||
*
|
||||
* @return The available colors.
|
||||
*/
|
||||
public int[] getColors() {
|
||||
return colors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the available colors.
|
||||
*
|
||||
* @param colors The available colors.
|
||||
*/
|
||||
public void setColors(int[] colors) {
|
||||
this.colors = colors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the available colors should be sorted automatically based on their HSV
|
||||
* values.
|
||||
*
|
||||
* @return Whether the available colors should be sorted automatically based on their HSV
|
||||
* values.
|
||||
*/
|
||||
public boolean isSortColors() {
|
||||
return sortColors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the available colors should be sorted automatically based on their HSV
|
||||
* values. The sorting does not modify the order of the original colors supplied via
|
||||
* {@link #setColors(int[])} or the XML attribute {@code app:colors}.
|
||||
*
|
||||
* @param sortColors Whether the available colors should be sorted automatically based on their
|
||||
* HSV values.
|
||||
*/
|
||||
public void setSortColors(boolean sortColors) {
|
||||
this.sortColors = sortColors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the available colors' descriptions that can be used by accessibility services.
|
||||
*
|
||||
* @return The available colors' descriptions.
|
||||
*/
|
||||
public CharSequence[] getColorDescriptions() {
|
||||
return colorDescriptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the available colors' descriptions that can be used by accessibility services.
|
||||
*
|
||||
* @param colorDescriptions The available colors' descriptions.
|
||||
*/
|
||||
public void setColorDescriptions(CharSequence[] colorDescriptions) {
|
||||
this.colorDescriptions = colorDescriptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of columns to be used in the picker dialog for displaying the available
|
||||
* colors. If the value is less than or equals to 0, the number of columns will be determined
|
||||
* automatically by the system using FlexboxLayoutManager.
|
||||
*
|
||||
* @return The number of columns to be used in the picker dialog.
|
||||
* @see com.google.android.flexbox.FlexboxLayoutManager
|
||||
*/
|
||||
public int getColumns() {
|
||||
return columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of columns to be used in the picker dialog for displaying the available
|
||||
* colors. If the value is less than or equals to 0, the number of columns will be determined
|
||||
* automatically by the system using FlexboxLayoutManager.
|
||||
*
|
||||
* @param columns The number of columns to be used in the picker dialog. Use 0 to set it to
|
||||
* 'auto' mode.
|
||||
* @see com.google.android.flexbox.FlexboxLayoutManager
|
||||
*/
|
||||
public void setColumns(int columns) {
|
||||
this.columns = columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the color swatches in the dialog. It can be either
|
||||
* {@link ColorPickerDialog#SIZE_SMALL} or {@link ColorPickerDialog#SIZE_LARGE}.
|
||||
*
|
||||
* @return The size of the color swatches in the dialog.
|
||||
* @see ColorPickerDialog#SIZE_SMALL
|
||||
* @see ColorPickerDialog#SIZE_LARGE
|
||||
*/
|
||||
@Size
|
||||
public int getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the size of the color swatches in the dialog. It can be either
|
||||
* {@link ColorPickerDialog#SIZE_SMALL} or {@link ColorPickerDialog#SIZE_LARGE}.
|
||||
*
|
||||
* @param size The size of the color swatches in the dialog. It can be either
|
||||
* {@link ColorPickerDialog#SIZE_SMALL} or {@link ColorPickerDialog#SIZE_LARGE}.
|
||||
* @see ColorPickerDialog#SIZE_SMALL
|
||||
* @see ColorPickerDialog#SIZE_LARGE
|
||||
*/
|
||||
public void setSize(@Size int size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
private void setInternalColor(int color, boolean force) {
|
||||
int oldColor = getPersistedInt(0);
|
||||
|
||||
boolean changed = oldColor != color;
|
||||
|
||||
if (changed || force) {
|
||||
this.color = color;
|
||||
|
||||
persistInt(color);
|
||||
|
||||
setColorOnWidget(color);
|
||||
|
||||
if (listener != null) listener.onPreferenceChange(this, color);
|
||||
notifyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object onGetDefaultValue(TypedArray a, int index) {
|
||||
return a.getString(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetInitialValue(boolean restoreValue, Object defaultValueObj) {
|
||||
final String defaultValue = (String) defaultValueObj;
|
||||
setInternalColor(restoreValue ? getPersistedInt(0) : (!TextUtils.isEmpty(defaultValue) ? Color.parseColor(defaultValue) : 0), true);
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
package org.thoughtcrime.securesms.preferences.widgets;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
||||
|
||||
import com.takisoft.colorpicker.ColorPickerDialog;
|
||||
import com.takisoft.colorpicker.OnColorSelectedListener;
|
||||
|
||||
public class ColorPickerPreferenceDialogFragmentCompat extends PreferenceDialogFragmentCompat implements OnColorSelectedListener {
|
||||
|
||||
private int pickedColor;
|
||||
|
||||
public static ColorPickerPreferenceDialogFragmentCompat newInstance(String key) {
|
||||
ColorPickerPreferenceDialogFragmentCompat fragment = new ColorPickerPreferenceDialogFragmentCompat();
|
||||
Bundle b = new Bundle(1);
|
||||
b.putString(PreferenceDialogFragmentCompat.ARG_KEY, key);
|
||||
fragment.setArguments(b);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
ColorPickerPreference pref = getColorPickerPreference();
|
||||
|
||||
ColorPickerDialog.Params params = new ColorPickerDialog.Params.Builder(getContext())
|
||||
.setSelectedColor(pref.getColor())
|
||||
.setColors(pref.getColors())
|
||||
.setColorContentDescriptions(pref.getColorDescriptions())
|
||||
.setSize(pref.getSize())
|
||||
.setSortColors(pref.isSortColors())
|
||||
.setColumns(pref.getColumns())
|
||||
.build();
|
||||
|
||||
ColorPickerDialog dialog = new ColorPickerDialog(getActivity(), this, params);
|
||||
dialog.setTitle(pref.getDialogTitle());
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDialogClosed(boolean positiveResult) {
|
||||
ColorPickerPreference preference = getColorPickerPreference();
|
||||
|
||||
if (positiveResult) {
|
||||
preference.setColor(pickedColor);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onColorSelected(int color) {
|
||||
this.pickedColor = color;
|
||||
|
||||
super.onClick(getDialog(), DialogInterface.BUTTON_POSITIVE);
|
||||
}
|
||||
|
||||
ColorPickerPreference getColorPickerPreference() {
|
||||
return (ColorPickerPreference) getPreference();
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue