diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..3272c25
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+## 2.1.0
+
+- Add settings export/import
+- Improve Intent selector (multi-select, search)
+- Bump Gradle
diff --git a/app/build.gradle b/app/build.gradle
index eff24ff..0bad730 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -7,8 +7,8 @@ android {
minSdkVersion 21
compileSdkVersion 35
targetSdkVersion 35
- versionCode 19
- versionName "2.0.0"
+ versionCode 20
+ versionName "2.1.0"
archivesBaseName = "paperlaunch-v${defaultConfig.versionName}-${buildTime()}"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
@@ -60,6 +60,7 @@ dependencies {
implementation 'io.reactivex:rxandroid:1.1.0'
implementation 'io.reactivex:rxjava:1.1.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.android.support.constraint:constraint-layout:2.0.4'
testImplementation 'junit:junit:4.13.2'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index bf582af..7a0725f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -28,7 +28,8 @@
+ android:label="@string/activity_intentselector_label"
+ android:theme="@style/Theme.PaperLaunch.NoActionBar"/>
diff --git a/app/src/main/java/de/devmil/paperlaunch/service/LauncherOverlayService.kt b/app/src/main/java/de/devmil/paperlaunch/service/LauncherOverlayService.kt
index c75c198..b702e0a 100644
--- a/app/src/main/java/de/devmil/paperlaunch/service/LauncherOverlayService.kt
+++ b/app/src/main/java/de/devmil/paperlaunch/service/LauncherOverlayService.kt
@@ -174,8 +174,12 @@ class LauncherOverlayService : Service() {
}
private fun adaptState(forceReload: Boolean) {
+ if (forceReload) {
+ resetConfig()
+ resetData()
+ }
if (state.isActive) {
- ensureOverlayActive(forceReload)
+ ensureOverlayActive(false)
} else {
ensureOverlayInActive()
}
diff --git a/app/src/main/java/de/devmil/paperlaunch/storage/DataExporter.kt b/app/src/main/java/de/devmil/paperlaunch/storage/DataExporter.kt
new file mode 100644
index 0000000..bf5688e
--- /dev/null
+++ b/app/src/main/java/de/devmil/paperlaunch/storage/DataExporter.kt
@@ -0,0 +1,73 @@
+package de.devmil.paperlaunch.storage
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
+import android.util.Base64
+import com.google.gson.Gson
+import de.devmil.paperlaunch.model.IFolder
+import de.devmil.paperlaunch.model.Launch
+import de.devmil.paperlaunch.utils.BitmapUtils
+import de.devmil.paperlaunch.utils.IntentSerializer
+import java.io.ByteArrayOutputStream
+
+class DataExporter(private val context: Context) {
+
+ data class ExportData(
+ val version: Int = 1,
+ val entries: List
+ )
+
+ data class ExportEntry(
+ val type: String, // "folder" or "launch"
+ val name: String?,
+ val icon: String?, // Base64 encoded png
+ val intentUri: String?, // Only for launch
+ val entries: List? // Only for folder
+ )
+
+ fun exportToJson(): String {
+ var rootEntries: List = emptyList()
+ EntriesDataSource.instance.accessData(context, object : ITransactionAction {
+ override fun execute(transactionContext: ITransactionContext) {
+ val roots = transactionContext.loadRootContent()
+ rootEntries = roots.map { convertToExportEntry(it) }
+ }
+ })
+
+ val exportData = ExportData(entries = rootEntries)
+ return Gson().toJson(exportData)
+ }
+
+ private fun convertToExportEntry(entry: de.devmil.paperlaunch.model.IEntry): ExportEntry {
+ if (entry.isFolder) {
+ val folder = entry as IFolder
+ val subEntries = folder.subEntries?.map { convertToExportEntry(it) } ?: emptyList()
+ return ExportEntry(
+ type = "folder",
+ name = folder.name,
+ 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
+ intentUri = null,
+ entries = subEntries
+ )
+ } else {
+ val launch = entry as Launch
+ return ExportEntry(
+ type = "launch",
+ name = launch.name,
+ icon = encodeIcon(launch.dto.icon?.let { if (it is BitmapDrawable) it else null }),
+ intentUri = IntentSerializer.serialize(launch.dto.launchIntent),
+ entries = null
+ )
+ }
+ }
+
+ private fun encodeIcon(drawable: BitmapDrawable?): String? {
+ if (drawable == null) return null
+ val bitmap = drawable.bitmap ?: return null
+ val stream = ByteArrayOutputStream()
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
+ val byteArray = stream.toByteArray()
+ return Base64.encodeToString(byteArray, Base64.DEFAULT)
+ }
+}
diff --git a/app/src/main/java/de/devmil/paperlaunch/storage/DataImporter.kt b/app/src/main/java/de/devmil/paperlaunch/storage/DataImporter.kt
new file mode 100644
index 0000000..5f1ee53
--- /dev/null
+++ b/app/src/main/java/de/devmil/paperlaunch/storage/DataImporter.kt
@@ -0,0 +1,81 @@
+package de.devmil.paperlaunch.storage
+
+import android.content.Context
+import android.graphics.BitmapFactory
+import android.graphics.drawable.BitmapDrawable
+import android.util.Base64
+import com.google.gson.Gson
+import de.devmil.paperlaunch.utils.IntentSerializer
+import java.lang.Exception
+
+class DataImporter(private val context: Context) {
+
+ data class ExportData(
+ val version: Int = 1,
+ val entries: List
+ )
+
+ data class ExportEntry(
+ val type: String,
+ val name: String?,
+ val icon: String?,
+ val intentUri: String?,
+ val entries: List?
+ )
+
+ fun importFromJson(json: String) {
+ val data = Gson().fromJson(json, ExportData::class.java)
+
+ EntriesDataSource.instance.accessData(context, object : ITransactionAction {
+ override fun execute(transactionContext: ITransactionContext) {
+ // Clear existing data
+ transactionContext.clear()
+
+ // Restore data
+ data.entries.forEachIndexed { index, entry ->
+ restoreEntry(transactionContext, -1, entry, index, 0)
+ }
+ }
+ })
+ }
+
+ private fun restoreEntry(
+ transactionContext: ITransactionContext,
+ parentFolderId: Long,
+ entry: ExportEntry,
+ orderIndex: Int,
+ depth: Int
+ ) {
+ if (entry.type == "folder") {
+ val folder = transactionContext.createFolder(parentFolderId, orderIndex, depth)
+ folder.dto.name = entry.name
+ if (entry.icon != null) {
+ folder.dto.icon = decodeIcon(entry.icon)
+ }
+ transactionContext.updateFolderData(folder)
+
+ entry.entries?.forEachIndexed { index, childEntry ->
+ restoreEntry(transactionContext, folder.id, childEntry, index, depth + 1)
+ }
+ } else if (entry.type == "launch") {
+ val launch = transactionContext.createLaunch(parentFolderId, orderIndex)
+ launch.dto.name = entry.name
+ launch.dto.launchIntent = entry.intentUri?.let { IntentSerializer.deserialize(it) }
+ if (entry.icon != null) {
+ launch.dto.icon = decodeIcon(entry.icon)
+ }
+ transactionContext.updateLaunchData(launch)
+ }
+ }
+
+ private fun decodeIcon(base64: String): BitmapDrawable? {
+ return try {
+ val bytes = Base64.decode(base64, Base64.DEFAULT)
+ val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
+ BitmapDrawable(context.resources, bitmap)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
+}
diff --git a/app/src/main/java/de/devmil/paperlaunch/view/fragments/EditFolderFragment.kt b/app/src/main/java/de/devmil/paperlaunch/view/fragments/EditFolderFragment.kt
index f666181..1686902 100644
--- a/app/src/main/java/de/devmil/paperlaunch/view/fragments/EditFolderFragment.kt
+++ b/app/src/main/java/de/devmil/paperlaunch/view/fragments/EditFolderFragment.kt
@@ -103,6 +103,7 @@ class EditFolderFragment : Fragment() {
override fun onResume() {
super.onResume()
bottomSheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN
+ loadData()
}
@Deprecated("Deprecated in Java")
@@ -310,6 +311,7 @@ class EditFolderFragment : Fragment() {
intent.setClass(activity, IntentSelector::class.java)
intent.putExtra(IntentSelector.EXTRA_STRING_ACTIVITIES, resources.getString(R.string.folder_settings_add_app_activities))
intent.putExtra(IntentSelector.EXTRA_STRING_SHORTCUTS, resources.getString(R.string.folder_settings_add_app_shortcuts))
+ intent.putExtra(IntentSelector.EXTRA_ALLOW_MULTI_SELECT, true)
startActivityForResult(intent, REQUEST_ADD_APP)
}
@@ -331,7 +333,12 @@ class EditFolderFragment : Fragment() {
return
}
if(data != null) {
- addLaunch(data)
+ if (data.hasExtra(IntentSelector.EXTRA_RESULT_INTENTS)) {
+ val list = data.getParcelableArrayListExtra(IntentSelector.EXTRA_RESULT_INTENTS)
+ list?.let { addLaunches(it) }
+ } else {
+ addLaunch(data)
+ }
}
}
REQUEST_EDIT_FOLDER -> {
@@ -358,6 +365,27 @@ class EditFolderFragment : Fragment() {
notifyDataChanged()
}
+ private fun addLaunches(launchIntents: List) {
+ EntriesDataSource.instance.accessData(activity, object: ITransactionAction {
+ override fun execute(transactionContext: ITransactionContext) {
+ adapter?.let { itAdapter ->
+ val newEntries = ArrayList()
+ for (launchIntent in launchIntents) {
+ val l = transactionContext.createLaunch(folderId)
+ l.dto.launchIntent = launchIntent
+ transactionContext.updateLaunchData(l)
+ newEntries.add(l)
+ }
+ itAdapter.addEntries(newEntries)
+ folder?.let { itFolder ->
+ updateFolderImage(itFolder.dto, itAdapter.entries)
+ }
+ }
+ }
+ })
+ notifyDataChanged()
+ }
+
private fun updateFolderImage(folderDto: FolderDTO, entries: List) {
config?.let { itConfig ->
val imgWidth = itConfig.imageWidthDip
@@ -434,6 +462,12 @@ class EditFolderFragment : Fragment() {
notifyDataSetChanged()
}
+ fun addEntries(entries: List) {
+ mEntries.addAll(entries)
+ saveOrder()
+ notifyDataSetChanged()
+ }
+
override fun getPositionForId(id: Long): Int {
return mEntries.indices.firstOrNull { mEntries[it].entryId == id } ?: -1
}
diff --git a/app/src/main/java/de/devmil/paperlaunch/view/fragments/SettingsFragment.kt b/app/src/main/java/de/devmil/paperlaunch/view/fragments/SettingsFragment.kt
index 3c9b898..3f3ece7 100644
--- a/app/src/main/java/de/devmil/paperlaunch/view/fragments/SettingsFragment.kt
+++ b/app/src/main/java/de/devmil/paperlaunch/view/fragments/SettingsFragment.kt
@@ -31,13 +31,20 @@ import de.devmil.paperlaunch.R
import de.devmil.paperlaunch.config.LauncherGravity
import de.devmil.paperlaunch.config.UserSettings
import de.devmil.paperlaunch.service.LauncherOverlayService
+import de.devmil.paperlaunch.storage.DataExporter
+import de.devmil.paperlaunch.storage.DataImporter
import de.devmil.paperlaunch.view.preferences.SeekBarPreference
+import android.content.Intent
+import android.app.Activity
+import android.widget.Toast
class SettingsFragment : PreferenceFragment() {
private var userSettings: UserSettings? = null
private var activationParametersChangedListener: (() -> Unit)? = null
+ private val executor = java.util.concurrent.Executors.newSingleThreadExecutor()
+
@Deprecated("Deprecated in Java")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -51,6 +58,66 @@ class SettingsFragment : PreferenceFragment() {
addActivationSettings(context, screen)
addAppearanceSettings(context, screen)
+ addBackupSettings(context, screen)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ executor.shutdown()
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (resultCode == Activity.RESULT_OK && data != null && data.data != null) {
+ val uri = data.data!!
+ if (requestCode == REQUEST_CODE_EXPORT) {
+ executor.execute {
+ val context = activity
+ if (context == null || context.isFinishing) {
+ return@execute
+ }
+ try {
+ val json = DataExporter(context).exportToJson()
+ context.contentResolver.openOutputStream(uri)?.use { output ->
+ output.write(json.toByteArray())
+ }
+ context.runOnUiThread {
+ Toast.makeText(context, R.string.fragment_settings_backup_export_success, Toast.LENGTH_SHORT).show()
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ context.runOnUiThread {
+ Toast.makeText(context, R.string.fragment_settings_backup_export_error, Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ } else if (requestCode == REQUEST_CODE_IMPORT) {
+ executor.execute {
+ val context = activity
+ if (context == null || context.isFinishing) {
+ return@execute
+ }
+ try {
+ val json = context.contentResolver.openInputStream(uri)?.use { input ->
+ input.bufferedReader().use { it.readText() }
+ }
+ if (json != null) {
+ DataImporter(context).importFromJson(json)
+ context.runOnUiThread {
+ Toast.makeText(context, R.string.fragment_settings_backup_import_success, Toast.LENGTH_SHORT).show()
+ LauncherOverlayService.notifyDataChanged(context)
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ context.runOnUiThread {
+ Toast.makeText(context, R.string.fragment_settings_backup_import_error, Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ }
+ }
}
fun setOnActivationParametersChangedListener(listener: () -> Unit) {
@@ -304,4 +371,49 @@ class SettingsFragment : PreferenceFragment() {
}
}
}
+
+ private fun addBackupSettings(context: Context, screen: PreferenceScreen) {
+ val backupCategory = PreferenceCategory(context)
+ screen.addPreference(backupCategory)
+
+ backupCategory.isPersistent = false
+ backupCategory.setTitle(R.string.fragment_settings_category_backup_title)
+
+ val exportPreference = Preference(context)
+ backupCategory.addPreference(exportPreference)
+ exportPreference.setTitle(R.string.fragment_settings_backup_export_title)
+ exportPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ type = "application/json"
+ putExtra(Intent.EXTRA_TITLE, "paperlaunch_backup.json")
+ }
+ startActivityForResult(intent, REQUEST_CODE_EXPORT)
+ true
+ }
+
+ val importPreference = Preference(context)
+ backupCategory.addPreference(importPreference)
+ importPreference.setTitle(R.string.fragment_settings_backup_import_title)
+ importPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+ android.app.AlertDialog.Builder(activity)
+ .setTitle(R.string.fragment_settings_backup_import_warning_title)
+ .setMessage(R.string.fragment_settings_backup_import_warning_message)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ type = "application/json"
+ }
+ startActivityForResult(intent, REQUEST_CODE_IMPORT)
+ }
+ .setNegativeButton(android.R.string.cancel, null)
+ .show()
+ true
+ }
+ }
+
+ companion object {
+ private const val REQUEST_CODE_EXPORT = 101
+ private const val REQUEST_CODE_IMPORT = 102
+ }
}
diff --git a/app/src/main/java/de/devmil/paperlaunch/view/utils/IntentApplicationEntry.kt b/app/src/main/java/de/devmil/paperlaunch/view/utils/IntentApplicationEntry.kt
index 550b147..f4ac294 100644
--- a/app/src/main/java/de/devmil/paperlaunch/view/utils/IntentApplicationEntry.kt
+++ b/app/src/main/java/de/devmil/paperlaunch/view/utils/IntentApplicationEntry.kt
@@ -22,7 +22,10 @@ import android.content.pm.PackageManager.NameNotFoundException
import android.content.pm.ResolveInfo
import android.graphics.drawable.Drawable
import java.lang.ref.WeakReference
-import java.util.*
+import java.util.ArrayList
+import java.util.Collections
+import java.util.Comparator
+import java.util.Locale
class IntentApplicationEntry @Throws(NameNotFoundException::class)
constructor(context: Context, packageName: String) : Comparable {
@@ -35,6 +38,7 @@ constructor(context: Context, packageName: String) : Comparable()
@@ -43,8 +47,8 @@ constructor(context: Context, packageName: String) : Comparable? = null
init {
-
name = context.packageManager.getApplicationLabel(appInfo)
+ nameLowercase = name.toString().lowercase(Locale.getDefault())
}
fun addResolveInfo(info: ResolveInfo, intentType: IntentType) {
@@ -164,6 +168,9 @@ constructor(context: Context, packageName: String) : Comparable = WeakReference(context)
diff --git a/app/src/main/java/de/devmil/paperlaunch/view/utils/IntentSelector.kt b/app/src/main/java/de/devmil/paperlaunch/view/utils/IntentSelector.kt
index 3e11568..9cb244c 100644
--- a/app/src/main/java/de/devmil/paperlaunch/view/utils/IntentSelector.kt
+++ b/app/src/main/java/de/devmil/paperlaunch/view/utils/IntentSelector.kt
@@ -16,6 +16,11 @@
package de.devmil.paperlaunch.view.utils
import android.app.Activity
+import android.support.v7.app.AppCompatActivity
+import android.support.v7.widget.Toolbar
+import android.support.v7.widget.SearchView
+import android.view.Menu
+import android.view.MenuItem
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
@@ -32,7 +37,7 @@ import de.devmil.paperlaunch.R
import java.lang.ref.WeakReference
import java.util.*
-class IntentSelector : Activity() {
+class IntentSelector : AppCompatActivity(), SearchView.OnQueryTextListener {
private var llWait: LinearLayout? = null
private var lvActivities: ExpandableListView? = null
@@ -52,11 +57,6 @@ class IntentSelector : Activity() {
@Deprecated("Deprecated in Java")
override fun doInBackground(vararg params: Unit?) {
- //this approach can kill the PackageManager if there are too many apps installed
- // List shortcutResolved = getPackageManager().queryIntentActivities(shortcutIntent, PackageManager.GET_ACTIVITIES | PackageManager.GET_INTENT_FILTERS);
- // List mainResolved = getPackageManager().queryIntentActivities(mainIntent, PackageManager.GET_ACTIVITIES | PackageManager.GET_INTENT_FILTERS);
- // List launcherResolved = getPackageManager().queryIntentActivities(launcherIntent, PackageManager.GET_ACTIVITIES | PackageManager.GET_INTENT_FILTERS);
-
intentSelectorRef.get()?.let {
val pm = it.packageManager
@@ -64,75 +64,64 @@ class IntentSelector : Activity() {
val mainResolved = ArrayList()
val launcherResolved = ArrayList()
- val appInfos = pm.getInstalledApplications(PackageManager.GET_META_DATA)
-
val showAll = it.chkShowAllActivities!!.isChecked
- for (appInfo in appInfos) {
- if(isCancelled || isObsolete) {
- return
- }
- val shortcutIntent = Intent(Intent.ACTION_CREATE_SHORTCUT)
- shortcutIntent.`package` = appInfo.packageName
-
- val appShortcutResolved = pm.queryIntentActivities(shortcutIntent, PackageManager.GET_META_DATA)
- shortcutResolved.addAll(appShortcutResolved)
+ // Instead of getInstalledApplications, we query intents directly for better performance
- var addMainActivities = true
-
- if (showAll) {
- try {
- val pi = pm.getPackageInfo(appInfo.packageName, PackageManager.GET_ACTIVITIES or PackageManager.GET_INTENT_FILTERS)
- pi.activities?.let { it ->
- for (ai in it) {
- val ri = ResolveInfo()
- ri.activityInfo = ai
-
- mainResolved.add(ri)
- }
-
- }
-
- addMainActivities = false
- } catch (e: Exception) {
- }
+ // 1. Launcher Activities (Always needed)
+ val launcherIntent = Intent(Intent.ACTION_MAIN)
+ launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER)
+ try {
+ launcherResolved.addAll(pm.queryIntentActivities(launcherIntent, 0))
+ } catch (e: Exception) {
+ Log.e(TAG, "Error querying launcher activities", e)
+ }
- }
+ if (isCancelled || isObsolete) return
- if (addMainActivities) {
- val mainIntent = Intent(Intent.ACTION_MAIN)
- mainIntent.`package` = appInfo.packageName
+ // 2. Shortcuts (Always needed)
+ val shortcutIntent = Intent(Intent.ACTION_CREATE_SHORTCUT)
+ try {
+ shortcutResolved.addAll(pm.queryIntentActivities(shortcutIntent, 0))
+ } catch (e: Exception) {
+ Log.e(TAG, "Error querying shortcut activities", e)
+ }
- val appMainResolved = pm.queryIntentActivities(mainIntent, PackageManager.GET_META_DATA)
- mainResolved.addAll(appMainResolved)
+ if (isCancelled || isObsolete) return
+
+ // 3. Main Activities (Only if showAll is true)
+ // Note: Querying ACTION_MAIN without category can be huge, but if user requests it...
+ // We can optimize by iterating launcherResolved/shortcutResolved to find packages?
+ // Or just query only if checked.
+ if (showAll) {
+ val mainIntent = Intent(Intent.ACTION_MAIN)
+ try {
+ mainResolved.addAll(pm.queryIntentActivities(mainIntent, 0))
+ } catch (e: Exception) {
+ Log.e(TAG, "Error querying main activities", e)
}
+ }
- val launcherIntent = Intent(Intent.ACTION_MAIN)
- launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER)
- launcherIntent.`package` = appInfo.packageName
-
- val appLauncherResolved = pm.queryIntentActivities(launcherIntent, PackageManager.GET_META_DATA)
- launcherResolved.addAll(appLauncherResolved)
+ // Process Launcher Activities
+ for (ri in launcherResolved) {
+ if (isCancelled || isObsolete) return
+ addResolveInfo(ri, IntentApplicationEntry.IntentType.Launcher, false, entries)
}
+ // Process Shortcut Activities
for (ri in shortcutResolved) {
- if(isCancelled || isObsolete) {
- return
- }
- addResolveInfo(ri, IntentApplicationEntry.IntentType.Shortcut, false, entries)
- }
- for (ri in mainResolved) {
- if(isCancelled || isObsolete) {
- return
- }
- addResolveInfo(ri, IntentApplicationEntry.IntentType.Main, showAll, entries)
+ if (isCancelled || isObsolete) return
+ addResolveInfo(ri, IntentApplicationEntry.IntentType.Shortcut, false, entries)
}
- for (ri in launcherResolved) {
- if(isCancelled || isObsolete) {
- return
+
+ // Process Main Activities
+ if (showAll) {
+ for (ri in mainResolved) {
+ if (isCancelled || isObsolete) return
+ addResolveInfo(ri, IntentApplicationEntry.IntentType.Main, true, entries)
}
- addResolveInfo(ri, IntentApplicationEntry.IntentType.Launcher, false, entries)
}
+
//sort
val comparator = Comparator { object1, object2 -> object1.compareTo(object2) }
entries.sortWith(comparator)
@@ -158,6 +147,9 @@ class IntentSelector : Activity() {
localIntentSelector.lvActivities!!.setAdapter(localIntentSelector.adapterActivities)
localIntentSelector.lvShortcuts!!.setAdapter(localIntentSelector.adapterShortcuts)
+
+ localIntentSelector.adapterActivities?.filter(localIntentSelector.mCurrentQuery)
+ localIntentSelector.adapterShortcuts?.filter(localIntentSelector.mCurrentQuery)
}
if(!isAnotherSearchRunning) {
localIntentSelector.llWait!!.visibility = View.GONE
@@ -196,15 +188,19 @@ class IntentSelector : Activity() {
private var intentSelectorRef: WeakReference = WeakReference(intentSelector)
}
-
private var mSearchTask : SearchTask? = null
+ private var adapterActivities: IntentSelectorAdapter? = null
+ private var adapterShortcuts: IntentSelectorAdapter? = null
- internal var adapterActivities: IntentSelectorAdapter? = null
- internal var adapterShortcuts: IntentSelectorAdapter? = null
+ private var fabDone: android.support.design.widget.FloatingActionButton? = null
+ private var allowMultiSelect: Boolean = false
+ private val selectedIntents = ArrayList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ allowMultiSelect = intent.getBooleanExtra(EXTRA_ALLOW_MULTI_SELECT, false)
+
if(mSearchTask != null) {
mSearchTask!!.cancel(true)
mSearchTask = null
@@ -231,26 +227,50 @@ class IntentSelector : Activity() {
txtShortcuts = findViewById(R.id.common__intentSelector_txtShortcuts)
toolbar = findViewById(R.id.common__intentSelector_toolbar)
- setActionBar(toolbar)
+ setSupportActionBar(toolbar)
txtShortcuts!!.text = shortcutText
lvActivities!!.setOnChildClickListener { _, _, groupPosition, childPosition, _ ->
- val resultIntent = Intent(Intent.ACTION_MAIN)
val entry = adapterActivities!!.getChild(groupPosition, childPosition) as IntentApplicationEntry.IntentItem
+ val resultIntent = Intent(Intent.ACTION_MAIN)
resultIntent.setClassName(entry.packageName, entry.activityName)
- setResultIntent(resultIntent)
- true
+
+ if (allowMultiSelect) {
+ toggleSelection(resultIntent)
+ adapterActivities!!.notifyDataSetChanged()
+ true
+ } else {
+ setResultIntent(resultIntent)
+ true
+ }
}
chkShowAllActivities!!.setOnCheckedChangeListener { _, _ -> startSearch() }
lvShortcuts!!.setOnChildClickListener { _, _, groupPosition, childPosition, _ ->
- val shortcutIntent = Intent(Intent.ACTION_CREATE_SHORTCUT)
val entry = adapterShortcuts!!.getChild(groupPosition, childPosition) as IntentApplicationEntry.IntentItem
+ val shortcutIntent = Intent(Intent.ACTION_CREATE_SHORTCUT)
shortcutIntent.setClassName(entry.packageName, entry.activityName)
startActivityForResult(shortcutIntent, CREATE_SHORTCUT_REQUEST)
false
}
+ fabDone = findViewById(R.id.common__intentSelector_fab)
+ if (allowMultiSelect) {
+ fabDone!!.show()
+ fabDone!!.setOnClickListener {
+ val resultIntent = Intent()
+ resultIntent.putParcelableArrayListExtra(EXTRA_RESULT_INTENTS, selectedIntents)
+ setResult(Activity.RESULT_OK, resultIntent)
+ finish()
+ }
+ // Keep tabs but maybe hide shortcuts for multiselect if not supported?
+ // Assuming shortcuts are not supported in multi-select for now or handled same way?
+ // Existing logic launches helper activity for shortcuts. This breaks multi-select flow unless handled.
+ // For now, let's just make Multi-Select work for Apps which is the performance killer.
+ } else {
+ fabDone!!.hide()
+ }
+
val tabs = this.findViewById(android.R.id.tabhost)
tabs.setup()
val tspecActivities = tabs.newTabSpec(activitiesLabel)
@@ -273,6 +293,27 @@ class IntentSelector : Activity() {
startSearch()
}
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.menu_intent_selector, menu)
+ val searchItem = menu.findItem(R.id.action_search)
+ val searchView = searchItem.actionView as SearchView
+ searchView.setOnQueryTextListener(this)
+ return true
+ }
+
+ override fun onQueryTextSubmit(query: String?): Boolean {
+ return false
+ }
+
+ private var mCurrentQuery: String? = null
+
+ override fun onQueryTextChange(newText: String?): Boolean {
+ mCurrentQuery = newText
+ adapterActivities?.filter(newText)
+ adapterShortcuts?.filter(newText)
+ return true
+ }
+
private fun startSearch() {
if (mSearchTask != null) {
mSearchTask!!.isAnotherSearchRunning = true
@@ -300,13 +341,65 @@ class IntentSelector : Activity() {
super.onActivityResult(requestCode, resultCode, data)
}
+ private fun toggleSelection(intent: Intent) {
+ // Simple check based on component name
+ val component = intent.component ?: return
+ val existing = selectedIntents.find { it.component == component }
+ if (existing != null) {
+ selectedIntents.remove(existing)
+ } else {
+ selectedIntents.add(intent)
+ }
+ updateTitleInternal()
+ }
+
+ private fun updateTitleInternal() {
+ if (!allowMultiSelect) return
+ if (selectedIntents.size > 0) {
+ title = getString(R.string.activity_intentselector_title_count, selectedIntents.size)
+ } else {
+ title = getString(R.string.activity_intentselector_label)
+ }
+ }
+
+ private fun isSelected(intent: Intent): Boolean {
+ val component = intent.component ?: return false
+ return selectedIntents.any { it.component == component }
+ }
+
internal class IntentSelectorAdapter(private val context: Context, entriesList: List, private val intentType: IntentApplicationEntry.IntentType) : BaseExpandableListAdapter() {
- private val entries: MutableList
+ private var entries: MutableList
+ private val originalEntries: List
init {
- this.entries = entriesList
+ this.originalEntries = entriesList
.filter { getSubList(it).isNotEmpty() }
- .toMutableList()
+ this.entries = this.originalEntries.toMutableList()
+ }
+
+ fun filter(query: String?) {
+ entries.clear()
+ if (query.isNullOrEmpty()) {
+ entries.addAll(originalEntries)
+ } else {
+ val q = query.lowercase(Locale.getDefault())
+ for (entry in originalEntries) {
+ var matches = false
+ if (entry.nameLowercase.contains(q)) {
+ matches = true
+ } else {
+ val subList = getSubList(entry)
+ if (subList.any { it.displayNameLowercase.contains(q) }) {
+ matches = true
+ }
+ }
+
+ if (matches) {
+ entries.add(entry)
+ }
+ }
+ }
+ notifyDataSetChanged()
}
fun getSubList(entry: IntentApplicationEntry): List {
@@ -329,9 +422,21 @@ class IntentSelector : Activity() {
}
val txt = effectiveConvertView!!.findViewById(R.id.common__intentselectoritem_text)
val txtActivityName = effectiveConvertView.findViewById(R.id.common__intentselectoritem_activityName)
+ val checkbox = effectiveConvertView.findViewById(R.id.common__intentselectoritem_checkbox)
+
+ val item = getSubList(entries[groupPosition])[childPosition]
+ txt.text = item.displayName
+ txtActivityName.text = item.activityName
+
+ if ((context as IntentSelector).allowMultiSelect && intentType == IntentApplicationEntry.IntentType.Main) {
+ checkbox.visibility = View.VISIBLE
+ val testIntent = Intent(Intent.ACTION_MAIN)
+ testIntent.setClassName(item.packageName, item.activityName)
+ checkbox.isChecked = context.isSelected(testIntent)
+ } else {
+ checkbox.visibility = View.GONE
+ }
- txt.text = getSubList(entries[groupPosition])[childPosition].displayName
- txtActivityName.text = getSubList(entries[groupPosition])[childPosition].activityName
return effectiveConvertView
}
@@ -387,6 +492,8 @@ class IntentSelector : Activity() {
var EXTRA_SHORTCUT_TEXT = "de.devmil.common.extras.SHORTCUT_TEXT"
var EXTRA_STRING_SHORTCUTS = "de.devmil.common.extras.STRING_SHORTCUTS"
var EXTRA_STRING_ACTIVITIES = "de.devmil.common.extras.STRING_ACTIVITIES"
+ var EXTRA_ALLOW_MULTI_SELECT = "de.devmil.common.extras.ALLOW_MULTI_SELECT"
+ var EXTRA_RESULT_INTENTS = "de.devmil.common.extras.RESULT_INTENTS"
private fun getSubList(entry: IntentApplicationEntry, intentType: IntentApplicationEntry.IntentType): List {
when (intentType) {
diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml
new file mode 100644
index 0000000..4c630a8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_check.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/common__intentselectorgroup.xml b/app/src/main/res/layout/common__intentselectorgroup.xml
index 06e96c9..05dbff2 100644
--- a/app/src/main/res/layout/common__intentselectorgroup.xml
+++ b/app/src/main/res/layout/common__intentselectorgroup.xml
@@ -12,7 +12,8 @@
android:layout_width="fill_parent"
android:layout_height="45dip"
android:text="@string/common__intentselectorgroup_text_default"
- android:gravity="center_vertical|end"
+ android:gravity="center_vertical|start"
+ android:layout_marginStart="16dp"
android:paddingStart="5dip"
android:paddingEnd="5dip"
android:textStyle="bold"
diff --git a/app/src/main/res/layout/common__intentselectoritem.xml b/app/src/main/res/layout/common__intentselectoritem.xml
index 9528db8..2b87ffd 100644
--- a/app/src/main/res/layout/common__intentselectoritem.xml
+++ b/app/src/main/res/layout/common__intentselectoritem.xml
@@ -1,21 +1,54 @@
-
-
-
-
-
-
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:padding="8dp"
+ android:minHeight="?android:attr/listPreferredItemHeight">
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/common__intentselectorview.xml b/app/src/main/res/layout/common__intentselectorview.xml
index 67aedf8..f31555f 100644
--- a/app/src/main/res/layout/common__intentselectorview.xml
+++ b/app/src/main/res/layout/common__intentselectorview.xml
@@ -6,7 +6,7 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/toolbar_appcompat.xml b/app/src/main/res/layout/toolbar_appcompat.xml
new file mode 100644
index 0000000..6a8ad66
--- /dev/null
+++ b/app/src/main/res/layout/toolbar_appcompat.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/app/src/main/res/menu/menu_intent_selector.xml b/app/src/main/res/menu/menu_intent_selector.xml
new file mode 100644
index 0000000..5ecde58
--- /dev/null
+++ b/app/src/main/res/menu/menu_intent_selector.xml
@@ -0,0 +1,10 @@
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 731c38b..7a93c23 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -93,4 +93,14 @@
Show activation hint
Activation hint is shown
Activation hint is not shown
+ Backup & Restore
+ Export Configuration
+ Import Configuration
+ Export successful
+ Export failed
+ Import successful
+ Import failed
+ Overwrite Data?
+ This will overwrite your current configuration. Are you sure?
+ Select App (%d)
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index e1924bd..0047714 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -8,4 +8,14 @@
- @color/theme_primary_light
+
+
diff --git a/build.gradle b/build.gradle
index 9a1e5ef..df9881f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -8,7 +8,7 @@ buildscript {
google()
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.10.1'
+ classpath 'com.android.tools.build:gradle:8.13.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index e9a6ca5..96bb16c 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip