diff --git a/app/build.gradle b/app/build.gradle index eb2c16e953..ab0f5977ab 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -372,14 +372,26 @@ dependencies { testImplementation 'org.robolectric:shadows-multidex:4.4' implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.5' - implementation 'androidx.compose.ui:ui:1.5.2' - implementation 'androidx.compose.ui:ui-tooling:1.5.2' - implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.33.1-alpha" - implementation "com.google.accompanist:accompanist-pager-indicators:0.33.1-alpha" - implementation "androidx.compose.runtime:runtime-livedata:1.5.2" + implementation 'androidx.compose.ui:ui:1.6.2' + implementation 'androidx.compose.animation:animation:1.6.2' + implementation 'androidx.compose.ui:ui-tooling:1.6.2' + implementation "androidx.compose.runtime:runtime-livedata:1.6.2" + implementation 'androidx.compose.foundation:foundation-layout:1.6.2' + implementation 'androidx.compose.material:material:1.6.2' + androidTestImplementation 'androidx.compose.ui:ui-test-junit4-android:1.6.2' + debugImplementation 'androidx.compose.ui:ui-test-manifest:1.6.2' - implementation 'androidx.compose.foundation:foundation-layout:1.5.2' - implementation 'androidx.compose.material:material:1.5.2' + implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.33.1-alpha" + implementation "com.google.accompanist:accompanist-pager:0.33.1-alpha" + implementation "com.google.accompanist:accompanist-pager-indicators:0.33.1-alpha" + implementation "com.google.accompanist:accompanist-permissions:0.33.1-alpha" + + implementation "androidx.camera:camera-camera2:1.3.1" + implementation "androidx.camera:camera-lifecycle:1.3.1" + implementation "androidx.camera:camera-view:1.3.1" + + implementation 'com.google.firebase:firebase-core:21.1.1' + implementation "com.google.mlkit:barcode-scanning:17.2.0" } static def getLastCommitTimestamp() { diff --git a/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt b/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt index a20a3a2a67..258ba95183 100644 --- a/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt +++ b/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt @@ -22,6 +22,8 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiDevice import com.adevinta.android.barista.interaction.PermissionGranter import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable import org.hamcrest.Matcher @@ -49,9 +51,14 @@ class HomeActivityTests { private val activityMonitor = Instrumentation.ActivityMonitor(ConversationActivityV2::class.java.name, null, false) + private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + @Before fun setUp() { InstrumentationRegistry.getInstrumentation().addMonitor(activityMonitor) + } @After @@ -72,25 +79,34 @@ class HomeActivityTests { onView(isRoot()).perform(waitFor(500)) } + private fun objectFromDesc(id: Int) = device.findObject(By.desc(context.getString(id))) + private fun setupLoggedInState(hasViewedSeed: Boolean = false) { // landing activity - onView(withId(R.id.registerButton)).perform(ViewActions.click()) - // session ID - register activity - onView(withId(R.id.registerButton)).perform(ViewActions.click()) + objectFromDesc(R.string.onboardingAccountCreate).click() + // display name selection - onView(withId(R.id.displayNameEditText)).perform(ViewActions.typeText("test-user123")) - onView(withId(R.id.registerButton)).perform(ViewActions.click()) + objectFromDesc(R.string.displayNameEnter).click() + device.pressKeyCode(28) + device.pressKeyCode(29) + device.pressKeyCode(30) + + // Continue with display name + objectFromDesc(R.string.continue_2).click() + + // Continue with default push notification setting + objectFromDesc(R.string.continue_2).click() + // PN select if (hasViewedSeed) { // has viewed seed is set to false after register activity TextSecurePreferences.setHasViewedSeed(InstrumentationRegistry.getInstrumentation().targetContext, true) } - onView(withId(R.id.backgroundPollingOptionView)).perform(ViewActions.click()) - onView(withId(R.id.registerButton)).perform(ViewActions.click()) // allow notification permission PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS) } + private fun goToMyChat() { onView(withId(R.id.newConversationButton)).perform(ViewActions.click()) onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click()) @@ -111,8 +127,8 @@ class HomeActivityTests { @Test fun testLaunches_dismiss_seedView() { setupLoggedInState() - onView(allOf(withId(R.id.button), isDescendantOfA(withId(R.id.seedReminderView)))).perform(ViewActions.click()) - onView(withId(R.id.copyButton)).perform(ViewActions.click()) + objectFromDesc(R.string.continue_2).click() + objectFromDesc(R.string.copy).click() pressBack() onView(withId(R.id.seedReminderView)).check(matches(not(isDisplayed()))) } @@ -133,7 +149,7 @@ class HomeActivityTests { fun testChat_withSelf() { setupLoggedInState() goToMyChat() - TextSecurePreferences.setLinkPreviewsEnabled(InstrumentationRegistry.getInstrumentation().targetContext, true) + TextSecurePreferences.setLinkPreviewsEnabled(context, true) sendMessage("howdy") sendMessage("test") // tests url rewriter doesn't crash diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 79d55b37f8..9cbede54f6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -113,12 +113,17 @@ android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.Session.DayNight.FlatActionBar" /> + Unit = {}) { text(context.resources.getText(id)) } + fun view(view: View) = contentView.addView(view) fun view(@LayoutRes layout: Int): View = LayoutInflater.from(context).inflate(layout, contentView) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 76bf7b875f..b8e71cbfaf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -115,7 +115,6 @@ import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companio import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_RESEND import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog -import org.thoughtcrime.securesms.conversation.v2.dialogs.SendSeedDialog import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarRecordingViewDelegate @@ -162,6 +161,7 @@ import org.thoughtcrime.securesms.mms.MediaConstraints import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.mms.SlideDeck import org.thoughtcrime.securesms.mms.VideoSlide +import org.thoughtcrime.securesms.onboarding.recoverypassword.startRecoveryPasswordActivity import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment @@ -1605,9 +1605,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe val userPublicKey = textSecurePreferences.getLocalNumber() val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey) if (text.contains(seed) && !isNoteToSelf && !hasPermissionToSendSeed) { - val dialog = SendSeedDialog { sendTextOnlyMessage(true) } - dialog.show(supportFragmentManager, "Send Seed Dialog") - return null + startRecoveryPasswordActivity() } // Create the message val message = VisibleMessage().applyExpiryMode(viewModel.threadId) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index d5e28fb936..00550c8d52 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -364,7 +364,7 @@ fun FileDetails(fileDetails: List) { fun TitledErrorText(titledText: TitledText?) { TitledText( titledText, - valueStyle = LocalTextStyle.current.copy(color = colorDestructive) + style = LocalTextStyle.current.copy(color = colorDestructive) ) } @@ -372,7 +372,7 @@ fun TitledErrorText(titledText: TitledText?) { fun TitledMonospaceText(titledText: TitledText?) { TitledText( titledText, - valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace) + style = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace) ) } @@ -380,11 +380,11 @@ fun TitledMonospaceText(titledText: TitledText?) { fun TitledText( titledText: TitledText?, modifier: Modifier = Modifier, - valueStyle: TextStyle = LocalTextStyle.current, + style: TextStyle = LocalTextStyle.current, ) { titledText?.apply { TitledView(title, modifier) { - Text(text, style = valueStyle, modifier = Modifier.fillMaxWidth()) + Text(text, style = style, modifier = Modifier.fillMaxWidth()) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index c063f30538..c73fbce13c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -7,9 +7,32 @@ import android.content.ClipboardManager import android.content.Intent import android.os.Build import android.os.Bundle -import android.text.SpannableString import android.widget.Toast import androidx.activity.viewModels +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.foundation.layout.width +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle @@ -65,12 +88,19 @@ import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.notifications.PushRegistry -import org.thoughtcrime.securesms.onboarding.SeedActivity -import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate +import org.thoughtcrime.securesms.onboarding.recoverypassword.startRecoveryPasswordActivity import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.preferences.SettingsActivity import org.thoughtcrime.securesms.showMuteDialog import org.thoughtcrime.securesms.showSessionDialog +import org.thoughtcrime.securesms.ui.AppTheme +import org.thoughtcrime.securesms.ui.GetString +import org.thoughtcrime.securesms.ui.OutlineButton +import org.thoughtcrime.securesms.ui.PreviewTheme +import org.thoughtcrime.securesms.ui.SessionShieldIcon +import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider +import org.thoughtcrime.securesms.ui.h8 +import org.thoughtcrime.securesms.ui.small import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.IP2Country import org.thoughtcrime.securesms.util.disableClipping @@ -82,7 +112,6 @@ import javax.inject.Inject @AndroidEntryPoint class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickListener, - SeedReminderViewDelegate, GlobalSearchInputLayout.GlobalSearchInputLayoutListener { companion object { @@ -170,15 +199,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), binding.sessionToolbar.disableClipping() // Set up seed reminder view lifecycleScope.launchWhenStarted { - val hasViewedSeed = textSecurePreferences.getHasViewedSeed() - if (!hasViewedSeed) { - binding.seedReminderView.isVisible = true - binding.seedReminderView.title = SpannableString("You're almost finished! 80%") // Intentionally not yet translated - binding.seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1) - binding.seedReminderView.setProgress(80, false) - binding.seedReminderView.delegate = this@HomeActivity - } else { - binding.seedReminderView.isVisible = false + binding.seedReminderView.setContent { + if (!textSecurePreferences.getHasViewedSeed()) SeedReminder() } } // Set up recycler view @@ -194,7 +216,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } // Set up empty state view - binding.createNewPrivateChatButton.setOnClickListener { showNewConversation() } + binding.emptyStateContainer.setContent { EmptyView(ApplicationContext.getInstance(this).newAccount) } + IP2Country.configureIfNeeded(this@HomeActivity) // Set up new conversation button @@ -317,6 +340,79 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } } + @Preview + @Composable + fun PreviewMessageDetails( + @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int + ) { + PreviewTheme(themeResId) { + SeedReminder() + } + } + + @Composable + private fun SeedReminder() { + AppTheme { + Column { + Box( + Modifier + .fillMaxWidth() + .height(4.dp) + .background(MaterialTheme.colors.secondary)) + Row( + Modifier + .background(MaterialTheme.colors.surface) + .padding(horizontal = 24.dp, vertical = 16.dp) + ) { + Column(Modifier.weight(1f)) { + Row { + Text(stringResource(R.string.save_your_recovery_password), style = MaterialTheme.typography.h8) + Spacer(Modifier.requiredWidth(8.dp)) + SessionShieldIcon() + } + Text(stringResource(R.string.save_your_recovery_password_to_make_sure_you_don_t_lose_access_to_your_account), style = MaterialTheme.typography.small) + } + Spacer(Modifier.width(12.dp)) + OutlineButton( + stringResource(R.string.continue_2), + Modifier.align(Alignment.CenterVertically), + contentDescription = GetString(R.string.AccessibilityId_reveal_recovery_phrase_button) + ) { startRecoveryPasswordActivity() } + } + } + } + } + + @Composable + private fun EmptyView(newAccount: Boolean) { + AppTheme { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .padding(horizontal = 50.dp) + .padding(bottom = 12.dp) + ) { + Spacer(modifier = Modifier.weight(1f)) + Icon( + painter = painterResource(id = if (newAccount) R.drawable.emoji_tada_large else R.drawable.ic_logo_large), + contentDescription = null, + tint = Color.Unspecified + ) + if (newAccount) Text(stringResource(R.string.onboardingAccountCreated), style = MaterialTheme.typography.h4, textAlign = TextAlign.Center) + if (newAccount) Text(stringResource(R.string.welcome_to_session), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) + + Divider(modifier = Modifier.padding(vertical = 16.dp)) + Text( + stringResource(R.string.conversationsNone), + style = MaterialTheme.typography.h8, + textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = 12.dp)) + Text(stringResource(R.string.onboardingHitThePlusButton), textAlign = TextAlign.Center) + Spacer(modifier = Modifier.weight(2f)) + } + } + } + override fun onInputFocusChanged(hasFocus: Boolean) { if (hasFocus) { setSearchShown(true) @@ -406,11 +502,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), super.onBackPressed() } - override fun handleSeedReminderViewContinueButtonTapped() { - val intent = Intent(this, SeedActivity::class.java) - show(intent) - } - override fun onConversationClick(thread: ThreadRecord) { val intent = Intent(this, ConversationActivityV2::class.java) intent.putExtra(ConversationActivityV2.THREAD_ID, thread.threadId) @@ -434,7 +525,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), bottomSheet.dismiss() if (!thread.recipient.isGroupRecipient && !thread.recipient.isLocalNumber) { val clip = ClipData.newPlainText("Session ID", thread.recipient.address.toString()) - val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager + val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager manager.setPrimaryClip(clip) Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() } @@ -443,7 +534,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), val openGroup = DatabaseComponent.get(this@HomeActivity).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return@onCopyConversationId Unit val clip = ClipData.newPlainText("Community URL", openGroup.joinURL) - val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager + val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager manager.setPrimaryClip(clip) Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() } @@ -571,7 +662,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), val message = if (recipient.isGroupRecipient) { val group = groupDatabase.getGroup(recipient.address.toString()).orNull() if (group != null && group.admins.map { it.toString() }.contains(textSecurePreferences.getLocalNumber())) { - "Because you are the creator of this group it will be deleted for everyone. This cannot be undone." + getString(R.string.admin_group_leave_warning) } else { resources.getString(R.string.activity_home_leave_group_dialog_message) } @@ -627,7 +718,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), private fun hideMessageRequests() { showSessionDialog { - text("Hide message requests?") + text(getString(R.string.hide_message_requests)) button(R.string.yes) { textSecurePreferences.setHasHiddenMessageRequests() homeViewModel.tryReload() diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/DisplayNameActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/DisplayNameActivity.kt deleted file mode 100644 index c0699e3eb5..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/DisplayNameActivity.kt +++ /dev/null @@ -1,57 +0,0 @@ -package org.thoughtcrime.securesms.onboarding - -import android.content.Intent -import android.os.Bundle -import android.view.KeyEvent -import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputMethodManager -import android.widget.TextView.OnEditorActionListener -import android.widget.Toast -import network.loki.messenger.R -import network.loki.messenger.databinding.ActivityDisplayNameBinding -import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol -import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.util.push -import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo -import org.session.libsession.utilities.TextSecurePreferences - -class DisplayNameActivity : BaseActionBarActivity() { - private lateinit var binding: ActivityDisplayNameBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setUpActionBarSessionLogo() - binding = ActivityDisplayNameBinding.inflate(layoutInflater) - setContentView(binding.root) - with(binding) { - displayNameEditText.imeOptions = displayNameEditText.imeOptions or 16777216 // Always use incognito keyboard - displayNameEditText.setOnEditorActionListener( - OnEditorActionListener { _, actionID, event -> - if (actionID == EditorInfo.IME_ACTION_SEARCH || - actionID == EditorInfo.IME_ACTION_DONE || - (event.action == KeyEvent.ACTION_DOWN && - event.keyCode == KeyEvent.KEYCODE_ENTER)) { - register() - return@OnEditorActionListener true - } - false - }) - registerButton.setOnClickListener { register() } - } - } - - private fun register() { - val displayName = binding.displayNameEditText.text.toString().trim() - if (displayName.isEmpty()) { - return Toast.makeText(this, R.string.activity_display_name_display_name_missing_error, Toast.LENGTH_SHORT).show() - } - if (displayName.toByteArray().size > ProfileManagerProtocol.Companion.NAME_PADDED_LENGTH) { - return Toast.makeText(this, R.string.activity_display_name_display_name_too_long_error, Toast.LENGTH_SHORT).show() - } - val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager - inputMethodManager.hideSoftInputFromWindow(binding.displayNameEditText.windowToken, 0) - TextSecurePreferences.setProfileName(this, displayName) - val intent = Intent(this, PNModeActivity::class.java) - push(intent) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/FakeChatView.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/FakeChatView.kt deleted file mode 100644 index dcd4d783e7..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/FakeChatView.kt +++ /dev/null @@ -1,79 +0,0 @@ -package org.thoughtcrime.securesms.onboarding - -import android.animation.FloatEvaluator -import android.animation.ValueAnimator -import android.content.Context -import android.os.Handler -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.widget.ScrollView -import network.loki.messenger.R -import network.loki.messenger.databinding.ViewFakeChatBinding -import org.thoughtcrime.securesms.util.disableClipping - -class FakeChatView : ScrollView { - private lateinit var binding: ViewFakeChatBinding - // region Settings - private val spacing = context.resources.getDimension(R.dimen.medium_spacing) - private val startDelay: Long = 1000 - private val delayBetweenMessages: Long = 1500 - private val animationDuration: Long = 400 - // endregion - - // region Lifecycle - constructor(context: Context) : super(context) { - setUpViewHierarchy() - } - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { - setUpViewHierarchy() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { - setUpViewHierarchy() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { - setUpViewHierarchy() - } - - private fun setUpViewHierarchy() { - binding = ViewFakeChatBinding.inflate(LayoutInflater.from(context), this, true) - binding.root.disableClipping() - isVerticalScrollBarEnabled = false - } - // endregion - - // region Animation - fun startAnimating() { - listOf( binding.bubble1, binding.bubble2, binding.bubble3, binding.bubble4, binding.bubble5 ).forEach { it.alpha = 0.0f } - 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(binding.bubble1) - Handler().postDelayed({ - show(binding.bubble2) - Handler().postDelayed({ - show(binding.bubble3) - smoothScrollTo(0, (binding.bubble1.height + spacing).toInt()) - Handler().postDelayed({ - show(binding.bubble4) - smoothScrollTo(0, (binding.bubble1.height + spacing).toInt() + (binding.bubble2.height + spacing).toInt()) - Handler().postDelayed({ - show(binding.bubble5) - smoothScrollTo(0, (binding.bubble1.height + spacing).toInt() + (binding.bubble2.height + spacing).toInt() + (binding.bubble3.height + spacing).toInt()) - }, delayBetweenMessages) - }, delayBetweenMessages) - }, delayBetweenMessages) - }, delayBetweenMessages) - }, startDelay) - } - // endregion -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt index c878a79eef..7d3a2b7be7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt @@ -1,13 +1,47 @@ package org.thoughtcrime.securesms.onboarding import android.content.Intent +import android.net.Uri import android.os.Bundle -import network.loki.messenger.databinding.ActivityLandingBinding +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import network.loki.messenger.R import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.onboarding.pickname.startPickDisplayNameActivity import org.thoughtcrime.securesms.service.KeyCachingService -import org.thoughtcrime.securesms.util.push +import org.thoughtcrime.securesms.showSessionDialog +import org.thoughtcrime.securesms.ui.AppTheme +import org.thoughtcrime.securesms.ui.BorderlessButton +import org.thoughtcrime.securesms.ui.FilledButton +import org.thoughtcrime.securesms.ui.GetString +import org.thoughtcrime.securesms.ui.OutlineButton +import org.thoughtcrime.securesms.ui.PreviewTheme +import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider +import org.thoughtcrime.securesms.ui.classicDarkColors +import org.thoughtcrime.securesms.ui.contentDescription +import org.thoughtcrime.securesms.ui.session_accent import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo class LandingActivity : BaseActionBarActivity() { @@ -19,28 +53,128 @@ class LandingActivity : BaseActionBarActivity() { // Session then close this activity to resume the last activity from the previous instance. if (!isTaskRoot) { finish(); return } - val binding = ActivityLandingBinding.inflate(layoutInflater) - setContentView(binding.root) setUpActionBarSessionLogo(true) - with(binding) { - fakeChatView.startAnimating() - registerButton.setOnClickListener { register() } - restoreButton.setOnClickListener { link() } - linkButton.setOnClickListener { link() } - } + + ComposeView(this) + .apply { setContent { AppTheme { LandingScreen() } } } + .let(::setContentView) + IdentityKeyUtil.generateIdentityKeyPair(this) TextSecurePreferences.setPasswordDisabled(this, true) // AC: This is a temporary workaround to trick the old code that the screen is unlocked. KeyCachingService.setMasterSecret(applicationContext, Object()) } - private fun register() { - val intent = Intent(this, RegisterActivity::class.java) - push(intent) + @Preview + @Composable + private fun LandingScreen( + @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int + ) { + PreviewTheme(themeResId) { + LandingScreen() + } } - private fun link() { - val intent = Intent(this, LinkDeviceActivity::class.java) - push(intent) + @Composable + private fun LandingScreen() { + Column(modifier = Modifier.padding(horizontal = 36.dp)) { + Spacer(modifier = Modifier.weight(1f)) + Text(stringResource(R.string.onboardingBubblePrivacyInYourPocket), modifier = Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.h4, textAlign = TextAlign.Center) + Spacer(modifier = Modifier.height(24.dp)) + IncomingText(stringResource(R.string.onboardingBubbleWelcomeToSession)) + Spacer(modifier = Modifier.height(14.dp)) + OutgoingText(stringResource(R.string.onboardingBubbleSessionIsEngineered)) + Spacer(modifier = Modifier.height(14.dp)) + IncomingText(stringResource(R.string.onboardingBubbleNoPhoneNumber)) + Spacer(modifier = Modifier.height(14.dp)) + OutgoingText(stringResource(R.string.onboardingBubbleCreatingAnAccountIsEasy)) + Spacer(modifier = Modifier.weight(1f)) + + OutlineButton( + text = stringResource(R.string.onboardingAccountCreate), + modifier = Modifier + .width(262.dp) + .align(Alignment.CenterHorizontally), + contentDescription = GetString(R.string.AccessibilityId_create_account_button) + ) { startPickDisplayNameActivity() } + Spacer(modifier = Modifier.height(14.dp)) + FilledButton( + text = stringResource(R.string.onboardingAccountExists), + modifier = Modifier + .width(262.dp) + .align(Alignment.CenterHorizontally), + contentDescription = GetString(R.string.AccessibilityId_restore_account_button) + ) { startLinkDeviceActivity() } + Spacer(modifier = Modifier.height(8.dp)) + BorderlessButton( + text = stringResource(R.string.onboardingTosPrivacy), + modifier = Modifier + .width(262.dp) + .align(Alignment.CenterHorizontally), + contentDescription = GetString(R.string.AccessibilityId_open_url), + fontSize = 11.sp, + lineHeight = 13.sp + ) { openDialog() } + Spacer(modifier = Modifier.height(8.dp)) + } } -} \ No newline at end of file + + private fun openDialog() { + showSessionDialog { + title(R.string.urlOpen) + text(R.string.urlOpenBrowser) + button( + R.string.activity_landing_terms_of_service, + contentDescriptionRes = R.string.AccessibilityId_terms_of_service_link + ) { open("https://getsession.org/terms-of-service") } + button( + R.string.activity_landing_privacy_policy, + contentDescriptionRes = R.string.AccessibilityId_privacy_policy_link + ) { open("https://getsession.org/privacy-policy") } + } + } + + private fun open(url: String) { + Intent(Intent.ACTION_VIEW, Uri.parse(url)).let(::startActivity) + } + + @Composable + private fun IncomingText(text: String) { + ChatText( + text, + color = classicDarkColors[2] + ) + } + + @Composable + private fun ColumnScope.OutgoingText(text: String) { + ChatText( + text, + color = session_accent, + textColor = MaterialTheme.colors.primary, + modifier = Modifier.align(Alignment.End) + ) + } + + @Composable + private fun ChatText( + text: String, + color: Color, + modifier: Modifier = Modifier, + textColor: Color = Color.Unspecified + ) { + Text( + text, + fontSize = 16.sp, + lineHeight = 19.sp, + color = textColor, + modifier = modifier + .fillMaxWidth(0.666f) + .background( + color = color, + shape = RoundedCornerShape(size = 13.dp) + ) + .padding(horizontal = 16.dp, vertical = 12.dp) + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt index 1c10571dbd..79fa88101d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt @@ -1,230 +1,346 @@ package org.thoughtcrime.securesms.onboarding +import android.annotation.SuppressLint import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Bundle -import android.text.InputType -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputMethodManager -import android.widget.Toast -import androidx.core.view.isVisible -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentPagerAdapter +import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS +import androidx.activity.viewModels +import androidx.camera.core.CameraSelector +import androidx.camera.core.ExperimentalGetImage +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageAnalysis.Analyzer +import androidx.camera.core.ImageProxy +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Scaffold +import androidx.compose.material.Snackbar +import androidx.compose.material.SnackbarHost +import androidx.compose.material.Text +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.lifecycleScope -import com.google.android.material.snackbar.Snackbar +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.google.accompanist.permissions.shouldShowRationale +import com.google.mlkit.vision.barcode.BarcodeScanner +import com.google.mlkit.vision.barcode.BarcodeScannerOptions +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.common.InputImage import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import network.loki.messenger.R -import network.loki.messenger.databinding.ActivityLinkDeviceBinding -import network.loki.messenger.databinding.FragmentRecoveryPhraseBinding -import org.session.libsession.snode.SnodeModule import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.crypto.MnemonicCodec -import org.session.libsignal.database.LokiAPIDatabaseProtocol -import org.session.libsignal.utilities.Hex -import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.hexEncodedPublicKey -import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.crypto.KeyPairUtilities -import org.thoughtcrime.securesms.crypto.MnemonicUtilities -import org.thoughtcrime.securesms.dependencies.ConfigFactory -import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment -import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate -import org.thoughtcrime.securesms.util.push -import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo +import org.thoughtcrime.securesms.ui.AppTheme +import org.thoughtcrime.securesms.ui.OutlineButton +import org.thoughtcrime.securesms.ui.baseBold +import org.thoughtcrime.securesms.ui.components.SessionTabRow +import org.thoughtcrime.securesms.ui.contentDescription +import org.thoughtcrime.securesms.ui.outlinedTextFieldColors +import java.util.concurrent.Executors import javax.inject.Inject +private const val TAG = "LinkDeviceActivity" + +private val TITLES = listOf(R.string.sessionRecoveryPassword, R.string.qrScan) + @AndroidEntryPoint -class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { +@androidx.annotation.OptIn(ExperimentalGetImage::class) +class LinkDeviceActivity : BaseActionBarActivity() { @Inject - lateinit var configFactory: ConfigFactory + lateinit var prefs: TextSecurePreferences - private lateinit var binding: ActivityLinkDeviceBinding - internal val database: LokiAPIDatabaseProtocol - get() = SnodeModule.shared.storage - private val adapter = LinkDeviceActivityAdapter(this) - private var restoreJob: Job? = null + val viewModel: LinkDeviceViewModel by viewModels() - @Deprecated("Deprecated in Java") - override fun onBackPressed() { - if (restoreJob?.isActive == true) return // Don't allow going back with a pending job - super.onBackPressed() - } + val preview = androidx.camera.core.Preview.Builder().build() + val selector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_BACK) + .build() - // region Lifecycle override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setUpActionBarSessionLogo() - TextSecurePreferences.apply { - setHasViewedSeed(this@LinkDeviceActivity, true) - setConfigurationMessageSynced(this@LinkDeviceActivity, false) - setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis()) - setLastProfileUpdateTime(this@LinkDeviceActivity, 0) - } - binding = ActivityLinkDeviceBinding.inflate(layoutInflater) - setContentView(binding.root) - binding.viewPager.adapter = adapter - binding.tabLayout.setupWithViewPager(binding.viewPager) - } - // endregion + supportActionBar?.setTitle(R.string.activity_link_load_account) + prefs.setHasViewedSeed(true) + prefs.setConfigurationMessageSynced(false) + prefs.setRestorationTime(System.currentTimeMillis()) + prefs.setLastProfileUpdateTime(0) - // region Interaction - override fun handleQRCodeScanned(mnemonic: String) { - try { - val seed = Hex.fromStringCondensed(mnemonic) - continueWithSeed(seed) - } catch (e: Exception) { - Log.e("Loki","Error getting seed from QR code", e) - Toast.makeText(this, "An error occurred.", Toast.LENGTH_LONG).show() - } - } - - fun continueWithMnemonic(mnemonic: String) { - val loadFileContents: (String) -> String = { fileName -> - MnemonicUtilities.loadFileContents(this, fileName) - } - try { - val hexEncodedSeed = MnemonicCodec(loadFileContents).decode(mnemonic) - val seed = Hex.fromStringCondensed(hexEncodedSeed) - continueWithSeed(seed) - } catch (error: Exception) { - val message = if (error is MnemonicCodec.DecodingError) { - error.description - } else { - "An error occurred." + lifecycleScope.launch { + viewModel.eventFlow.collect { + startLoadingActivity(it.mnemonic) + finish() } - Toast.makeText(this, message, Toast.LENGTH_LONG).show() } - } - private fun continueWithSeed(seed: ByteArray) { - - // only have one sync job running at a time (prevent QR from trying to spawn a new job) - if (restoreJob?.isActive == true) return - - restoreJob = lifecycleScope.launch { - // This is here to resolve a case where the app restarts before a user completes onboarding - // which can result in an invalid database state - database.clearAllLastMessageHashes() - database.clearReceivedMessageHashValues() - - // RestoreActivity handles seed this way - val keyPairGenerationResult = KeyPairUtilities.generate(seed) - val x25519KeyPair = keyPairGenerationResult.x25519KeyPair - KeyPairUtilities.store(this@LinkDeviceActivity, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair) - configFactory.keyPairChanged() - val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey - val registrationID = KeyHelper.generateRegistrationId(false) - TextSecurePreferences.setLocalRegistrationId(this@LinkDeviceActivity, registrationID) - TextSecurePreferences.setLocalNumber(this@LinkDeviceActivity, userHexEncodedPublicKey) - TextSecurePreferences.setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis()) - TextSecurePreferences.setHasViewedSeed(this@LinkDeviceActivity, true) - - binding.loader.isVisible = true - val snackBar = Snackbar.make(binding.containerLayout, R.string.activity_link_device_skip_prompt,Snackbar.LENGTH_INDEFINITE) - .setAction(R.string.registration_activity__skip) { register(true) } - - val skipJob = launch { - delay(15_000L) - snackBar.show() - } - // start polling and wait for updated message - ApplicationContext.getInstance(this@LinkDeviceActivity).apply { - startPollingIfNeeded() - } - TextSecurePreferences.events.filter { it == TextSecurePreferences.CONFIGURATION_SYNCED }.collect { - // handle we've synced - snackBar.dismiss() - skipJob.cancel() - register(false) - } - - binding.loader.isVisible = false - } - } - - private fun register(skipped: Boolean) { - restoreJob?.cancel() - binding.loader.isVisible = false - TextSecurePreferences.setLastConfigurationSyncTime(this, System.currentTimeMillis()) - val intent = Intent(this@LinkDeviceActivity, if (skipped) DisplayNameActivity::class.java else PNModeActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - push(intent) - } - // endregion -} - -// region Adapter -private class LinkDeviceActivityAdapter(private val activity: LinkDeviceActivity) : FragmentPagerAdapter(activity.supportFragmentManager) { - val recoveryPhraseFragment = RecoveryPhraseFragment() - - override fun getCount(): Int { - return 2 - } - - override fun getItem(index: Int): Fragment { - return when (index) { - 0 -> recoveryPhraseFragment - 1 -> { - val result = ScanQRCodeWrapperFragment() - result.delegate = activity - result.message = activity.getString(R.string.activity_link_device_qr_message) - result - } - else -> throw IllegalStateException() - } - } - - override fun getPageTitle(index: Int): CharSequence { - return when (index) { - 0 -> activity.getString(R.string.activity_link_device_recovery_phrase) - 1 -> activity.getString(R.string.activity_link_device_scan_qr_code) - else -> throw IllegalStateException() - } - } -} -// endregion - -// region Recovery Phrase Fragment -class RecoveryPhraseFragment : Fragment() { - private lateinit var binding: FragmentRecoveryPhraseBinding - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - binding = FragmentRecoveryPhraseBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - with(binding) { - mnemonicEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard - mnemonicEditText.setRawInputType(InputType.TYPE_CLASS_TEXT) - mnemonicEditText.setOnEditorActionListener { v, actionID, _ -> - if (actionID == EditorInfo.IME_ACTION_DONE) { - val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(v.windowToken, 0) - handleContinueButtonTapped() - true - } else { - false + ComposeView(this).apply { + setContent { + val state by viewModel.stateFlow.collectAsState() + AppTheme { + LoadAccountScreen(state, viewModel::onChange, viewModel::onContinue) + } + } + }.let(::setContentView) + } + + + + @OptIn(ExperimentalFoundationApi::class) + @Composable + fun LoadAccountScreen(state: LinkDeviceState, onChange: (String) -> Unit = {}, onContinue: () -> Unit = {}) { + val pagerState = rememberPagerState { TITLES.size } + + Column { + val localContext = LocalContext.current + val cameraProvider = remember { ProcessCameraProvider.getInstance(localContext) } + SessionTabRow(pagerState, TITLES) + HorizontalPager( + state = pagerState, + modifier = Modifier.weight(1f) + ) { page -> + val title = TITLES[page] + + val options = BarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_QR_CODE) + .build() + val scanner = BarcodeScanning.getClient(options) + + runCatching { + cameraProvider.get().unbindAll() + if (title == R.string.qrScan) { + LocalSoftwareKeyboardController.current?.hide() + cameraProvider.get().bindToLifecycle( + LocalLifecycleOwner.current, + selector, + preview, + buildAnalysisUseCase(scanner, viewModel::scan) + ) + } + }.onFailure { Log.e(TAG, "error binding camera", it) } + when (title) { + R.string.sessionRecoveryPassword -> RecoveryPassword(state, onChange, onContinue) + R.string.qrScan -> MaybeScanQrCode() } } - continueButton.setOnClickListener { handleContinueButtonTapped() } } } - private fun handleContinueButtonTapped() { - val mnemonic = binding.mnemonicEditText.text?.trim().toString() - (requireActivity() as LinkDeviceActivity).continueWithMnemonic(mnemonic) + + @OptIn(ExperimentalPermissionsApi::class) + @Composable + fun MaybeScanQrCode() { + Box(modifier = Modifier.fillMaxSize()) { + val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA) + + if (cameraPermissionState.status.isGranted) { + ScanQrCode(preview, viewModel.qrErrorsFlow) + } else if (cameraPermissionState.status.shouldShowRationale) { + Column( + modifier = Modifier + .align(Alignment.Center) + .padding(horizontal = 60.dp) + ) { + Text( + stringResource(R.string.activity_link_camera_permission_permanently_denied_configure_in_settings), + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.size(20.dp)) + OutlineButton( + text = stringResource(R.string.sessionSettings), + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Intent(ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", packageName, null) + }.let(::startActivity) + } + } + } else { + OutlineButton( + text = stringResource(R.string.cameraGrantAccess), + modifier = Modifier.align(Alignment.Center) + ) { + cameraPermissionState.run { launchPermissionRequest() } + } + } + } + } + + @Composable + fun ScanQrCode(preview: androidx.camera.core.Preview, errors: Flow) { + val scaffoldState = rememberScaffoldState() + + LaunchedEffect(Unit) { + errors.collect { error -> + lifecycleScope.launch { + scaffoldState.snackbarHostState.showSnackbar(message = error) + } + } + } + + Scaffold( + scaffoldState = scaffoldState, + snackbarHost = { + SnackbarHost( + hostState = scaffoldState.snackbarHostState, + modifier = Modifier.padding(16.dp) + ) { data -> + Snackbar( + snackbarData = data, + modifier = Modifier.padding(16.dp) + ) + } + } + ) { padding -> + Box(modifier = Modifier.padding(padding)) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { PreviewView(it).apply { preview.setSurfaceProvider(surfaceProvider) } } + ) + + Box( + Modifier + .aspectRatio(1f) + .padding(20.dp) + .clip(shape = RoundedCornerShape(20.dp)) + .background(Color(0x33ffffff)) + .align(Alignment.Center) + ) + } + } + } +} + +@Preview +@Composable +fun PreviewRecoveryPassword() = RecoveryPassword(state = LinkDeviceState()) + +@Composable +fun RecoveryPassword(state: LinkDeviceState, onChange: (String) -> Unit = {}, onContinue: () -> Unit = {}) { + Column( + modifier = Modifier.padding(horizontal = 60.dp) + ) { + Spacer(Modifier.weight(1f)) + Row { + Text(stringResource(R.string.sessionRecoveryPassword), style = MaterialTheme.typography.h4) + Spacer(Modifier.width(6.dp)) + Icon( + painter = painterResource(id = R.drawable.ic_shield_outline), + contentDescription = null, + ) + } + Spacer(Modifier.size(28.dp)) + Text(stringResource(R.string.activity_link_enter_your_recovery_password_to_load_your_account_if_you_haven_t_saved_it_you_can_find_it_in_your_app_settings)) + Spacer(Modifier.size(24.dp)) + OutlinedTextField( + value = state.recoveryPhrase, + onValueChange = { onChange(it) }, + modifier = Modifier.contentDescription(R.string.AccessibilityId_recovery_phrase_input), + placeholder = { Text(stringResource(R.string.recoveryPasswordEnter)) }, + colors = outlinedTextFieldColors(state.error != null), + singleLine = true, + keyboardActions = KeyboardActions( + onDone = { onContinue() }, + onGo = { onContinue() }, + onSearch = { onContinue() }, + onSend = { onContinue() }, + ), + isError = state.error != null, + shape = RoundedCornerShape(12.dp) + ) + Spacer(Modifier.size(12.dp)) + state.error?.let { + Text( + it, + modifier = Modifier.contentDescription(R.string.AccessibilityId_error_message), + style = MaterialTheme.typography.baseBold, + color = MaterialTheme.colors.error + ) + } + Spacer(Modifier.weight(2f)) + OutlineButton( + text = stringResource(id = R.string.continue_2), + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(horizontal = 64.dp, vertical = 20.dp) + .width(200.dp) + ) { onContinue() } + } +} + +fun Context.startLinkDeviceActivity() { + Intent(this, LinkDeviceActivity::class.java).let(::startActivity) +} + +@SuppressLint("UnsafeOptInUsageError") +private fun buildAnalysisUseCase( + scanner: BarcodeScanner, + onBarcodeScanned: (String) -> Unit +): ImageAnalysis = ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build().apply { + setAnalyzer(Executors.newSingleThreadExecutor(), Analyzer(scanner, onBarcodeScanned)) + } + +class Analyzer( + private val scanner: BarcodeScanner, + private val onBarcodeScanned: (String) -> Unit +): Analyzer { + @SuppressLint("UnsafeOptInUsageError") + override fun analyze(image: ImageProxy) { + InputImage.fromMediaImage( + image.image!!, + image.imageInfo.rotationDegrees + ).let(scanner::process).apply { + addOnSuccessListener { barcodes -> + barcodes.filter { it.valueType == Barcode.TYPE_TEXT }.forEach { + it.rawValue?.let(onBarcodeScanned) + } + } + addOnCompleteListener { + image.close() + } + } } } -// endregion diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceState.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceState.kt new file mode 100644 index 0000000000..5627e31bbc --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceState.kt @@ -0,0 +1,6 @@ +package org.thoughtcrime.securesms.onboarding + +data class LinkDeviceState( + val recoveryPhrase: String = "", + val error: String? = null +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceViewModel.kt new file mode 100644 index 0000000000..69368c41da --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceViewModel.kt @@ -0,0 +1,92 @@ +package org.thoughtcrime.securesms.onboarding + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import network.loki.messenger.R +import org.session.libsignal.crypto.MnemonicCodec +import org.session.libsignal.crypto.MnemonicCodec.DecodingError.InputTooShort +import org.session.libsignal.crypto.MnemonicCodec.DecodingError.InvalidWord +import org.session.libsignal.utilities.Hex +import org.thoughtcrime.securesms.crypto.MnemonicUtilities +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds + +class LinkDeviceEvent(val mnemonic: ByteArray) + +@HiltViewModel +class LinkDeviceViewModel @Inject constructor( + private val application: Application +): AndroidViewModel(application) { + private val QR_ERROR_TIME = 3.seconds + private val state = MutableStateFlow(LinkDeviceState()) + val stateFlow = state.asStateFlow() + + private val event = Channel() + val eventFlow = event.receiveAsFlow().take(1) + private val qrErrors = Channel() + val qrErrorsFlow = qrErrors.receiveAsFlow() + .debounce(QR_ERROR_TIME) + .takeWhile { event.isEmpty } + .mapNotNull { application.getString(R.string.qrNotRecoveryPassword) } + + private val codec by lazy { MnemonicCodec { MnemonicUtilities.loadFileContents(getApplication(), it) } } + + fun onContinue() { + viewModelScope.launch { + runDecodeCatching(state.value.recoveryPhrase) + .onSuccess(::onSuccess) + .onFailure(::onFailure) + } + } + + fun scan(string: String) { + viewModelScope.launch { + runDecodeCatching(string) + .onSuccess(::onSuccess) + .onFailure(::onScanFailure) + } + } + + fun onChange(recoveryPhrase: String) { + state.value = LinkDeviceState(recoveryPhrase) + } + private fun onSuccess(seed: ByteArray) { + viewModelScope.launch { event.send(LinkDeviceEvent(seed)) } + } + + private fun onFailure(error: Throwable) { + state.update { + it.copy( + error = when (error) { + is InputTooShort -> R.string.recoveryPasswordErrorMessageShort + is InvalidWord -> R.string.recoveryPasswordErrorMessageIncorrect + else -> R.string.recoveryPasswordErrorMessageGeneric + }.let(application::getString) + ) + } + } + + private fun onScanFailure(error: Throwable) { + viewModelScope.launch { qrErrors.send(error) } + } + + private fun runDecodeCatching(mnemonic: String) = runCatching { + decode(mnemonic) + } + private fun decode(mnemonic: String) = codec.decode(mnemonic).let(Hex::fromStringCondensed)!! +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingActivity.kt new file mode 100644 index 0000000000..af1b551631 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingActivity.kt @@ -0,0 +1,122 @@ +package org.thoughtcrime.securesms.onboarding + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.TweenSpec +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.lifecycleScope +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import network.loki.messenger.R +import org.session.libsession.utilities.AppTextSecurePreferences +import org.session.libsession.utilities.TextSecurePreferences +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.BaseActionBarActivity +import org.thoughtcrime.securesms.dependencies.ConfigFactory +import org.thoughtcrime.securesms.onboarding.messagenotifications.MessageNotificationsActivity +import org.thoughtcrime.securesms.onboarding.messagenotifications.startMessageNotificationsActivity +import org.thoughtcrime.securesms.onboarding.pickname.startPickDisplayNameActivity +import org.thoughtcrime.securesms.ui.AppTheme +import org.thoughtcrime.securesms.ui.ProgressArc +import org.thoughtcrime.securesms.ui.contentDescription +import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo +import javax.inject.Inject + +private const val EXTRA_MNEMONIC = "mnemonic" + +@AndroidEntryPoint +class LoadingActivity: BaseActionBarActivity() { + + @Inject + lateinit var configFactory: ConfigFactory + + @Inject + lateinit var prefs: TextSecurePreferences + + private val viewModel: LoadingViewModel by viewModels() + + @Deprecated("Deprecated in Java") + override fun onBackPressed() { + return + } + + private fun register(skipped: Boolean) { + prefs.setLastConfigurationSyncTime(System.currentTimeMillis()) + + val flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + + when { + skipped -> startPickDisplayNameActivity(true, flags) + else -> startMessageNotificationsActivity(flags) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + ApplicationContext.getInstance(this).newAccount = false + + ComposeView(this) + .apply { setContent { LoadingScreen() } } + .let(::setContentView) + + setUpActionBarSessionLogo(true) + + viewModel.restore(application, intent.getByteArrayExtra(EXTRA_MNEMONIC)!!) + + lifecycleScope.launch { + viewModel.eventFlow.collect { + when (it) { + Event.TIMEOUT -> register(skipped = true) + Event.SUCCESS -> register(skipped = false) + } + } + } + } + + @Composable + fun LoadingScreen() { + val state by viewModel.stateFlow.collectAsState() + + val animatable = remember { Animatable(initialValue = 0f, visibilityThreshold = 0.005f) } + + LaunchedEffect(state) { + animatable.stop() + animatable.animateTo( + targetValue = 1f, + animationSpec = TweenSpec(durationMillis = state.duration.inWholeMilliseconds.toInt()) + ) + } + + AppTheme { + Column { + Spacer(modifier = Modifier.weight(1f)) + ProgressArc(animatable.value, modifier = Modifier.align(Alignment.CenterHorizontally).contentDescription(R.string.AccessibilityId_loading_animation)) + Text(stringResource(R.string.waitOneMoment), modifier = Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.h6) + Text(stringResource(R.string.loadAccountProgressMessage), modifier = Modifier.align(Alignment.CenterHorizontally)) + Spacer(modifier = Modifier.weight(2f)) + } + } + } +} + +fun Context.startLoadingActivity(mnemonic: ByteArray) { + Intent(this, LoadingActivity::class.java) + .apply { putExtra(EXTRA_MNEMONIC, mnemonic) } + .also(::startActivity) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingViewModel.kt new file mode 100644 index 0000000000..bf50baa736 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingViewModel.kt @@ -0,0 +1,101 @@ +package org.thoughtcrime.securesms.onboarding + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import org.session.libsession.snode.SnodeModule +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.database.LokiAPIDatabaseProtocol +import org.session.libsignal.utilities.KeyHelper +import org.session.libsignal.utilities.hexEncodedPublicKey +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.crypto.KeyPairUtilities +import org.thoughtcrime.securesms.dependencies.ConfigFactory +import javax.inject.Inject +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +data class State(val duration: Duration) + +private val DONE_TIME = 1.seconds +private val DONE_ANIMATE_TIME = 500.milliseconds + +private val TOTAL_ANIMATE_TIME = 14.seconds +private val TOTAL_TIME = 15.seconds + +@HiltViewModel +class LoadingViewModel @Inject constructor( + private val configFactory: ConfigFactory, + private val prefs: TextSecurePreferences, +) : ViewModel() { + + private val state = MutableStateFlow(State(TOTAL_ANIMATE_TIME)) + val stateFlow = state.asStateFlow() + + private val event = Channel() + val eventFlow = event.receiveAsFlow() + + private var restoreJob: Job? = null + + internal val database: LokiAPIDatabaseProtocol + get() = SnodeModule.shared.storage + + fun restore(context: Context, seed: ByteArray) { + + // only have one sync job running at a time (prevent QR from trying to spawn a new job) + if (restoreJob?.isActive == true) return + + restoreJob = viewModelScope.launch(Dispatchers.IO) { + // This is here to resolve a case where the app restarts before a user completes onboarding + // which can result in an invalid database state + database.clearAllLastMessageHashes() + database.clearReceivedMessageHashValues() + + // RestoreActivity handles seed this way + val keyPairGenerationResult = KeyPairUtilities.generate(seed) + val x25519KeyPair = keyPairGenerationResult.x25519KeyPair + KeyPairUtilities.store(context, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair) + configFactory.keyPairChanged() + val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey + val registrationID = KeyHelper.generateRegistrationId(false) + prefs.apply { + setLocalRegistrationId(registrationID) + setLocalNumber(userHexEncodedPublicKey) + setRestorationTime(System.currentTimeMillis()) + setHasViewedSeed(true) + } + + val skipJob = launch(Dispatchers.IO) { + delay(TOTAL_TIME) + event.send(Event.TIMEOUT) + } + + // start polling and wait for updated message + ApplicationContext.getInstance(context).apply { startPollingIfNeeded() } + TextSecurePreferences.events.filter { it == TextSecurePreferences.CONFIGURATION_SYNCED }.collect { + // handle we've synced + skipJob.cancel() + + state.value = State(DONE_ANIMATE_TIME) + delay(DONE_TIME) + event.send(Event.SUCCESS) + } + } + } +} + +sealed interface Event { + object SUCCESS: Event + object TIMEOUT: Event +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt deleted file mode 100644 index e4e8e6a9a6..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt +++ /dev/null @@ -1,179 +0,0 @@ -package org.thoughtcrime.securesms.onboarding - -import android.animation.ArgbEvaluator -import android.animation.ValueAnimator -import android.content.Intent -import android.graphics.drawable.TransitionDrawable -import android.net.Uri -import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.widget.Toast -import androidx.annotation.ColorInt -import androidx.annotation.DrawableRes -import dagger.hilt.android.AndroidEntryPoint -import network.loki.messenger.R -import network.loki.messenger.databinding.ActivityPnModeBinding -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.ThemeUtil -import org.thoughtcrime.securesms.ApplicationContext -import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.home.HomeActivity -import org.thoughtcrime.securesms.notifications.PushManager -import org.thoughtcrime.securesms.notifications.PushRegistry -import org.thoughtcrime.securesms.showSessionDialog -import org.thoughtcrime.securesms.util.GlowViewUtilities -import org.thoughtcrime.securesms.util.PNModeView -import org.thoughtcrime.securesms.util.disableClipping -import org.thoughtcrime.securesms.util.getAccentColor -import org.thoughtcrime.securesms.util.getColorWithID -import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo -import org.thoughtcrime.securesms.util.show -import javax.inject.Inject - -@AndroidEntryPoint -class PNModeActivity : BaseActionBarActivity() { - - @Inject lateinit var pushRegistry: PushRegistry - - private lateinit var binding: ActivityPnModeBinding - private var selectedOptionView: PNModeView? = null - - // region Lifecycle - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setUpActionBarSessionLogo(true) - TextSecurePreferences.setHasSeenWelcomeScreen(this, true) - binding = ActivityPnModeBinding.inflate(layoutInflater) - setContentView(binding.root) - with(binding) { - contentView.disableClipping() - fcmOptionView.setOnClickListener { toggleFCM() } - fcmOptionView.mainColor = ThemeUtil.getThemedColor(root.context, R.attr.colorPrimary) - fcmOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme) - backgroundPollingOptionView.setOnClickListener { toggleBackgroundPolling() } - backgroundPollingOptionView.mainColor = ThemeUtil.getThemedColor(root.context, R.attr.colorPrimary) - backgroundPollingOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme) - registerButton.setOnClickListener { register() } - } - toggleFCM() - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_pn_mode, menu) - return true - } - // endregion - - // region Animation - private fun performTransition(@DrawableRes transitionID: Int, subject: View) { - val drawable = resources.getDrawable(transitionID, theme) as TransitionDrawable - subject.background = drawable - drawable.startTransition(250) - } - // endregion - - // region Interaction - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when(item.itemId) { - R.id.learnMoreButton -> learnMore() - else -> { /* Do nothing */ } - } - return super.onOptionsItemSelected(item) - } - - private fun learnMore() { - try { - val url = "https://getsession.org/faq/#privacy" - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - startActivity(intent) - } catch (e: Exception) { - Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show() - } - } - - private fun toggleFCM() = with(binding) { - val accentColor = getAccentColor() - when (selectedOptionView) { - null -> { - performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView) - GlowViewUtilities.animateShadowColorChange(fcmOptionView, resources.getColorWithID(R.color.transparent, theme), accentColor) - animateStrokeColorChange(fcmOptionView, resources.getColorWithID(R.color.pn_option_border, theme), accentColor) - selectedOptionView = fcmOptionView - } - fcmOptionView -> { - performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView) - GlowViewUtilities.animateShadowColorChange(fcmOptionView, accentColor, resources.getColorWithID(R.color.transparent, theme)) - animateStrokeColorChange(fcmOptionView, accentColor, resources.getColorWithID(R.color.pn_option_border, theme)) - selectedOptionView = null - } - backgroundPollingOptionView -> { - performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView) - GlowViewUtilities.animateShadowColorChange(fcmOptionView, resources.getColorWithID(R.color.transparent, theme), accentColor) - animateStrokeColorChange(fcmOptionView, resources.getColorWithID(R.color.pn_option_border, theme), accentColor) - performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView) - GlowViewUtilities.animateShadowColorChange(backgroundPollingOptionView, accentColor, resources.getColorWithID(R.color.transparent, theme)) - animateStrokeColorChange(backgroundPollingOptionView, accentColor, resources.getColorWithID(R.color.pn_option_border, theme)) - selectedOptionView = fcmOptionView - } - } - } - - private fun toggleBackgroundPolling() = with(binding) { - val accentColor = getAccentColor() - when (selectedOptionView) { - null -> { - performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView) - GlowViewUtilities.animateShadowColorChange(backgroundPollingOptionView, resources.getColorWithID(R.color.transparent, theme), accentColor) - animateStrokeColorChange(backgroundPollingOptionView, resources.getColorWithID(R.color.pn_option_border, theme), accentColor) - selectedOptionView = backgroundPollingOptionView - } - backgroundPollingOptionView -> { - performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView) - GlowViewUtilities.animateShadowColorChange(backgroundPollingOptionView, accentColor, resources.getColorWithID(R.color.transparent, theme)) - animateStrokeColorChange(backgroundPollingOptionView, accentColor, resources.getColorWithID(R.color.pn_option_border, theme)) - selectedOptionView = null - } - fcmOptionView -> { - performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView) - GlowViewUtilities.animateShadowColorChange(backgroundPollingOptionView, resources.getColorWithID(R.color.transparent, theme), accentColor) - animateStrokeColorChange(backgroundPollingOptionView, resources.getColorWithID(R.color.pn_option_border, theme), accentColor) - performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView) - GlowViewUtilities.animateShadowColorChange(fcmOptionView, accentColor, resources.getColorWithID(R.color.transparent, theme)) - animateStrokeColorChange(fcmOptionView, accentColor, resources.getColorWithID(R.color.pn_option_border, theme)) - selectedOptionView = backgroundPollingOptionView - } - } - } - - private fun animateStrokeColorChange(bubble: PNModeView, @ColorInt startColor: Int, @ColorInt endColor: Int) { - 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) { - showSessionDialog { - title(R.string.activity_pn_mode_no_option_picked_dialog_title) - button(R.string.ok) - } - return - } - - TextSecurePreferences.setPushEnabled(this, (selectedOptionView == binding.fcmOptionView)) - val application = ApplicationContext.getInstance(this) - application.startPollingIfNeeded() - pushRegistry.refresh(true) - val intent = Intent(this, HomeActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - intent.putExtra(HomeActivity.FROM_ONBOARDING, true) - show(intent) - } - // endregion -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt deleted file mode 100644 index 13e5b51f0e..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt +++ /dev/null @@ -1,165 +0,0 @@ -package org.thoughtcrime.securesms.onboarding - -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent -import android.graphics.Typeface -import android.net.Uri -import android.os.Bundle -import android.os.Handler -import android.text.Spannable -import android.text.SpannableStringBuilder -import android.text.method.LinkMovementMethod -import android.text.style.ClickableSpan -import android.text.style.StyleSpan -import android.view.View -import android.widget.Toast -import com.goterl.lazysodium.utils.KeyPair -import dagger.hilt.android.AndroidEntryPoint -import network.loki.messenger.R -import network.loki.messenger.databinding.ActivityRegisterBinding -import org.session.libsession.snode.SnodeModule -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.crypto.ecc.ECKeyPair -import org.session.libsignal.database.LokiAPIDatabaseProtocol -import org.session.libsignal.utilities.KeyHelper -import org.session.libsignal.utilities.hexEncodedPublicKey -import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.crypto.KeyPairUtilities -import org.thoughtcrime.securesms.dependencies.ConfigFactory -import org.thoughtcrime.securesms.util.push -import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo -import javax.inject.Inject - -@AndroidEntryPoint -class RegisterActivity : BaseActionBarActivity() { - - private val temporarySeedKey = "TEMPORARY_SEED_KEY" - - @Inject - lateinit var configFactory: ConfigFactory - - private lateinit var binding: ActivityRegisterBinding - internal val database: LokiAPIDatabaseProtocol - get() = SnodeModule.shared.storage - private var seed: ByteArray? = null - private var ed25519KeyPair: KeyPair? = null - private var x25519KeyPair: ECKeyPair? = null - set(value) { field = value; updatePublicKeyTextView() } - - // region Lifecycle - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityRegisterBinding.inflate(layoutInflater) - setContentView(binding.root) - setUpActionBarSessionLogo() - TextSecurePreferences.apply { - setHasViewedSeed(this@RegisterActivity, false) - setConfigurationMessageSynced(this@RegisterActivity, true) - setRestorationTime(this@RegisterActivity, 0) - setLastProfileUpdateTime(this@RegisterActivity, System.currentTimeMillis()) - } - binding.registerButton.setOnClickListener { register() } - binding.copyButton.setOnClickListener { copyPublicKey() } - val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy") - termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - termsExplanation.setSpan(object : ClickableSpan() { - - override fun onClick(widget: View) { - openURL("https://getsession.org/terms-of-service/") - } - }, 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - termsExplanation.setSpan(object : ClickableSpan() { - - override fun onClick(widget: View) { - openURL("https://getsession.org/privacy-policy/") - } - }, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - binding.termsTextView.movementMethod = LinkMovementMethod.getInstance() - binding.termsTextView.text = termsExplanation - updateKeyPair(savedInstanceState?.getByteArray(temporarySeedKey)) - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - seed?.let { tempSeed -> - outState.putByteArray(temporarySeedKey, tempSeed) - } - } - // endregion - - // region Updating - private fun updateKeyPair(temporaryKey: ByteArray?) { - val keyPairGenerationResult = temporaryKey?.let(KeyPairUtilities::generate) ?: KeyPairUtilities.generate() - seed = keyPairGenerationResult.seed - ed25519KeyPair = keyPairGenerationResult.ed25519KeyPair - x25519KeyPair = keyPairGenerationResult.x25519KeyPair - } - - private fun updatePublicKeyTextView() { - val hexEncodedPublicKey = x25519KeyPair!!.hexEncodedPublicKey - val characterCount = hexEncodedPublicKey.count() - var count = 0 - val limit = 32 - fun animate() { - val numberOfIndexesToShuffle = 32 - count - val indexesToShuffle = (0 until characterCount).shuffled().subList(0, numberOfIndexesToShuffle) - var mangledHexEncodedPublicKey = hexEncodedPublicKey - for (index in indexesToShuffle) { - try { - mangledHexEncodedPublicKey = mangledHexEncodedPublicKey.substring(0, index) + "0123456789abcdef__".random() + mangledHexEncodedPublicKey.substring(index + 1, mangledHexEncodedPublicKey.count()) - } catch (exception: Exception) { - // Do nothing - } - } - count += 1 - if (count < limit) { - binding.publicKeyTextView.text = mangledHexEncodedPublicKey - Handler().postDelayed({ - animate() - }, 32) - } else { - binding.publicKeyTextView.text = hexEncodedPublicKey - } - } - animate() - } - // endregion - - // region Interaction - private fun register() { - // This is here to resolve a case where the app restarts before a user completes onboarding - // which can result in an invalid database state - database.clearAllLastMessageHashes() - database.clearReceivedMessageHashValues() - KeyPairUtilities.store(this, seed!!, ed25519KeyPair!!, x25519KeyPair!!) - configFactory.keyPairChanged() - val userHexEncodedPublicKey = x25519KeyPair!!.hexEncodedPublicKey - val registrationID = KeyHelper.generateRegistrationId(false) - TextSecurePreferences.setLocalRegistrationId(this, registrationID) - TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey) - TextSecurePreferences.setRestorationTime(this, 0) - TextSecurePreferences.setHasViewedSeed(this, false) - val intent = Intent(this, DisplayNameActivity::class.java) - push(intent) - } - - private fun copyPublicKey() { - val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("Session ID", x25519KeyPair!!.hexEncodedPublicKey) - clipboard.setPrimaryClip(clip) - Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() - } - - private fun openURL(url: String) { - try { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - startActivity(intent) - } catch (e: Exception) { - Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show() - } - } - // endregion -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedActivity.kt deleted file mode 100644 index 0eab58fa0c..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedActivity.kt +++ /dev/null @@ -1,95 +0,0 @@ -package org.thoughtcrime.securesms.onboarding - -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.os.Bundle -import android.text.Spannable -import android.text.SpannableString -import android.text.style.ForegroundColorSpan -import android.widget.LinearLayout -import android.widget.Toast -import network.loki.messenger.R -import network.loki.messenger.databinding.ActivitySeedBinding -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.getColorFromAttr -import org.session.libsignal.crypto.MnemonicCodec -import org.session.libsignal.utilities.hexEncodedPrivateKey -import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil -import org.thoughtcrime.securesms.crypto.MnemonicUtilities -import org.thoughtcrime.securesms.util.getAccentColor - -class SeedActivity : BaseActionBarActivity() { - - private lateinit var binding: ActivitySeedBinding - - private val seed by lazy { - var hexEncodedSeed = IdentityKeyUtil.retrieve(this, IdentityKeyUtil.LOKI_SEED) - if (hexEncodedSeed == null) { - hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(this).hexEncodedPrivateKey // Legacy account - } - val loadFileContents: (String) -> String = { fileName -> - MnemonicUtilities.loadFileContents(this, fileName) - } - MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english) - } - - // region Lifecycle - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivitySeedBinding.inflate(layoutInflater) - setContentView(binding.root) - supportActionBar!!.title = resources.getString(R.string.activity_seed_title) - val seedReminderViewTitle = SpannableString("You're almost finished! 90%") // Intentionally not yet translated - seedReminderViewTitle.setSpan(ForegroundColorSpan(getAccentColor()), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - with(binding) { - seedReminderView.title = seedReminderViewTitle - seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_2) - seedReminderView.setProgress(90, false) - seedReminderView.hideContinueButton() - var redactedSeed = seed - var index = 0 - for (character in seed) { - if (character.isLetter()) { - redactedSeed = redactedSeed.replaceRange(index, index + 1, "▆") - } - index += 1 - } - seedTextView.setTextColor(getAccentColor()) - seedTextView.text = redactedSeed - seedTextView.setOnLongClickListener { revealSeed(); true } - revealButton.setOnLongClickListener { revealSeed(); true } - copyButton.setOnClickListener { copySeed() } - } - } - // endregion - - // region Updating - private fun revealSeed() { - val seedReminderViewTitle = SpannableString("Account secured! 100%") // Intentionally not yet translated - seedReminderViewTitle.setSpan(ForegroundColorSpan(getAccentColor()), 17, 21, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - with(binding) { - seedReminderView.title = seedReminderViewTitle - seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_3) - seedReminderView.setProgress(100, true) - val seedTextViewLayoutParams = seedTextView.layoutParams as LinearLayout.LayoutParams - seedTextViewLayoutParams.height = seedTextView.height - seedTextView.layoutParams = seedTextViewLayoutParams - seedTextView.setTextColor(getColorFromAttr(android.R.attr.textColorPrimary)) - seedTextView.text = seed - } - TextSecurePreferences.setHasViewedSeed(this, true) - } - // endregion - - // region Interaction - private fun copySeed() { - revealSeed() - val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("Seed", seed) - clipboard.setPrimaryClip(clip) - Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() - } - // endregion -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedReminderView.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedReminderView.kt deleted file mode 100644 index 28611985fa..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedReminderView.kt +++ /dev/null @@ -1,59 +0,0 @@ -package org.thoughtcrime.securesms.onboarding - -import android.content.Context -import android.os.Build -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.widget.FrameLayout -import network.loki.messenger.databinding.ViewSeedReminderBinding - -class SeedReminderView : FrameLayout { - private lateinit var binding: ViewSeedReminderBinding - - var title: CharSequence - get() = binding.titleTextView.text - set(value) { binding.titleTextView.text = value } - var subtitle: CharSequence - get() = binding.subtitleTextView.text - set(value) { binding.subtitleTextView.text = value } - var delegate: SeedReminderViewDelegate? = null - - constructor(context: Context) : super(context) { - setUpViewHierarchy() - } - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { - setUpViewHierarchy() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { - setUpViewHierarchy() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { - setUpViewHierarchy() - } - - private fun setUpViewHierarchy() { - binding = ViewSeedReminderBinding.inflate(LayoutInflater.from(context), this, true) - binding.button.setOnClickListener { delegate?.handleSeedReminderViewContinueButtonTapped() } - } - - fun setProgress(progress: Int, isAnimated: Boolean) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - binding.progressBar.setProgress(progress, isAnimated) - } else { - binding.progressBar.progress = progress - } - } - - fun hideContinueButton() { - binding.button.visibility = View.GONE - } -} - -interface SeedReminderViewDelegate { - - fun handleSeedReminderViewContinueButtonTapped() -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt new file mode 100644 index 0000000000..aba66a4629 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt @@ -0,0 +1,172 @@ +package org.thoughtcrime.securesms.onboarding.messagenotifications + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.annotation.StringRes +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedButton +import androidx.compose.material.RadioButton +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import dagger.hilt.android.AndroidEntryPoint +import network.loki.messenger.R +import org.session.libsession.utilities.TextSecurePreferences +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.BaseActionBarActivity +import org.thoughtcrime.securesms.home.HomeActivity +import org.thoughtcrime.securesms.notifications.PushRegistry +import org.thoughtcrime.securesms.ui.AppTheme +import org.thoughtcrime.securesms.ui.OutlineButton +import org.thoughtcrime.securesms.ui.PreviewTheme +import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider +import org.thoughtcrime.securesms.ui.contentDescription +import org.thoughtcrime.securesms.ui.h8 +import org.thoughtcrime.securesms.ui.h9 +import org.thoughtcrime.securesms.ui.session_accent +import org.thoughtcrime.securesms.ui.small +import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo +import javax.inject.Inject + +@AndroidEntryPoint +class MessageNotificationsActivity : BaseActionBarActivity() { + + @Inject lateinit var pushRegistry: PushRegistry + + private val viewModel: MessageNotificationsViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setUpActionBarSessionLogo(true) + TextSecurePreferences.setHasSeenWelcomeScreen(this, true) + + ComposeView(this) + .apply { setContent { MessageNotificationsScreen() } } + .let(::setContentView) + } + + @Composable + private fun MessageNotificationsScreen() { + val state by viewModel.stateFlow.collectAsState() + + AppTheme { + MessageNotificationsScreen(state, viewModel::setEnabled, ::register) + } + } + + private fun register() { + TextSecurePreferences.setPushEnabled(this, viewModel.stateFlow.value.pushEnabled) + ApplicationContext.getInstance(this).startPollingIfNeeded() + pushRegistry.refresh(true) + Intent(this, HomeActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + putExtra(HomeActivity.FROM_ONBOARDING, true) + }.also(::startActivity) + } +} + +@Preview +@Composable +fun MessageNotificationsScreenPreview( + @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int +) { + PreviewTheme(themeResId) { + MessageNotificationsScreen() + } +} + +@Composable +fun MessageNotificationsScreen( + state: MessageNotificationsState = MessageNotificationsState(), + setEnabled: (Boolean) -> Unit = {}, + onContinue: () -> Unit = {} +) { + Column(Modifier.padding(horizontal = 32.dp)) { + Spacer(Modifier.weight(1f)) + Text(stringResource(R.string.notificationsMessage), style = MaterialTheme.typography.h4) + Spacer(Modifier.height(16.dp)) + Text(stringResource(R.string.onboardingMessageNotificationExplaination)) + Spacer(Modifier.height(16.dp)) + NotificationRadioButton( + R.string.activity_pn_mode_fast_mode, + R.string.activity_pn_mode_fast_mode_explanation, + R.string.activity_pn_mode_recommended_option_tag, + contentDescription = R.string.AccessibilityId_fast_mode_notifications_button, + selected = state.pushEnabled, + onClick = { setEnabled(true) } + ) + Spacer(Modifier.height(16.dp)) + NotificationRadioButton( + R.string.activity_pn_mode_slow_mode, + R.string.activity_pn_mode_slow_mode_explanation, + contentDescription = R.string.AccessibilityId_slow_mode_notifications_button, + selected = state.pushDisabled, + onClick = { setEnabled(false) } + ) + Spacer(Modifier.weight(1f)) + OutlineButton( + stringResource(R.string.continue_2), + modifier = Modifier + .align(Alignment.CenterHorizontally) + .width(262.dp), + onClick = onContinue + ) + Spacer(modifier = Modifier.height(12.dp)) + } +} + +@Composable +fun NotificationRadioButton( + @StringRes title: Int, + @StringRes explanation: Int, + @StringRes tag: Int? = null, + @StringRes contentDescription: Int? = null, + selected: Boolean = false, + onClick: () -> Unit = {} +) { + Row { + OutlinedButton( + onClick = onClick, + modifier = Modifier.weight(1f).contentDescription(contentDescription), + colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.background, contentColor = Color.White), + border = if (selected) BorderStroke(ButtonDefaults.OutlinedBorderSize, session_accent) else ButtonDefaults.outlinedBorder, + shape = RoundedCornerShape(8.dp) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text(stringResource(title), style = MaterialTheme.typography.h8) + Text(stringResource(explanation), style = MaterialTheme.typography.small) + tag?.let { Text(stringResource(it), color = session_accent, style = MaterialTheme.typography.h9) } + } + } + RadioButton(selected = selected, modifier = Modifier.align(Alignment.CenterVertically), onClick = onClick) + } +} + +fun Context.startMessageNotificationsActivity(flags: Int = 0) { + Intent(this, MessageNotificationsActivity::class.java) + .also { it.flags = flags } + .also(::startActivity) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsViewModel.kt new file mode 100644 index 0000000000..f913e04448 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsViewModel.kt @@ -0,0 +1,22 @@ +package org.thoughtcrime.securesms.onboarding.messagenotifications + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +class MessageNotificationsViewModel @Inject constructor(): ViewModel() { + private val state = MutableStateFlow(MessageNotificationsState()) + val stateFlow = state.asStateFlow() + + fun setEnabled(enabled: Boolean) { + state.update { MessageNotificationsState(pushEnabled = enabled) } + } +} + +data class MessageNotificationsState(val pushEnabled: Boolean = true) { + val pushDisabled get() = !pushEnabled +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt new file mode 100644 index 0000000000..c83ecf2dcf --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt @@ -0,0 +1,149 @@ +package org.thoughtcrime.securesms.onboarding.pickname + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.material.ContentAlpha +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material.LocalContentColor +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.material.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.lifecycleScope +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import network.loki.messenger.R +import org.session.libsession.utilities.AppTextSecurePreferences +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.BaseActionBarActivity +import org.thoughtcrime.securesms.onboarding.messagenotifications.MessageNotificationsActivity +import org.thoughtcrime.securesms.onboarding.messagenotifications.startMessageNotificationsActivity +import org.thoughtcrime.securesms.ui.AppTheme +import org.thoughtcrime.securesms.ui.OutlineButton +import org.thoughtcrime.securesms.ui.PreviewTheme +import org.thoughtcrime.securesms.ui.base +import org.thoughtcrime.securesms.ui.baseBold +import org.thoughtcrime.securesms.ui.contentDescription +import org.thoughtcrime.securesms.ui.outlinedTextFieldColors +import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo +import javax.inject.Inject + +private const val EXTRA_PICK_NEW_NAME = "extra_pick_new_name" + +@AndroidEntryPoint +class PickDisplayNameActivity : BaseActionBarActivity() { + + @Inject + lateinit var viewModelFactory: PickDisplayNameViewModel.AssistedFactory + + private val viewModel: PickDisplayNameViewModel by viewModels { + val pickNewName = intent.getBooleanExtra(EXTRA_PICK_NEW_NAME, false) + viewModelFactory.create(pickNewName) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setUpActionBarSessionLogo() + + ComposeView(this) + .apply { setContent { DisplayNameScreen(viewModel) } } + .let(::setContentView) + + lifecycleScope.launch { + viewModel.eventFlow.collect { + startMessageNotificationsActivity() + } + } + } + + @Composable + private fun DisplayNameScreen(viewModel: PickDisplayNameViewModel) { + val state = viewModel.stateFlow.collectAsState() + + AppTheme { + DisplayName(state.value, viewModel::onChange) { viewModel.onContinue(this) } + } + } + + @Preview + @Composable + fun PreviewDisplayName() { + PreviewTheme(R.style.Classic_Dark) { + DisplayName(State()) + } + } + + @Composable + fun DisplayName(state: State, onChange: (String) -> Unit = {}, onContinue: () -> Unit = {}) { + Column( + verticalArrangement = Arrangement.spacedBy(20.dp), + modifier = Modifier + .padding(horizontal = 50.dp) + .padding(bottom = 12.dp) + ) { + Spacer(modifier = Modifier.weight(1f)) + Text(stringResource(state.title), style = MaterialTheme.typography.h4) + Text( + stringResource(state.description), + style = MaterialTheme.typography.base, + modifier = Modifier.padding(bottom = 12.dp)) + + OutlinedTextField( + value = state.displayName, + modifier = Modifier.contentDescription(R.string.AccessibilityId_enter_display_name), + onValueChange = { onChange(it) }, + placeholder = { Text(stringResource(R.string.displayNameEnter)) }, + colors = outlinedTextFieldColors(state.error != null), + singleLine = true, + keyboardActions = KeyboardActions( + onDone = { onContinue() }, + onGo = { onContinue() }, + onSearch = { onContinue() }, + onSend = { onContinue() }, + ), + isError = state.error != null, + shape = RoundedCornerShape(12.dp) + ) + + state.error?.let { + Text(stringResource(it), style = MaterialTheme.typography.baseBold, color = MaterialTheme.colors.error) + } + + Spacer(modifier = Modifier.weight(2f)) + + OutlineButton( + stringResource(R.string.continue_2), + modifier = Modifier + .align(Alignment.CenterHorizontally) + .width(262.dp) + ) { onContinue() } + } + } +} + +fun Context.startPickDisplayNameActivity(failedToLoad: Boolean = false, flags: Int = 0) { + ApplicationContext.getInstance(this).newAccount = !failedToLoad + + Intent(this, PickDisplayNameActivity::class.java) + .apply { putExtra(EXTRA_PICK_NEW_NAME, failedToLoad) } + .also { it.flags = flags } + .also(::startActivity) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameViewModel.kt new file mode 100644 index 0000000000..3da9bcf31a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameViewModel.kt @@ -0,0 +1,115 @@ +package org.thoughtcrime.securesms.onboarding.pickname + +import android.content.Context +import androidx.annotation.StringRes +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import network.loki.messenger.R +import org.session.libsession.snode.SnodeModule +import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol.Companion.NAME_PADDED_LENGTH +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.database.LokiAPIDatabaseProtocol +import org.session.libsignal.utilities.KeyHelper +import org.session.libsignal.utilities.hexEncodedPublicKey +import org.thoughtcrime.securesms.crypto.KeyPairUtilities +import org.thoughtcrime.securesms.dependencies.ConfigFactory + +class PickDisplayNameViewModel( + pickNewName: Boolean, + private val prefs: TextSecurePreferences, + private val configFactory: ConfigFactory +): ViewModel() { + private val state = MutableStateFlow(if (pickNewName) pickNewNameState() else State()) + val stateFlow = state.asStateFlow() + + private val event = Channel() + val eventFlow = event.receiveAsFlow() + + private val database: LokiAPIDatabaseProtocol + get() = SnodeModule.shared.storage + + fun onContinue(context: Context) { + state.update { it.copy(displayName = it.displayName.trim()) } + + val displayName = state.value.displayName + + val keyPairGenerationResult = KeyPairUtilities.generate() + val seed = keyPairGenerationResult.seed + val ed25519KeyPair = keyPairGenerationResult.ed25519KeyPair + val x25519KeyPair = keyPairGenerationResult.x25519KeyPair + + when { + displayName.isEmpty() -> { state.update { it.copy(error = R.string.displayNameErrorDescription) } } + displayName.length > NAME_PADDED_LENGTH -> { state.update { it.copy(error = R.string.displayNameErrorDescriptionShorter) } } + else -> { + prefs.setProfileName(displayName) + + // This is here to resolve a case where the app restarts before a user completes onboarding + // which can result in an invalid database state + database.clearAllLastMessageHashes() + database.clearReceivedMessageHashValues() + + KeyPairUtilities.store(context, seed, ed25519KeyPair, x25519KeyPair) + configFactory.keyPairChanged() + val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey + val registrationID = KeyHelper.generateRegistrationId(false) + prefs.setLocalRegistrationId(registrationID) + prefs.setLocalNumber(userHexEncodedPublicKey) + prefs.setRestorationTime(0) + prefs.setHasViewedSeed(false) + + viewModelScope.launch { event.send(Event.DONE) } + } + } + } + + fun onChange(value: String) { + state.update { state -> state.copy( + displayName = value, + error = value.takeIf { it.length > NAME_PADDED_LENGTH }?.let { R.string.displayNameErrorDescriptionShorter } + ) + } + } + + @dagger.assisted.AssistedFactory + interface AssistedFactory { + fun create(pickNewName: Boolean): Factory + } + + @Suppress("UNCHECKED_CAST") + class Factory @AssistedInject constructor( + @Assisted private val pickNewName: Boolean, + private val prefs: TextSecurePreferences, + private val configFactory: ConfigFactory + ) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + return PickDisplayNameViewModel(pickNewName, prefs, configFactory) as T + } + } +} + +data class State( + @StringRes val title: Int = R.string.displayNamePick, + @StringRes val description: Int = R.string.displayNameDescription, + @StringRes val error: Int? = null, + val displayName: String = "" +) + +fun pickNewNameState() = State( + title = R.string.displayNameNew, + description = R.string.displayNameErrorNew +) + +sealed interface Event { + object DONE: Event +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt new file mode 100644 index 0000000000..45890980f9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt @@ -0,0 +1,250 @@ +package org.thoughtcrime.securesms.onboarding.recoverypassword + +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.os.Bundle +import androidx.activity.viewModels +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import network.loki.messenger.R +import org.thoughtcrime.securesms.BaseActionBarActivity +import org.thoughtcrime.securesms.showSessionDialog +import org.thoughtcrime.securesms.ui.AppTheme +import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin +import org.thoughtcrime.securesms.ui.GetString +import org.thoughtcrime.securesms.ui.LaunchedEffectAsync +import org.thoughtcrime.securesms.ui.LocalExtraColors +import org.thoughtcrime.securesms.ui.OutlineButton +import org.thoughtcrime.securesms.ui.PreviewTheme +import org.thoughtcrime.securesms.ui.SessionShieldIcon +import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider +import org.thoughtcrime.securesms.ui.classicDarkColors +import org.thoughtcrime.securesms.ui.colorDestructive +import org.thoughtcrime.securesms.ui.contentDescription +import org.thoughtcrime.securesms.ui.h8 +import org.thoughtcrime.securesms.ui.small +import kotlin.time.Duration.Companion.seconds + +class RecoveryPasswordActivity : BaseActionBarActivity() { + + private val viewModel: RecoveryPasswordViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + supportActionBar!!.title = resources.getString(R.string.sessionRecoveryPassword) + + ComposeView(this).apply { + setContent { + RecoveryPassword(viewModel.seed, viewModel.qrBitmap, { viewModel.copySeed(context) }) { onHide() } + } + }.let(::setContentView) + } + + private fun onHide() { + showSessionDialog { + title(R.string.recoveryPasswordHidePermanently) + htmlText(R.string.recoveryPasswordHidePermanentlyDescription1) + destructiveButton(R.string.continue_2) { onHideConfirm() } + cancelButton() + } + } + + private fun onHideConfirm() { + showSessionDialog { + title(R.string.recoveryPasswordHidePermanently) + text(R.string.recoveryPasswordHidePermanentlyDescription2) + cancelButton() + destructiveButton( + R.string.yes, + contentDescription = R.string.AccessibilityId_confirm_button + ) { + viewModel.permanentlyHidePassword() + finish() + } + } + } +} + +@Preview +@Composable +fun PreviewRecoveryPassword( + @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int +) { + PreviewTheme(themeResId) { + RecoveryPassword(seed = "Voyage urban toyed maverick peculiar tuxedo penguin tree grass building listen speak withdraw terminal plane") + } +} + +@Composable +fun RecoveryPassword( + seed: String = "", + qrBitmap: Bitmap? = null, + copySeed:() -> Unit = {}, + onHide:() -> Unit = {} +) { + AppTheme { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(bottom = 16.dp) + ) { + RecoveryPasswordCell(seed, qrBitmap, copySeed) + HideRecoveryPasswordCell(onHide) + } + } +} + +@Composable +fun RecoveryPasswordCell(seed: String = "", qrBitmap: Bitmap? = null, copySeed:() -> Unit = {}) { + val showQr = remember { + mutableStateOf(false) + } + + CellWithPaddingAndMargin { + Column { + Row { + Text(stringResource(R.string.sessionRecoveryPassword)) + Spacer(Modifier.width(8.dp)) + SessionShieldIcon() + } + + Text(stringResource(R.string.recoveryPasswordDescription)) + + AnimatedVisibility(!showQr.value) { + Text( + seed, + modifier = Modifier + .contentDescription(R.string.AccessibilityId_hide_recovery_password_button) + .padding(vertical = 24.dp) + .border( + width = 1.dp, + color = classicDarkColors[3], + shape = RoundedCornerShape(11.dp) + ) + .padding(24.dp), + style = MaterialTheme.typography.small.copy(fontFamily = FontFamily.Monospace), + color = LocalExtraColors.current.prominentButtonColor, + ) + } + + AnimatedVisibility(showQr.value, modifier = Modifier.align(Alignment.CenterHorizontally)) { + Card( + backgroundColor = LocalExtraColors.current.lightCell, + elevation = 0.dp, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(vertical = 24.dp) + ) { + Box { + qrBitmap?.let { + Image( + bitmap = it.asImageBitmap(), + contentDescription = "QR code of your recovery password", + colorFilter = ColorFilter.tint(LocalExtraColors.current.onLightCell) + ) + } + + Icon( + painter = painterResource(id = R.drawable.session_shield), + contentDescription = "", + tint = LocalExtraColors.current.onLightCell, + modifier = Modifier + .align(Alignment.Center) + .width(46.dp) + .height(56.dp) + .background(color = LocalExtraColors.current.lightCell) + .padding(horizontal = 3.dp, vertical = 1.dp) + ) + } + } + } + + AnimatedVisibility(!showQr.value) { + Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) { + OutlineButton( + modifier = Modifier.weight(1f), + color = MaterialTheme.colors.onPrimary, + onClick = copySeed, + temporaryContent = { Text(stringResource(R.string.copied)) } + ) { + Text(stringResource(R.string.copy)) + } + OutlineButton(text = stringResource(R.string.qrView), modifier = Modifier.weight(1f), color = MaterialTheme.colors.onPrimary) { showQr.toggle() } + } + } + + AnimatedVisibility(showQr.value, modifier = Modifier.align(Alignment.CenterHorizontally)) { + OutlineButton( + text = stringResource(R.string.recoveryPasswordView), + color = MaterialTheme.colors.onPrimary, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { showQr.toggle() } + } + } + } +} + +private fun MutableState.toggle() { value = !value } + +@Composable +fun HideRecoveryPasswordCell(onHide: () -> Unit = {}) { + CellWithPaddingAndMargin { + Row { + Column(Modifier.weight(1f)) { + Text(text = stringResource(R.string.recoveryPasswordHideRecoveryPassword), style = MaterialTheme.typography.h8) + Text(text = stringResource(R.string.recoveryPasswordHideRecoveryPasswordDescription)) + } + OutlineButton( + stringResource(R.string.hide), + contentDescription = GetString(R.string.AccessibilityId_hide_recovery_password_button), + modifier = Modifier.align(Alignment.CenterVertically), + color = colorDestructive + ) { onHide() } + } + } +} + +fun Context.startRecoveryPasswordActivity() { + Intent(this, RecoveryPasswordActivity::class.java).also(::startActivity) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordViewModel.kt new file mode 100644 index 0000000000..9fee061dc4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordViewModel.kt @@ -0,0 +1,54 @@ +package org.thoughtcrime.securesms.onboarding.recoverypassword + +import android.app.Application +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.graphics.Bitmap +import androidx.lifecycle.AndroidViewModel +import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback +import dagger.hilt.android.lifecycle.HiltViewModel +import org.session.libsession.utilities.AppTextSecurePreferences +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.crypto.MnemonicCodec +import org.session.libsignal.utilities.hexEncodedPrivateKey +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.crypto.MnemonicUtilities +import org.thoughtcrime.securesms.util.QRCodeUtilities +import org.thoughtcrime.securesms.util.toPx +import javax.inject.Inject + +@HiltViewModel +class RecoveryPasswordViewModel @Inject constructor( + private val application: Application +): AndroidViewModel(application) { + + val prefs = AppTextSecurePreferences(application) + + fun permanentlyHidePassword() { + prefs.setHidePassword(true) + } + + fun copySeed(context: Context) { + TextSecurePreferences.setHasViewedSeed(context, true) + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("Seed", seed) + clipboard.setPrimaryClip(clip) + } + + val seed by lazy { + val hexEncodedSeed = IdentityKeyUtil.retrieve(application, IdentityKeyUtil.LOKI_SEED) + ?: IdentityKeyUtil.getIdentityKeyPair(application).hexEncodedPrivateKey // Legacy account + MnemonicCodec { MnemonicUtilities.loadFileContents(application, it) } + .encode(hexEncodedSeed, MnemonicCodec.Language.Configuration.english) + } + + val qrBitmap by lazy { + QRCodeUtilities.encode( + data = seed, + size = toPx(280, application.resources), + isInverted = false, + hasTransparentBackground = true + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt deleted file mode 100644 index bae5f19605..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.thoughtcrime.securesms.preferences - -import android.app.Dialog -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.os.Bundle -import android.widget.Toast -import androidx.fragment.app.DialogFragment -import network.loki.messenger.R -import org.session.libsignal.crypto.MnemonicCodec -import org.session.libsignal.utilities.hexEncodedPrivateKey -import org.thoughtcrime.securesms.createSessionDialog -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil -import org.thoughtcrime.securesms.crypto.MnemonicUtilities - -class SeedDialog: DialogFragment() { - private val seed by lazy { - val hexEncodedSeed = IdentityKeyUtil.retrieve(requireContext(), IdentityKeyUtil.LOKI_SEED) - ?: IdentityKeyUtil.getIdentityKeyPair(requireContext()).hexEncodedPrivateKey // Legacy account - - MnemonicCodec { fileName -> MnemonicUtilities.loadFileContents(requireContext(), fileName) } - .encode(hexEncodedSeed, MnemonicCodec.Language.Configuration.english) - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { - title(R.string.dialog_seed_title) - text(R.string.dialog_seed_explanation) - text(seed, R.style.SessionIDTextView) - button(R.string.copy, R.string.AccessibilityId_copy_recovery_phrase) { copySeed() } - button(R.string.close) { dismiss() } - } - - private fun copySeed() { - val clipboard = requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("Seed", seed) - clipboard.setPrimaryClip(clip) - Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() - dismiss() - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index b66df5d255..91d90a5ef5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -20,6 +20,7 @@ import android.view.View import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.Toast +import androidx.core.view.isGone import androidx.core.view.isVisible import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.BuildConfig @@ -46,6 +47,7 @@ import org.thoughtcrime.securesms.home.PathActivity import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.onboarding.recoverypassword.startRecoveryPasswordActivity import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints @@ -65,6 +67,10 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { @Inject lateinit var configFactory: ConfigFactory + @Inject + lateinit var prefs: TextSecurePreferences + + private lateinit var binding: ActivitySettingsBinding private var displayNameEditActionMode: ActionMode? = null @@ -87,13 +93,17 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { super.onCreate(savedInstanceState, isReady) binding = ActivitySettingsBinding.inflate(layoutInflater) setContentView(binding.root) - val displayName = getDisplayName() glide = GlideApp.with(this) - with(binding) { + } + + override fun onStart() { + super.onStart() + + binding.run { setupProfilePictureView(profilePictureView) profilePictureView.setOnClickListener { showEditProfilePictureUI() } ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) } - btnGroupNameDisplay.text = displayName + btnGroupNameDisplay.text = getDisplayName() publicKeyTextView.text = hexEncodedPublicKey copyButton.setOnClickListener { copyPublicKey() } shareButton.setOnClickListener { sharePublicKey() } @@ -106,7 +116,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { appearanceButton.setOnClickListener { showAppearanceSettings() } inviteFriendButton.setOnClickListener { sendInvitation() } helpButton.setOnClickListener { showHelp() } - seedButton.setOnClickListener { showSeed() } + passwordDivider.isGone = prefs.getHidePassword() + passwordButton.isGone = prefs.getHidePassword() + passwordButton.setOnClickListener { showPassword() } clearAllDataButton.setOnClickListener { clearAllData() } val gitCommitFirstSixChars = BuildConfig.GIT_HASH.take(6) @@ -403,8 +415,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { show(intent) } - private fun showSeed() { - SeedDialog().show(supportFragmentManager, "Recovery Phrase Dialog") + private fun showPassword() { + startRecoveryPasswordActivity() } private fun clearAllData() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt index 55bc1be62e..0951290ea6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt @@ -1,10 +1,25 @@ package org.thoughtcrime.securesms.ui +import android.content.Context +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Card import androidx.compose.material.Colors +import androidx.compose.material.ContentAlpha +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material.LocalContentColor import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.primarySurface import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp val colorDestructive = Color(0xffFF453A) @@ -42,6 +57,7 @@ const val oceanLight5 = 0xffE7F3F4 const val oceanLight6 = 0xffECFAFB const val oceanLight7 = 0xffFCFFFF +val session_accent = Color(0xFF31F196) val ocean_accent = Color(0xff57C9FA) val oceanLights = arrayOf(oceanLight0, oceanLight1, oceanLight2, oceanLight3, oceanLight4, oceanLight5, oceanLight6, oceanLight7) @@ -61,3 +77,56 @@ fun transparentButtonColors() = ButtonDefaults.buttonColors(backgroundColor = Co @Composable fun destructiveButtonColors() = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent, contentColor = colorDestructive) + +@Preview +@Composable +fun PreviewMessageDetails( + @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int +) { + PreviewTheme(themeResId) { + Colors() + } +} + +@Composable +private fun Colors() { + AppTheme { + Column { + Box(Modifier.background(MaterialTheme.colors.primary)) { + Text("primary") + } + Box(Modifier.background(MaterialTheme.colors.primaryVariant)) { + Text("primaryVariant") + } + Box(Modifier.background(MaterialTheme.colors.secondary)) { + Text("secondary") + } + Box(Modifier.background(MaterialTheme.colors.secondaryVariant)) { + Text("secondaryVariant") + } + Box(Modifier.background(MaterialTheme.colors.surface)) { + Text("surface") + } + Box(Modifier.background(MaterialTheme.colors.primarySurface)) { + Text("primarySurface") + } + Box(Modifier.background(MaterialTheme.colors.background)) { + Text("background") + } + Box(Modifier.background(MaterialTheme.colors.error)) { + Text("error") + } + } + } +} + +@Composable +fun outlinedTextFieldColors( + isError: Boolean +) = TextFieldDefaults.outlinedTextFieldColors( + textColor = if (isError) colorDestructive else LocalContentColor.current, + cursorColor = if (isError) colorDestructive else LocalContentColor.current, + focusedBorderColor = Color(classicDark3), + unfocusedBorderColor = Color(classicDark3), + placeholderColor = if (isError) colorDestructive else MaterialTheme.colors.onSurface.copy(ContentAlpha.medium) +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 6c223a45f2..e4d2d4c37a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -1,7 +1,9 @@ package org.thoughtcrime.securesms.ui import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Canvas import androidx.compose.foundation.ScrollState import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -17,6 +19,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape @@ -31,28 +34,170 @@ import androidx.compose.material.RadioButton import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import network.loki.messenger.R import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.runIf import org.thoughtcrime.securesms.components.ProfilePictureView import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.OptionsCard import kotlin.math.min +import kotlin.math.roundToInt +import kotlin.time.Duration.Companion.seconds + +@Composable +fun OutlineButton( + text: String, + modifier: Modifier = Modifier, + contentDescription: GetString = GetString(text), + color: Color = LocalExtraColors.current.prominentButtonColor, + onClick: () -> Unit +) { + OutlinedButton( + modifier = modifier.contentDescription(contentDescription), + onClick = onClick, + border = BorderStroke(1.dp, color), + shape = RoundedCornerShape(50), // = 50% percent + colors = ButtonDefaults.outlinedButtonColors( + contentColor = color, + backgroundColor = Color.Unspecified + ) + ) { + Text(text = text) + } +} + +@Composable +fun OutlineButton( + modifier: Modifier = Modifier, + color: Color = LocalExtraColors.current.prominentButtonColor, + onClick: () -> Unit = {}, + content: @Composable () -> Unit = {} +) { + OutlinedButton( + modifier = modifier, + onClick = onClick, + border = BorderStroke(1.dp, color), + shape = RoundedCornerShape(percent = 50), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = color, + backgroundColor = Color.Unspecified + ) + ) { + content() + } +} + +@Composable +fun OutlineButton( + temporaryContent: @Composable () -> Unit, + modifier: Modifier = Modifier, + color: Color = LocalExtraColors.current.prominentButtonColor, + onClick: () -> Unit = {}, + content: @Composable () -> Unit = {} +) { + var clicked by remember { mutableStateOf(false) } + if (clicked) LaunchedEffectAsync { + delay(2.seconds) + clicked = false + } + + OutlinedButton( + modifier = modifier, + onClick = { + onClick() + clicked = true + }, + border = BorderStroke(1.dp, color), + shape = RoundedCornerShape(percent = 50), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = color, + backgroundColor = Color.Unspecified + ) + ) { + AnimatedVisibility(clicked) { + temporaryContent() + } + AnimatedVisibility(!clicked) { + content() + } + } +} + +@Composable +fun FilledButton( + text: String, + modifier: Modifier = Modifier, + contentDescription: GetString? = GetString(text), + onClick: () -> Unit) { + OutlinedButton( + modifier = modifier.size(108.dp, 34.dp), + onClick = onClick, + shape = RoundedCornerShape(50), // = 50% percent + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colors.background, + backgroundColor = LocalExtraColors.current.prominentButtonColor + ) + ) { + Text(text = text) + } +} + +@Composable +fun BorderlessButton( + text: String, + modifier: Modifier = Modifier, + contentDescription: GetString = GetString(text), + fontSize: TextUnit = TextUnit.Unspecified, + lineHeight: TextUnit = TextUnit.Unspecified, + onClick: () -> Unit) { + TextButton( + onClick = onClick, + modifier = modifier.contentDescription(contentDescription), + shape = RoundedCornerShape(50), // = 50% percent + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colors.onBackground, + backgroundColor = MaterialTheme.colors.background + ) + ) { + Text( + text = text, + textAlign = TextAlign.Center, + fontSize = fontSize, + lineHeight = lineHeight, + modifier = Modifier.padding(horizontal = 2.dp) + ) + } +} interface Callbacks { fun onSetClick(): Any? @@ -187,8 +332,16 @@ fun TitledRadioButton(option: RadioOption, onClick: () -> Unit) { @Composable fun Modifier.contentDescription(text: GetString?): Modifier { + return text?.let { + val context = LocalContext.current + semantics { contentDescription = it(context) } + } ?: this +} + +@Composable +fun Modifier.contentDescription(id: Int?): Modifier { val context = LocalContext.current - return text?.let { semantics { contentDescription = it(context) } } ?: this + return id?.let { semantics { contentDescription = context.getString(it) } } ?: this } @Composable @@ -274,3 +427,64 @@ fun RowScope.Avatar(recipient: Recipient) { ) } } + +@Composable +fun ProgressArc(progress: Float, modifier: Modifier = Modifier) { + val text = (progress * 100).roundToInt() + + Box(modifier = modifier) { + Arc(percentage = progress, modifier = Modifier.align(Alignment.Center)) + Text("${text}%", color = Color.White, modifier = Modifier.align(Alignment.Center), style = MaterialTheme.typography.h2) + } +} + +@Composable +fun Arc( + modifier: Modifier = Modifier, + percentage: Float = 0.25f, + fillColor: Color = session_accent, + backgroundColor: Color = classicDarkColors[3], + strokeWidth: Dp = 18.dp, + sweepAngle: Float = 310f, + startAngle: Float = (360f - sweepAngle) / 2 + 90f +) { + Canvas( + modifier = modifier + .padding(strokeWidth) + .size(186.dp) + ) { + // Background Line + drawArc( + color = backgroundColor, + startAngle, + sweepAngle, + false, + style = Stroke(strokeWidth.toPx(), cap = StrokeCap.Round), + size = Size(size.width, size.height) + ) + + drawArc( + color = fillColor, + startAngle, + percentage * sweepAngle, + false, + style = Stroke(strokeWidth.toPx(), cap = StrokeCap.Round), + size = Size(size.width, size.height) + ) + } +} + +@Composable +fun RowScope.SessionShieldIcon() { + Icon( + painter = painterResource(R.drawable.session_shield), + contentDescription = null, + modifier = Modifier.align(Alignment.CenterVertically) + .wrapContentSize(unbounded = true) + ) +} + +@Composable +fun LaunchedEffectAsync(block: suspend CoroutineScope.() -> Unit) { + rememberCoroutineScope().apply { LaunchedEffect(Unit) { launch { block() } } } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt index 3fa861fb71..9e681868b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt @@ -5,15 +5,25 @@ import androidx.annotation.AttrRes import androidx.appcompat.view.ContextThemeWrapper import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.text.selection.LocalTextSelectionColors +import androidx.compose.foundation.text.selection.TextSelectionColors +import androidx.compose.material.LocalContentColor import androidx.compose.material.MaterialTheme +import androidx.compose.material.Shapes +import androidx.compose.material.Typography import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.google.accompanist.themeadapter.appcompat.AppCompatTheme +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.sp +import com.google.accompanist.themeadapter.appcompat.createAppCompatTheme import com.google.android.material.color.MaterialColors import network.loki.messenger.R @@ -22,7 +32,9 @@ val LocalExtraColors = staticCompositionLocalOf { error("No Custom data class ExtraColors( val settingsBackground: Color, - val prominentButtonColor: Color + val prominentButtonColor: Color, + val lightCell: Color, + val onLightCell: Color, ) /** @@ -32,20 +44,91 @@ data class ExtraColors( fun AppTheme( content: @Composable () -> Unit ) { - val extraColors = LocalContext.current.run { + val context = LocalContext.current + + val extraColors = context.run { ExtraColors( settingsBackground = getColorFromTheme(R.attr.colorSettingsBackground), prominentButtonColor = getColorFromTheme(R.attr.prominentButtonColor), + lightCell = getColorFromTheme(R.attr.lightCell), + onLightCell = getColorFromTheme(R.attr.onLightCell), ) } + val surface = context.getColorFromTheme(R.attr.colorSettingsBackground) + + CompositionLocalProvider(LocalExtraColors provides extraColors) { - AppCompatTheme { - content() + AppCompatTheme(surface = surface) { + CompositionLocalProvider(LocalTextSelectionColors provides TextSelectionColors( + handleColor = MaterialTheme.colors.secondary, + backgroundColor = MaterialTheme.colors.secondary.copy(alpha = 0.5f) + )) { + content() + } } } } +@Composable +fun AppCompatTheme( + context: Context = LocalContext.current, + readColors: Boolean = true, + typography: Typography = sessionTypography, + shapes: Shapes = MaterialTheme.shapes, + surface: Color? = null, + content: @Composable () -> Unit +) { + val themeParams = remember(context.theme) { + context.createAppCompatTheme( + readColors = readColors, + readTypography = false + ) + } + + val colors = themeParams.colors ?: MaterialTheme.colors + + MaterialTheme( + colors = colors.copy( + surface = surface ?: colors.surface + ), + typography = typography, + shapes = shapes, + ) { + // We update the LocalContentColor to match our onBackground. This allows the default + // content color to be more appropriate to the theme background + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colors.onBackground, + content = content + ) + } +} + +fun boldStyle(size: TextUnit) = TextStyle.Default.copy( + fontWeight = FontWeight.Bold, + fontSize = size +) + +fun defaultStyle(size: TextUnit) = TextStyle.Default.copy(fontSize = size) + +val sessionTypography = Typography( + h1 = boldStyle(36.sp), + h2 = boldStyle(32.sp), + h3 = boldStyle(29.sp), + h4 = boldStyle(26.sp), + h5 = boldStyle(23.sp), + h6 = boldStyle(20.sp), +) + +val Typography.base get() = defaultStyle(14.sp) +val Typography.baseBold get() = boldStyle(14.sp) +val Typography.small get() = defaultStyle(12.sp) +val Typography.extraSmall get() = defaultStyle(11.sp) + +val Typography.h7 get() = boldStyle(18.sp) +val Typography.h8 get() = boldStyle(16.sp) +val Typography.h9 get() = boldStyle(14.sp) + fun Context.getColorFromTheme(@AttrRes attr: Int, defaultValue: Int = 0x0): Color = MaterialColors.getColor(this, attr, defaultValue).let(::Color) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/SessionTabRow.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/SessionTabRow.kt new file mode 100644 index 0000000000..2b51632fd6 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/SessionTabRow.kt @@ -0,0 +1,64 @@ +package org.thoughtcrime.securesms.ui.components + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Tab +import androidx.compose.material.TabRow +import androidx.compose.material.TabRowDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import network.loki.messenger.R +import org.thoughtcrime.securesms.ui.LocalExtraColors +import org.thoughtcrime.securesms.ui.PreviewTheme +import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider + +private val TITLES = listOf(R.string.sessionRecoveryPassword, R.string.qrScan) + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun SessionTabRow(pagerState: PagerState, titles: List) { + TabRow( + backgroundColor = Color.Unspecified, + selectedTabIndex = pagerState.currentPage, + contentColor = LocalExtraColors.current.prominentButtonColor, + divider = { TabRowDefaults.Divider(color = MaterialTheme.colors.onPrimary.copy(alpha = TabRowDefaults.DividerOpacity)) }, + modifier = Modifier + .height(48.dp) + .background(color = Color.Unspecified) + ) { + val animationScope = rememberCoroutineScope() + titles.forEachIndexed { i, it -> + Tab( + i == pagerState.currentPage, + onClick = { animationScope.launch { pagerState.animateScrollToPage(i) } }, + selectedContentColor = MaterialTheme.colors.onPrimary, + unselectedContentColor = MaterialTheme.colors.onPrimary, + ) { + Text(stringResource(id = it)) + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@androidx.compose.ui.tooling.preview.Preview +@Composable +fun PreviewSessionTabRow( + @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int +) { + PreviewTheme(themeResId) { + val pagerState = rememberPagerState { TITLES.size } + SessionTabRow(pagerState = pagerState, titles = TITLES) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/QRCodeUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/QRCodeUtilities.kt index f7d1e3e8ad..d70bb3be87 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/QRCodeUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/QRCodeUtilities.kt @@ -9,17 +9,26 @@ import com.google.zxing.qrcode.QRCodeWriter object QRCodeUtilities { - fun encode(data: String, size: Int, isInverted: Boolean = false, hasTransparentBackground: Boolean = true): Bitmap { + fun encode( + data: String, + size: Int, + isInverted: Boolean = false, + hasTransparentBackground: Boolean = true, + dark: Int = Color.BLACK, + light: Int = Color.WHITE, + ): Bitmap { try { val hints = hashMapOf( EncodeHintType.MARGIN to 1 ) val result = QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, size, size, hints) val bitmap = Bitmap.createBitmap(result.width, result.height, Bitmap.Config.ARGB_8888) + val color = if (isInverted) light else dark + val background = if (isInverted) dark else light for (y in 0 until result.height) { for (x in 0 until result.width) { if (result.get(x, y)) { - bitmap.setPixel(x, y, if (isInverted) Color.WHITE else Color.BLACK) + bitmap.setPixel(x, y, color) } else if (!hasTransparentBackground) { - bitmap.setPixel(x, y, if (isInverted) Color.BLACK else Color.WHITE) + bitmap.setPixel(x, y, background) } } } diff --git a/app/src/main/res/drawable/emoji_tada_large.xml b/app/src/main/res/drawable/emoji_tada_large.xml new file mode 100644 index 0000000000..ed802646ff --- /dev/null +++ b/app/src/main/res/drawable/emoji_tada_large.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_logo_large.xml b/app/src/main/res/drawable/ic_logo_large.xml new file mode 100644 index 0000000000..b494b17663 --- /dev/null +++ b/app/src/main/res/drawable/ic_logo_large.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_shield_outline.xml b/app/src/main/res/drawable/ic_shield_outline.xml new file mode 100644 index 0000000000..3db98f53d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_shield_outline.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/session_logo.xml b/app/src/main/res/drawable/session_logo.xml index f88a4f21a9..b2f931990f 100644 --- a/app/src/main/res/drawable/session_logo.xml +++ b/app/src/main/res/drawable/session_logo.xml @@ -1,9 +1,9 @@ - - - - - + + diff --git a/app/src/main/res/drawable/session_shield.xml b/app/src/main/res/drawable/session_shield.xml new file mode 100644 index 0000000000..a7c6d1a24a --- /dev/null +++ b/app/src/main/res/drawable/session_shield.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout-sw400dp/activity_display_name.xml b/app/src/main/res/layout-sw400dp/activity_display_name.xml deleted file mode 100644 index d62faca064..0000000000 --- a/app/src/main/res/layout-sw400dp/activity_display_name.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - -