diff --git a/build.gradle.kts b/build.gradle.kts
index e31667a..c3773e6 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -6,7 +6,7 @@ buildscript{
dependencies {
- classpath("com.android.tools.build:gradle:7.1.2")
+ classpath("com.android.tools.build:gradle:7.2.2")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21")
// NOTE: Do not place your application dependencies here; they belong
diff --git a/customization_example/customizer_pkg/PARAM.INI b/customization_example/customizer_pkg/PARAM.INI
index 07e9006..d5ddc25 100644
--- a/customization_example/customizer_pkg/PARAM.INI
+++ b/customization_example/customizer_pkg/PARAM.INI
@@ -1,22 +1,4 @@
-[CROSS_LAUNCHER_CUSTOMIZER]
-; Type of the customization, it can be SYSTEM, PLUGIN, GAME or APPS
-; SYSTEM : launcher's built-in icons
-; PLUGIN : Plugins
-; GAME or APPS : Android Game or Application (they behave the same)
-TYPE=GAME
+[PKG_METADATA]
+Name=
-; ID of the customized item icon
-; for Android apps / games, it's usually "app.packageName_activityName"
-; The activity name will have it's prefix removed in case the prefix is equals to app packageName
-; e.g "id.psw.vshlauncher_activities.XMB" (This app), "jp.co.bandainamcoent.BNEI0242_stage.StageUnityPlayerActivity" (iMAS CGSS)
-; for plugins, check the plugin icon customization guide, or find their icon ID
-TITLE_ID=id.psw.vshlauncher_activities.XMB
-
-; Author of the customization content
-; The one (or the corporation / company) who made the content, e.g Icons, Animated Icons, Backdrop, Background Sound
-AUTHOR=Icons by EmiyaSyahriel, Background Sound by PSW Software
-
-; Author of the package
-PACKAGER=EmiyaSyahriel
-
-;
\ No newline at end of file
+[P01]
diff --git a/customization_example/customizer_pkg/PARAM.INI.README b/customization_example/customizer_pkg/PARAM.INI.README
new file mode 100644
index 0000000..ff119f9
--- /dev/null
+++ b/customization_example/customizer_pkg/PARAM.INI.README
@@ -0,0 +1,40 @@
+[PKG_METADATA]
+; Title of this package file
+TITLE=CrossLauncher custom package
+
+; Author of the customization content
+; The one (or the corporation / company) who made the content, e.g Icons, Animated Icons, Backdrop, Background Sound
+AUTHOR=Icons by EmiyaSyahriel, Background Sound by PSW Software
+
+; Author of the package
+PACKAGER=EmiyaSyahriel
+
+; If present, Only installable if any of these apps is installed, separated by semicolon (';')
+CHECK_INSTALL=id.psw.crosslauncher;id.psw.xl
+
+; Main icon of this package file
+ICON=pkg/pkg_icon.png
+
+; Folder name, case sensitive
+[P01]
+
+; Type of the customization, it can be SYSTEM, PLUGIN, GAME or APPS
+; SYSTEM : launcher's built-in icons
+; PLUGIN : Plugins
+; GAME or APPS : Android Game or Application (they behave the same)
+TYPE=GAME
+
+; ID of the customized item icon
+; for Android apps / games, it's usually "app.packageName_activityName"
+; The activity name will have it's prefix removed in case the prefix is equals to app packageName
+; e.g "id.psw.vshlauncher_activities.XMB" (This app), "jp.co.bandainamcoent.BNEI0242_stage.StageUnityPlayerActivity" (iMAS CGSS)
+; for plugins, check the plugin icon customization guide, or find their icon ID
+TITLE_ID=id.psw.vshlauncher_activities.XMB
+
+; Where to unpack this folder's files, depending on specified type:
+; SYSTEM : relative to CrossLauncher's Virtual PS3 File System
+; PLUGIN : (fsroot)/home/00000000/plugins/(TITLE_ID)
+; GAME/APPS : (fsroot)/games/(TITLE_ID)
+; (fsroot) = CrossLauncher
+ROOT=/
+
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 006d150..b6a5a9d 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Thu Jan 13 08:34:12 ICT 2022
distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
diff --git a/launcher_app/build.gradle.kts b/launcher_app/build.gradle.kts
index 7dfb262..92dee68 100644
--- a/launcher_app/build.gradle.kts
+++ b/launcher_app/build.gradle.kts
@@ -8,7 +8,7 @@ plugins{
}
android {
- compileSdk = 32
+ compileSdk = 33
// buildToolVersion = "30.0.2"
diff --git a/launcher_app/src/main/AndroidManifest.xml b/launcher_app/src/main/AndroidManifest.xml
index 7b8b3b9..b18a412 100644
--- a/launcher_app/src/main/AndroidManifest.xml
+++ b/launcher_app/src/main/AndroidManifest.xml
@@ -97,6 +97,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
= Build.VERSION_CODES.Q) {
+ Log.d("SHORTCUT", "Getting shortcuts...")
+ val apl = getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
+ if(apl.hasShortcutHostPermission()){
+ for(p in apl.profiles){
+ val q = LauncherApps.ShortcutQuery()
+ val ss = apl.getShortcuts(q, p)
+ if( ss != null){
+ for(s in ss){
+ Log.d("SHORTCUT", "${s.id} - ${s.`package`} - ${s.intent} - ${s.activity?.packageName}")
+ }
+ }
+ }
+ }
+ } else {
+ TODO("VERSION.SDK_INT < LOLLIPOP")
+ }
+
+ threadPool.execute {
+ val h = addLoadHandle()
+ val c = categories.find { it.id == ITEM_CATEGORY_SHORTCUT }!!
+
+ for(i in c.content){
+ if(i.icon != XMBItem.TRANSPARENT_BITMAP){
+ i.icon.recycle()
+ }
+ }
+
+ c.content.clear()
+
+ val paths = getAllPathsFor(VshBaseDirs.USER_DIR, "shortcuts")
+ for(path in paths){
+ if(path.exists()){
+ val inis = path.listFiles { a, b ->
+ b.endsWith("ini", true)
+ }
+ if(inis != null){
+ for(ini in inis){
+ if(ini.exists()){
+ c.content.add(XMBShortcutItem(vsh, ini))
+ }
+ }
+ }
+ }
+ }
+
+ if(c.content.isNotEmpty()){
+ if(hiddenCategories.contains(ITEM_CATEGORY_SHORTCUT)){
+ hiddenCategories.remove(ITEM_CATEGORY_SHORTCUT)
+ }
+ }else{
+ hiddenCategories.add(ITEM_CATEGORY_SHORTCUT)
+ }
+
+ setLoadingFinished(h)
+ }
+}
\ No newline at end of file
diff --git a/launcher_app/src/main/java/id/psw/vshlauncher/VSH.kt b/launcher_app/src/main/java/id/psw/vshlauncher/VSH.kt
index 726718a..2afce81 100644
--- a/launcher_app/src/main/java/id/psw/vshlauncher/VSH.kt
+++ b/launcher_app/src/main/java/id/psw/vshlauncher/VSH.kt
@@ -9,7 +9,9 @@ import android.graphics.Typeface
import android.media.AudioManager
import android.media.MediaPlayer
import android.media.SoundPool
+import android.net.Uri
import android.os.*
+import android.provider.Settings
import android.util.Log
import androidx.annotation.DrawableRes
import androidx.core.content.res.ResourcesCompat
@@ -47,6 +49,7 @@ class VSH : Application(), ServiceConnection {
const val ITEM_CATEGORY_APPS = "vsh_apps"
const val ITEM_CATEGORY_GAME = "vsh_game"
const val ITEM_CATEGORY_VIDEO = "vsh_video"
+ const val ITEM_CATEGORY_SHORTCUT = "vsh_shortcut"
const val ITEM_CATEGORY_MUSIC = "vsh_music"
const val ITEM_CATEGORY_SETTINGS = "vsh_settings"
const val COPY_DATA_SIZE_BUFFER = 10240
@@ -130,7 +133,7 @@ class VSH : Application(), ServiceConnection {
val notifications = arrayListOf()
val threadPool: ExecutorService = Executors.newFixedThreadPool(8)
val loadingHandles = arrayListOf()
- private val hiddenCategories = arrayListOf(ITEM_CATEGORY_MUSIC, ITEM_CATEGORY_VIDEO)
+ val hiddenCategories = arrayListOf(ITEM_CATEGORY_MUSIC, ITEM_CATEGORY_VIDEO)
val bgmPlayer = MediaPlayer()
val systemBgmPlayer = MediaPlayer()
@@ -170,6 +173,7 @@ class VSH : Application(), ServiceConnection {
listInstalledIconPlugins()
listInstalledWaveRenderPlugins()
reloadAppList()
+ reloadShortcutList()
fillSettingsCategory()
loadSfxData()
addHomeScreen()
@@ -237,6 +241,9 @@ class VSH : Application(), ServiceConnection {
selectedCategoryId = items[cIdx].id
xmbView?.state?.itemMenu?.selectedIndex = 0
vsh.playSfx(SFXType.Selection)
+
+ xmbView?.state?.crossMenu?.verticalMenu?.nameTextXOffset = 0.0f
+ xmbView?.state?.crossMenu?.verticalMenu?.descTextXOffset = 0.0f
}
}
@@ -271,6 +278,9 @@ class VSH : Application(), ServiceConnection {
selectedItemId = items[cIdx].id
}
+ xmbView?.state?.crossMenu?.verticalMenu?.nameTextXOffset = 0.0f
+ xmbView?.state?.crossMenu?.verticalMenu?.descTextXOffset = 0.0f
+
// Update hovering
items.forEach {
it.isHovered = it.id == selectedItemId
@@ -342,6 +352,7 @@ class VSH : Application(), ServiceConnection {
categories.add(XMBItemCategory(this, ITEM_CATEGORY_HOME, R.string.category_home, R.drawable.category_home, defaultSortIndex = 0))
categories.add(XMBItemCategory(this, ITEM_CATEGORY_SETTINGS, R.string.category_settings, R.drawable.category_setting, defaultSortIndex = 1))
categories.add(XMBItemCategory(this, ITEM_CATEGORY_VIDEO, R.string.category_videos, R.drawable.category_video, true, defaultSortIndex = 2))
+ categories.add(XMBItemCategory(this, ITEM_CATEGORY_SHORTCUT, R.string.category_shortcut, R.drawable.category_shortcut, true, defaultSortIndex = 2))
categories.add(XMBItemCategory(this, ITEM_CATEGORY_MUSIC, R.string.category_music, R.drawable.category_music, true, defaultSortIndex = 3))
categories.add(XMBItemCategory(this, ITEM_CATEGORY_GAME, R.string.category_games, R.drawable.category_games, true, defaultSortIndex = 4))
categories.add(XMBItemCategory(this, ITEM_CATEGORY_APPS, R.string.category_apps, R.drawable.category_apps, true, defaultSortIndex = 5))
@@ -420,14 +431,6 @@ class VSH : Application(), ServiceConnection {
}
}
- fun saveCustomShortcut(intent: Intent) {
-
- }
-
- fun installShortcut(intent: Intent) {
-
- }
-
fun restart() {
val pm = vsh.packageManager
val sndi = pm.getLaunchIntentForPackage(vsh.packageName)
@@ -444,4 +447,12 @@ class VSH : Application(), ServiceConnection {
NativeGL.destroy()
super.onTerminate()
}
+
+ fun showAppInfo(app: XMBAppItem) {
+ val i = Intent()
+ i.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
+ i.data = Uri.fromParts("package", app.resInfo.activityInfo.applicationInfo.packageName, null)
+ i.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ startActivity(i)
+ }
}
diff --git a/launcher_app/src/main/java/id/psw/vshlauncher/activities/XMB.kt b/launcher_app/src/main/java/id/psw/vshlauncher/activities/XMB.kt
index bf3cb8e..29ebb15 100644
--- a/launcher_app/src/main/java/id/psw/vshlauncher/activities/XMB.kt
+++ b/launcher_app/src/main/java/id/psw/vshlauncher/activities/XMB.kt
@@ -2,12 +2,14 @@ package id.psw.vshlauncher.activities
import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
+import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.PointF
+import android.hardware.input.InputManager
import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
@@ -18,6 +20,7 @@ import android.view.*
import androidx.core.content.ContextCompat
import id.psw.vshlauncher.*
import id.psw.vshlauncher.submodules.GamepadSubmodule
+import id.psw.vshlauncher.types.items.XMBAppItem
import id.psw.vshlauncher.views.VshViewPage
import id.psw.vshlauncher.views.XmbView
import id.psw.vshlauncher.views.dialogviews.UITestDialogView
@@ -38,6 +41,8 @@ class XMB : AppCompatActivity() {
super.onCreate(savedInstanceState)
readPreferences()
+ XMBAppItem.showHiddenByConfig = false // To make sure
+
if(vsh.useInternalWave){
setContentView(R.layout.layout_xmb)
xmbView = findViewById(R.id.xmb_view)
@@ -71,6 +76,9 @@ class XMB : AppCompatActivity() {
isShareIntent(intent) -> {
showShareIntentDialog(intent)
}
+ vsh.isXPKGIntent(intent) -> {
+ vsh.showInstallPkgDialog(intent)
+ }
intent.action == Consts.ACTION_WAVE_SETTINGS_WIZARD -> {
vsh.showXMBLiveWallpaperWizard()
}
diff --git a/launcher_app/src/main/java/id/psw/vshlauncher/submodules/GamepadSubmodule.kt b/launcher_app/src/main/java/id/psw/vshlauncher/submodules/GamepadSubmodule.kt
index 0919b63..25012f3 100644
--- a/launcher_app/src/main/java/id/psw/vshlauncher/submodules/GamepadSubmodule.kt
+++ b/launcher_app/src/main/java/id/psw/vshlauncher/submodules/GamepadSubmodule.kt
@@ -202,8 +202,8 @@ class GamepadSubmodule(ctx: VSH) {
if(remap.map.containsKey(key)) {
return remap.map[key]!!
}else{
- val kbdRemap = keyRemaps[ANDROID_KEYBOARD]
- if(kbdRemap.map.containsKey(key)) return kbdRemap.map[key] ?: Key.None
+ val kbdRemap = defaultAndroidKeyboardMap
+ if(kbdRemap.containsKey(key)) return kbdRemap[key] ?: Key.None
}
}
return Key.None
diff --git a/launcher_app/src/main/java/id/psw/vshlauncher/types/ExternalXMBItemMenu.kt b/launcher_app/src/main/java/id/psw/vshlauncher/types/ExternalXMBItemMenu.kt
index 3ee7aa7..0887752 100644
--- a/launcher_app/src/main/java/id/psw/vshlauncher/types/ExternalXMBItemMenu.kt
+++ b/launcher_app/src/main/java/id/psw/vshlauncher/types/ExternalXMBItemMenu.kt
@@ -40,14 +40,14 @@ class ExternalXMBItemMenu() : Parcelable {
override fun describeContents(): Int = 0
- override fun writeToParcel(parcel: Parcel?, flags: Int) {
- parcel?.writeString(XMB_MENU_HEADER)
- parcel?.writeInt(baseId)
- parcel?.writeInt(menuId)
- parcel?.writeString(packageId)
- parcel?.writeString(XMB_MENU_METADATA_SEPARATOR)
- parcel?.writeString(displayName)
- parcel?.writeByteBoolean(isDisabled)
- parcel?.writeByteBoolean(isSeparator)
+ override fun writeToParcel(parcel: Parcel, flags: Int) {
+ parcel.writeString(XMB_MENU_HEADER)
+ parcel.writeInt(baseId)
+ parcel.writeInt(menuId)
+ parcel.writeString(packageId)
+ parcel.writeString(XMB_MENU_METADATA_SEPARATOR)
+ parcel.writeString(displayName)
+ parcel.writeByteBoolean(isDisabled)
+ parcel.writeByteBoolean(isSeparator)
}
}
\ No newline at end of file
diff --git a/launcher_app/src/main/java/id/psw/vshlauncher/types/INIFile.kt b/launcher_app/src/main/java/id/psw/vshlauncher/types/INIFile.kt
index eab6b44..31b331c 100644
--- a/launcher_app/src/main/java/id/psw/vshlauncher/types/INIFile.kt
+++ b/launcher_app/src/main/java/id/psw/vshlauncher/types/INIFile.kt
@@ -1,26 +1,28 @@
package id.psw.vshlauncher.types
import java.io.File
-import java.io.IOException
+import java.io.OutputStreamWriter
-class INIFile(private var src:String, is_path:Boolean) {
+class INIFile() {
- init {
- if(is_path){
- val f = File(src)
- src = f.readText(Charsets.UTF_8)
- }
+ private var _path = ""
+ val path get()=_path
- parse(src)
- }
+ private var sections = mutableMapOf>()
- private lateinit var sections : Map>
+ fun write(path:String?){
+ val p = path ?: _path
- fun writeToFile(path:String){
- val f = File(path)
+ val f = File(p)
if(!f.exists()) f.createNewFile()
val o = f.outputStream().writer(Charsets.UTF_8)
+ write(o)
+ o.flush()
+ o.close()
+ }
+
+ fun write(o : OutputStreamWriter){
for(sect in sections){
o.write("[${sect.key}]\n")
for(keys in sect.value){
@@ -29,19 +31,34 @@ class INIFile(private var src:String, is_path:Boolean) {
o.write("\n")
o.flush()
}
- o.flush()
- o.close()
}
- private fun parse(src:String){
+ fun parseFile(path: String?){
+ val p = path ?: _path
+
+ val f = File(p)
+ parse(f.readText(Charsets.UTF_8))
+
+ if(path != null){
+ _path = path
+ }
+ }
+
+
+ fun parse(src:String){
+ for(cSect in sections){
+ cSect.value.clear()
+ }
+ sections.clear()
+
val lines = src.lines()
- val sects = mutableMapOf>()
+ val sects = mutableMapOf>()
var sname = ""
var sect1 = mutableMapOf()
fun pushPrevious(){
if(sname.isNotEmpty()){
- sects[sname] = sect1.toMap()
+ sects[sname] = sect1
}
sect1 = mutableMapOf()
}
@@ -51,7 +68,7 @@ class INIFile(private var src:String, is_path:Boolean) {
val line = u_line.trim()
if(line.startsWith('[') && line.endsWith(']')){
pushPrevious()
- sname = line.substring(1, line.length - 2)
+ sname = line.substring(1, line.length - 1)
}else if(line.length > 3 && line.contains('=', ignoreCase = true)){
val spl = line.split('=', ignoreCase = true, limit = 2)
if(spl.size >= 2){
@@ -64,14 +81,29 @@ class INIFile(private var src:String, is_path:Boolean) {
sections = sects
}
+ fun remove(section:String, key:String) = sections[section]?.remove(key)
+ fun count(section: String) : Int = sections[section]?.size ?: -1
+ fun count() : Int = sections.size
+
+
+ operator fun get(sect:String, key:String): String? {
+ return sections[sect]?.get(key)
+ }
+
+ operator fun set(section: String, key:String, value: String){
+ if(!sections.containsKey(section)){
+ sections[section] = mutableMapOf()
+ }
+
+ sections[section]?.set(key, value)
+ }
+
fun hasKey(section: String, key:String) : Boolean =
if(sections.containsKey(section)) sections[section]?.containsKey(key) == true else false
- fun get(section: String, key:String, defVal:String) : String = sections[section]?.get(key) ?: defVal
-
fun get(section: String, key:String, defVal: T, cnv: (String) -> T) : T {
return if(hasKey(section, key))
- cnv(get(section, key, ""))
+ cnv(this[section, key] ?: "")
else defVal
}
}
\ No newline at end of file
diff --git a/launcher_app/src/main/java/id/psw/vshlauncher/types/XMBShortcutInfo.kt b/launcher_app/src/main/java/id/psw/vshlauncher/types/XMBShortcutInfo.kt
new file mode 100644
index 0000000..d90b9b2
--- /dev/null
+++ b/launcher_app/src/main/java/id/psw/vshlauncher/types/XMBShortcutInfo.kt
@@ -0,0 +1,144 @@
+package id.psw.vshlauncher.types
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.LauncherApps
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.os.Build
+import android.os.Parcel
+import android.os.UserHandle
+import android.util.Base64
+import androidx.core.graphics.drawable.toBitmap
+import id.psw.vshlauncher.VSH
+import id.psw.vshlauncher.select
+import java.io.File
+
+class XMBShortcutInfo {
+
+ companion object {
+ const val INI_TYPE = "CrossLauncher.ShortcutInfo"
+ const val INI_ID = "ID"
+ const val INI_NAME = "SNAME"
+ const val INI_LNAME = "LNAME"
+ const val INI_PKG = "PACKAGE"
+ const val INI_ENABLED = "BOOTABLE"
+ const val INI_DISABLED_MSG = "NBOOTMSG"
+ const val INI_HANDLE = "USER"
+
+ const val DEF_ID = "ko.id!"
+ const val DEF_NAME = "Unknown"
+ const val DEF_LNAME = "Unknown Shortcut"
+ const val DEF_PKG = "id.psw.vshlauncher"
+ const val DEF_DISABLED_MSG = "???"
+ const val DEF_HANDLE = ""
+ }
+
+ var icon : Bitmap = XMBItem.TRANSPARENT_BITMAP
+ var id : String = DEF_ID
+ var name : String = DEF_NAME
+ var longName : String = DEF_LNAME
+ var packageName : String = DEF_PKG
+ var enabled : Boolean = false
+ var disabledMsg : String = DEF_DISABLED_MSG
+ var userHandle : UserHandle? = null
+ var pinItem : LauncherApps.PinItemRequest? = null
+
+ private val ini = INIFile()
+
+ constructor(vsh: VSH, intent: Intent){
+ var isNextGen = false
+
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
+ if(intent.action!!.equals(LauncherApps.ACTION_CONFIRM_PIN_SHORTCUT, true)){
+ val lcher = vsh.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
+ val _pinItem = lcher.getPinItemRequest(intent)
+
+ val shInfo = _pinItem.shortcutInfo
+ if(shInfo != null){
+ icon = lcher
+ .getShortcutIconDrawable(shInfo, vsh.resources.displayMetrics.densityDpi)
+ .toBitmap(100,100)
+ id = shInfo.id
+ name = shInfo.shortLabel?.toString() ?: name
+ longName = shInfo.longLabel?.toString() ?: longName
+ packageName = shInfo.activity?.toShortString() ?: packageName
+ enabled = shInfo.isEnabled
+ disabledMsg = shInfo.disabledMessage?.toString() ?: DEF_DISABLED_MSG
+ userHandle = shInfo.userHandle
+ isNextGen = true
+ }
+
+ pinItem = _pinItem
+ }
+ }
+
+
+ // Use old Android static shortcut installation
+ if(!isNextGen){
+ if(intent.action!!.equals(Intent.ACTION_CREATE_SHORTCUT, true)){
+ name = intent.getStringExtra(Intent.EXTRA_SHORTCUT_NAME) ?: name
+ icon = intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON) ?: XMBItem.WHITE_BITMAP
+ packageName = intent.component?.toShortString() ?: packageName
+ }
+ }
+ }
+
+ constructor(vsh:VSH, iniPath: File){
+ ini.parseFile(iniPath.absolutePath)
+ id = ini[INI_TYPE, INI_ID] ?: DEF_ID
+ name = ini[INI_TYPE, INI_NAME] ?: DEF_NAME
+ longName = ini[INI_TYPE, INI_LNAME] ?: DEF_LNAME
+ packageName = ini[INI_TYPE, INI_PKG] ?: DEF_PKG
+ enabled = ini[INI_TYPE, INI_ENABLED] == "true"
+ disabledMsg = ini[INI_TYPE, INI_DISABLED_MSG] ?: DEF_DISABLED_MSG
+ val uHandleStr = ini[INI_TYPE, INI_HANDLE] ?: ""
+
+ if(uHandleStr.length > 0){
+ val p = Parcel.obtain()
+ val dat = Base64.decode(uHandleStr, Base64.DEFAULT)
+ p.writeByteArray(dat)
+ p.setDataPosition(0)
+ userHandle = UserHandle.readFromParcel(p)
+ p.recycle()
+ }
+
+ val iconFile = File(iniPath.parent, "${iniPath.nameWithoutExtension}.png")
+ if(iconFile.exists()){
+ icon = BitmapFactory.decodeFile(iconFile.absolutePath)
+ }
+ }
+
+ fun write(iniPath: File){
+ ini[INI_TYPE, INI_ID] = id
+ ini[INI_TYPE, INI_NAME] = name
+ ini[INI_TYPE, INI_LNAME] = longName
+ ini[INI_TYPE, INI_PKG] = packageName
+ ini[INI_TYPE, INI_DISABLED_MSG] = disabledMsg
+ ini[INI_TYPE, INI_ENABLED] = enabled.select("true","false")
+
+ val h = userHandle
+ var uHandleStr = ""
+ if(h != null){
+ val p = Parcel.obtain()
+ val len = p.dataSize()
+ val ba = ByteArray(len)
+ h.writeToParcel(p, 0)
+ p.setDataPosition(0)
+ p.readByteArray(ba)
+ uHandleStr = Base64.encodeToString(ba, Base64.DEFAULT)
+ p.recycle()
+ }
+
+ ini[INI_TYPE, INI_HANDLE] = uHandleStr
+
+ val iconFile = File(iniPath.parent, "${iniPath.nameWithoutExtension}.png")
+
+ if(!iconFile.exists()){
+ iconFile.createNewFile()
+ }
+ icon.compress(Bitmap.CompressFormat.PNG, 100, iconFile.outputStream())
+
+ ini.write(iniPath.absolutePath)
+ }
+}
\ No newline at end of file
diff --git a/launcher_app/src/main/java/id/psw/vshlauncher/types/XPKGFile.kt b/launcher_app/src/main/java/id/psw/vshlauncher/types/XPKGFile.kt
new file mode 100644
index 0000000..c7cb586
--- /dev/null
+++ b/launcher_app/src/main/java/id/psw/vshlauncher/types/XPKGFile.kt
@@ -0,0 +1,27 @@
+package id.psw.vshlauncher.types
+
+import java.util.zip.ZipFile
+
+class XPKGFile(zip:ZipFile) {
+
+ companion object {
+ const val INI_PKG_METADATA = "PKG_METADATA"
+ const val INI_CUSTOMIZATION = "CUSTOMIZATION"
+ }
+
+ val fileNames = arrayListOf()
+ val info = INIFile()
+
+ init {
+ val ie = zip.entries()
+ while(ie.hasMoreElements()){
+ val e = ie.nextElement()
+ fileNames.add(e.name)
+
+ if(e.name.equals("PARAM.INI", true)){
+ val zis = zip.getInputStream(e)
+ info.parse(zis.reader(Charsets.UTF_8).readText())
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/launcher_app/src/main/java/id/psw/vshlauncher/types/items/XMBAppItem.kt b/launcher_app/src/main/java/id/psw/vshlauncher/types/items/XMBAppItem.kt
index cefa97e..9e0ec39 100644
--- a/launcher_app/src/main/java/id/psw/vshlauncher/types/items/XMBAppItem.kt
+++ b/launcher_app/src/main/java/id/psw/vshlauncher/types/items/XMBAppItem.kt
@@ -3,16 +3,28 @@ package id.psw.vshlauncher.types.items
import android.app.ActivityManager
import android.content.Context
import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
+import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory
+import android.os.Build
import id.psw.vshlauncher.*
+import id.psw.vshlauncher.types.INIFile
import id.psw.vshlauncher.types.Ref
import id.psw.vshlauncher.types.XMBItem
import id.psw.vshlauncher.types.sequentialimages.*
import id.psw.vshlauncher.views.bootInto
+import id.psw.vshlauncher.views.dialogviews.AppInfoDialogView
+import id.psw.vshlauncher.views.showDialog
import java.io.File
import java.lang.StringBuilder
+import android.text.format.DateFormat
+import id.psw.vshlauncher.views.asBytes
+import java.text.SimpleDateFormat
+import java.util.*
+import kotlin.collections.ArrayList
class XMBAppItem(private val vsh: VSH, val resInfo : ResolveInfo) : XMBItem(vsh) {
enum class DescriptionDisplay {
@@ -25,18 +37,28 @@ class XMBAppItem(private val vsh: VSH, val resInfo : ResolveInfo) : XMBItem(vsh)
companion object {
private const val TAG = "XMBAppItem"
+ const val ENABLE_EMBEDDED_MEDIA = false
+ const val INI_KEY_TYPE = "CrossLauncher.AppInfo"
+ const val INI_KEY_TITLE = "TITLE"
+ const val INI_KEY_ALBUM = "ALBUM"
+ const val INI_KEY_CATEGORY = "CATEGORY"
+ const val INI_KEY_BOOTABLE = "BOOTABLE"
+ const val INI_KEY_SUBTITLE = "SUBTITLE"
+
var descriptionDisplay : DescriptionDisplay = DescriptionDisplay.PackageName
var disableAnimatedIcon = false
var disableBackSound = false
var disableBackdrop = false
var disableBackdropOverlay = false
+ var showHiddenByConfig = false
private val ios = mutableMapOf>()
private val ioc = mutableMapOf>()
}
+ private var _customAppDesc: String =""
private var _icon = TRANSPARENT_BITMAP
private var hasIconLoaded = false
@@ -113,6 +135,7 @@ class XMBAppItem(private val vsh: VSH, val resInfo : ResolveInfo) : XMBItem(vsh)
private var _backdropSync = Object()
private var _portBackdropSync = Object()
private var _backSoundSync = Object()
+ private var iniFile = INIFile()
private fun MutableMap>.getOrMake(k:File, refDefVal:K) = getOrMake>(k){ Ref(refDefVal) }
override val isIconLoaded: Boolean get()= hasIconLoaded
@@ -147,10 +170,42 @@ class XMBAppItem(private val vsh: VSH, val resInfo : ResolveInfo) : XMBItem(vsh)
it.delayedExistenceCheck(ioc.getOrMake(it, 0), ios.getOrMake(it, false))
}
override val hasMenu: Boolean get() = true
+ private var _customAppLabel = ""
+ private var _appAlbum = ""
+ private var _appCategory = ""
+
+ var appCustomLabel : String
+ get() = _customAppLabel
+ set(value) {
+ _customAppLabel = value
+ writeAppConfig()
+ }
+
+
+ var appCustomDesc: String
+ get() = _customAppDesc
+ set(value) {
+ _customAppDesc = value
+ writeAppConfig()
+ }
+
+ var appAlbum : String
+ get() = _appAlbum
+ set(value) {
+ _appAlbum = value
+ writeAppConfig()
+ }
+
+ var appCategory : String
+ get() = _appCategory
+ set(value) {
+ _appCategory = value
+ writeAppConfig()
+ }
override val id: String get()= iconId
override val description: String get()= displayedDescription
- override val displayName: String get()= appLabel
+ override val displayName: String get()= _customAppLabel.isEmpty().select(appLabel, _customAppLabel)
override val icon: Bitmap get()= synchronized(_icon) { _icon }
override val backdrop: Bitmap get() = _backdrop
override val backSound: File get() = _backSound
@@ -158,27 +213,226 @@ class XMBAppItem(private val vsh: VSH, val resInfo : ResolveInfo) : XMBItem(vsh)
override val hasDescription: Boolean get() = description.isNotEmpty()
override val menuItems: ArrayList = arrayListOf()
private lateinit var apkFile : File
+ private lateinit var pkgInfo : PackageInfo
+ private lateinit var apkSplits : Array
+ private lateinit var externalResource : Resources
+
+ private val latestApkSplit : File get() {
+ return if(apkSplits.size > 1){
+ apkSplits.maxByOrNull { it.lastModified() } ?: apkFile
+ }else{
+ apkFile
+ }
+ }
+
+ val sortUpdateTime get() = if(latestApkSplit.exists()) latestApkSplit.lastModified().toString() else "0"
+ val displayUpdateTime : String get() {
+ return if(apkFile.exists()){
+ val fmt = DateFormat.is24HourFormat(vsh).select( "d/M/yyyy k:m", "d/M/yyyy h:m a")
+ val sdf =
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
+ SimpleDateFormat(fmt, vsh.resources.configuration.locales.get(0))
+ }else{
+ SimpleDateFormat(fmt, vsh.resources.configuration.locale)
+ }
+ sdf.format(latestApkSplit.lastModified())
+ }else{
+ "Unknown"
+ }
+ }
- val sortUpdateTime get() = if(apkFile.exists()) apkFile.lastModified().toString() else "0"
- val fileSize get() = if(apkFile.exists()) apkFile.length().toString() else "0"
+ val fileSize : String get() {
+ var l = 0L
+ if(apkSplits.size > 1){
+ for(apk in apkSplits){
+ l += apk.length()
+ }
+ }else{
+ l += apkFile.length()
+ }
+ return l.asBytes()
+ }
+
+ val version : String get() = pkgInfo.versionName
private val isSystemApp : Boolean get() {
return resInfo.activityInfo.applicationInfo.flags hasFlag (ApplicationInfo.FLAG_UPDATED_SYSTEM_APP or ApplicationInfo.FLAG_SYSTEM)
}
+ private var _isHidden = false
+
+ override val isHidden: Boolean
+ get() = showHiddenByConfig.select(false, _isHidden)
+
+ val isHiddenByCfg get()= _isHidden
+
+ fun hide(hide: Boolean){
+ _isHidden = hide
+ writeAppConfig()
+ }
+
+ val packageName get() = pkgInfo.packageName
+
+ private fun readAppConfig(){
+ val files = requestCustomizationFiles("PARAM.INI")
+ val validFile = files.firstOrNull { it.exists() }
+ if(validFile != null){
+ iniFile.parseFile(validFile.absolutePath)
+ }
+
+ _customAppLabel = iniFile[INI_KEY_TYPE, INI_KEY_TITLE] ?: ""
+
+ if(_customAppLabel.isEmpty()){
+ forEveryLocale {
+ if(_customAppLabel.isEmpty()){
+ _customAppLabel = iniFile[INI_KEY_TYPE, "$INI_KEY_TITLE-$it"] ?: ""
+ }
+ }
+ }
+
+ _customAppDesc = iniFile[INI_KEY_TYPE, INI_KEY_SUBTITLE] ?: ""
+
+ if(_customAppDesc.isEmpty()){
+ forEveryLocale {
+ if(_customAppDesc.isEmpty()){
+ _customAppDesc = iniFile[INI_KEY_TYPE, "$INI_KEY_SUBTITLE-$it"] ?: ""
+ }
+ }
+ }
+
+ _isHidden = (iniFile[INI_KEY_TYPE, INI_KEY_BOOTABLE] ?: "true") == "false"
+ _appAlbum = iniFile[INI_KEY_TYPE, INI_KEY_ALBUM] ?: ""
+ _appCategory = iniFile[INI_KEY_TYPE, INI_KEY_CATEGORY] ?: ""
+ }
+
+ private fun forEveryLocale(act: ((String) -> Unit)){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
+ val locList = vsh.resources.configuration.locales
+ val locCount = locList.size()
+ for(i in 0 .. locCount){
+ val loc = locList[i]
+ if(loc != null){
+ act(loc.createName())
+ }
+ }
+ }else{
+ act(vsh.resources.configuration.locale.createName())
+ }
+ }
+
+ private fun Locale.createName() : String {
+ val sb = StringBuilder()
+
+ val data = arrayOf(language, country, variant, "")
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ data[3] = script
+ }
+
+ for(dat in data){
+ if(dat.isNotEmpty()){
+ if(sb.isNotEmpty()) sb.append("-")
+ sb.append(dat)
+ }
+ }
+
+ return sb.toString()
+ }
+
+ private fun writeAppConfig(){
+ iniFile[INI_KEY_TYPE, INI_KEY_TITLE] = _customAppLabel
+ iniFile[INI_KEY_TYPE, INI_KEY_SUBTITLE] = _customAppDesc
+ iniFile[INI_KEY_TYPE, INI_KEY_ALBUM] = _appAlbum
+ iniFile[INI_KEY_TYPE, INI_KEY_BOOTABLE] = _isHidden.select("false", "true")
+ iniFile[INI_KEY_TYPE, INI_KEY_CATEGORY] = _appCategory
+
+ if(iniFile.path.isEmpty()){
+ val haveCustomFolder = vsh.getAllPathsFor(VshBaseDirs.APPS_DIR, resInfo.uniqueActivityName).any { it.exists() }
+ if(!haveCustomFolder){
+ createAppCustomDirectory()
+ }
+
+ val files = requestCustomizationFiles("PARAM.INI")
+ val validFile = files.firstOrNull { it.exists() } ?: files[0]
+ if(!validFile.exists()) validFile.createNewFile()
+
+ iniFile.write(validFile.absolutePath)
+
+ }else{
+ iniFile.write(null)
+ }
+ }
+
+ private var embeddedIconId = 0
+ private var embeddedBackgroundId = 0
+ private var embeddedBackOverlayId = 0
+ private var embeddedBackSoundId = 0
+ private var embeddedAnimIconId = 0
+
init {
+ readAppConfig()
vsh.threadPool.execute {
apkFile = File(resInfo.activityInfo.applicationInfo.publicSourceDir)
+
+ pkgInfo = if(Build.VERSION.SDK_INT >= 33){
+ vsh.packageManager.getPackageInfo(
+ resInfo.activityInfo.applicationInfo.packageName,
+ PackageManager.PackageInfoFlags.of(0L)
+ )
+ }else{
+ vsh.packageManager.getPackageInfo(resInfo.activityInfo.applicationInfo.packageName, 0)
+ }
+
+ if(ENABLE_EMBEDDED_MEDIA){
+ externalResource = vsh.packageManager.getResourcesForApplication(pkgInfo.packageName)
+ embeddedIconId = externalResource.getIdentifier("vsh_icon", "drawable", pkgInfo.packageName)
+ embeddedBackgroundId = externalResource.getIdentifier("vsh_background", "drawable", pkgInfo.packageName)
+ embeddedBackSoundId = externalResource.getIdentifier("vsh_background", "raw", pkgInfo.packageName)
+ embeddedBackOverlayId = externalResource.getIdentifier("vsh_back_overlay", "drawable", pkgInfo.packageName)
+ embeddedAnimIconId = externalResource.getIdentifier("vsh_anim_icon", "raw", pkgInfo.packageName)
+ }
+
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
+ val splits : Array? = resInfo.activityInfo.applicationInfo.splitPublicSourceDirs
+
+ apkSplits = if(splits != null){
+ if(splits.size > 1){
+ Array(splits.size){ File(splits[it]) }
+ }else{
+ Array(0){ apkFile }
+ }
+ }else{
+ Array(0){ apkFile }
+ }
+ }
+
val handle = vsh.addLoadHandle()
appLabel = resInfo.loadLabel(vsh.packageManager).toString()
vsh.setLoadingFinished(handle)
menuItems.add(
XMBMenuItem.XMBMenuItemLambda({ vsh.getString(R.string.app_launch) }, { false }, 0){ _launch(this) })
+ menuItems.add(
+ XMBMenuItem.XMBMenuItemLambda({ vsh.getString(R.string.menu_app_info) },
+ { false },1){
+ vsh.xmbView?.showDialog(AppInfoDialogView(vsh, this))
+ }
+ )
+
+ menuItems.add(
+ XMBMenuItem.XMBMenuItemLambda({vsh.getString(R.string.app_create_customization_folder)}, {false}, 2){
+ createAppCustomDirectory()
+ }
+ )
+ menuItems.add(
+ XMBMenuItem.XMBMenuItemLambda({ vsh.getString(R.string.app_find_on_playstore) }, { false }, 3) {
+ vsh.xmbView?.context?.xmb?.appOpenInPlayStore(resInfo.activityInfo.packageName)
+ }
+ )
menuItems.add(
XMBMenuItem.XMBMenuItemLambda({vsh.getString(R.string.app_force_kill)},
- { false }, 1)
+ { false }, 5)
{
val actMan = vsh.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
actMan.killBackgroundProcesses(resInfo.activityInfo.processName)
@@ -190,38 +444,12 @@ class XMBAppItem(private val vsh: VSH, val resInfo : ResolveInfo) : XMBItem(vsh)
menuItems.add(
XMBMenuItem.XMBMenuItemLambda({ vsh.getString(R.string.app_uninstall) },
- { isSystemApp },2){
+ { isSystemApp },6){
vsh.xmbView?.context?.xmb?.appRequestUninstall(resInfo.activityInfo.packageName)
}
)
- menuItems.add(
- XMBMenuItem.XMBMenuItemLambda({ vsh.getString(R.string.app_find_on_playstore) }, { false }, 4) {
- vsh.xmbView?.context?.xmb?.appOpenInPlayStore(resInfo.activityInfo.packageName)
- }
- )
-
- menuItems.add(
- XMBMenuItem.XMBMenuItemLambda({vsh.getString(R.string.app_create_customization_folder)}, {false}, 5){
- vsh.getAllPathsFor(VshBaseDirs.APPS_DIR, resInfo.uniqueActivityName).forEach { file ->
- var found = file.exists()
- if(!found){
- found = file.mkdirs()
- }
- val sb = StringBuilder()
- for(i in file.absolutePath.indices){
- if((i + 1) % 50 == 0) sb.append('\n')
- sb.append(file.absolutePath[i])
- }
-
- if(found){
- vsh.postNotification(null, vsh.getString(R.string.app_customization_file_created), sb.toString(), 10.0f)
- }
- }
- }
- )
-
menuItems.add(
XMBMenuItem.XMBMenuItemLambda({ vsh.getString(R.string.app_category_switch_sort) }, {false}, -2){
vsh.doCategorySorting()
@@ -230,6 +458,24 @@ class XMBAppItem(private val vsh: VSH, val resInfo : ResolveInfo) : XMBItem(vsh)
}
}
+ private fun createAppCustomDirectory() {
+ vsh.getAllPathsFor(VshBaseDirs.APPS_DIR, resInfo.uniqueActivityName).forEach { file ->
+ var found = file.exists()
+ if(!found){
+ found = file.mkdirs()
+ }
+ val sb = StringBuilder()
+ for(i in file.absolutePath.indices){
+ if((i + 1) % 50 == 0) sb.append('\n')
+ sb.append(file.absolutePath[i])
+ }
+
+ if(found){
+ vsh.postNotification(null, vsh.getString(R.string.app_customization_file_created), sb.toString(), 10.0f)
+ }
+ }
+ }
+
private fun pIconLoad(){
synchronized(_iconSync){
if(!hasIconLoaded){
@@ -364,4 +610,4 @@ class XMBAppItem(private val vsh: VSH, val resInfo : ResolveInfo) : XMBItem(vsh)
}
override val onLaunch: (XMBItem) -> Unit get()= ::_launch
-}
\ No newline at end of file
+}
diff --git a/launcher_app/src/main/java/id/psw/vshlauncher/types/items/XMBShortcutCategory.kt b/launcher_app/src/main/java/id/psw/vshlauncher/types/items/XMBShortcutCategory.kt
deleted file mode 100644
index ca0a96c..0000000
--- a/launcher_app/src/main/java/id/psw/vshlauncher/types/items/XMBShortcutCategory.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package id.psw.vshlauncher.types.items
-
-import id.psw.vshlauncher.VSH
-import id.psw.vshlauncher.types.XMBItem
-
-class XMBShortcutCategory(vsh: VSH) : XMBItem(vsh) {
- private val items = arrayListOf()
-
- override val content: ArrayList get() = items
- override val hasContent: Boolean get() = items.isNotEmpty()
- override val isHidden: Boolean get() = !hasContent
- override val hasIcon: Boolean get() = true
-
-}
\ No newline at end of file
diff --git a/launcher_app/src/main/java/id/psw/vshlauncher/types/items/XMBShortcutItem.kt b/launcher_app/src/main/java/id/psw/vshlauncher/types/items/XMBShortcutItem.kt
index 5e87955..b9722a4 100644
--- a/launcher_app/src/main/java/id/psw/vshlauncher/types/items/XMBShortcutItem.kt
+++ b/launcher_app/src/main/java/id/psw/vshlauncher/types/items/XMBShortcutItem.kt
@@ -1,10 +1,45 @@
package id.psw.vshlauncher.types.items
+import android.content.Context
+import android.content.pm.LauncherApps
+import android.content.pm.ShortcutInfo
+import android.content.pm.ShortcutManager
+import android.graphics.Bitmap
+import android.os.Build
+import android.os.UserHandle
import id.psw.vshlauncher.VSH
+import id.psw.vshlauncher.types.INIFile
import id.psw.vshlauncher.types.XMBItem
+import id.psw.vshlauncher.types.XMBShortcutInfo
+import java.io.File
-class XMBShortcutItem(vsh: VSH, c:String) : XMBItem(vsh) {
+class XMBShortcutItem(val vsh: VSH, file: File) : XMBItem(vsh) {
- override val onLaunch: (XMBItem) -> Unit
- get() = super.onLaunch
+ private val shortcut = XMBShortcutInfo(vsh, file)
+
+ override val onLaunch: (XMBItem) -> Unit = {
+ launch(it)
+ }
+
+ override val displayName: String
+ get() = shortcut.longName
+
+ override val icon: Bitmap
+ get() = shortcut.icon
+
+ override val description: String
+ get() = shortcut.longName
+
+ fun launch(x:XMBItem){
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
+ val lcher = vsh.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
+ val uid = android.os.Process.myUid()
+ lcher.startShortcut(shortcut.packageName, shortcut.id, null, null, UserHandle.getUserHandleForUid(uid))
+ }
+ }
+
+
+ init {
+
+ }
}
\ No newline at end of file
diff --git a/launcher_app/src/main/java/id/psw/vshlauncher/views/Extensions.kt b/launcher_app/src/main/java/id/psw/vshlauncher/views/Extensions.kt
index 8913ac8..11a40dc 100644
--- a/launcher_app/src/main/java/id/psw/vshlauncher/views/Extensions.kt
+++ b/launcher_app/src/main/java/id/psw/vshlauncher/views/Extensions.kt
@@ -5,6 +5,7 @@ import android.os.Build
import id.psw.vshlauncher.FittingMode
import id.psw.vshlauncher.select
import id.psw.vshlauncher.toLerp
+import kotlin.math.floor
private val drawBitmapFitRectFBuffer = RectF()
fun Canvas.drawBitmap(bm:Bitmap, src: Rect?, dst: RectF, paint: Paint?, fitMode:FittingMode, anchorX:Float = 0.5f, anchorY:Float = 0.5f){
@@ -97,4 +98,37 @@ fun Paint.wrapText(source:String, maxWidth:Float) : String {
}
msb.appendLine(line.toString())
return msb.toString().trimEnd('\n')
+}
+
+val byteSuffix = arrayOf("B","KB","MB","GB","TB","PB","EB","ZB","YB")
+val biByteSuffix = arrayOf("B","KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB")
+
+fun Long.asBytes(useBi:Boolean = false): String {
+ val suf = useBi.select(biByteSuffix, byteSuffix)
+ val dif = useBi.select( 1024.0, 1000.0)
+ var i = 0
+ var d = this * 1.0
+ while(d > dif && i < suf.size){
+ i++
+ d /= dif
+ }
+ d = floor(d * 100.0) / 100.0
+ return "$d ${suf[i]}"
+}
+
+fun Long.asMBytes(useBi:Boolean = false) : String {
+ val suf = useBi.select(biByteSuffix, byteSuffix)
+ val dif = useBi.select( 1024.0, 1000.0)
+ var i = 0
+ var d = this * 1.0
+ while(d > dif && i < 3){
+ i++
+ d /= dif
+ }
+ d = floor(d * 100.0) / 100.0
+ return "$d ${suf[i]}"
+}
+
+fun String.substituteIfEmpty(substitute:String) : String{
+ return ifEmpty { substitute }
}
\ No newline at end of file
diff --git a/launcher_app/src/main/java/id/psw/vshlauncher/views/XmbView.CrossMenu.kt b/launcher_app/src/main/java/id/psw/vshlauncher/views/XmbView.CrossMenu.kt
index d8047d5..536a481 100644
--- a/launcher_app/src/main/java/id/psw/vshlauncher/views/XmbView.CrossMenu.kt
+++ b/launcher_app/src/main/java/id/psw/vshlauncher/views/XmbView.CrossMenu.kt
@@ -67,7 +67,9 @@ class VshViewMainMenuState {
data class VerticalMenu(
var playAnimatedIcon : Boolean = true,
var playBackSound : Boolean = true,
- var showBackdrop : Boolean = true
+ var showBackdrop : Boolean = true,
+ var nameTextXOffset : Float = 0.0f,
+ var descTextXOffset : Float = 0.0f
)
class Formatter{
@@ -568,6 +570,7 @@ fun XmbView.menu3HorizontalMenu(ctx:Canvas){
}
private val verticalRectF = RectF()
+private val textNameRectF = RectF()
fun XmbView.menuRenderVerticalMenu(ctx:Canvas){
val items = context.vsh.items?.visibleItems?.filterBySearch(context.vsh)
diff --git a/launcher_app/src/main/java/id/psw/vshlauncher/views/dialogviews/AppInfoDialogView.kt b/launcher_app/src/main/java/id/psw/vshlauncher/views/dialogviews/AppInfoDialogView.kt
new file mode 100644
index 0000000..7f4f5e9
--- /dev/null
+++ b/launcher_app/src/main/java/id/psw/vshlauncher/views/dialogviews/AppInfoDialogView.kt
@@ -0,0 +1,256 @@
+package id.psw.vshlauncher.views.dialogviews
+
+import android.app.ProgressDialog.show
+import android.graphics.*
+import android.os.Build
+import android.text.TextPaint
+import androidx.core.content.res.ResourcesCompat
+import androidx.core.graphics.drawable.toBitmap
+import androidx.core.graphics.withRotation
+import id.psw.vshlauncher.*
+import id.psw.vshlauncher.submodules.GamepadSubmodule
+import id.psw.vshlauncher.types.XMBItem
+import id.psw.vshlauncher.types.items.XMBAppItem
+import id.psw.vshlauncher.views.VshViewPage
+import id.psw.vshlauncher.views.XmbDialogSubview
+import id.psw.vshlauncher.views.drawBitmap
+import id.psw.vshlauncher.views.nativedlg.NativeEditTextDialog
+
+class AppInfoDialogView(private val vsh: VSH, private val app : XMBAppItem) : XmbDialogSubview(vsh) {
+ companion object {
+ const val POS_NAME = 0
+ const val POS_DESC = 2
+ const val POS_ALBUM = 3
+ const val POS_HIDDEN = 4
+ const val POS_CATEGORY = 5
+ const val POS_OPEN_IN_SYSTEM = 9
+ const val TRANSITE_TIME = 0.125f
+
+ private val bmpRectF = RectF()
+ private val selRectF = RectF()
+ private val szBufRectF = RectF()
+ private val validSelections = arrayOf(POS_NAME, POS_DESC, POS_ALBUM, POS_HIDDEN, POS_CATEGORY, POS_OPEN_IN_SYSTEM)
+ }
+
+ override val hasNegativeButton: Boolean = true
+ override val hasPositiveButton: Boolean get() = true
+ override val title: String
+ get() = vsh.getString(R.string.view_app_info)
+
+ private var cursorPos = 0
+ private var transiteTime = 0.0f
+ private lateinit var loadIcon : Bitmap
+ private var tPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
+ color = Color.WHITE
+ textSize = 20.0f
+ }
+
+ private var iconPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+ private var rectPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply{
+ color = Color.WHITE
+ style = Paint.Style.STROKE
+ strokeWidth = 2.0f
+ }
+
+ private var _icon =vsh.loadTexture(R.drawable.icon_info, true)
+
+ override val icon: Bitmap
+ get() = _icon
+
+ override fun onClose() {
+ if(_icon != XMBItem.WHITE_BITMAP) _icon.recycle()
+ if(loadIcon != XMBItem.WHITE_BITMAP) loadIcon.recycle()
+ }
+
+ override val negativeButton: String
+ get() = vsh.getString(R.string.common_back)
+
+ override val positiveButton: String
+ get() = vsh.getString(R.string.common_edit)
+
+ override fun onStart() {
+ loadIcon = ResourcesCompat.getDrawable(vsh.resources,R.drawable.ic_sync_loading,null)?.toBitmap(256,256) ?: XMBItem.WHITE_BITMAP
+ super.onStart()
+ }
+
+ private fun drawLoading(ctx:Canvas){
+ val time = vsh.xmbView?.time?.currentTime ?: (System.currentTimeMillis() / 1000.0f)
+ ctx.withRotation(
+ ((time + 0.375f) * -360.0f) % 360.0f, bmpRectF.centerX(), bmpRectF.centerY()) {
+ ctx.drawBitmap(loadIcon, null, bmpRectF, iconPaint, FittingMode.FIT, 0.5f, 0.5f)
+ }
+
+ }
+
+ override fun onDraw(ctx: Canvas, drawBound: RectF, deltaTime: Float) {
+ if(transiteTime < TRANSITE_TIME){
+ transiteTime += vsh.xmbView?.time?.deltaTime ?: 0.015f
+ }
+ transiteTime = transiteTime.coerceIn(0.0f, TRANSITE_TIME)
+
+ val t = transiteTime / TRANSITE_TIME
+
+ val cSizeY = 132.0f
+ val sizeX = t.toLerp(320.0f, 240.0f)
+ val sizeY = t.toLerp(176.0f, cSizeY)
+
+ val hSizeX = sizeX / 2.0f
+ val hSizeY = sizeY / 2.0f
+
+ val iconCX = t.toLerp(
+ (0.3f).toLerp(drawBound.left, drawBound.right),
+ drawBound.centerX())
+ val iconCY = t.toLerp(
+ drawBound.centerY(),
+ drawBound.top + 20.0f + hSizeY
+ )
+
+ bmpRectF.set(
+ iconCX - hSizeX,
+ iconCY - hSizeY,
+ iconCX + hSizeX,
+ iconCY + hSizeY
+ )
+
+ if(app.hasAnimatedIcon){
+ if(app.isAnimatedIconLoaded){
+ val tt = vsh.xmbView?.time?.deltaTime ?: 0.015f
+ ctx.drawBitmap(app.animatedIcon.getFrame(tt), null, bmpRectF, iconPaint, FittingMode.FIT, 0.5f, 0.5f)
+ }else{
+ drawLoading(ctx)
+ }
+ }else if(app.hasIcon){
+ if(app.isIconLoaded){
+ ctx.drawBitmap(app.icon, null, bmpRectF, iconPaint, FittingMode.FIT, 0.5f, 0.5f)
+ }else{
+ drawLoading(ctx)
+ }
+ }
+
+ var sY = drawBound.top + cSizeY + 50.0f
+ val cX = drawBound.centerX() - 100.0f
+
+ var i = 0
+ mapOf(
+ R.string.dlg_info_name to app.displayName,
+ R.string.dlg_info_pkg_name to app.packageName,
+ R.string.dlg_info_desc to app.appCustomDesc.ifEmpty { "-" },
+ R.string.dlg_info_album to app.appAlbum.ifEmpty { "-" },
+ R.string.dlg_info_hidden to vsh.getString(app.isHiddenByCfg.select(R.string.common_yes, R.string.common_no)),
+ R.string.dlg_info_category to app.appCategory.ifEmpty { "-" },
+ R.string.dlg_info_update to app.displayUpdateTime,
+ R.string.dlg_info_apk_size to app.fileSize,
+ R.string.dlg_info_version to app.version
+ ).forEach { l ->
+ tPaint.textAlign = Paint.Align.RIGHT
+ ctx.drawText(vsh.getString(l.key), cX, sY, tPaint)
+ val str = l.value
+
+ val isSelected = validSelections[cursorPos] == i
+
+ if(isSelected){
+ val w = tPaint.measureText(str)
+
+ selRectF.set(cX + 20.0f, sY - tPaint.textSize , cX + 50.0f + w, sY+ 5.0f)
+ ctx.drawRoundRect(selRectF, 5.0f, 5.0f, rectPaint)
+ }
+
+ tPaint.textAlign = Paint.Align.LEFT
+ ctx.drawText(str, cX + 30.0f, sY, tPaint)
+ sY += tPaint.textSize * 1.2f
+ i++
+ }
+
+ tPaint.textAlign = Paint.Align.CENTER
+ if(cursorPos == 5){
+ tPaint.setShadowLayer(5.0f, 0.0f, 0.0f, Color.WHITE)
+ }
+ ctx.drawText(vsh.getString(R.string.app_info_by_system), drawBound.centerX(), sY + 20.0f, tPaint)
+ if(cursorPos == 5){
+ tPaint.setShadowLayer(0.0f, 0.0f, 0.0f, Color.WHITE)
+ }
+ }
+
+ override fun onGamepad(key: GamepadSubmodule.Key, isPress: Boolean): Boolean {
+ when(key){
+ GamepadSubmodule.Key.PadU -> {
+ if(isPress){
+ cursorPos--
+ cursorPos = cursorPos.coerceIn(0, validSelections.size-1)
+ return true
+ }
+ }
+ GamepadSubmodule.Key.PadD -> {
+ if(isPress){
+ cursorPos ++
+ cursorPos = cursorPos.coerceIn(0, validSelections.size-1)
+ return true
+ }
+ }
+ else -> {
+
+ }
+ }
+
+ return super.onGamepad(key, isPress)
+ }
+
+ override fun onTouch(a: PointF, b: PointF, act: Int) {
+ super.onTouch(a, b, act)
+ }
+
+ override fun onDialogButton(isPositive: Boolean) {
+ if(isPositive){
+ when(cursorPos){
+ 0 -> {
+ // Rename
+ NativeEditTextDialog(vsh)
+ .setTitle(vsh.getString(R.string.dlg_info_rename))
+ .setOnFinish {
+ app.appCustomLabel = it
+ }
+ .setValue(app.displayName)
+ .show()
+ }
+ 1 -> {
+ NativeEditTextDialog(vsh)
+ .setTitle(vsh.getString(R.string.dlg_info_redesc))
+ .setOnFinish {
+ app.appCustomDesc = it
+ }
+ .setValue(app.appCustomDesc)
+ .show()
+ }
+ 2 -> {
+ NativeEditTextDialog(vsh)
+ .setTitle(vsh.getString(R.string.dlg_info_album))
+ .setOnFinish {
+ app.appAlbum = it
+ }
+ .setValue(app.appAlbum)
+ .show()
+ }
+ 3 -> {
+ app.hide(!app.isHiddenByCfg)
+ // Change is Hidden
+ }
+ 4 -> {
+ // Set Category
+ NativeEditTextDialog(vsh)
+ .setTitle(vsh.getString(R.string.dlg_info_album))
+ .setOnFinish {
+ app.appCategory = it
+ }
+ .setValue(app.appCategory)
+ .show()
+ }
+ 5 -> {
+ // Show in Android
+ vsh.showAppInfo(app)
+ }
+ }
+ }
+
+ if(!isPositive) finish(VshViewPage.MainMenu)
+ }
+}
\ No newline at end of file
diff --git a/launcher_app/src/main/java/id/psw/vshlauncher/views/dialogviews/InstallShortcutDialogView.kt b/launcher_app/src/main/java/id/psw/vshlauncher/views/dialogviews/InstallShortcutDialogView.kt
index 6657a14..b46125d 100644
--- a/launcher_app/src/main/java/id/psw/vshlauncher/views/dialogviews/InstallShortcutDialogView.kt
+++ b/launcher_app/src/main/java/id/psw/vshlauncher/views/dialogviews/InstallShortcutDialogView.kt
@@ -1,24 +1,19 @@
package id.psw.vshlauncher.views.dialogviews
-import android.content.Context
import android.content.Intent
-import android.content.pm.LauncherApps
import android.graphics.*
import android.os.Build
+import android.util.Base64
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap
-import id.psw.vshlauncher.FittingMode
-import id.psw.vshlauncher.R
-import id.psw.vshlauncher.VSH
-import id.psw.vshlauncher.submodules.GamepadSubmodule
-import id.psw.vshlauncher.toLerp
+import id.psw.vshlauncher.*
import id.psw.vshlauncher.types.XMBItem
+import id.psw.vshlauncher.types.XMBShortcutInfo
import id.psw.vshlauncher.typography.FontCollections
import id.psw.vshlauncher.views.VshViewPage
import id.psw.vshlauncher.views.XmbDialogSubview
import id.psw.vshlauncher.views.drawBitmap
import id.psw.vshlauncher.views.drawText
-import id.psw.vshlauncher.submodules.GamepadSubmodule.Key as Key
class InstallShortcutDialogView(private val vsh: VSH, private val intent: Intent) : XmbDialogSubview(vsh) {
override val title: String
@@ -31,61 +26,43 @@ class InstallShortcutDialogView(private val vsh: VSH, private val intent: Intent
override val icon: Bitmap = ResourcesCompat.getDrawable(vsh.resources, R.drawable.category_shortcut, null)?.toBitmap(50,50) ?: XMBItem.WHITE_BITMAP
- private lateinit var shortcutIcon : Bitmap
- private var shortcutId : String = "KO.ID!"
- private var shortcutName : String = "No Name"
- private var shortcutNameLong : String = ""
- private var shortcutPackage : String = "com.package.what"
+ private val shortcut = XMBShortcutInfo(vsh, intent)
+
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
typeface = FontCollections.masterFont
textSize = 25.0f
color = Color.WHITE
}
- init {
- var isNextGen = false
-
- if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
- if(intent.action!!.equals(LauncherApps.ACTION_CONFIRM_PIN_SHORTCUT, true)){
- val lcher = vsh.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
- val pinItem = lcher.getPinItemRequest(intent)
- val shInfo = pinItem.shortcutInfo
- if(shInfo != null){
- shortcutIcon = lcher
- .getShortcutIconDrawable(shInfo, vsh.resources.displayMetrics.densityDpi)
- .toBitmap(100,100)
- shortcutId = shInfo.id
- shortcutName = shInfo.shortLabel?.toString() ?: shortcutName
- shortcutNameLong = shInfo.longLabel?.toString() ?: shortcutNameLong
- shortcutPackage = shInfo.activity?.toShortString() ?: shortcutPackage
- isNextGen = true
- }
- }
- }
-
- // Use old Android static shortcut installation
- if(!isNextGen){
- if(intent.action!!.equals(Intent.ACTION_CREATE_SHORTCUT, true)){
- shortcutName = intent.getStringExtra(Intent.EXTRA_SHORTCUT_NAME) ?: shortcutName
- shortcutIcon = intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON) ?: XMBItem.WHITE_BITMAP
- shortcutPackage = intent.component?.toShortString() ?: shortcutPackage
- }
- }
-
- }
override fun onClose() {
if(icon != XMBItem.WHITE_BITMAP){
icon.recycle()
}
- if(shortcutIcon != XMBItem.WHITE_BITMAP){
- shortcutIcon.recycle()
- }
}
override fun onDialogButton(isPositive: Boolean) {
if(isPositive){
- vsh.installShortcut(intent)
+ val req = shortcut.pinItem
+ if(req != null){
+ val id = "${shortcut.packageName}_${shortcut.id}"
+ val idb = id.toByteArray(Charsets.UTF_16)
+ val b64 = Base64.encode(idb, Base64.DEFAULT)
+
+ val files = vsh.getAllPathsFor(VshBaseDirs.USER_DIR, "shortcuts", "${b64}.ini", createParentDir = true)
+ var file = files.find { it.exists() }
+ if(file == null){
+ files[0].createNewFile()
+ file = files[0]
+ }
+
+ shortcut.write(file)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ req.accept()
+ }
+ vsh.reloadShortcutList()
+ }
}
finish(VshViewPage.MainMenu)
}
@@ -99,15 +76,15 @@ class InstallShortcutDialogView(private val vsh: VSH, private val intent: Intent
drawBound.centerX() + 50.0f,
bTop + 125.0f
)
- ctx.drawBitmap(shortcutIcon, null, tmpRectF, null, FittingMode.FIT, 0.5f, 0.5f)
+ ctx.drawBitmap(shortcut.icon, null, tmpRectF, null, FittingMode.FIT, 0.5f, 0.5f)
var textTop = bTop + 150.0f
val textCenter = 0.40f.toLerp(drawBound.left, drawBound.right)
arrayOf(
- "ID" to shortcutId,
- "Name" to shortcutName,
- "Long Name" to shortcutNameLong,
- "Package" to shortcutPackage,
+ "ID" to shortcut.id,
+ "Name" to shortcut.name,
+ "Long Name" to shortcut.longName,
+ "Package" to shortcut.packageName,
).forEach{
if(it.second.isNotEmpty()){
textPaint.textAlign = Paint.Align.RIGHT
diff --git a/launcher_app/src/main/res/values/strings.xml b/launcher_app/src/main/res/values/strings.xml
index 8e5a1c7..2e233ef 100644
--- a/launcher_app/src/main/res/values/strings.xml
+++ b/launcher_app/src/main/res/values/strings.xml
@@ -318,4 +318,20 @@
Set resolution for drawing and then scaled to device screen
Hide Status Bar
Do not show status bar at top-right portion of screen
+ App Info
+ Title
+ Description
+ Album
+ Is Hidden
+ Category
+ Package Size
+ Updated
+ Version
+ Package Name
+ Set app custom label
+ Show hidden apps,disabled on next restart
+ Show hidden apps
+ Shortcuts
+ Set app custom description
+ This app\'s info in System Settings
\ No newline at end of file
diff --git a/launcher_xlib/build.gradle.kts b/launcher_xlib/build.gradle.kts
index b8f30b3..1350214 100644
--- a/launcher_xlib/build.gradle.kts
+++ b/launcher_xlib/build.gradle.kts
@@ -4,7 +4,7 @@ plugins {
}
android {
- compileSdk = 29
+ compileSdk = 33
defaultConfig {
minSdk = 19
diff --git a/plugin_sample/build.gradle.kts b/plugin_sample/build.gradle.kts
index e18bda5..a771619 100644
--- a/plugin_sample/build.gradle.kts
+++ b/plugin_sample/build.gradle.kts
@@ -8,7 +8,7 @@ plugins{
}
android {
- compileSdk = 31
+ compileSdk = 33
defaultConfig {
applicationId = "id.psw.vshlauncher.plugin_example"