Skip to content

Commit 9c294c2

Browse files
devmilCopilot
andauthored
Add settings ex-/import and Intent selector improvements (#28)
* Add settings ex-/import improve Intent selector (multi-select, search) bump Gradle * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * review remark: lowercase performance * review remark: threading * version bump + changelog --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 13ba4dd commit 9c294c2

20 files changed

+619
-103
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## 2.1.0
2+
3+
- Add settings export/import
4+
- Improve Intent selector (multi-select, search)
5+
- Bump Gradle

app/build.gradle

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ android {
77
minSdkVersion 21
88
compileSdkVersion 35
99
targetSdkVersion 35
10-
versionCode 19
11-
versionName "2.0.0"
10+
versionCode 20
11+
versionName "2.1.0"
1212
archivesBaseName = "paperlaunch-v${defaultConfig.versionName}-${buildTime()}"
1313
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
1414
}
@@ -60,6 +60,7 @@ dependencies {
6060
implementation 'io.reactivex:rxandroid:1.1.0'
6161
implementation 'io.reactivex:rxjava:1.1.0'
6262
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
63+
implementation 'com.google.code.gson:gson:2.10.1'
6364
implementation 'com.android.support.constraint:constraint-layout:2.0.4'
6465

6566
testImplementation 'junit:junit:4.13.2'

app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
</activity>
2929
<activity
3030
android:name=".view.utils.IntentSelector"
31-
android:label="@string/activity_intentselector_label" />
31+
android:label="@string/activity_intentselector_label"
32+
android:theme="@style/Theme.PaperLaunch.NoActionBar"/>
3233
<activity
3334
android:name=".view.utils.UrlSelector"
3435
android:label="@string/activity_urlselector_label" />

app/src/main/java/de/devmil/paperlaunch/service/LauncherOverlayService.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,12 @@ class LauncherOverlayService : Service() {
174174
}
175175

176176
private fun adaptState(forceReload: Boolean) {
177+
if (forceReload) {
178+
resetConfig()
179+
resetData()
180+
}
177181
if (state.isActive) {
178-
ensureOverlayActive(forceReload)
182+
ensureOverlayActive(false)
179183
} else {
180184
ensureOverlayInActive()
181185
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package de.devmil.paperlaunch.storage
2+
3+
import android.content.Context
4+
import android.graphics.Bitmap
5+
import android.graphics.drawable.BitmapDrawable
6+
import android.util.Base64
7+
import com.google.gson.Gson
8+
import de.devmil.paperlaunch.model.IFolder
9+
import de.devmil.paperlaunch.model.Launch
10+
import de.devmil.paperlaunch.utils.BitmapUtils
11+
import de.devmil.paperlaunch.utils.IntentSerializer
12+
import java.io.ByteArrayOutputStream
13+
14+
class DataExporter(private val context: Context) {
15+
16+
data class ExportData(
17+
val version: Int = 1,
18+
val entries: List<ExportEntry>
19+
)
20+
21+
data class ExportEntry(
22+
val type: String, // "folder" or "launch"
23+
val name: String?,
24+
val icon: String?, // Base64 encoded png
25+
val intentUri: String?, // Only for launch
26+
val entries: List<ExportEntry>? // Only for folder
27+
)
28+
29+
fun exportToJson(): String {
30+
var rootEntries: List<ExportEntry> = emptyList()
31+
EntriesDataSource.instance.accessData(context, object : ITransactionAction {
32+
override fun execute(transactionContext: ITransactionContext) {
33+
val roots = transactionContext.loadRootContent()
34+
rootEntries = roots.map { convertToExportEntry(it) }
35+
}
36+
})
37+
38+
val exportData = ExportData(entries = rootEntries)
39+
return Gson().toJson(exportData)
40+
}
41+
42+
private fun convertToExportEntry(entry: de.devmil.paperlaunch.model.IEntry): ExportEntry {
43+
if (entry.isFolder) {
44+
val folder = entry as IFolder
45+
val subEntries = folder.subEntries?.map { convertToExportEntry(it) } ?: emptyList()
46+
return ExportEntry(
47+
type = "folder",
48+
name = folder.name,
49+
icon = encodeIcon(folder.icon?.let { if (it is BitmapDrawable) it else null }), // Only encode if it's a BitmapDrawable (custom icon), otherwise null implies default
50+
intentUri = null,
51+
entries = subEntries
52+
)
53+
} else {
54+
val launch = entry as Launch
55+
return ExportEntry(
56+
type = "launch",
57+
name = launch.name,
58+
icon = encodeIcon(launch.dto.icon?.let { if (it is BitmapDrawable) it else null }),
59+
intentUri = IntentSerializer.serialize(launch.dto.launchIntent),
60+
entries = null
61+
)
62+
}
63+
}
64+
65+
private fun encodeIcon(drawable: BitmapDrawable?): String? {
66+
if (drawable == null) return null
67+
val bitmap = drawable.bitmap ?: return null
68+
val stream = ByteArrayOutputStream()
69+
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
70+
val byteArray = stream.toByteArray()
71+
return Base64.encodeToString(byteArray, Base64.DEFAULT)
72+
}
73+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package de.devmil.paperlaunch.storage
2+
3+
import android.content.Context
4+
import android.graphics.BitmapFactory
5+
import android.graphics.drawable.BitmapDrawable
6+
import android.util.Base64
7+
import com.google.gson.Gson
8+
import de.devmil.paperlaunch.utils.IntentSerializer
9+
import java.lang.Exception
10+
11+
class DataImporter(private val context: Context) {
12+
13+
data class ExportData(
14+
val version: Int = 1,
15+
val entries: List<ExportEntry>
16+
)
17+
18+
data class ExportEntry(
19+
val type: String,
20+
val name: String?,
21+
val icon: String?,
22+
val intentUri: String?,
23+
val entries: List<ExportEntry>?
24+
)
25+
26+
fun importFromJson(json: String) {
27+
val data = Gson().fromJson(json, ExportData::class.java)
28+
29+
EntriesDataSource.instance.accessData(context, object : ITransactionAction {
30+
override fun execute(transactionContext: ITransactionContext) {
31+
// Clear existing data
32+
transactionContext.clear()
33+
34+
// Restore data
35+
data.entries.forEachIndexed { index, entry ->
36+
restoreEntry(transactionContext, -1, entry, index, 0)
37+
}
38+
}
39+
})
40+
}
41+
42+
private fun restoreEntry(
43+
transactionContext: ITransactionContext,
44+
parentFolderId: Long,
45+
entry: ExportEntry,
46+
orderIndex: Int,
47+
depth: Int
48+
) {
49+
if (entry.type == "folder") {
50+
val folder = transactionContext.createFolder(parentFolderId, orderIndex, depth)
51+
folder.dto.name = entry.name
52+
if (entry.icon != null) {
53+
folder.dto.icon = decodeIcon(entry.icon)
54+
}
55+
transactionContext.updateFolderData(folder)
56+
57+
entry.entries?.forEachIndexed { index, childEntry ->
58+
restoreEntry(transactionContext, folder.id, childEntry, index, depth + 1)
59+
}
60+
} else if (entry.type == "launch") {
61+
val launch = transactionContext.createLaunch(parentFolderId, orderIndex)
62+
launch.dto.name = entry.name
63+
launch.dto.launchIntent = entry.intentUri?.let { IntentSerializer.deserialize(it) }
64+
if (entry.icon != null) {
65+
launch.dto.icon = decodeIcon(entry.icon)
66+
}
67+
transactionContext.updateLaunchData(launch)
68+
}
69+
}
70+
71+
private fun decodeIcon(base64: String): BitmapDrawable? {
72+
return try {
73+
val bytes = Base64.decode(base64, Base64.DEFAULT)
74+
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
75+
BitmapDrawable(context.resources, bitmap)
76+
} catch (e: Exception) {
77+
e.printStackTrace()
78+
null
79+
}
80+
}
81+
}

app/src/main/java/de/devmil/paperlaunch/view/fragments/EditFolderFragment.kt

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class EditFolderFragment : Fragment() {
103103
override fun onResume() {
104104
super.onResume()
105105
bottomSheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN
106+
loadData()
106107
}
107108

108109
@Deprecated("Deprecated in Java")
@@ -310,6 +311,7 @@ class EditFolderFragment : Fragment() {
310311
intent.setClass(activity, IntentSelector::class.java)
311312
intent.putExtra(IntentSelector.EXTRA_STRING_ACTIVITIES, resources.getString(R.string.folder_settings_add_app_activities))
312313
intent.putExtra(IntentSelector.EXTRA_STRING_SHORTCUTS, resources.getString(R.string.folder_settings_add_app_shortcuts))
314+
intent.putExtra(IntentSelector.EXTRA_ALLOW_MULTI_SELECT, true)
313315

314316
startActivityForResult(intent, REQUEST_ADD_APP)
315317
}
@@ -331,7 +333,12 @@ class EditFolderFragment : Fragment() {
331333
return
332334
}
333335
if(data != null) {
334-
addLaunch(data)
336+
if (data.hasExtra(IntentSelector.EXTRA_RESULT_INTENTS)) {
337+
val list = data.getParcelableArrayListExtra<Intent>(IntentSelector.EXTRA_RESULT_INTENTS)
338+
list?.let { addLaunches(it) }
339+
} else {
340+
addLaunch(data)
341+
}
335342
}
336343
}
337344
REQUEST_EDIT_FOLDER -> {
@@ -358,6 +365,27 @@ class EditFolderFragment : Fragment() {
358365
notifyDataChanged()
359366
}
360367

368+
private fun addLaunches(launchIntents: List<Intent>) {
369+
EntriesDataSource.instance.accessData(activity, object: ITransactionAction {
370+
override fun execute(transactionContext: ITransactionContext) {
371+
adapter?.let { itAdapter ->
372+
val newEntries = ArrayList<IEntry>()
373+
for (launchIntent in launchIntents) {
374+
val l = transactionContext.createLaunch(folderId)
375+
l.dto.launchIntent = launchIntent
376+
transactionContext.updateLaunchData(l)
377+
newEntries.add(l)
378+
}
379+
itAdapter.addEntries(newEntries)
380+
folder?.let { itFolder ->
381+
updateFolderImage(itFolder.dto, itAdapter.entries)
382+
}
383+
}
384+
}
385+
})
386+
notifyDataChanged()
387+
}
388+
361389
private fun updateFolderImage(folderDto: FolderDTO, entries: List<IEntry>) {
362390
config?.let { itConfig ->
363391
val imgWidth = itConfig.imageWidthDip
@@ -434,6 +462,12 @@ class EditFolderFragment : Fragment() {
434462
notifyDataSetChanged()
435463
}
436464

465+
fun addEntries(entries: List<IEntry>) {
466+
mEntries.addAll(entries)
467+
saveOrder()
468+
notifyDataSetChanged()
469+
}
470+
437471
override fun getPositionForId(id: Long): Int {
438472
return mEntries.indices.firstOrNull { mEntries[it].entryId == id } ?: -1
439473
}

0 commit comments

Comments
 (0)