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"