Skip to content

Commit e8f10a2

Browse files
committed
#1562 fix: use separate activity because singleInstance launchMode breaks all startActivityForResult
1 parent 05ffa58 commit e8f10a2

7 files changed

Lines changed: 279 additions & 59 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,10 @@
8484
android:theme="@style/AppTheme.NoActionBar"
8585
tools:ignore="GoogleAppIndexingWarning">
8686

87-
<!-- relinquishTaskIdentity is required because the intent-filter has the
88-
DEFAULT category. If relinquishTaskIdentity is false then when the user resumes the
89-
app from the launcher they are taken back to the SplashActivity, and consequently the
90-
home fragment in MainActivity. -->
9187
<activity
9288
android:name=".MainActivity"
9389
android:exported="true"
94-
android:relinquishTaskIdentity="true"
9590
android:theme="@style/Theme.App.Starting"
96-
android:launchMode="singleInstance"
9791
android:windowSoftInputMode="adjustResize">
9892
<!-- IMPORTANT! windowSoftInputMode is needed for IME padding to work
9993
properly in the ChooseActionScreen search bar for example. -->
@@ -111,22 +105,6 @@
111105
<!-- Set as default so the Assistant Trigger app isn't opened by default. -->
112106
<category android:name="android.intent.category.DEFAULT" />
113107
</intent-filter>
114-
115-
<intent-filter>
116-
<action android:name="android.intent.action.VIEW" />
117-
118-
<category android:name="android.intent.category.DEFAULT" />
119-
<category android:name="android.intent.category.BROWSABLE" />
120-
121-
<data
122-
android:mimeType="application/zip"
123-
android:scheme="content" />
124-
125-
<data
126-
android:mimeType="application/json"
127-
android:scheme="content" />
128-
129-
</intent-filter>
130108
</activity>
131109

132110
<!-- Use a different task affinity so this activity doesn't close other activities -->
@@ -154,6 +132,32 @@
154132
</intent-filter>
155133
</activity>
156134

135+
<activity
136+
android:name=".backup.RestoreKeyMapsActivity"
137+
android:excludeFromRecents="true"
138+
android:exported="true"
139+
android:launchMode="singleInstance"
140+
android:taskAffinity="io.github.sds100.keymapper.restore_key_maps"
141+
android:theme="@style/AppTheme.DialogActivity"
142+
android:windowSoftInputMode="adjustResize">
143+
144+
<intent-filter>
145+
<action android:name="android.intent.action.VIEW" />
146+
147+
<category android:name="android.intent.category.DEFAULT" />
148+
<category android:name="android.intent.category.BROWSABLE" />
149+
150+
<data
151+
android:mimeType="application/zip"
152+
android:scheme="content" />
153+
154+
<data
155+
android:mimeType="application/json"
156+
android:scheme="content" />
157+
158+
</intent-filter>
159+
</activity>
160+
157161
<receiver
158162
android:name=".system.notifications.NotificationClickReceiver"
159163
android:exported="true">

app/src/main/java/io/github/sds100/keymapper/BaseMainActivity.kt

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ abstract class BaseMainActivity : AppCompatActivity() {
7474
ActivityViewModel.Factory(ServiceLocator.resourceProvider(this))
7575
}
7676

77-
val homeViewModel by viewModels<HomeViewModel> {
77+
private val homeViewModel by viewModels<HomeViewModel> {
7878
Inject.homeViewModel(this)
7979
}
8080

@@ -185,8 +185,6 @@ abstract class BaseMainActivity : AppCompatActivity() {
185185
ContextCompat.RECEIVER_EXPORTED,
186186
)
187187
}
188-
189-
importKeyMaps(intent)
190188
}
191189

192190
override fun onResume() {
@@ -208,12 +206,6 @@ abstract class BaseMainActivity : AppCompatActivity() {
208206
super.onDestroy()
209207
}
210208

211-
override fun onNewIntent(intent: Intent) {
212-
super.onNewIntent(intent)
213-
214-
importKeyMaps(intent)
215-
}
216-
217209
override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
218210
event ?: return super.onGenericMotionEvent(event)
219211

@@ -228,19 +220,6 @@ abstract class BaseMainActivity : AppCompatActivity() {
228220
}
229221
}
230222

231-
private fun importKeyMaps(intent: Intent?) {
232-
intent ?: return
233-
234-
if (intent.action == Intent.ACTION_VIEW) {
235-
intent.data?.let {
236-
homeViewModel.onChooseImportFile(it.toString())
237-
238-
// Do not want to import again on a configuration change so set it to null
239-
this.intent = null
240-
}
241-
}
242-
}
243-
244223
private fun saveFile(originalFile: Uri, targetFile: Uri) {
245224
lifecycleScope.launch(Dispatchers.IO) {
246225
targetFile.openOutputStream(this@BaseMainActivity)?.use { output ->

app/src/main/java/io/github/sds100/keymapper/backup/BackupManager.kt

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.sds100.keymapper.backup
22

3+
import android.database.sqlite.SQLiteConstraintException
34
import com.github.salomonbrys.kotson.byInt
45
import com.github.salomonbrys.kotson.byNullableArray
56
import com.github.salomonbrys.kotson.byNullableInt
@@ -24,9 +25,8 @@ import io.github.sds100.keymapper.data.entities.ConstraintEntity
2425
import io.github.sds100.keymapper.data.entities.EntityExtra
2526
import io.github.sds100.keymapper.data.entities.FingerprintMapEntity
2627
import io.github.sds100.keymapper.data.entities.FloatingButtonEntity
27-
import io.github.sds100.keymapper.data.entities.FloatingButtonEntityWithLayout
28+
import io.github.sds100.keymapper.data.entities.FloatingButtonKeyEntity
2829
import io.github.sds100.keymapper.data.entities.FloatingLayoutEntity
29-
import io.github.sds100.keymapper.data.entities.FloatingLayoutEntityWithButtons
3030
import io.github.sds100.keymapper.data.entities.KeyMapEntity
3131
import io.github.sds100.keymapper.data.entities.TriggerEntity
3232
import io.github.sds100.keymapper.data.entities.TriggerKeyEntity
@@ -486,7 +486,21 @@ class BackupManagerImpl(
486486
}
487487

488488
if (backupContent.floatingLayouts != null) {
489-
floatingLayoutRepository.insert(*backupContent.floatingLayouts.toTypedArray())
489+
for (layout in backupContent.floatingLayouts) {
490+
var entity = layout
491+
var subCount = 0
492+
493+
while (true) {
494+
try {
495+
floatingLayoutRepository.insert(layout)
496+
break
497+
} catch (_: SQLiteConstraintException) {
498+
// If the name already exists try creating it with a new name.
499+
entity = layout.copy(name = "${entity.name} (${subCount + 1})")
500+
subCount++
501+
}
502+
}
503+
}
490504
}
491505

492506
if (backupContent.floatingButtons != null) {
@@ -535,17 +549,26 @@ class BackupManagerImpl(
535549
// delete the contents of the file
536550
output.clear()
537551

538-
val floatingLayouts = floatingLayoutRepository.layouts
539-
.filterIsInstance<State.Data<List<FloatingLayoutEntityWithButtons>>>()
540-
.first()
541-
.data
542-
.map { it.layout }
552+
val floatingLayouts: MutableList<FloatingLayoutEntity> = mutableListOf()
553+
val floatingButtons: MutableList<FloatingButtonEntity> = mutableListOf()
543554

544-
val floatingButtons = floatingButtonRepository.buttonsList
545-
.filterIsInstance<State.Data<List<FloatingButtonEntityWithLayout>>>()
546-
.first()
547-
.data
548-
.map { it.button }
555+
if (keyMapList != null) {
556+
val floatingButtonTriggerKeys = keyMapList
557+
.flatMap { it.trigger.keys }
558+
.filterIsInstance<FloatingButtonKeyEntity>()
559+
.map { it.buttonUid }
560+
.distinct()
561+
562+
for (buttonUid in floatingButtonTriggerKeys) {
563+
val buttonWithLayout = floatingButtonRepository.get(buttonUid) ?: continue
564+
565+
if (floatingLayouts.none { it.uid == buttonWithLayout.layout.uid }) {
566+
floatingLayouts.add(buttonWithLayout.layout)
567+
}
568+
569+
floatingButtons.add(buttonWithLayout.button)
570+
}
571+
}
549572

550573
val backupContent = BackupContent(
551574
AppDatabase.DATABASE_VERSION,
@@ -581,8 +604,8 @@ class BackupManagerImpl(
581604
.get(Keys.defaultVibrateDuration)
582605
.first()
583606
.takeIf { it != PreferenceDefaults.VIBRATION_DURATION },
584-
floatingLayouts = floatingLayouts,
585-
floatingButtons = floatingButtons,
607+
floatingLayouts = floatingLayouts.takeIf { it.isNotEmpty() },
608+
floatingButtons = floatingButtons.takeIf { it.isNotEmpty() },
586609
)
587610

588611
val json = gson.toJson(backupContent)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.github.sds100.keymapper.backup
2+
3+
sealed class ImportExportState {
4+
data object Idle : ImportExportState()
5+
data object Exporting : ImportExportState()
6+
data class FinishedExport(val uri: String) : ImportExportState()
7+
8+
data class ConfirmImport(val fileUri: String, val keyMapCount: Int) : ImportExportState()
9+
data object Importing : ImportExportState()
10+
data object FinishedImport : ImportExportState()
11+
data class Error(val error: String) : ImportExportState()
12+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package io.github.sds100.keymapper.backup
2+
3+
import android.content.Intent
4+
import android.os.Bundle
5+
import androidx.activity.ComponentActivity
6+
import androidx.activity.compose.setContent
7+
import androidx.activity.viewModels
8+
import androidx.compose.foundation.layout.Spacer
9+
import androidx.compose.foundation.layout.width
10+
import androidx.compose.material3.Text
11+
import androidx.compose.material3.TextButton
12+
import androidx.compose.runtime.getValue
13+
import androidx.compose.ui.Modifier
14+
import androidx.compose.ui.platform.LocalContext
15+
import androidx.compose.ui.res.pluralStringResource
16+
import androidx.compose.ui.res.stringResource
17+
import androidx.compose.ui.unit.dp
18+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
19+
import io.github.sds100.keymapper.MainActivity
20+
import io.github.sds100.keymapper.R
21+
import io.github.sds100.keymapper.ServiceLocator
22+
import io.github.sds100.keymapper.compose.KeyMapperTheme
23+
import io.github.sds100.keymapper.util.ui.compose.CustomDialogContent
24+
25+
class RestoreKeyMapsActivity : ComponentActivity() {
26+
27+
private val viewModel by viewModels<RestoreKeyMapsViewModel> {
28+
RestoreKeyMapsViewModel.Factory(
29+
useCase = BackupRestoreMappingsUseCaseImpl(
30+
fileAdapter = ServiceLocator.fileAdapter(this),
31+
backupManager = ServiceLocator.backupManager(this),
32+
),
33+
resourceProvider = ServiceLocator.resourceProvider(this),
34+
)
35+
}
36+
37+
override fun onCreate(savedInstanceState: Bundle?) {
38+
super.onCreate(savedInstanceState)
39+
40+
setTheme(R.style.AppTheme_DialogActivity)
41+
42+
setFinishOnTouchOutside(true)
43+
44+
importKeyMaps(intent)
45+
intent = null
46+
47+
setContent {
48+
KeyMapperTheme {
49+
val state by viewModel.importExportState.collectAsStateWithLifecycle()
50+
51+
val title = when (val state = state) {
52+
ImportExportState.Idle -> stringResource(R.string.import_dialog_title_loading)
53+
is ImportExportState.ConfirmImport -> pluralStringResource(
54+
R.plurals.home_importing_dialog_title,
55+
state.keyMapCount,
56+
state.keyMapCount,
57+
)
58+
59+
is ImportExportState.Error -> stringResource(R.string.import_dialog_title_error)
60+
ImportExportState.FinishedImport -> stringResource(R.string.import_dialog_title_success)
61+
ImportExportState.Importing -> stringResource(R.string.import_dialog_title_importing)
62+
else -> ""
63+
}
64+
65+
val text = when (val state = state) {
66+
is ImportExportState.ConfirmImport -> stringResource(R.string.home_importing_dialog_text)
67+
else -> null
68+
}
69+
70+
val ctx = LocalContext.current
71+
72+
CustomDialogContent(
73+
title = title,
74+
text = text,
75+
confirmButton = {
76+
if (state is ImportExportState.FinishedImport) {
77+
TextButton(onClick = {
78+
finish()
79+
80+
Intent(ctx, MainActivity::class.java).apply {
81+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
82+
startActivity(this)
83+
}
84+
}) {
85+
Text(stringResource(R.string.import_dialog_button_launch_key_mapper))
86+
}
87+
} else if (state !is ImportExportState.Idle && state !is ImportExportState.ConfirmImport) {
88+
TextButton(onClick = { finish() }) {
89+
Text(stringResource(R.string.pos_done))
90+
}
91+
} else {
92+
TextButton(
93+
onClick = { viewModel.onConfirmImport(RestoreType.APPEND) },
94+
enabled = state is ImportExportState.ConfirmImport,
95+
) {
96+
Text(stringResource(R.string.home_importing_dialog_append))
97+
}
98+
}
99+
},
100+
dismissButton = {
101+
if (state is ImportExportState.FinishedImport) {
102+
TextButton(onClick = { finish() }) {
103+
Text(stringResource(R.string.home_importing_dialog_dismiss))
104+
}
105+
} else if (state is ImportExportState.Idle || state is ImportExportState.ConfirmImport) {
106+
TextButton(onClick = { finish() }) {
107+
Text(stringResource(R.string.home_importing_dialog_cancel))
108+
}
109+
110+
Spacer(Modifier.width(16.dp))
111+
112+
TextButton(
113+
onClick = { viewModel.onConfirmImport(RestoreType.REPLACE) },
114+
enabled = state is ImportExportState.ConfirmImport,
115+
) {
116+
Text(stringResource(R.string.home_importing_dialog_replace))
117+
}
118+
}
119+
},
120+
) { }
121+
}
122+
}
123+
}
124+
125+
private fun importKeyMaps(intent: Intent?) {
126+
intent ?: return
127+
128+
if (intent.action == Intent.ACTION_VIEW) {
129+
intent.data?.let {
130+
viewModel.onChooseImportFile(it.toString())
131+
132+
// Do not want to import again on a configuration change so set it to null
133+
this.intent = null
134+
}
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)