Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="AllFilesAccessPolicy,ScopedStorage" />
Expand Down Expand Up @@ -74,5 +77,14 @@
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>

<service
android:name=".service.KeepAliveService"
android:exported="false"
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="keep the IDE process alive while backgrounded" />
</service>
</application>
</manifest>
20 changes: 20 additions & 0 deletions app/src/main/kotlin/org/cosmicide/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@

package org.cosmicide

import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.CompositionLocalProvider
Expand All @@ -24,6 +29,7 @@ import org.cosmicide.common.Prefs
import org.cosmicide.editor.EditorExtensionPoints
import org.cosmicide.editor.lsp.LspEditorLanguageProvider
import org.cosmicide.plugin.CosmicPluginHost
import org.cosmicide.service.KeepAliveService
import org.cosmicide.ui.IDENavigation
import org.cosmicide.ui.donation.DonationPromptTracker
import org.cosmicide.ui.editor.resolveTheme
Expand All @@ -41,6 +47,7 @@ class MainActivity : ComponentActivity() {
if (savedInstanceState == null) {
DonationPromptTracker.recordLaunch(applicationContext)
}
startKeepAliveService()
val appContainer = AppContainer(applicationContext)

setContent {
Expand Down Expand Up @@ -95,6 +102,19 @@ class MainActivity : ComponentActivity() {
LspEditorLanguageProvider.updateColors(colorScheme)
}

private fun startKeepAliveService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 100
)
}
runCatching { KeepAliveService.start(this) }
.onFailure { Log.w(TAG, "Could not start keep-alive service", it) }
}

private companion object {
const val TAG = "MainActivity"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,11 @@ class ComposeSignatureHelpLayout : SignatureHelpLayout {
}
}

// SignatureHelpWindow measures before attaching its PopupWindow.
createComposition(ComposeViewContext(window.editor))
// SignatureHelpWindow measures before attaching its PopupWindow. Only possible once
// the editor is attached; otherwise the strategy creates composition on attach.
if (window.editor.isAttachedToWindow) {
createComposition(ComposeViewContext(window.editor))
}
}
return composeView
}
Expand Down Expand Up @@ -171,7 +174,11 @@ class ComposeSignatureHelpLayout : SignatureHelpLayout {
* initial empty state.
*/
private fun composeBeforeHostMeasurement() {
if (!::composeView.isInitialized || composeView.isAttachedToWindow) return
if (
!::composeView.isInitialized ||
composeView.isAttachedToWindow ||
!window.editor.isAttachedToWindow
) return
composeView.disposeComposition()
composeView.createComposition(ComposeViewContext(window.editor))
}
Expand Down
7 changes: 5 additions & 2 deletions app/src/main/kotlin/org/cosmicide/editor/lsp/HoverLayout.kt
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,11 @@ class HoverLayout : HoverLayout {
}
}

// HoverWindow measures before attaching the PopupWindow.
createComposition(ComposeViewContext(window.editor))
// HoverWindow measures before attaching the PopupWindow. Only possible once the
// editor is attached; otherwise the strategy creates composition on attach.
if (window.editor.isAttachedToWindow) {
createComposition(ComposeViewContext(window.editor))
}
}
}

Expand Down
60 changes: 13 additions & 47 deletions app/src/main/kotlin/org/cosmicide/editor/lsp/LspEditorAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.cosmicide.common.AppDispatchers
import org.cosmicide.common.IndexManager
import org.cosmicide.editor.LspServerConnection
import org.cosmicide.editor.LspServerDefinition
import org.cosmicide.editor.LspServerRequest
Expand All @@ -47,6 +46,7 @@ import java.io.InputStream
import java.io.OutputStream
import java.net.URI
import java.net.URL

import java.security.MessageDigest
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
Expand Down Expand Up @@ -164,6 +164,9 @@ fun CodeEditor.configureLspLanguage(
)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
editable = true
}
lspEditor.dispose()
Log.w(TAG, "Failed to connect to ${definition.displayName}", e)
LspLogStore.error(definition.displayName, "Failed to connect", e)
Expand Down Expand Up @@ -447,44 +450,24 @@ private fun createTextMateLanguage(
return createTextMateLanguage(definition, grammarText)
}

var cachedGrammarText: String? = readGrammarViaIndexManager(grammarLink)
val cachedGrammarText = runCatching {
grammarCacheFile(context, grammarLink).takeIf(File::exists)?.readText()
}.getOrNull()

if (cachedGrammarText != null) {
try {
return createTextMateLanguage(definition, cachedGrammarText)
} catch (e: Exception) {
Log.w(TAG, "Discarding invalid grammar cache for $grammarLink", e)
IndexManager.invalidateProject("grammar:$grammarLink")
cachedGrammarText = null
runCatching { grammarCacheFile(context, grammarLink).delete() }
}
}

val refreshedGrammarText = try {
openGrammarStream(context, grammarLink).readGrammarText()
} catch (refreshFailure: Exception) {
val staleGrammarText = cachedGrammarText ?: throw refreshFailure
Log.w(TAG, "Grammar refresh failed; using stale cache for $grammarLink", refreshFailure)
return createTextMateLanguage(definition, staleGrammarText)
}

return try {
createTextMateLanguage(definition, refreshedGrammarText).also {
runCatching { cacheGrammarViaIndexManager(grammarLink, refreshedGrammarText) }
.onFailure { error ->
Log.w(TAG, "Unable to cache grammar from $grammarLink", error)
}
}
} catch (refreshFailure: Exception) {
val staleGrammarText = cachedGrammarText ?: throw refreshFailure
Log.w(
TAG,
"Refreshed grammar is invalid; using stale cache for $grammarLink",
refreshFailure
)
runCatching { createTextMateLanguage(definition, staleGrammarText) }
.getOrElse { staleFailure ->
refreshFailure.addSuppressed(staleFailure)
throw refreshFailure
val refreshedGrammarText = openGrammarStream(context, grammarLink).readGrammarText()
return createTextMateLanguage(definition, refreshedGrammarText).also {
runCatching { grammarCacheFile(context, grammarLink).writeText(refreshedGrammarText) }
.onFailure { error ->
Log.w(TAG, "Unable to cache grammar from $grammarLink", error)
}
}
}
Expand Down Expand Up @@ -542,23 +525,6 @@ private fun grammarCacheFile(context: Context, grammarLink: String): File {
.resolve("$cacheKey.grammar")
}

private fun cacheGrammarViaIndexManager(grammarLink: String, grammarText: String) {
val key = "grammar:${grammarLink}"
IndexManager.getOrBuildIndex(key, "text") { grammarText.toByteArray(Charsets.UTF_8) }
}

private fun readGrammarViaIndexManager(grammarLink: String): String? {
val key = "grammar:${grammarLink}"
return try {
val segment = IndexManager.getOrBuildIndex(key, "text") {
throw IllegalStateException("Grammar not cached: $grammarLink")
}
String(segment.readBlock(0, segment.size.toInt()), Charsets.UTF_8)
} catch (e: Exception) {
null
}
}

private fun openGrammarStream(context: Context, link: String): InputStream {
val uri = link.toUri()
return when (uri.scheme?.lowercase()) {
Expand Down
77 changes: 77 additions & 0 deletions app/src/main/kotlin/org/cosmicide/service/KeepAliveService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* This file is part of Cosmic IDE.
* Cosmic IDE is a free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* Cosmic IDE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
* You should have received a copy of the GNU General Public License along with Cosmic IDE. If not, see <https://www.gnu.org/licenses/>.
*/

package org.cosmicide.service

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import org.cosmicide.MainActivity
import org.cosmicide.R

/**
* Foreground service that keeps the app process alive in the background, like Termux.
*/
class KeepAliveService : Service() {

override fun onCreate() {
super.onCreate()
startForegroundCompat()
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(NotificationId, buildNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(NotificationId, buildNotification())
}
return START_STICKY
}

override fun onBind(intent: Intent?): IBinder? = null

private fun startForegroundCompat() = onStartCommand(null, 0, 0)

private fun buildNotification(): Notification {
val channelId = "keepalive"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId, "Foreground service", NotificationManager.IMPORTANCE_MIN
)
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
}

val contentIntent = PendingIntent.getActivity(
this, 0,
Intent(this, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)

return Notification.Builder(this, channelId)
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.keep_alive_notification_text))
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setOngoing(true)
.setContentIntent(contentIntent)
.build()
}

companion object {
private const val NotificationId = 1

fun start(context: Context) {
context.startForegroundService(Intent(context, KeepAliveService::class.java))
}
}
}
4 changes: 4 additions & 0 deletions app/src/main/kotlin/org/cosmicide/ui/editor/EditorToolbar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ internal fun EditorToolbar(
}
}
DropdownMenuItem(text = { Text("Editor") }, children = {
DropdownMenuItem(text = { Text("Find & Replace") }, onClick = {
editor.beginSearchMode()
showMenu = false
})
DropdownMenuItem(text = { Text("Format") }, onClick = {
editor.formatCodeAsync()
showMenu = false
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<string name="create_java_class">Create java class</string>

<string name="terminal">Terminal</string>
<string name="keep_alive_notification_text">Cosmic IDE is running in the background</string>
<string name="lsp_diagnostic_copy">Copy</string>
<string name="lsp_diagnostic_quick_fixes">Quick fixes</string>
</resources>
Loading