diff --git a/.drone.jsonnet b/.drone.jsonnet new file mode 100644 index 000000000..dc81115ce --- /dev/null +++ b/.drone.jsonnet @@ -0,0 +1,88 @@ +local docker_base = 'registry.oxen.rocks/lokinet-ci-'; + +// Log a bunch of version information to make it easier for debugging +local version_info = { + name: 'Version Information', + image: docker_base + 'android', + commands: [ + 'cmake --version', + 'apt --installed list' + ] +}; + + +// Intentionally doing a depth of 2 as libSession-util has it's own submodules (and libLokinet likely will as well) +local clone_submodules = { + name: 'Clone Submodules', + image: 'drone/git', + commands: ['git fetch --tags', 'git submodule update --init --recursive --depth=2 --jobs=4'] +}; + +// cmake options for static deps mirror +local ci_dep_mirror(want_mirror) = (if want_mirror then ' -DLOCAL_MIRROR=https://oxen.rocks/deps ' else ''); + +[ + // Unit tests (PRs only) + { + kind: 'pipeline', + type: 'docker', + name: 'Unit Tests', + platform: { arch: 'amd64' }, + trigger: { event: { exclude: [ 'push' ] } }, + steps: [ + version_info, + clone_submodules, + { + name: 'Run Unit Tests', + image: docker_base + 'android', + pull: 'always', + environment: { ANDROID_HOME: '/usr/lib/android-sdk' }, + commands: [ + 'apt-get install -y ninja-build', + './gradlew testPlayDebugUnitTestCoverageReport' + ], + } + ], + }, + // Validate build artifact was created by the direct branch push (PRs only) + { + kind: 'pipeline', + type: 'docker', + name: 'Check Build Artifact Existence', + platform: { arch: 'amd64' }, + trigger: { event: { exclude: [ 'push' ] } }, + steps: [ + { + name: 'Poll for build artifact existence', + image: docker_base + 'android', + pull: 'always', + commands: [ + './scripts/drone-upload-exists.sh' + ] + } + ] + }, + // Debug APK build (non-PRs only) + { + kind: 'pipeline', + type: 'docker', + name: 'Debug APK Build', + platform: { arch: 'amd64' }, + trigger: { event: { exclude: [ 'pull_request' ] } }, + steps: [ + version_info, + clone_submodules, + { + name: 'Build and upload', + image: docker_base + 'android', + pull: 'always', + environment: { SSH_KEY: { from_secret: 'SSH_KEY' }, ANDROID_HOME: '/usr/lib/android-sdk' }, + commands: [ + 'apt-get install -y ninja-build', + './gradlew assemblePlayDebug', + './scripts/drone-static-upload.sh' + ], + } + ], + } +] \ No newline at end of file diff --git a/.gitignore b/.gitignore index 023fc8101..be928b393 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ ffpr *.sh pkcs11.password app/play -app/huawei \ No newline at end of file +app/huawei + +!/scripts/drone-static-upload.sh +!/scripts/drone-upload-exists.sh \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index a98704ca2..ca8b3e6d5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,8 +31,8 @@ configurations.all { exclude module: "commons-logging" } -def canonicalVersionCode = 359 -def canonicalVersionName = "1.17.4" +def canonicalVersionCode = 360 +def canonicalVersionName = "1.17.5" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1, @@ -124,6 +124,7 @@ android { debug { isDefault true minifyEnabled false + enableUnitTestCoverage true } } @@ -201,6 +202,27 @@ android { } } } + + task testPlayDebugUnitTestCoverageReport(type: JacocoReport, dependsOn: "testPlayDebugUnitTest") { + reports { + xml.enabled = true + } + + // Add files that should not be listed in the report (e.g. generated Files from dagger) + def fileFilter = [] + def mainSrc = "$projectDir/src/main/java" + def kotlinDebugTree = fileTree(dir: "${buildDir}/tmp/kotlin-classes/playDebug", excludes: fileFilter) + + // Compiled Kotlin class files are written into build-variant-specific subdirectories of 'build/tmp/kotlin-classes'. + classDirectories.from = files([kotlinDebugTree]) + + // To produce an accurate report, the bytecode is mapped back to the original source code. + sourceDirectories.from = files([mainSrc]) + + // Execution data generated when running the tests against classes instrumented by the JaCoCo agent. + // This is enabled with 'enableUnitTestCoverage' in the 'debug' build type. + executionData.from = "${project.buildDir}/outputs/unit_test_code_coverage/playDebugUnitTest/testPlayDebugUnitTest.exec" + } } dependencies { 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 9019322a6..a508c88dd 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 @@ -1955,6 +1955,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe override fun saveAttachment(messages: Set) { val message = messages.first() as MmsMessageRecord + + // Do not allow the user to download a file attachment before it has finished downloading + // TODO: Localise the msg in this toast! + if (message.isMediaPending) { + Toast.makeText(this, resources.getString(R.string.conversation_activity__wait_until_attachment_has_finished_downloading), Toast.LENGTH_LONG).show() + return + } + SaveAttachmentTask.showWarningDialog(this) { Permissions.with(this) .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt index 73e2d571c..5959c41d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt @@ -37,6 +37,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li private val vMargin by lazy { toDp(4, resources) } private val minHeight by lazy { toPx(56, resources) } private var linkPreviewDraftView: LinkPreviewDraftView? = null + private var quoteView: QuoteView? = null var delegate: InputBarDelegate? = null var additionalContentHeight = 0 var quote: MessageRecord? = null @@ -98,7 +99,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li binding.inputBarEditText.imeOptions = EditorInfo.IME_ACTION_NONE binding.inputBarEditText.inputType = binding.inputBarEditText.inputType or - InputType.TYPE_TEXT_FLAG_CAP_SENTENCES + InputType.TYPE_TEXT_FLAG_CAP_SENTENCES } val incognitoFlag = if (TextSecurePreferences.isIncognitoKeyboardEnabled(context)) 16777216 else 0 binding.inputBarEditText.imeOptions = binding.inputBarEditText.imeOptions or incognitoFlag // Always use incognito keyboard if setting enabled @@ -138,53 +139,64 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li delegate?.startRecordingVoiceMessage() } - // Drafting quotes and drafting link previews is mutually exclusive, i.e. you can't draft - // a quote and a link preview at the same time. - fun draftQuote(thread: Recipient, message: MessageRecord, glide: GlideRequests) { quote = message - linkPreview = null - linkPreviewDraftView = null - binding.inputBarAdditionalContentContainer.removeAllViews() - // inflate quoteview with typed array here + // If we already have a link preview View then clear the 'additional content' layout so that + // our quote View is always the first element (i.e., at the top of the reply). + if (linkPreview != null && linkPreviewDraftView != null) { + binding.inputBarAdditionalContentContainer.removeAllViews() + } + + // Inflate quote View with typed array here val layout = LayoutInflater.from(context).inflate(R.layout.view_quote_draft, binding.inputBarAdditionalContentContainer, false) - val quoteView = layout.findViewById(R.id.mainQuoteViewContainer) - quoteView.delegate = this - binding.inputBarAdditionalContentContainer.addView(layout) - val attachments = (message as? MmsMessageRecord)?.slideDeck - val sender = if (message.isOutgoing) TextSecurePreferences.getLocalNumber(context)!! else message.individualRecipient.address.serialize() - quoteView.bind(sender, message.body, attachments, - thread, true, message.isOpenGroupInvitation, message.threadId, false, glide) + quoteView = layout.findViewById(R.id.mainQuoteViewContainer).also { + it.delegate = this + binding.inputBarAdditionalContentContainer.addView(layout) + val attachments = (message as? MmsMessageRecord)?.slideDeck + val sender = if (message.isOutgoing) TextSecurePreferences.getLocalNumber(context)!! else message.individualRecipient.address.serialize() + it.bind(sender, message.body, attachments, thread, true, message.isOpenGroupInvitation, message.threadId, false, glide) + } + + // Before we request a layout update we'll add back any LinkPreviewDraftView that might + // exist - as this goes into the LinearLayout second it will be below the quote View. + if (linkPreview != null && linkPreviewDraftView != null) { + binding.inputBarAdditionalContentContainer.addView(linkPreviewDraftView) + } requestLayout() } override fun cancelQuoteDraft() { + binding.inputBarAdditionalContentContainer.removeView(quoteView) quote = null - binding.inputBarAdditionalContentContainer.removeAllViews() + quoteView = null requestLayout() } fun draftLinkPreview() { - quote = null - binding.inputBarAdditionalContentContainer.removeAllViews() - val linkPreviewDraftView = LinkPreviewDraftView(context) - linkPreviewDraftView.delegate = this - this.linkPreviewDraftView = linkPreviewDraftView + // As `draftLinkPreview` is called before `updateLinkPreview` when we modify a URI in a + // message we'll bail early if a link preview View already exists and just let + // `updateLinkPreview` get called to update the existing View. + if (linkPreview != null && linkPreviewDraftView != null) return + + linkPreviewDraftView = LinkPreviewDraftView(context).also { it.delegate = this } + + // Add the link preview View. Note: If there's already a quote View in the 'additional + // content' container then this preview View will be added after / below it - which is fine. binding.inputBarAdditionalContentContainer.addView(linkPreviewDraftView) requestLayout() } - fun updateLinkPreviewDraft(glide: GlideRequests, linkPreview: LinkPreview) { - this.linkPreview = linkPreview - val linkPreviewDraftView = this.linkPreviewDraftView ?: return - linkPreviewDraftView.update(glide, linkPreview) + fun updateLinkPreviewDraft(glide: GlideRequests, updatedLinkPreview: LinkPreview) { + // Update our `linkPreview` property with the new (provided as an argument to this function) + // then update the View from that. + linkPreview = updatedLinkPreview.also { linkPreviewDraftView?.update(glide, it) } } override fun cancelLinkPreviewDraft() { - if (quote != null) { return } + binding.inputBarAdditionalContentContainer.removeView(linkPreviewDraftView) linkPreview = null - binding.inputBarAdditionalContentContainer.removeAllViews() + linkPreviewDraftView = null requestLayout() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt index f4f1a2cd9..0614b52e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt @@ -5,11 +5,13 @@ import android.content.res.ColorStateList import android.util.AttributeSet import android.widget.LinearLayout import androidx.annotation.ColorInt +import androidx.core.view.isVisible import network.loki.messenger.databinding.ViewDocumentBinding import org.thoughtcrime.securesms.database.model.MmsMessageRecord class DocumentView : LinearLayout { private val binding: ViewDocumentBinding by lazy { ViewDocumentBinding.bind(this) } + // region Lifecycle constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet) : super(context, attrs) @@ -22,6 +24,12 @@ class DocumentView : LinearLayout { binding.documentTitleTextView.text = document.fileName.or("Untitled File") binding.documentTitleTextView.setTextColor(textColor) binding.documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor) + + // Show the progress spinner if the attachment is downloading, otherwise show + // the document icon (and always remove the other, whichever one that is) + binding.documentViewProgress.isVisible = message.isMediaPending + binding.documentViewIconImageView.isVisible = !message.isMediaPending } // endregion + } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index c812d0f73..1b4162521 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -318,4 +318,5 @@ class VisibleMessageContentView : ConstraintLayout { } } // endregion + } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index c15768ad0..0a3b2a35b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -66,7 +66,6 @@ private const val TAG = "VisibleMessageView" @AndroidEntryPoint class VisibleMessageView : LinearLayout { - @Inject lateinit var threadDb: ThreadDatabase @Inject lateinit var lokiThreadDb: LokiThreadDatabase @Inject lateinit var lokiApiDb: LokiAPIDatabase @@ -141,8 +140,7 @@ class VisibleMessageView : LinearLayout { val isGroupThread = thread.isGroupRecipient val isStartOfMessageCluster = isStartOfMessageCluster(message, previous, isGroupThread) val isEndOfMessageCluster = isEndOfMessageCluster(message, next, isGroupThread) - // Show profile picture and sender name if this is a group thread AND - // the message is incoming + // Show profile picture and sender name if this is a group thread AND the message is incoming binding.moderatorIconImageView.isVisible = false binding.profilePictureView.visibility = when { thread.isGroupRecipient && !message.isOutgoing && isEndOfMessageCluster -> View.VISIBLE 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 f67f0fbaa..c878a79ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt @@ -14,6 +14,11 @@ class LandingActivity : BaseActionBarActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + // We always hit this LandingActivity on launch - but if there is a previous instance of + // 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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt index 6e082e000..13e5b51f0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt @@ -35,6 +35,8 @@ import javax.inject.Inject @AndroidEntryPoint class RegisterActivity : BaseActionBarActivity() { + private val temporarySeedKey = "TEMPORARY_SEED_KEY" + @Inject lateinit var configFactory: ConfigFactory @@ -77,16 +79,23 @@ class RegisterActivity : BaseActionBarActivity() { }, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) binding.termsTextView.movementMethod = LinkMovementMethod.getInstance() binding.termsTextView.text = termsExplanation - updateKeyPair() + 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() { - val keyPairGenerationResult = KeyPairUtilities.generate() - seed = keyPairGenerationResult.seed + private fun updateKeyPair(temporaryKey: ByteArray?) { + val keyPairGenerationResult = temporaryKey?.let(KeyPairUtilities::generate) ?: KeyPairUtilities.generate() + seed = keyPairGenerationResult.seed ed25519KeyPair = keyPairGenerationResult.ed25519KeyPair - x25519KeyPair = keyPairGenerationResult.x25519KeyPair + x25519KeyPair = keyPairGenerationResult.x25519KeyPair } private fun updatePublicKeyTextView() { @@ -125,7 +134,6 @@ class RegisterActivity : BaseActionBarActivity() { // which can result in an invalid database state database.clearAllLastMessageHashes() database.clearReceivedMessageHashValues() - KeyPairUtilities.store(this, seed!!, ed25519KeyPair!!, x25519KeyPair!!) configFactory.keyPairChanged() val userHexEncodedPublicKey = x25519KeyPair!!.hexEncodedPublicKey diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt index 26d8fc223..4835ab0dc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt @@ -50,7 +50,7 @@ class PeerConnectionWrapper(private val context: Context, private fun initPeerConnection() { val random = SecureRandom().asKotlinRandom() - val iceServers = listOf("freyr","fenrir","frigg","angus","hereford","holstein", "brahman").shuffled(random).take(2).map { sub -> + val iceServers = listOf("freyr","angus","hereford","holstein", "brahman").shuffled(random).take(2).map { sub -> PeerConnection.IceServer.builder("turn:$sub.getsession.org") .setUsername("session202111") .setPassword("053c268164bc7bd7") diff --git a/app/src/main/res/layout/fragment_enter_community_url.xml b/app/src/main/res/layout/fragment_enter_community_url.xml index 03767f4df..a94ff805a 100644 --- a/app/src/main/res/layout/fragment_enter_community_url.xml +++ b/app/src/main/res/layout/fragment_enter_community_url.xml @@ -26,6 +26,7 @@ android:layout_marginTop="@dimen/large_spacing" android:gravity="center_vertical" android:hint="@string/fragment_enter_community_url_edit_text_hint" + android:contentDescription="@string/AccessibilityId_community_input_box" android:inputType="textUri" android:maxLines="3" android:paddingTop="0dp" @@ -92,6 +93,7 @@