Android changes

This commit is contained in:
KeatonTheBot
2025-05-16 23:42:48 -05:00
parent df21f6019e
commit e8e4c5aa61
464 changed files with 23351 additions and 1429 deletions
+12
View File
@@ -0,0 +1,12 @@
.idea/
*.iml
.gradle
local.properties
.DS_Store
build/
captures
.externalNativeBuild
.cxx/
app/src/main/jniLibs/arm64-v8a/**
!app/src/main/jniLibs/arm64-v8a/.gitkeep
+129
View File
@@ -0,0 +1,129 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'org.kenjinx.android'
compileSdk 34
defaultConfig {
applicationId "org.kenjinx.android"
minSdk 30
targetSdk 34
versionCode 10044
versionName '2.0.3'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
ndk {
//noinspection ChromeOsAbiSupport
abiFilters 'arm64-v8a'
}
externalNativeBuild {
cmake {
cppFlags "-std=c++17 -O3 -ffast-math -march=armv8-a -funroll-loops"
arguments "-DANDROID_STL=c++_shared",
"-DCMAKE_BUILD_TYPE=Release",
"-DANDROID_ARM_NEON=TRUE"
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
debuggable false
jniDebuggable false
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
kotlinOptions {
jvmTarget = '21'
freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn", "-Xallow-jvm-ir-dependencies"]
}
buildFeatures {
compose true
prefab true
buildConfig true
}
composeOptions {
kotlinCompilerExtensionVersion '1.5.13'
}
packagingOptions {
jniLibs {
keepDebugSymbols += '**/libkenjinx.so'
useLegacyPackaging true
}
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.22.1'
}
}
sourceSets {
main {
jniLibs.srcDirs = ['src/main/extLibs', 'src/main/jniLibs']
}
}
}
tasks.named("preBuild") {
dependsOn ':libkenjinx:assemble'
}
dependencies {
implementation "androidx.compose.material:material-icons-extended:1.6.5"
implementation 'net.java.dev.jna:jna:5.14.0@aar'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.12.0'
implementation platform('androidx.compose:compose-bom:2024.05.00')
implementation platform('androidx.compose:compose-bom:2024.05.00')
androidTestImplementation platform('androidx.compose:compose-bom:2024.05.00')
androidTestImplementation platform('androidx.compose:compose-bom:2024.05.00')
runtimeOnly project(":libkenjinx")
implementation 'androidx.core:core-ktx:1.13.1'
implementation platform('org.jetbrains.kotlin:kotlin-bom:1.9.24')
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation "androidx.navigation:navigation-compose:2.7.7"
implementation 'androidx.activity:activity-compose:1.9.0'
implementation platform('androidx.compose:compose-bom:2024.05.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation 'com.github.swordfish90:radialgamepad:2.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "com.anggrayudi:storage:1.5.5"
implementation "androidx.preference:preference-ktx:1.2.1"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'net.lingala.zip4j:zip4j:2.11.5'
implementation("br.com.devsrsouza.compose.icons:css-gg:1.1.0")
implementation 'io.coil-kt:coil-compose:2.6.0'
implementation("com.halilibo.compose-richtext:richtext-ui:0.20.0")
implementation("com.halilibo.compose-richtext:richtext-commonmark:0.20.0")
implementation("com.halilibo.compose-richtext:richtext-ui-material3:0.20.0")
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation platform('androidx.compose:compose-bom:2024.05.00')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
}
+33
View File
@@ -0,0 +1,33 @@
-optimizationpasses 5
-optimizations !code/simplification/arithmetic
-optimizations !code/simplification/cast
-optimizations !field/*
-optimizations !class/merging/*
-optimizations !method/inlining/short
-optimizations !method/inlining/unique
-optimizations method/inlining/tailrecursion
-optimizations method/removal/parameter
-optimizations code/merging
-optimizations code/simplification/variable
-allowaccessmodification
-repackageclasses ''
-keepattributes Exceptions,InnerClasses,Signature,*Annotation*
-dontpreverify
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends androidx.fragment.app.Fragment
-assumenosideeffects class java.lang.Math {
public static double random();
public static double sin(...);
public static double cos(...);
public static double sqrt(...);
}
-assumenosideeffects public class ** {
public boolean is*();
public boolean get*();
public boolean has*();
public void set*(***);
}
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.hardware.audio.output"
android:required="true" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:name=".KenjinxApplication"
android:allowBackup="true"
android:appCategory="game"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:isGame="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.KenjinxAndroid"
android:largeHeap="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:exported="true"
android:screenOrientation="sensorLandscape"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
android:hardwareAccelerated="true"
android:theme="@style/Theme.KenjinxAndroid">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="org.kenjinx.android.LAUNCH_GAME" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="org.kenjinx.android.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<provider
android:name=".providers.DocumentProvider"
android:authorities="org.kenjinx.android.providers"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
</application>
</manifest>
@@ -0,0 +1,81 @@
include(FetchContent)
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.22.1)
# Declares and names the project.
project("kenjinxjni")
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
FetchContent_Declare(
adrenotools
GIT_REPOSITORY https://github.com/bylaws/libadrenotools.git
GIT_TAG deec5f75ee1a8ccbe32c8780b1d17284fc87b0f1 # v1.0-14-gdeec5f7
)
FetchContent_MakeAvailable(adrenotools)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
kenjinxjni
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
vulkan_wrapper.cpp
kenjinx.cpp)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
kenjinxjni
# Links the target library to the log library
# included in the NDK.
${log-lib}
-lvulkan
-landroid
adrenotools
)
# Build external libraries if prebuilt files don't exist
set(JNI_PATH ../jniLibs/${CMAKE_ANDROID_ARCH_ABI})
cmake_path(ABSOLUTE_PATH JNI_PATH NORMALIZE)
cmake_path(APPEND JNI_PATH libcrypto.so OUTPUT_VARIABLE LIBCRYPTO_JNI_PATH)
cmake_path(APPEND JNI_PATH libssl.so OUTPUT_VARIABLE LIBSSL_JNI_PATH)
# Add OpenAL
add_subdirectory(libraries/openal)
set_target_properties(OpenAL PROPERTIES
LIBRARY_OUTPUT_DIRECTORY ${JNI_PATH}
ARCHIVE_OUTPUT_DIRECTORY ${JNI_PATH}
RUNTIME_OUTPUT_DIRECTORY ${JNI_PATH}
)
@@ -0,0 +1,254 @@
// Write C++ code here.
//
// Do not forget to dynamically load the C++ library into your application.
//
// For instance,
//
// In MainActivity.java:
// static {
// System.loadLibrary("kenjinxjni");
// }
//
// Or, in MainActivity.kt:
// companion object {
// init {
// System.loadLibrary("kenjinxjni")
// }
// }
#include "kenjinx.h"
#include "pthread.h"
#include <chrono>
#include <csignal>
std::chrono::time_point<std::chrono::steady_clock, std::chrono::nanoseconds> _currentTimePoint;
extern "C"
{
JNIEXPORT jlong JNICALL
Java_org_kenjinx_android_NativeHelpers_getNativeWindow(
JNIEnv *env,
jobject instance,
jobject surface) {
auto nativeWindow = ANativeWindow_fromSurface(env, surface);
return nativeWindow == NULL ? -1 : (jlong) nativeWindow;
}
JNIEXPORT void JNICALL
Java_org_kenjinx_android_NativeHelpers_releaseNativeWindow(
JNIEnv *env,
jobject instance,
jlong window) {
auto nativeWindow = (ANativeWindow *) window;
if (nativeWindow != NULL)
ANativeWindow_release(nativeWindow);
}
long createSurface(long native_surface, long instance) {
auto nativeWindow = (ANativeWindow *) native_surface;
VkSurfaceKHR surface;
auto vkInstance = (VkInstance) instance;
auto fpCreateAndroidSurfaceKHR =
reinterpret_cast<PFN_vkCreateAndroidSurfaceKHR>(vkGetInstanceProcAddr(vkInstance,
"vkCreateAndroidSurfaceKHR"));
if (fpCreateAndroidSurfaceKHR == nullptr)
LOGE("Could not get function pointer to CreateAndroidSurfaceKHR");
VkAndroidSurfaceCreateInfoKHR info;
info.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR;
info.pNext = nullptr;
info.flags = 0;
info.window = nativeWindow;
VK_CHECK(fpCreateAndroidSurfaceKHR(vkInstance, &info, nullptr, &surface));
return (long) surface;
}
JNIEXPORT jlong JNICALL
Java_org_kenjinx_android_NativeHelpers_getCreateSurfacePtr(
JNIEnv *env,
jobject instance) {
return (jlong) createSurface;
}
char *getStringPointer(
JNIEnv *env,
jstring jS) {
const char *cparam = env->GetStringUTFChars(jS, 0);
auto len = env->GetStringUTFLength(jS);
char *s = new char[len + 1]; //null terminator
strcpy(s, cparam);
env->ReleaseStringUTFChars(jS, cparam);
return s;
}
jstring createString(
JNIEnv *env,
char *ch) {
auto str = env->NewStringUTF(ch);
return str;
}
jstring createStringFromStdString(
JNIEnv *env,
std::string s) {
auto str = env->NewStringUTF(s.c_str());
return str;
}
}
extern "C"
void setRenderingThread() {
auto currentId = pthread_self();
_renderingThreadId = currentId;
_currentTimePoint = std::chrono::high_resolution_clock::now();
}
extern "C"
JNIEXPORT void JNICALL
Java_org_kenjinx_android_MainActivity_initVm(JNIEnv *env, jobject thiz) {
JavaVM *vm = nullptr;
env->GetJavaVM(&vm);
_vm = vm;
_mainActivity = thiz;
_mainActivityClass = env->GetObjectClass(thiz);
}
bool isInitialOrientationFlipped = true;
extern "C"
void setCurrentTransform(long native_window, int transform) {
if (native_window == 0 || native_window == -1)
return;
auto nativeWindow = (ANativeWindow *) native_window;
auto nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_IDENTITY;
transform = transform >> 1;
// transform is a valid VkSurfaceTransformFlagBitsKHR
switch (transform) {
case 0x1:
nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_IDENTITY;
break;
case 0x2:
nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_90;
break;
case 0x4:
nativeTransform = isInitialOrientationFlipped
? ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_IDENTITY
: ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_180;
break;
case 0x8:
nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_270;
break;
case 0x10:
nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_MIRROR_HORIZONTAL;
break;
case 0x20:
nativeTransform = static_cast<ANativeWindowTransform>(
ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_MIRROR_HORIZONTAL |
ANATIVEWINDOW_TRANSFORM_ROTATE_90);
break;
case 0x40:
nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_MIRROR_VERTICAL;
break;
case 0x80:
nativeTransform = static_cast<ANativeWindowTransform>(
ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_MIRROR_VERTICAL |
ANATIVEWINDOW_TRANSFORM_ROTATE_90);
break;
case 0x100:
nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_IDENTITY;
break;
}
nativeWindow->perform(nativeWindow, NATIVE_WINDOW_SET_BUFFERS_TRANSFORM,
static_cast<int32_t>(nativeTransform));
}
extern "C"
JNIEXPORT jlong JNICALL
Java_org_kenjinx_android_NativeHelpers_loadDriver(JNIEnv *env, jobject thiz,
jstring native_lib_path,
jstring private_apps_path,
jstring driver_name) {
auto libPath = getStringPointer(env, native_lib_path);
auto privateAppsPath = getStringPointer(env, private_apps_path);
auto driverName = getStringPointer(env, driver_name);
auto handle = adrenotools_open_libvulkan(
RTLD_NOW,
ADRENOTOOLS_DRIVER_CUSTOM,
nullptr,
libPath,
privateAppsPath,
driverName,
nullptr,
nullptr
);
delete libPath;
delete privateAppsPath;
delete driverName;
return (jlong) handle;
}
extern "C"
void debug_break(int code) {
if (code >= 3)
int r = 0;
}
extern "C"
JNIEXPORT void JNICALL
Java_org_kenjinx_android_NativeHelpers_setTurboMode(JNIEnv *env, jobject thiz, jboolean enable) {
adrenotools_set_turbo(enable);
}
extern "C"
JNIEXPORT jint JNICALL
Java_org_kenjinx_android_NativeHelpers_getMaxSwapInterval(JNIEnv *env, jobject thiz,
jlong native_window) {
auto nativeWindow = (ANativeWindow *) native_window;
return nativeWindow->maxSwapInterval;
}
extern "C"
JNIEXPORT jint JNICALL
Java_org_kenjinx_android_NativeHelpers_getMinSwapInterval(JNIEnv *env, jobject thiz,
jlong native_window) {
auto nativeWindow = (ANativeWindow *) native_window;
return nativeWindow->minSwapInterval;
}
extern "C"
JNIEXPORT jint JNICALL
Java_org_kenjinx_android_NativeHelpers_setSwapInterval(JNIEnv *env, jobject thiz,
jlong native_window, jint swap_interval) {
auto nativeWindow = (ANativeWindow *) native_window;
return nativeWindow->setSwapInterval(nativeWindow, swap_interval);
}
extern "C"
JNIEXPORT jstring JNICALL
Java_org_kenjinx_android_NativeHelpers_getStringJava(JNIEnv *env, jobject thiz, jlong ptr) {
return createString(env, (char*)ptr);
}
extern "C"
JNIEXPORT void JNICALL
Java_org_kenjinx_android_NativeHelpers_setIsInitialOrientationFlipped(JNIEnv *env, jobject thiz,
jboolean is_flipped) {
isInitialOrientationFlipped = is_flipped;
}
@@ -0,0 +1,60 @@
//
// Created by Emmanuel Hansen on 6/19/2023.
//
#ifndef KENJINXNATIVE_KENJINX_H
#define KENJINXNATIVE_KENJINX_H
#include <cassert>
#include <dlfcn.h>
#include <exception>
#include <fcntl.h>
#include <jni.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <android/log.h>
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include "vulkan_wrapper.h"
#include <vulkan/vulkan_android.h>
#include "adrenotools/driver.h"
#include "native_window.h"
// Android log function wrappers
static const char* TAG = "Kenjinx";
#define LOGI(...) \
((void)__android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__))
#define LOGW(...) \
((void)__android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__))
#define LOGE(...) \
((void)__android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__))
// A macro to pass call to Vulkan and check for return value for success
#define CALL_VK(func) \
if (VK_SUCCESS != (func)) { \
__android_log_print(ANDROID_LOG_ERROR, "Tutorial ", \
"Vulkan error. File[%s], line[%d]", __FILE__, \
__LINE__); \
assert(false); \
}
// A macro to check value is VK_SUCCESS
// Used also for non-vulkan functions but return VK_SUCCESS
#define VK_CHECK(x) CALL_VK(x)
#define LoadLib(a) dlopen(a, RTLD_NOW)
void *_kenjinxNative = nullptr;
// Kenjinx imported functions
bool (*initialize)(char *) = nullptr;
long _renderingThreadId = 0;
JavaVM *_vm = nullptr;
jobject _mainActivity = nullptr;
jclass _mainActivityClass = nullptr;
#endif //KENJINXNATIVE_KENJINX_H
Submodule src/KenjinxAndroid/app/src/main/cpp/libraries/adrenotools added at 8fae8ce254
Submodule src/KenjinxAndroid/app/src/main/cpp/libraries/openal added at 8f2fe93faf
@@ -0,0 +1,305 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2021 Skyline Team and Contributors (https://github.com/skyline-emu/)
// Copyright © 2021 The Android Open Source Project
#pragma once
/* A collection of various types from AOSP that allow us to access private APIs for Native Window which we utilize for emulating the guest SF more accurately */
/**
* @url https://cs.android.com/android/platform/superproject/+/android11-release:frameworks/native/libs/nativebase/include/nativebase/nativebase.h;l=29;drc=cb496acbe593326e8d5d563847067d02b2df40ec
*/
#define ANDROID_NATIVE_UNSIGNED_CAST(x) static_cast<unsigned int>(x)
/**
* @url https://cs.android.com/android/platform/superproject/+/android11-release:frameworks/native/libs/nativebase/include/nativebase/nativebase.h;l=34-38;drc=cb496acbe593326e8d5d563847067d02b2df40ec
*/
#define ANDROID_NATIVE_MAKE_CONSTANT(a, b, c, d) \
((ANDROID_NATIVE_UNSIGNED_CAST(a) << 24) | \
(ANDROID_NATIVE_UNSIGNED_CAST(b) << 16) | \
(ANDROID_NATIVE_UNSIGNED_CAST(c) << 8) | \
(ANDROID_NATIVE_UNSIGNED_CAST(d)))
/**
* @url https://cs.android.com/android/platform/superproject/+/android11-release:frameworks/native/libs/nativewindow/include/system/window.h;l=60;drc=401cda638e7d17f6697b5a65c9a5ad79d056202d
*/
#define ANDROID_NATIVE_WINDOW_MAGIC ANDROID_NATIVE_MAKE_CONSTANT('_','w','n','d')
constexpr int AndroidNativeWindowMagic{ANDROID_NATIVE_WINDOW_MAGIC};
#undef ANDROID_NATIVE_WINDOW_MAGIC
#undef ANDROID_NATIVE_MAKE_CONSTANT
#undef ANDROID_NATIVE_UNSIGNED_CAST
/**
* @url https://cs.android.com/android/platform/superproject/+/android11-release:frameworks/native/libs/nativewindow/include/system/window.h;l=325-331;drc=401cda638e7d17f6697b5a65c9a5ad79d056202d
*/
constexpr int64_t NativeWindowTimestampAuto{-9223372036854775807LL - 1};
/**
* @url https://cs.android.com/android/platform/superproject/+/android11-release:frameworks/native/libs/nativewindow/include/system/window.h;l=198-259;drc=401cda638e7d17f6697b5a65c9a5ad79d056202d
*/
enum {
NATIVE_WINDOW_CONNECT = 1, /* deprecated */
NATIVE_WINDOW_DISCONNECT = 2, /* deprecated */
NATIVE_WINDOW_SET_CROP = 3, /* private */
NATIVE_WINDOW_SET_BUFFER_COUNT = 4,
NATIVE_WINDOW_SET_BUFFERS_TRANSFORM = 6,
NATIVE_WINDOW_SET_BUFFERS_TIMESTAMP = 7,
NATIVE_WINDOW_SET_BUFFERS_DIMENSIONS = 8,
NATIVE_WINDOW_SET_SCALING_MODE = 10, /* private */
NATIVE_WINDOW_LOCK = 11, /* private */
NATIVE_WINDOW_UNLOCK_AND_POST = 12, /* private */
NATIVE_WINDOW_API_CONNECT = 13, /* private */
NATIVE_WINDOW_API_DISCONNECT = 14, /* private */
NATIVE_WINDOW_SET_BUFFERS_USER_DIMENSIONS = 15, /* private */
NATIVE_WINDOW_SET_POST_TRANSFORM_CROP = 16, /* deprecated, unimplemented */
NATIVE_WINDOW_SET_BUFFERS_STICKY_TRANSFORM = 17, /* private */
NATIVE_WINDOW_SET_SIDEBAND_STREAM = 18,
NATIVE_WINDOW_SET_BUFFERS_DATASPACE = 19,
NATIVE_WINDOW_SET_SURFACE_DAMAGE = 20, /* private */
NATIVE_WINDOW_SET_SHARED_BUFFER_MODE = 21,
NATIVE_WINDOW_SET_AUTO_REFRESH = 22,
NATIVE_WINDOW_GET_REFRESH_CYCLE_DURATION = 23,
NATIVE_WINDOW_GET_NEXT_FRAME_ID = 24,
NATIVE_WINDOW_ENABLE_FRAME_TIMESTAMPS = 25,
NATIVE_WINDOW_GET_COMPOSITOR_TIMING = 26,
NATIVE_WINDOW_GET_FRAME_TIMESTAMPS = 27,
NATIVE_WINDOW_GET_WIDE_COLOR_SUPPORT = 28,
NATIVE_WINDOW_GET_HDR_SUPPORT = 29,
NATIVE_WINDOW_GET_CONSUMER_USAGE64 = 31,
NATIVE_WINDOW_SET_BUFFERS_SMPTE2086_METADATA = 32,
NATIVE_WINDOW_SET_BUFFERS_CTA861_3_METADATA = 33,
NATIVE_WINDOW_SET_BUFFERS_HDR10_PLUS_METADATA = 34,
NATIVE_WINDOW_SET_AUTO_PREROTATION = 35,
NATIVE_WINDOW_GET_LAST_DEQUEUE_START = 36, /* private */
NATIVE_WINDOW_SET_DEQUEUE_TIMEOUT = 37, /* private */
NATIVE_WINDOW_GET_LAST_DEQUEUE_DURATION = 38, /* private */
NATIVE_WINDOW_GET_LAST_QUEUE_DURATION = 39, /* private */
NATIVE_WINDOW_SET_FRAME_RATE = 40,
NATIVE_WINDOW_SET_CANCEL_INTERCEPTOR = 41, /* private */
NATIVE_WINDOW_SET_DEQUEUE_INTERCEPTOR = 42, /* private */
NATIVE_WINDOW_SET_PERFORM_INTERCEPTOR = 43, /* private */
NATIVE_WINDOW_SET_QUEUE_INTERCEPTOR = 44, /* private */
NATIVE_WINDOW_ALLOCATE_BUFFERS = 45, /* private */
NATIVE_WINDOW_GET_LAST_QUEUED_BUFFER = 46, /* private */
NATIVE_WINDOW_SET_QUERY_INTERCEPTOR = 47, /* private */
NATIVE_WINDOW_GET_LAST_QUEUED_BUFFER2 = 50, /* private */
};
/**
* @url https://cs.android.com/android/platform/superproject/+/android11-release:frameworks/native/libs/nativebase/include/nativebase/nativebase.h;l=43-56;drc=cb496acbe593326e8d5d563847067d02b2df40ec
*/
struct android_native_base_t {
int magic;
int version;
void *reserved[4];
void (*incRef)(android_native_base_t *);
void (*decRef)(android_native_base_t *);
};
/**
* @url https://cs.android.com/android/platform/superproject/+/android11-release:frameworks/native/libs/nativewindow/include/system/window.h;l=341-560;drc=401cda638e7d17f6697b5a65c9a5ad79d056202d
*/
struct ANativeWindow {
struct android_native_base_t common;
/* flags describing some attributes of this surface or its updater */
const uint32_t flags;
/* min swap interval supported by this updated */
const int minSwapInterval;
/* max swap interval supported by this updated */
const int maxSwapInterval;
/* horizontal and vertical resolution in DPI */
const float xdpi;
const float ydpi;
/* Some storage reserved for the OEM's driver. */
intptr_t oem[4];
/*
* Set the swap interval for this surface.
*
* Returns 0 on success or -errno on error.
*/
int (*setSwapInterval)(struct ANativeWindow *window,
int interval);
/*
* Hook called by EGL to acquire a buffer. After this call, the buffer
* is not locked, so its content cannot be modified. This call may block if
* no buffers are available.
*
* The window holds a reference to the buffer between dequeueBuffer and
* either queueBuffer or cancelBuffer, so clients only need their own
* reference if they might use the buffer after queueing or canceling it.
* Holding a reference to a buffer after queueing or canceling it is only
* allowed if a specific buffer count has been set.
*
* Returns 0 on success or -errno on error.
*
* XXX: This function is deprecated. It will continue to work for some
* time for binary compatibility, but the new dequeueBuffer function that
* outputs a fence file descriptor should be used in its place.
*/
int (*dequeueBuffer_DEPRECATED)(struct ANativeWindow *window,
struct ANativeWindowBuffer **buffer);
/*
* hook called by EGL to lock a buffer. This MUST be called before modifying
* the content of a buffer. The buffer must have been acquired with
* dequeueBuffer first.
*
* Returns 0 on success or -errno on error.
*
* XXX: This function is deprecated. It will continue to work for some
* time for binary compatibility, but it is essentially a no-op, and calls
* to it should be removed.
*/
int (*lockBuffer_DEPRECATED)(struct ANativeWindow *window,
struct ANativeWindowBuffer *buffer);
/*
* Hook called by EGL when modifications to the render buffer are done.
* This unlocks and post the buffer.
*
* The window holds a reference to the buffer between dequeueBuffer and
* either queueBuffer or cancelBuffer, so clients only need their own
* reference if they might use the buffer after queueing or canceling it.
* Holding a reference to a buffer after queueing or canceling it is only
* allowed if a specific buffer count has been set.
*
* Buffers MUST be queued in the same order than they were dequeued.
*
* Returns 0 on success or -errno on error.
*
* XXX: This function is deprecated. It will continue to work for some
* time for binary compatibility, but the new queueBuffer function that
* takes a fence file descriptor should be used in its place (pass a value
* of -1 for the fence file descriptor if there is no valid one to pass).
*/
int (*queueBuffer_DEPRECATED)(struct ANativeWindow *window,
struct ANativeWindowBuffer *buffer);
/*
* hook used to retrieve information about the native window.
*
* Returns 0 on success or -errno on error.
*/
int (*query)(const struct ANativeWindow *window,
int what, int *value);
/*
* hook used to perform various operations on the surface.
* (*perform)() is a generic mechanism to add functionality to
* ANativeWindow while keeping backward binary compatibility.
*
* DO NOT CALL THIS HOOK DIRECTLY. Instead, use the helper functions
* defined below.
*
* (*perform)() returns -ENOENT if the 'what' parameter is not supported
* by the surface's implementation.
*
* See above for a list of valid operations, such as
* NATIVE_WINDOW_SET_USAGE or NATIVE_WINDOW_CONNECT
*/
int (*perform)(struct ANativeWindow *window,
int operation, ...);
/*
* Hook used to cancel a buffer that has been dequeued.
* No synchronization is performed between dequeue() and cancel(), so
* either external synchronization is needed, or these functions must be
* called from the same thread.
*
* The window holds a reference to the buffer between dequeueBuffer and
* either queueBuffer or cancelBuffer, so clients only need their own
* reference if they might use the buffer after queueing or canceling it.
* Holding a reference to a buffer after queueing or canceling it is only
* allowed if a specific buffer count has been set.
*
* XXX: This function is deprecated. It will continue to work for some
* time for binary compatibility, but the new cancelBuffer function that
* takes a fence file descriptor should be used in its place (pass a value
* of -1 for the fence file descriptor if there is no valid one to pass).
*/
int (*cancelBuffer_DEPRECATED)(struct ANativeWindow *window,
struct ANativeWindowBuffer *buffer);
/*
* Hook called by EGL to acquire a buffer. This call may block if no
* buffers are available.
*
* The window holds a reference to the buffer between dequeueBuffer and
* either queueBuffer or cancelBuffer, so clients only need their own
* reference if they might use the buffer after queueing or canceling it.
* Holding a reference to a buffer after queueing or canceling it is only
* allowed if a specific buffer count has been set.
*
* The libsync fence file descriptor returned in the int pointed to by the
* fenceFd argument will refer to the fence that must signal before the
* dequeued buffer may be written to. A value of -1 indicates that the
* caller may access the buffer immediately without waiting on a fence. If
* a valid file descriptor is returned (i.e. any value except -1) then the
* caller is responsible for closing the file descriptor.
*
* Returns 0 on success or -errno on error.
*/
int (*dequeueBuffer)(struct ANativeWindow *window,
struct ANativeWindowBuffer **buffer, int *fenceFd);
/*
* Hook called by EGL when modifications to the render buffer are done.
* This unlocks and post the buffer.
*
* The window holds a reference to the buffer between dequeueBuffer and
* either queueBuffer or cancelBuffer, so clients only need their own
* reference if they might use the buffer after queueing or canceling it.
* Holding a reference to a buffer after queueing or canceling it is only
* allowed if a specific buffer count has been set.
*
* The fenceFd argument specifies a libsync fence file descriptor for a
* fence that must signal before the buffer can be accessed. If the buffer
* can be accessed immediately then a value of -1 should be used. The
* caller must not use the file descriptor after it is passed to
* queueBuffer, and the ANativeWindow implementation is responsible for
* closing it.
*
* Returns 0 on success or -errno on error.
*/
int (*queueBuffer)(struct ANativeWindow *window,
struct ANativeWindowBuffer *buffer, int fenceFd);
/*
* Hook used to cancel a buffer that has been dequeued.
* No synchronization is performed between dequeue() and cancel(), so
* either external synchronization is needed, or these functions must be
* called from the same thread.
*
* The window holds a reference to the buffer between dequeueBuffer and
* either queueBuffer or cancelBuffer, so clients only need their own
* reference if they might use the buffer after queueing or canceling it.
* Holding a reference to a buffer after queueing or canceling it is only
* allowed if a specific buffer count has been set.
*
* The fenceFd argument specifies a libsync fence file decsriptor for a
* fence that must signal before the buffer can be accessed. If the buffer
* can be accessed immediately then a value of -1 should be used.
*
* Note that if the client has not waited on the fence that was returned
* from dequeueBuffer, that same fence should be passed to cancelBuffer to
* ensure that future uses of the buffer are preceded by a wait on that
* fence. The caller must not use the file descriptor after it is passed
* to cancelBuffer, and the ANativeWindow implementation is responsible for
* closing it.
*
* Returns 0 on success or -errno on error.
*/
int (*cancelBuffer)(struct ANativeWindow *window,
struct ANativeWindowBuffer *buffer, int fenceFd);
};
@@ -0,0 +1,404 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This file is generated.
#include "vulkan_wrapper.h"
#include <dlfcn.h>
int InitVulkan(void) {
void* libvulkan = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL);
if (!libvulkan)
return 0;
// Vulkan supported, set function addresses
vkCreateInstance = reinterpret_cast<PFN_vkCreateInstance>(dlsym(libvulkan, "vkCreateInstance"));
vkDestroyInstance = reinterpret_cast<PFN_vkDestroyInstance>(dlsym(libvulkan, "vkDestroyInstance"));
vkEnumeratePhysicalDevices = reinterpret_cast<PFN_vkEnumeratePhysicalDevices>(dlsym(libvulkan, "vkEnumeratePhysicalDevices"));
vkGetPhysicalDeviceFeatures = reinterpret_cast<PFN_vkGetPhysicalDeviceFeatures>(dlsym(libvulkan, "vkGetPhysicalDeviceFeatures"));
vkGetPhysicalDeviceFormatProperties = reinterpret_cast<PFN_vkGetPhysicalDeviceFormatProperties>(dlsym(libvulkan, "vkGetPhysicalDeviceFormatProperties"));
vkGetPhysicalDeviceImageFormatProperties = reinterpret_cast<PFN_vkGetPhysicalDeviceImageFormatProperties>(dlsym(libvulkan, "vkGetPhysicalDeviceImageFormatProperties"));
vkGetPhysicalDeviceProperties = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties>(dlsym(libvulkan, "vkGetPhysicalDeviceProperties"));
vkGetPhysicalDeviceQueueFamilyProperties = reinterpret_cast<PFN_vkGetPhysicalDeviceQueueFamilyProperties>(dlsym(libvulkan, "vkGetPhysicalDeviceQueueFamilyProperties"));
vkGetPhysicalDeviceMemoryProperties = reinterpret_cast<PFN_vkGetPhysicalDeviceMemoryProperties>(dlsym(libvulkan, "vkGetPhysicalDeviceMemoryProperties"));
vkGetInstanceProcAddr = reinterpret_cast<PFN_vkGetInstanceProcAddr>(dlsym(libvulkan, "vkGetInstanceProcAddr"));
vkGetDeviceProcAddr = reinterpret_cast<PFN_vkGetDeviceProcAddr>(dlsym(libvulkan, "vkGetDeviceProcAddr"));
vkCreateDevice = reinterpret_cast<PFN_vkCreateDevice>(dlsym(libvulkan, "vkCreateDevice"));
vkDestroyDevice = reinterpret_cast<PFN_vkDestroyDevice>(dlsym(libvulkan, "vkDestroyDevice"));
vkEnumerateInstanceExtensionProperties = reinterpret_cast<PFN_vkEnumerateInstanceExtensionProperties>(dlsym(libvulkan, "vkEnumerateInstanceExtensionProperties"));
vkEnumerateDeviceExtensionProperties = reinterpret_cast<PFN_vkEnumerateDeviceExtensionProperties>(dlsym(libvulkan, "vkEnumerateDeviceExtensionProperties"));
vkEnumerateInstanceLayerProperties = reinterpret_cast<PFN_vkEnumerateInstanceLayerProperties>(dlsym(libvulkan, "vkEnumerateInstanceLayerProperties"));
vkEnumerateDeviceLayerProperties = reinterpret_cast<PFN_vkEnumerateDeviceLayerProperties>(dlsym(libvulkan, "vkEnumerateDeviceLayerProperties"));
vkGetDeviceQueue = reinterpret_cast<PFN_vkGetDeviceQueue>(dlsym(libvulkan, "vkGetDeviceQueue"));
vkQueueSubmit = reinterpret_cast<PFN_vkQueueSubmit>(dlsym(libvulkan, "vkQueueSubmit"));
vkQueueWaitIdle = reinterpret_cast<PFN_vkQueueWaitIdle>(dlsym(libvulkan, "vkQueueWaitIdle"));
vkDeviceWaitIdle = reinterpret_cast<PFN_vkDeviceWaitIdle>(dlsym(libvulkan, "vkDeviceWaitIdle"));
vkAllocateMemory = reinterpret_cast<PFN_vkAllocateMemory>(dlsym(libvulkan, "vkAllocateMemory"));
vkFreeMemory = reinterpret_cast<PFN_vkFreeMemory>(dlsym(libvulkan, "vkFreeMemory"));
vkMapMemory = reinterpret_cast<PFN_vkMapMemory>(dlsym(libvulkan, "vkMapMemory"));
vkUnmapMemory = reinterpret_cast<PFN_vkUnmapMemory>(dlsym(libvulkan, "vkUnmapMemory"));
vkFlushMappedMemoryRanges = reinterpret_cast<PFN_vkFlushMappedMemoryRanges>(dlsym(libvulkan, "vkFlushMappedMemoryRanges"));
vkInvalidateMappedMemoryRanges = reinterpret_cast<PFN_vkInvalidateMappedMemoryRanges>(dlsym(libvulkan, "vkInvalidateMappedMemoryRanges"));
vkGetDeviceMemoryCommitment = reinterpret_cast<PFN_vkGetDeviceMemoryCommitment>(dlsym(libvulkan, "vkGetDeviceMemoryCommitment"));
vkBindBufferMemory = reinterpret_cast<PFN_vkBindBufferMemory>(dlsym(libvulkan, "vkBindBufferMemory"));
vkBindImageMemory = reinterpret_cast<PFN_vkBindImageMemory>(dlsym(libvulkan, "vkBindImageMemory"));
vkGetBufferMemoryRequirements = reinterpret_cast<PFN_vkGetBufferMemoryRequirements>(dlsym(libvulkan, "vkGetBufferMemoryRequirements"));
vkGetImageMemoryRequirements = reinterpret_cast<PFN_vkGetImageMemoryRequirements>(dlsym(libvulkan, "vkGetImageMemoryRequirements"));
vkGetImageSparseMemoryRequirements = reinterpret_cast<PFN_vkGetImageSparseMemoryRequirements>(dlsym(libvulkan, "vkGetImageSparseMemoryRequirements"));
vkGetPhysicalDeviceSparseImageFormatProperties = reinterpret_cast<PFN_vkGetPhysicalDeviceSparseImageFormatProperties>(dlsym(libvulkan, "vkGetPhysicalDeviceSparseImageFormatProperties"));
vkQueueBindSparse = reinterpret_cast<PFN_vkQueueBindSparse>(dlsym(libvulkan, "vkQueueBindSparse"));
vkCreateFence = reinterpret_cast<PFN_vkCreateFence>(dlsym(libvulkan, "vkCreateFence"));
vkDestroyFence = reinterpret_cast<PFN_vkDestroyFence>(dlsym(libvulkan, "vkDestroyFence"));
vkResetFences = reinterpret_cast<PFN_vkResetFences>(dlsym(libvulkan, "vkResetFences"));
vkGetFenceStatus = reinterpret_cast<PFN_vkGetFenceStatus>(dlsym(libvulkan, "vkGetFenceStatus"));
vkWaitForFences = reinterpret_cast<PFN_vkWaitForFences>(dlsym(libvulkan, "vkWaitForFences"));
vkCreateSemaphore = reinterpret_cast<PFN_vkCreateSemaphore>(dlsym(libvulkan, "vkCreateSemaphore"));
vkDestroySemaphore = reinterpret_cast<PFN_vkDestroySemaphore>(dlsym(libvulkan, "vkDestroySemaphore"));
vkCreateEvent = reinterpret_cast<PFN_vkCreateEvent>(dlsym(libvulkan, "vkCreateEvent"));
vkDestroyEvent = reinterpret_cast<PFN_vkDestroyEvent>(dlsym(libvulkan, "vkDestroyEvent"));
vkGetEventStatus = reinterpret_cast<PFN_vkGetEventStatus>(dlsym(libvulkan, "vkGetEventStatus"));
vkSetEvent = reinterpret_cast<PFN_vkSetEvent>(dlsym(libvulkan, "vkSetEvent"));
vkResetEvent = reinterpret_cast<PFN_vkResetEvent>(dlsym(libvulkan, "vkResetEvent"));
vkCreateQueryPool = reinterpret_cast<PFN_vkCreateQueryPool>(dlsym(libvulkan, "vkCreateQueryPool"));
vkDestroyQueryPool = reinterpret_cast<PFN_vkDestroyQueryPool>(dlsym(libvulkan, "vkDestroyQueryPool"));
vkGetQueryPoolResults = reinterpret_cast<PFN_vkGetQueryPoolResults>(dlsym(libvulkan, "vkGetQueryPoolResults"));
vkCreateBuffer = reinterpret_cast<PFN_vkCreateBuffer>(dlsym(libvulkan, "vkCreateBuffer"));
vkDestroyBuffer = reinterpret_cast<PFN_vkDestroyBuffer>(dlsym(libvulkan, "vkDestroyBuffer"));
vkCreateBufferView = reinterpret_cast<PFN_vkCreateBufferView>(dlsym(libvulkan, "vkCreateBufferView"));
vkDestroyBufferView = reinterpret_cast<PFN_vkDestroyBufferView>(dlsym(libvulkan, "vkDestroyBufferView"));
vkCreateImage = reinterpret_cast<PFN_vkCreateImage>(dlsym(libvulkan, "vkCreateImage"));
vkDestroyImage = reinterpret_cast<PFN_vkDestroyImage>(dlsym(libvulkan, "vkDestroyImage"));
vkGetImageSubresourceLayout = reinterpret_cast<PFN_vkGetImageSubresourceLayout>(dlsym(libvulkan, "vkGetImageSubresourceLayout"));
vkCreateImageView = reinterpret_cast<PFN_vkCreateImageView>(dlsym(libvulkan, "vkCreateImageView"));
vkDestroyImageView = reinterpret_cast<PFN_vkDestroyImageView>(dlsym(libvulkan, "vkDestroyImageView"));
vkCreateShaderModule = reinterpret_cast<PFN_vkCreateShaderModule>(dlsym(libvulkan, "vkCreateShaderModule"));
vkDestroyShaderModule = reinterpret_cast<PFN_vkDestroyShaderModule>(dlsym(libvulkan, "vkDestroyShaderModule"));
vkCreatePipelineCache = reinterpret_cast<PFN_vkCreatePipelineCache>(dlsym(libvulkan, "vkCreatePipelineCache"));
vkDestroyPipelineCache = reinterpret_cast<PFN_vkDestroyPipelineCache>(dlsym(libvulkan, "vkDestroyPipelineCache"));
vkGetPipelineCacheData = reinterpret_cast<PFN_vkGetPipelineCacheData>(dlsym(libvulkan, "vkGetPipelineCacheData"));
vkMergePipelineCaches = reinterpret_cast<PFN_vkMergePipelineCaches>(dlsym(libvulkan, "vkMergePipelineCaches"));
vkCreateGraphicsPipelines = reinterpret_cast<PFN_vkCreateGraphicsPipelines>(dlsym(libvulkan, "vkCreateGraphicsPipelines"));
vkCreateComputePipelines = reinterpret_cast<PFN_vkCreateComputePipelines>(dlsym(libvulkan, "vkCreateComputePipelines"));
vkDestroyPipeline = reinterpret_cast<PFN_vkDestroyPipeline>(dlsym(libvulkan, "vkDestroyPipeline"));
vkCreatePipelineLayout = reinterpret_cast<PFN_vkCreatePipelineLayout>(dlsym(libvulkan, "vkCreatePipelineLayout"));
vkDestroyPipelineLayout = reinterpret_cast<PFN_vkDestroyPipelineLayout>(dlsym(libvulkan, "vkDestroyPipelineLayout"));
vkCreateSampler = reinterpret_cast<PFN_vkCreateSampler>(dlsym(libvulkan, "vkCreateSampler"));
vkDestroySampler = reinterpret_cast<PFN_vkDestroySampler>(dlsym(libvulkan, "vkDestroySampler"));
vkCreateDescriptorSetLayout = reinterpret_cast<PFN_vkCreateDescriptorSetLayout>(dlsym(libvulkan, "vkCreateDescriptorSetLayout"));
vkDestroyDescriptorSetLayout = reinterpret_cast<PFN_vkDestroyDescriptorSetLayout>(dlsym(libvulkan, "vkDestroyDescriptorSetLayout"));
vkCreateDescriptorPool = reinterpret_cast<PFN_vkCreateDescriptorPool>(dlsym(libvulkan, "vkCreateDescriptorPool"));
vkDestroyDescriptorPool = reinterpret_cast<PFN_vkDestroyDescriptorPool>(dlsym(libvulkan, "vkDestroyDescriptorPool"));
vkResetDescriptorPool = reinterpret_cast<PFN_vkResetDescriptorPool>(dlsym(libvulkan, "vkResetDescriptorPool"));
vkAllocateDescriptorSets = reinterpret_cast<PFN_vkAllocateDescriptorSets>(dlsym(libvulkan, "vkAllocateDescriptorSets"));
vkFreeDescriptorSets = reinterpret_cast<PFN_vkFreeDescriptorSets>(dlsym(libvulkan, "vkFreeDescriptorSets"));
vkUpdateDescriptorSets = reinterpret_cast<PFN_vkUpdateDescriptorSets>(dlsym(libvulkan, "vkUpdateDescriptorSets"));
vkCreateFramebuffer = reinterpret_cast<PFN_vkCreateFramebuffer>(dlsym(libvulkan, "vkCreateFramebuffer"));
vkDestroyFramebuffer = reinterpret_cast<PFN_vkDestroyFramebuffer>(dlsym(libvulkan, "vkDestroyFramebuffer"));
vkCreateRenderPass = reinterpret_cast<PFN_vkCreateRenderPass>(dlsym(libvulkan, "vkCreateRenderPass"));
vkDestroyRenderPass = reinterpret_cast<PFN_vkDestroyRenderPass>(dlsym(libvulkan, "vkDestroyRenderPass"));
vkGetRenderAreaGranularity = reinterpret_cast<PFN_vkGetRenderAreaGranularity>(dlsym(libvulkan, "vkGetRenderAreaGranularity"));
vkCreateCommandPool = reinterpret_cast<PFN_vkCreateCommandPool>(dlsym(libvulkan, "vkCreateCommandPool"));
vkDestroyCommandPool = reinterpret_cast<PFN_vkDestroyCommandPool>(dlsym(libvulkan, "vkDestroyCommandPool"));
vkResetCommandPool = reinterpret_cast<PFN_vkResetCommandPool>(dlsym(libvulkan, "vkResetCommandPool"));
vkAllocateCommandBuffers = reinterpret_cast<PFN_vkAllocateCommandBuffers>(dlsym(libvulkan, "vkAllocateCommandBuffers"));
vkFreeCommandBuffers = reinterpret_cast<PFN_vkFreeCommandBuffers>(dlsym(libvulkan, "vkFreeCommandBuffers"));
vkBeginCommandBuffer = reinterpret_cast<PFN_vkBeginCommandBuffer>(dlsym(libvulkan, "vkBeginCommandBuffer"));
vkEndCommandBuffer = reinterpret_cast<PFN_vkEndCommandBuffer>(dlsym(libvulkan, "vkEndCommandBuffer"));
vkResetCommandBuffer = reinterpret_cast<PFN_vkResetCommandBuffer>(dlsym(libvulkan, "vkResetCommandBuffer"));
vkCmdBindPipeline = reinterpret_cast<PFN_vkCmdBindPipeline>(dlsym(libvulkan, "vkCmdBindPipeline"));
vkCmdSetViewport = reinterpret_cast<PFN_vkCmdSetViewport>(dlsym(libvulkan, "vkCmdSetViewport"));
vkCmdSetScissor = reinterpret_cast<PFN_vkCmdSetScissor>(dlsym(libvulkan, "vkCmdSetScissor"));
vkCmdSetLineWidth = reinterpret_cast<PFN_vkCmdSetLineWidth>(dlsym(libvulkan, "vkCmdSetLineWidth"));
vkCmdSetDepthBias = reinterpret_cast<PFN_vkCmdSetDepthBias>(dlsym(libvulkan, "vkCmdSetDepthBias"));
vkCmdSetBlendConstants = reinterpret_cast<PFN_vkCmdSetBlendConstants>(dlsym(libvulkan, "vkCmdSetBlendConstants"));
vkCmdSetDepthBounds = reinterpret_cast<PFN_vkCmdSetDepthBounds>(dlsym(libvulkan, "vkCmdSetDepthBounds"));
vkCmdSetStencilCompareMask = reinterpret_cast<PFN_vkCmdSetStencilCompareMask>(dlsym(libvulkan, "vkCmdSetStencilCompareMask"));
vkCmdSetStencilWriteMask = reinterpret_cast<PFN_vkCmdSetStencilWriteMask>(dlsym(libvulkan, "vkCmdSetStencilWriteMask"));
vkCmdSetStencilReference = reinterpret_cast<PFN_vkCmdSetStencilReference>(dlsym(libvulkan, "vkCmdSetStencilReference"));
vkCmdBindDescriptorSets = reinterpret_cast<PFN_vkCmdBindDescriptorSets>(dlsym(libvulkan, "vkCmdBindDescriptorSets"));
vkCmdBindIndexBuffer = reinterpret_cast<PFN_vkCmdBindIndexBuffer>(dlsym(libvulkan, "vkCmdBindIndexBuffer"));
vkCmdBindVertexBuffers = reinterpret_cast<PFN_vkCmdBindVertexBuffers>(dlsym(libvulkan, "vkCmdBindVertexBuffers"));
vkCmdDraw = reinterpret_cast<PFN_vkCmdDraw>(dlsym(libvulkan, "vkCmdDraw"));
vkCmdDrawIndexed = reinterpret_cast<PFN_vkCmdDrawIndexed>(dlsym(libvulkan, "vkCmdDrawIndexed"));
vkCmdDrawIndirect = reinterpret_cast<PFN_vkCmdDrawIndirect>(dlsym(libvulkan, "vkCmdDrawIndirect"));
vkCmdDrawIndexedIndirect = reinterpret_cast<PFN_vkCmdDrawIndexedIndirect>(dlsym(libvulkan, "vkCmdDrawIndexedIndirect"));
vkCmdDispatch = reinterpret_cast<PFN_vkCmdDispatch>(dlsym(libvulkan, "vkCmdDispatch"));
vkCmdDispatchIndirect = reinterpret_cast<PFN_vkCmdDispatchIndirect>(dlsym(libvulkan, "vkCmdDispatchIndirect"));
vkCmdCopyBuffer = reinterpret_cast<PFN_vkCmdCopyBuffer>(dlsym(libvulkan, "vkCmdCopyBuffer"));
vkCmdCopyImage = reinterpret_cast<PFN_vkCmdCopyImage>(dlsym(libvulkan, "vkCmdCopyImage"));
vkCmdBlitImage = reinterpret_cast<PFN_vkCmdBlitImage>(dlsym(libvulkan, "vkCmdBlitImage"));
vkCmdCopyBufferToImage = reinterpret_cast<PFN_vkCmdCopyBufferToImage>(dlsym(libvulkan, "vkCmdCopyBufferToImage"));
vkCmdCopyImageToBuffer = reinterpret_cast<PFN_vkCmdCopyImageToBuffer>(dlsym(libvulkan, "vkCmdCopyImageToBuffer"));
vkCmdUpdateBuffer = reinterpret_cast<PFN_vkCmdUpdateBuffer>(dlsym(libvulkan, "vkCmdUpdateBuffer"));
vkCmdFillBuffer = reinterpret_cast<PFN_vkCmdFillBuffer>(dlsym(libvulkan, "vkCmdFillBuffer"));
vkCmdClearColorImage = reinterpret_cast<PFN_vkCmdClearColorImage>(dlsym(libvulkan, "vkCmdClearColorImage"));
vkCmdClearDepthStencilImage = reinterpret_cast<PFN_vkCmdClearDepthStencilImage>(dlsym(libvulkan, "vkCmdClearDepthStencilImage"));
vkCmdClearAttachments = reinterpret_cast<PFN_vkCmdClearAttachments>(dlsym(libvulkan, "vkCmdClearAttachments"));
vkCmdResolveImage = reinterpret_cast<PFN_vkCmdResolveImage>(dlsym(libvulkan, "vkCmdResolveImage"));
vkCmdSetEvent = reinterpret_cast<PFN_vkCmdSetEvent>(dlsym(libvulkan, "vkCmdSetEvent"));
vkCmdResetEvent = reinterpret_cast<PFN_vkCmdResetEvent>(dlsym(libvulkan, "vkCmdResetEvent"));
vkCmdWaitEvents = reinterpret_cast<PFN_vkCmdWaitEvents>(dlsym(libvulkan, "vkCmdWaitEvents"));
vkCmdPipelineBarrier = reinterpret_cast<PFN_vkCmdPipelineBarrier>(dlsym(libvulkan, "vkCmdPipelineBarrier"));
vkCmdBeginQuery = reinterpret_cast<PFN_vkCmdBeginQuery>(dlsym(libvulkan, "vkCmdBeginQuery"));
vkCmdEndQuery = reinterpret_cast<PFN_vkCmdEndQuery>(dlsym(libvulkan, "vkCmdEndQuery"));
vkCmdResetQueryPool = reinterpret_cast<PFN_vkCmdResetQueryPool>(dlsym(libvulkan, "vkCmdResetQueryPool"));
vkCmdWriteTimestamp = reinterpret_cast<PFN_vkCmdWriteTimestamp>(dlsym(libvulkan, "vkCmdWriteTimestamp"));
vkCmdCopyQueryPoolResults = reinterpret_cast<PFN_vkCmdCopyQueryPoolResults>(dlsym(libvulkan, "vkCmdCopyQueryPoolResults"));
vkCmdPushConstants = reinterpret_cast<PFN_vkCmdPushConstants>(dlsym(libvulkan, "vkCmdPushConstants"));
vkCmdBeginRenderPass = reinterpret_cast<PFN_vkCmdBeginRenderPass>(dlsym(libvulkan, "vkCmdBeginRenderPass"));
vkCmdNextSubpass = reinterpret_cast<PFN_vkCmdNextSubpass>(dlsym(libvulkan, "vkCmdNextSubpass"));
vkCmdEndRenderPass = reinterpret_cast<PFN_vkCmdEndRenderPass>(dlsym(libvulkan, "vkCmdEndRenderPass"));
vkCmdExecuteCommands = reinterpret_cast<PFN_vkCmdExecuteCommands>(dlsym(libvulkan, "vkCmdExecuteCommands"));
vkDestroySurfaceKHR = reinterpret_cast<PFN_vkDestroySurfaceKHR>(dlsym(libvulkan, "vkDestroySurfaceKHR"));
vkGetPhysicalDeviceSurfaceSupportKHR = reinterpret_cast<PFN_vkGetPhysicalDeviceSurfaceSupportKHR>(dlsym(libvulkan, "vkGetPhysicalDeviceSurfaceSupportKHR"));
vkGetPhysicalDeviceSurfaceCapabilitiesKHR = reinterpret_cast<PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR>(dlsym(libvulkan, "vkGetPhysicalDeviceSurfaceCapabilitiesKHR"));
vkGetPhysicalDeviceSurfaceFormatsKHR = reinterpret_cast<PFN_vkGetPhysicalDeviceSurfaceFormatsKHR>(dlsym(libvulkan, "vkGetPhysicalDeviceSurfaceFormatsKHR"));
vkGetPhysicalDeviceSurfacePresentModesKHR = reinterpret_cast<PFN_vkGetPhysicalDeviceSurfacePresentModesKHR>(dlsym(libvulkan, "vkGetPhysicalDeviceSurfacePresentModesKHR"));
vkCreateSwapchainKHR = reinterpret_cast<PFN_vkCreateSwapchainKHR>(dlsym(libvulkan, "vkCreateSwapchainKHR"));
vkDestroySwapchainKHR = reinterpret_cast<PFN_vkDestroySwapchainKHR>(dlsym(libvulkan, "vkDestroySwapchainKHR"));
vkGetSwapchainImagesKHR = reinterpret_cast<PFN_vkGetSwapchainImagesKHR>(dlsym(libvulkan, "vkGetSwapchainImagesKHR"));
vkAcquireNextImageKHR = reinterpret_cast<PFN_vkAcquireNextImageKHR>(dlsym(libvulkan, "vkAcquireNextImageKHR"));
vkQueuePresentKHR = reinterpret_cast<PFN_vkQueuePresentKHR>(dlsym(libvulkan, "vkQueuePresentKHR"));
vkGetPhysicalDeviceDisplayPropertiesKHR = reinterpret_cast<PFN_vkGetPhysicalDeviceDisplayPropertiesKHR>(dlsym(libvulkan, "vkGetPhysicalDeviceDisplayPropertiesKHR"));
vkGetPhysicalDeviceDisplayPlanePropertiesKHR = reinterpret_cast<PFN_vkGetPhysicalDeviceDisplayPlanePropertiesKHR>(dlsym(libvulkan, "vkGetPhysicalDeviceDisplayPlanePropertiesKHR"));
vkGetDisplayPlaneSupportedDisplaysKHR = reinterpret_cast<PFN_vkGetDisplayPlaneSupportedDisplaysKHR>(dlsym(libvulkan, "vkGetDisplayPlaneSupportedDisplaysKHR"));
vkGetDisplayModePropertiesKHR = reinterpret_cast<PFN_vkGetDisplayModePropertiesKHR>(dlsym(libvulkan, "vkGetDisplayModePropertiesKHR"));
vkCreateDisplayModeKHR = reinterpret_cast<PFN_vkCreateDisplayModeKHR>(dlsym(libvulkan, "vkCreateDisplayModeKHR"));
vkGetDisplayPlaneCapabilitiesKHR = reinterpret_cast<PFN_vkGetDisplayPlaneCapabilitiesKHR>(dlsym(libvulkan, "vkGetDisplayPlaneCapabilitiesKHR"));
vkCreateDisplayPlaneSurfaceKHR = reinterpret_cast<PFN_vkCreateDisplayPlaneSurfaceKHR>(dlsym(libvulkan, "vkCreateDisplayPlaneSurfaceKHR"));
vkCreateSharedSwapchainsKHR = reinterpret_cast<PFN_vkCreateSharedSwapchainsKHR>(dlsym(libvulkan, "vkCreateSharedSwapchainsKHR"));
#ifdef VK_USE_PLATFORM_XLIB_KHR
vkCreateXlibSurfaceKHR = reinterpret_cast<PFN_vkCreateXlibSurfaceKHR>(dlsym(libvulkan, "vkCreateXlibSurfaceKHR"));
vkGetPhysicalDeviceXlibPresentationSupportKHR = reinterpret_cast<PFN_vkGetPhysicalDeviceXlibPresentationSupportKHR>(dlsym(libvulkan, "vkGetPhysicalDeviceXlibPresentationSupportKHR"));
#endif
#ifdef VK_USE_PLATFORM_XCB_KHR
vkCreateXcbSurfaceKHR = reinterpret_cast<PFN_vkCreateXcbSurfaceKHR>(dlsym(libvulkan, "vkCreateXcbSurfaceKHR"));
vkGetPhysicalDeviceXcbPresentationSupportKHR = reinterpret_cast<PFN_vkGetPhysicalDeviceXcbPresentationSupportKHR>(dlsym(libvulkan, "vkGetPhysicalDeviceXcbPresentationSupportKHR"));
#endif
#ifdef VK_USE_PLATFORM_WAYLAND_KHR
vkCreateWaylandSurfaceKHR = reinterpret_cast<PFN_vkCreateWaylandSurfaceKHR>(dlsym(libvulkan, "vkCreateWaylandSurfaceKHR"));
vkGetPhysicalDeviceWaylandPresentationSupportKHR = reinterpret_cast<PFN_vkGetPhysicalDeviceWaylandPresentationSupportKHR>(dlsym(libvulkan, "vkGetPhysicalDeviceWaylandPresentationSupportKHR"));
#endif
#ifdef VK_USE_PLATFORM_MIR_KHR
vkCreateMirSurfaceKHR = reinterpret_cast<PFN_vkCreateMirSurfaceKHR>(dlsym(libvulkan, "vkCreateMirSurfaceKHR"));
vkGetPhysicalDeviceMirPresentationSupportKHR = reinterpret_cast<PFN_vkGetPhysicalDeviceMirPresentationSupportKHR>(dlsym(libvulkan, "vkGetPhysicalDeviceMirPresentationSupportKHR"));
#endif
#ifdef VK_USE_PLATFORM_ANDROID_KHR
vkCreateAndroidSurfaceKHR = reinterpret_cast<PFN_vkCreateAndroidSurfaceKHR>(dlsym(libvulkan, "vkCreateAndroidSurfaceKHR"));
#endif
#ifdef VK_USE_PLATFORM_WIN32_KHR
vkCreateWin32SurfaceKHR = reinterpret_cast<PFN_vkCreateWin32SurfaceKHR>(dlsym(libvulkan, "vkCreateWin32SurfaceKHR"));
vkGetPhysicalDeviceWin32PresentationSupportKHR = reinterpret_cast<PFN_vkGetPhysicalDeviceWin32PresentationSupportKHR>(dlsym(libvulkan, "vkGetPhysicalDeviceWin32PresentationSupportKHR"));
#endif
#ifdef USE_DEBUG_EXTENTIONS
vkCreateDebugReportCallbackEXT = reinterpret_cast<PFN_vkCreateDebugReportCallbackEXT>(dlsym(libvulkan, "vkCreateDebugReportCallbackEXT"));
vkDestroyDebugReportCallbackEXT = reinterpret_cast<PFN_vkDestroyDebugReportCallbackEXT>(dlsym(libvulkan, "vkDestroyDebugReportCallbackEXT"));
vkDebugReportMessageEXT = reinterpret_cast<PFN_vkDebugReportMessageEXT>(dlsym(libvulkan, "vkDebugReportMessageEXT"));
#endif
return 1;
}
// No Vulkan support, do not set function addresses
PFN_vkCreateInstance vkCreateInstance;
PFN_vkDestroyInstance vkDestroyInstance;
PFN_vkEnumeratePhysicalDevices vkEnumeratePhysicalDevices;
PFN_vkGetPhysicalDeviceFeatures vkGetPhysicalDeviceFeatures;
PFN_vkGetPhysicalDeviceFormatProperties vkGetPhysicalDeviceFormatProperties;
PFN_vkGetPhysicalDeviceImageFormatProperties vkGetPhysicalDeviceImageFormatProperties;
PFN_vkGetPhysicalDeviceProperties vkGetPhysicalDeviceProperties;
PFN_vkGetPhysicalDeviceQueueFamilyProperties vkGetPhysicalDeviceQueueFamilyProperties;
PFN_vkGetPhysicalDeviceMemoryProperties vkGetPhysicalDeviceMemoryProperties;
PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr;
PFN_vkGetDeviceProcAddr vkGetDeviceProcAddr;
PFN_vkCreateDevice vkCreateDevice;
PFN_vkDestroyDevice vkDestroyDevice;
PFN_vkEnumerateInstanceExtensionProperties vkEnumerateInstanceExtensionProperties;
PFN_vkEnumerateDeviceExtensionProperties vkEnumerateDeviceExtensionProperties;
PFN_vkEnumerateInstanceLayerProperties vkEnumerateInstanceLayerProperties;
PFN_vkEnumerateDeviceLayerProperties vkEnumerateDeviceLayerProperties;
PFN_vkGetDeviceQueue vkGetDeviceQueue;
PFN_vkQueueSubmit vkQueueSubmit;
PFN_vkQueueWaitIdle vkQueueWaitIdle;
PFN_vkDeviceWaitIdle vkDeviceWaitIdle;
PFN_vkAllocateMemory vkAllocateMemory;
PFN_vkFreeMemory vkFreeMemory;
PFN_vkMapMemory vkMapMemory;
PFN_vkUnmapMemory vkUnmapMemory;
PFN_vkFlushMappedMemoryRanges vkFlushMappedMemoryRanges;
PFN_vkInvalidateMappedMemoryRanges vkInvalidateMappedMemoryRanges;
PFN_vkGetDeviceMemoryCommitment vkGetDeviceMemoryCommitment;
PFN_vkBindBufferMemory vkBindBufferMemory;
PFN_vkBindImageMemory vkBindImageMemory;
PFN_vkGetBufferMemoryRequirements vkGetBufferMemoryRequirements;
PFN_vkGetImageMemoryRequirements vkGetImageMemoryRequirements;
PFN_vkGetImageSparseMemoryRequirements vkGetImageSparseMemoryRequirements;
PFN_vkGetPhysicalDeviceSparseImageFormatProperties vkGetPhysicalDeviceSparseImageFormatProperties;
PFN_vkQueueBindSparse vkQueueBindSparse;
PFN_vkCreateFence vkCreateFence;
PFN_vkDestroyFence vkDestroyFence;
PFN_vkResetFences vkResetFences;
PFN_vkGetFenceStatus vkGetFenceStatus;
PFN_vkWaitForFences vkWaitForFences;
PFN_vkCreateSemaphore vkCreateSemaphore;
PFN_vkDestroySemaphore vkDestroySemaphore;
PFN_vkCreateEvent vkCreateEvent;
PFN_vkDestroyEvent vkDestroyEvent;
PFN_vkGetEventStatus vkGetEventStatus;
PFN_vkSetEvent vkSetEvent;
PFN_vkResetEvent vkResetEvent;
PFN_vkCreateQueryPool vkCreateQueryPool;
PFN_vkDestroyQueryPool vkDestroyQueryPool;
PFN_vkGetQueryPoolResults vkGetQueryPoolResults;
PFN_vkCreateBuffer vkCreateBuffer;
PFN_vkDestroyBuffer vkDestroyBuffer;
PFN_vkCreateBufferView vkCreateBufferView;
PFN_vkDestroyBufferView vkDestroyBufferView;
PFN_vkCreateImage vkCreateImage;
PFN_vkDestroyImage vkDestroyImage;
PFN_vkGetImageSubresourceLayout vkGetImageSubresourceLayout;
PFN_vkCreateImageView vkCreateImageView;
PFN_vkDestroyImageView vkDestroyImageView;
PFN_vkCreateShaderModule vkCreateShaderModule;
PFN_vkDestroyShaderModule vkDestroyShaderModule;
PFN_vkCreatePipelineCache vkCreatePipelineCache;
PFN_vkDestroyPipelineCache vkDestroyPipelineCache;
PFN_vkGetPipelineCacheData vkGetPipelineCacheData;
PFN_vkMergePipelineCaches vkMergePipelineCaches;
PFN_vkCreateGraphicsPipelines vkCreateGraphicsPipelines;
PFN_vkCreateComputePipelines vkCreateComputePipelines;
PFN_vkDestroyPipeline vkDestroyPipeline;
PFN_vkCreatePipelineLayout vkCreatePipelineLayout;
PFN_vkDestroyPipelineLayout vkDestroyPipelineLayout;
PFN_vkCreateSampler vkCreateSampler;
PFN_vkDestroySampler vkDestroySampler;
PFN_vkCreateDescriptorSetLayout vkCreateDescriptorSetLayout;
PFN_vkDestroyDescriptorSetLayout vkDestroyDescriptorSetLayout;
PFN_vkCreateDescriptorPool vkCreateDescriptorPool;
PFN_vkDestroyDescriptorPool vkDestroyDescriptorPool;
PFN_vkResetDescriptorPool vkResetDescriptorPool;
PFN_vkAllocateDescriptorSets vkAllocateDescriptorSets;
PFN_vkFreeDescriptorSets vkFreeDescriptorSets;
PFN_vkUpdateDescriptorSets vkUpdateDescriptorSets;
PFN_vkCreateFramebuffer vkCreateFramebuffer;
PFN_vkDestroyFramebuffer vkDestroyFramebuffer;
PFN_vkCreateRenderPass vkCreateRenderPass;
PFN_vkDestroyRenderPass vkDestroyRenderPass;
PFN_vkGetRenderAreaGranularity vkGetRenderAreaGranularity;
PFN_vkCreateCommandPool vkCreateCommandPool;
PFN_vkDestroyCommandPool vkDestroyCommandPool;
PFN_vkResetCommandPool vkResetCommandPool;
PFN_vkAllocateCommandBuffers vkAllocateCommandBuffers;
PFN_vkFreeCommandBuffers vkFreeCommandBuffers;
PFN_vkBeginCommandBuffer vkBeginCommandBuffer;
PFN_vkEndCommandBuffer vkEndCommandBuffer;
PFN_vkResetCommandBuffer vkResetCommandBuffer;
PFN_vkCmdBindPipeline vkCmdBindPipeline;
PFN_vkCmdSetViewport vkCmdSetViewport;
PFN_vkCmdSetScissor vkCmdSetScissor;
PFN_vkCmdSetLineWidth vkCmdSetLineWidth;
PFN_vkCmdSetDepthBias vkCmdSetDepthBias;
PFN_vkCmdSetBlendConstants vkCmdSetBlendConstants;
PFN_vkCmdSetDepthBounds vkCmdSetDepthBounds;
PFN_vkCmdSetStencilCompareMask vkCmdSetStencilCompareMask;
PFN_vkCmdSetStencilWriteMask vkCmdSetStencilWriteMask;
PFN_vkCmdSetStencilReference vkCmdSetStencilReference;
PFN_vkCmdBindDescriptorSets vkCmdBindDescriptorSets;
PFN_vkCmdBindIndexBuffer vkCmdBindIndexBuffer;
PFN_vkCmdBindVertexBuffers vkCmdBindVertexBuffers;
PFN_vkCmdDraw vkCmdDraw;
PFN_vkCmdDrawIndexed vkCmdDrawIndexed;
PFN_vkCmdDrawIndirect vkCmdDrawIndirect;
PFN_vkCmdDrawIndexedIndirect vkCmdDrawIndexedIndirect;
PFN_vkCmdDispatch vkCmdDispatch;
PFN_vkCmdDispatchIndirect vkCmdDispatchIndirect;
PFN_vkCmdCopyBuffer vkCmdCopyBuffer;
PFN_vkCmdCopyImage vkCmdCopyImage;
PFN_vkCmdBlitImage vkCmdBlitImage;
PFN_vkCmdCopyBufferToImage vkCmdCopyBufferToImage;
PFN_vkCmdCopyImageToBuffer vkCmdCopyImageToBuffer;
PFN_vkCmdUpdateBuffer vkCmdUpdateBuffer;
PFN_vkCmdFillBuffer vkCmdFillBuffer;
PFN_vkCmdClearColorImage vkCmdClearColorImage;
PFN_vkCmdClearDepthStencilImage vkCmdClearDepthStencilImage;
PFN_vkCmdClearAttachments vkCmdClearAttachments;
PFN_vkCmdResolveImage vkCmdResolveImage;
PFN_vkCmdSetEvent vkCmdSetEvent;
PFN_vkCmdResetEvent vkCmdResetEvent;
PFN_vkCmdWaitEvents vkCmdWaitEvents;
PFN_vkCmdPipelineBarrier vkCmdPipelineBarrier;
PFN_vkCmdBeginQuery vkCmdBeginQuery;
PFN_vkCmdEndQuery vkCmdEndQuery;
PFN_vkCmdResetQueryPool vkCmdResetQueryPool;
PFN_vkCmdWriteTimestamp vkCmdWriteTimestamp;
PFN_vkCmdCopyQueryPoolResults vkCmdCopyQueryPoolResults;
PFN_vkCmdPushConstants vkCmdPushConstants;
PFN_vkCmdBeginRenderPass vkCmdBeginRenderPass;
PFN_vkCmdNextSubpass vkCmdNextSubpass;
PFN_vkCmdEndRenderPass vkCmdEndRenderPass;
PFN_vkCmdExecuteCommands vkCmdExecuteCommands;
PFN_vkDestroySurfaceKHR vkDestroySurfaceKHR;
PFN_vkGetPhysicalDeviceSurfaceSupportKHR vkGetPhysicalDeviceSurfaceSupportKHR;
PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR vkGetPhysicalDeviceSurfaceCapabilitiesKHR;
PFN_vkGetPhysicalDeviceSurfaceFormatsKHR vkGetPhysicalDeviceSurfaceFormatsKHR;
PFN_vkGetPhysicalDeviceSurfacePresentModesKHR vkGetPhysicalDeviceSurfacePresentModesKHR;
PFN_vkCreateSwapchainKHR vkCreateSwapchainKHR;
PFN_vkDestroySwapchainKHR vkDestroySwapchainKHR;
PFN_vkGetSwapchainImagesKHR vkGetSwapchainImagesKHR;
PFN_vkAcquireNextImageKHR vkAcquireNextImageKHR;
PFN_vkQueuePresentKHR vkQueuePresentKHR;
PFN_vkGetPhysicalDeviceDisplayPropertiesKHR vkGetPhysicalDeviceDisplayPropertiesKHR;
PFN_vkGetPhysicalDeviceDisplayPlanePropertiesKHR vkGetPhysicalDeviceDisplayPlanePropertiesKHR;
PFN_vkGetDisplayPlaneSupportedDisplaysKHR vkGetDisplayPlaneSupportedDisplaysKHR;
PFN_vkGetDisplayModePropertiesKHR vkGetDisplayModePropertiesKHR;
PFN_vkCreateDisplayModeKHR vkCreateDisplayModeKHR;
PFN_vkGetDisplayPlaneCapabilitiesKHR vkGetDisplayPlaneCapabilitiesKHR;
PFN_vkCreateDisplayPlaneSurfaceKHR vkCreateDisplayPlaneSurfaceKHR;
PFN_vkCreateSharedSwapchainsKHR vkCreateSharedSwapchainsKHR;
#ifdef VK_USE_PLATFORM_XLIB_KHR
PFN_vkCreateXlibSurfaceKHR vkCreateXlibSurfaceKHR;
PFN_vkGetPhysicalDeviceXlibPresentationSupportKHR vkGetPhysicalDeviceXlibPresentationSupportKHR;
#endif
#ifdef VK_USE_PLATFORM_XCB_KHR
PFN_vkCreateXcbSurfaceKHR vkCreateXcbSurfaceKHR;
PFN_vkGetPhysicalDeviceXcbPresentationSupportKHR vkGetPhysicalDeviceXcbPresentationSupportKHR;
#endif
#ifdef VK_USE_PLATFORM_WAYLAND_KHR
PFN_vkCreateWaylandSurfaceKHR vkCreateWaylandSurfaceKHR;
PFN_vkGetPhysicalDeviceWaylandPresentationSupportKHR vkGetPhysicalDeviceWaylandPresentationSupportKHR;
#endif
#ifdef VK_USE_PLATFORM_MIR_KHR
PFN_vkCreateMirSurfaceKHR vkCreateMirSurfaceKHR;
PFN_vkGetPhysicalDeviceMirPresentationSupportKHR vkGetPhysicalDeviceMirPresentationSupportKHR;
#endif
#ifdef VK_USE_PLATFORM_ANDROID_KHR
PFN_vkCreateAndroidSurfaceKHR vkCreateAndroidSurfaceKHR;
#endif
#ifdef VK_USE_PLATFORM_WIN32_KHR
PFN_vkCreateWin32SurfaceKHR vkCreateWin32SurfaceKHR;
PFN_vkGetPhysicalDeviceWin32PresentationSupportKHR vkGetPhysicalDeviceWin32PresentationSupportKHR;
#endif
PFN_vkCreateDebugReportCallbackEXT vkCreateDebugReportCallbackEXT;
PFN_vkDestroyDebugReportCallbackEXT vkDestroyDebugReportCallbackEXT;
PFN_vkDebugReportMessageEXT vkDebugReportMessageEXT;
@@ -0,0 +1,236 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This file is generated.
#ifndef VULKAN_WRAPPER_H
#define VULKAN_WRAPPER_H
#define VK_NO_PROTOTYPES 1
#include <vulkan/vulkan.h>
/* Initialize the Vulkan function pointer variables declared in this header.
* Returns 0 if vulkan is not available, non-zero if it is available.
*/
int InitVulkan(void);
// VK_core
extern PFN_vkCreateInstance vkCreateInstance;
extern PFN_vkDestroyInstance vkDestroyInstance;
extern PFN_vkEnumeratePhysicalDevices vkEnumeratePhysicalDevices;
extern PFN_vkGetPhysicalDeviceFeatures vkGetPhysicalDeviceFeatures;
extern PFN_vkGetPhysicalDeviceFormatProperties vkGetPhysicalDeviceFormatProperties;
extern PFN_vkGetPhysicalDeviceImageFormatProperties vkGetPhysicalDeviceImageFormatProperties;
extern PFN_vkGetPhysicalDeviceProperties vkGetPhysicalDeviceProperties;
extern PFN_vkGetPhysicalDeviceQueueFamilyProperties vkGetPhysicalDeviceQueueFamilyProperties;
extern PFN_vkGetPhysicalDeviceMemoryProperties vkGetPhysicalDeviceMemoryProperties;
extern PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr;
extern PFN_vkGetDeviceProcAddr vkGetDeviceProcAddr;
extern PFN_vkCreateDevice vkCreateDevice;
extern PFN_vkDestroyDevice vkDestroyDevice;
extern PFN_vkEnumerateInstanceExtensionProperties vkEnumerateInstanceExtensionProperties;
extern PFN_vkEnumerateDeviceExtensionProperties vkEnumerateDeviceExtensionProperties;
extern PFN_vkEnumerateInstanceLayerProperties vkEnumerateInstanceLayerProperties;
extern PFN_vkEnumerateDeviceLayerProperties vkEnumerateDeviceLayerProperties;
extern PFN_vkGetDeviceQueue vkGetDeviceQueue;
extern PFN_vkQueueSubmit vkQueueSubmit;
extern PFN_vkQueueWaitIdle vkQueueWaitIdle;
extern PFN_vkDeviceWaitIdle vkDeviceWaitIdle;
extern PFN_vkAllocateMemory vkAllocateMemory;
extern PFN_vkFreeMemory vkFreeMemory;
extern PFN_vkMapMemory vkMapMemory;
extern PFN_vkUnmapMemory vkUnmapMemory;
extern PFN_vkFlushMappedMemoryRanges vkFlushMappedMemoryRanges;
extern PFN_vkInvalidateMappedMemoryRanges vkInvalidateMappedMemoryRanges;
extern PFN_vkGetDeviceMemoryCommitment vkGetDeviceMemoryCommitment;
extern PFN_vkBindBufferMemory vkBindBufferMemory;
extern PFN_vkBindImageMemory vkBindImageMemory;
extern PFN_vkGetBufferMemoryRequirements vkGetBufferMemoryRequirements;
extern PFN_vkGetImageMemoryRequirements vkGetImageMemoryRequirements;
extern PFN_vkGetImageSparseMemoryRequirements vkGetImageSparseMemoryRequirements;
extern PFN_vkGetPhysicalDeviceSparseImageFormatProperties vkGetPhysicalDeviceSparseImageFormatProperties;
extern PFN_vkQueueBindSparse vkQueueBindSparse;
extern PFN_vkCreateFence vkCreateFence;
extern PFN_vkDestroyFence vkDestroyFence;
extern PFN_vkResetFences vkResetFences;
extern PFN_vkGetFenceStatus vkGetFenceStatus;
extern PFN_vkWaitForFences vkWaitForFences;
extern PFN_vkCreateSemaphore vkCreateSemaphore;
extern PFN_vkDestroySemaphore vkDestroySemaphore;
extern PFN_vkCreateEvent vkCreateEvent;
extern PFN_vkDestroyEvent vkDestroyEvent;
extern PFN_vkGetEventStatus vkGetEventStatus;
extern PFN_vkSetEvent vkSetEvent;
extern PFN_vkResetEvent vkResetEvent;
extern PFN_vkCreateQueryPool vkCreateQueryPool;
extern PFN_vkDestroyQueryPool vkDestroyQueryPool;
extern PFN_vkGetQueryPoolResults vkGetQueryPoolResults;
extern PFN_vkCreateBuffer vkCreateBuffer;
extern PFN_vkDestroyBuffer vkDestroyBuffer;
extern PFN_vkCreateBufferView vkCreateBufferView;
extern PFN_vkDestroyBufferView vkDestroyBufferView;
extern PFN_vkCreateImage vkCreateImage;
extern PFN_vkDestroyImage vkDestroyImage;
extern PFN_vkGetImageSubresourceLayout vkGetImageSubresourceLayout;
extern PFN_vkCreateImageView vkCreateImageView;
extern PFN_vkDestroyImageView vkDestroyImageView;
extern PFN_vkCreateShaderModule vkCreateShaderModule;
extern PFN_vkDestroyShaderModule vkDestroyShaderModule;
extern PFN_vkCreatePipelineCache vkCreatePipelineCache;
extern PFN_vkDestroyPipelineCache vkDestroyPipelineCache;
extern PFN_vkGetPipelineCacheData vkGetPipelineCacheData;
extern PFN_vkMergePipelineCaches vkMergePipelineCaches;
extern PFN_vkCreateGraphicsPipelines vkCreateGraphicsPipelines;
extern PFN_vkCreateComputePipelines vkCreateComputePipelines;
extern PFN_vkDestroyPipeline vkDestroyPipeline;
extern PFN_vkCreatePipelineLayout vkCreatePipelineLayout;
extern PFN_vkDestroyPipelineLayout vkDestroyPipelineLayout;
extern PFN_vkCreateSampler vkCreateSampler;
extern PFN_vkDestroySampler vkDestroySampler;
extern PFN_vkCreateDescriptorSetLayout vkCreateDescriptorSetLayout;
extern PFN_vkDestroyDescriptorSetLayout vkDestroyDescriptorSetLayout;
extern PFN_vkCreateDescriptorPool vkCreateDescriptorPool;
extern PFN_vkDestroyDescriptorPool vkDestroyDescriptorPool;
extern PFN_vkResetDescriptorPool vkResetDescriptorPool;
extern PFN_vkAllocateDescriptorSets vkAllocateDescriptorSets;
extern PFN_vkFreeDescriptorSets vkFreeDescriptorSets;
extern PFN_vkUpdateDescriptorSets vkUpdateDescriptorSets;
extern PFN_vkCreateFramebuffer vkCreateFramebuffer;
extern PFN_vkDestroyFramebuffer vkDestroyFramebuffer;
extern PFN_vkCreateRenderPass vkCreateRenderPass;
extern PFN_vkDestroyRenderPass vkDestroyRenderPass;
extern PFN_vkGetRenderAreaGranularity vkGetRenderAreaGranularity;
extern PFN_vkCreateCommandPool vkCreateCommandPool;
extern PFN_vkDestroyCommandPool vkDestroyCommandPool;
extern PFN_vkResetCommandPool vkResetCommandPool;
extern PFN_vkAllocateCommandBuffers vkAllocateCommandBuffers;
extern PFN_vkFreeCommandBuffers vkFreeCommandBuffers;
extern PFN_vkBeginCommandBuffer vkBeginCommandBuffer;
extern PFN_vkEndCommandBuffer vkEndCommandBuffer;
extern PFN_vkResetCommandBuffer vkResetCommandBuffer;
extern PFN_vkCmdBindPipeline vkCmdBindPipeline;
extern PFN_vkCmdSetViewport vkCmdSetViewport;
extern PFN_vkCmdSetScissor vkCmdSetScissor;
extern PFN_vkCmdSetLineWidth vkCmdSetLineWidth;
extern PFN_vkCmdSetDepthBias vkCmdSetDepthBias;
extern PFN_vkCmdSetBlendConstants vkCmdSetBlendConstants;
extern PFN_vkCmdSetDepthBounds vkCmdSetDepthBounds;
extern PFN_vkCmdSetStencilCompareMask vkCmdSetStencilCompareMask;
extern PFN_vkCmdSetStencilWriteMask vkCmdSetStencilWriteMask;
extern PFN_vkCmdSetStencilReference vkCmdSetStencilReference;
extern PFN_vkCmdBindDescriptorSets vkCmdBindDescriptorSets;
extern PFN_vkCmdBindIndexBuffer vkCmdBindIndexBuffer;
extern PFN_vkCmdBindVertexBuffers vkCmdBindVertexBuffers;
extern PFN_vkCmdDraw vkCmdDraw;
extern PFN_vkCmdDrawIndexed vkCmdDrawIndexed;
extern PFN_vkCmdDrawIndirect vkCmdDrawIndirect;
extern PFN_vkCmdDrawIndexedIndirect vkCmdDrawIndexedIndirect;
extern PFN_vkCmdDispatch vkCmdDispatch;
extern PFN_vkCmdDispatchIndirect vkCmdDispatchIndirect;
extern PFN_vkCmdCopyBuffer vkCmdCopyBuffer;
extern PFN_vkCmdCopyImage vkCmdCopyImage;
extern PFN_vkCmdBlitImage vkCmdBlitImage;
extern PFN_vkCmdCopyBufferToImage vkCmdCopyBufferToImage;
extern PFN_vkCmdCopyImageToBuffer vkCmdCopyImageToBuffer;
extern PFN_vkCmdUpdateBuffer vkCmdUpdateBuffer;
extern PFN_vkCmdFillBuffer vkCmdFillBuffer;
extern PFN_vkCmdClearColorImage vkCmdClearColorImage;
extern PFN_vkCmdClearDepthStencilImage vkCmdClearDepthStencilImage;
extern PFN_vkCmdClearAttachments vkCmdClearAttachments;
extern PFN_vkCmdResolveImage vkCmdResolveImage;
extern PFN_vkCmdSetEvent vkCmdSetEvent;
extern PFN_vkCmdResetEvent vkCmdResetEvent;
extern PFN_vkCmdWaitEvents vkCmdWaitEvents;
extern PFN_vkCmdPipelineBarrier vkCmdPipelineBarrier;
extern PFN_vkCmdBeginQuery vkCmdBeginQuery;
extern PFN_vkCmdEndQuery vkCmdEndQuery;
extern PFN_vkCmdResetQueryPool vkCmdResetQueryPool;
extern PFN_vkCmdWriteTimestamp vkCmdWriteTimestamp;
extern PFN_vkCmdCopyQueryPoolResults vkCmdCopyQueryPoolResults;
extern PFN_vkCmdPushConstants vkCmdPushConstants;
extern PFN_vkCmdBeginRenderPass vkCmdBeginRenderPass;
extern PFN_vkCmdNextSubpass vkCmdNextSubpass;
extern PFN_vkCmdEndRenderPass vkCmdEndRenderPass;
extern PFN_vkCmdExecuteCommands vkCmdExecuteCommands;
// VK_KHR_surface
extern PFN_vkDestroySurfaceKHR vkDestroySurfaceKHR;
extern PFN_vkGetPhysicalDeviceSurfaceSupportKHR vkGetPhysicalDeviceSurfaceSupportKHR;
extern PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR vkGetPhysicalDeviceSurfaceCapabilitiesKHR;
extern PFN_vkGetPhysicalDeviceSurfaceFormatsKHR vkGetPhysicalDeviceSurfaceFormatsKHR;
extern PFN_vkGetPhysicalDeviceSurfacePresentModesKHR vkGetPhysicalDeviceSurfacePresentModesKHR;
// VK_KHR_swapchain
extern PFN_vkCreateSwapchainKHR vkCreateSwapchainKHR;
extern PFN_vkDestroySwapchainKHR vkDestroySwapchainKHR;
extern PFN_vkGetSwapchainImagesKHR vkGetSwapchainImagesKHR;
extern PFN_vkAcquireNextImageKHR vkAcquireNextImageKHR;
extern PFN_vkQueuePresentKHR vkQueuePresentKHR;
// VK_KHR_display
extern PFN_vkGetPhysicalDeviceDisplayPropertiesKHR vkGetPhysicalDeviceDisplayPropertiesKHR;
extern PFN_vkGetPhysicalDeviceDisplayPlanePropertiesKHR vkGetPhysicalDeviceDisplayPlanePropertiesKHR;
extern PFN_vkGetDisplayPlaneSupportedDisplaysKHR vkGetDisplayPlaneSupportedDisplaysKHR;
extern PFN_vkGetDisplayModePropertiesKHR vkGetDisplayModePropertiesKHR;
extern PFN_vkCreateDisplayModeKHR vkCreateDisplayModeKHR;
extern PFN_vkGetDisplayPlaneCapabilitiesKHR vkGetDisplayPlaneCapabilitiesKHR;
extern PFN_vkCreateDisplayPlaneSurfaceKHR vkCreateDisplayPlaneSurfaceKHR;
// VK_KHR_display_swapchain
extern PFN_vkCreateSharedSwapchainsKHR vkCreateSharedSwapchainsKHR;
#ifdef VK_USE_PLATFORM_XLIB_KHR
// VK_KHR_xlib_surface
extern PFN_vkCreateXlibSurfaceKHR vkCreateXlibSurfaceKHR;
extern PFN_vkGetPhysicalDeviceXlibPresentationSupportKHR vkGetPhysicalDeviceXlibPresentationSupportKHR;
#endif
#ifdef VK_USE_PLATFORM_XCB_KHR
// VK_KHR_xcb_surface
extern PFN_vkCreateXcbSurfaceKHR vkCreateXcbSurfaceKHR;
extern PFN_vkGetPhysicalDeviceXcbPresentationSupportKHR vkGetPhysicalDeviceXcbPresentationSupportKHR;
#endif
#ifdef VK_USE_PLATFORM_WAYLAND_KHR
// VK_KHR_wayland_surface
extern PFN_vkCreateWaylandSurfaceKHR vkCreateWaylandSurfaceKHR;
extern PFN_vkGetPhysicalDeviceWaylandPresentationSupportKHR vkGetPhysicalDeviceWaylandPresentationSupportKHR;
#endif
#ifdef VK_USE_PLATFORM_MIR_KHR
// VK_KHR_mir_surface
extern PFN_vkCreateMirSurfaceKHR vkCreateMirSurfaceKHR;
extern PFN_vkGetPhysicalDeviceMirPresentationSupportKHR vkGetPhysicalDeviceMirPresentationSupportKHR;
#endif
#ifdef VK_USE_PLATFORM_ANDROID_KHR
// VK_KHR_android_surface
extern PFN_vkCreateAndroidSurfaceKHR vkCreateAndroidSurfaceKHR;
#endif
#ifdef VK_USE_PLATFORM_WIN32_KHR
// VK_KHR_win32_surface
extern PFN_vkCreateWin32SurfaceKHR vkCreateWin32SurfaceKHR;
extern PFN_vkGetPhysicalDeviceWin32PresentationSupportKHR vkGetPhysicalDeviceWin32PresentationSupportKHR;
#endif
#ifdef USE_DEBUG_EXTENTIONS
#include <vulkan/vk_sdk_platform.h>
// VK_EXT_debug_report
extern PFN_vkCreateDebugReportCallbackEXT vkCreateDebugReportCallbackEXT;
extern PFN_vkDestroyDebugReportCallbackEXT vkDestroyDebugReportCallbackEXT;
extern PFN_vkDebugReportMessageEXT vkDebugReportMessageEXT;
#endif
#endif // VULKAN_WRAPPER_H
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

@@ -0,0 +1,7 @@
package org.kenjinx.android
enum class BackendThreading {
Auto,
Off,
On
}
@@ -0,0 +1,9 @@
package org.kenjinx.android
import androidx.activity.ComponentActivity
abstract class BaseActivity : ComponentActivity() {
companion object {
val crashHandler = CrashHandler()
}
}
@@ -0,0 +1,95 @@
package org.kenjinx.android
import android.os.Build
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.lang.Thread.UncaughtExceptionHandler
/**
* Handles uncaught exceptions in the application and logs them to a file
* Location: src/main/java/org/kenjinx/android/CrashHandler.kt
*/
class CrashHandler : UncaughtExceptionHandler {
private var crashLog: StringBuilder = StringBuilder()
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
companion object {
private const val LOG_FOLDER = "Logs"
private const val CRASH_LOG_FILE = "crash.log"
private const val MAX_LOG_SIZE = 5 * 1024 * 1024 // 5MB
}
override fun uncaughtException(t: Thread, e: Throwable) {
try {
generateCrashLog(t, e)
saveCrashLog()
} catch (writeError: Exception) {
// If we can't write to the crash log, print to system log as fallback
writeError.printStackTrace()
}
}
private fun generateCrashLog(thread: Thread, throwable: Throwable) {
crashLog.apply {
append("\n=== Crash Report ===\n")
append("Date: ${dateFormat.format(Date())}\n")
// Device Information
append("\n=== Device Info ===\n")
append("Device: ${Build.MANUFACTURER} ${Build.MODEL}\n")
append("Android Version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})\n")
append("Device Fingerprint: ${Build.FINGERPRINT}\n")
// Thread Information
append("\n=== Thread Info ===\n")
append("Thread Name: ${thread.name}\n")
append("Thread ID: ${thread.id}\n")
append("Thread Priority: ${thread.priority}\n")
append("Thread State: ${thread.state}\n")
// Exception Details
append("\n=== Exception Info ===\n")
append("Exception Class: ${throwable.javaClass.name}\n")
append("Exception Message: ${throwable.message}\n")
// Stack Trace
append("\n=== Stack Trace ===\n")
val sw = StringWriter()
throwable.printStackTrace(PrintWriter(sw))
append(sw.toString())
// Cause (if any)
throwable.cause?.let { cause ->
append("\n=== Cause ===\n")
val causeSw = StringWriter()
cause.printStackTrace(PrintWriter(causeSw))
append(causeSw.toString())
}
append("\n=== End of Crash Report ===\n")
append("----------------------------------------\n")
}
}
private fun saveCrashLog() {
val logDir = File(MainActivity.AppPath, LOG_FOLDER).apply {
if (!exists()) mkdirs()
}
val crashLogFile = File(logDir, CRASH_LOG_FILE)
// Rotate log if it's too large
if (crashLogFile.exists() && crashLogFile.length() > MAX_LOG_SIZE) {
val backupFile = File(logDir, "${CRASH_LOG_FILE}.old")
if (backupFile.exists()) backupFile.delete()
crashLogFile.renameTo(backupFile)
}
// Write the crash log
crashLogFile.appendText(crashLog.toString())
}
}
@@ -0,0 +1,435 @@
package org.kenjinx.android
import android.app.Activity
import android.content.Context
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.math.MathUtils
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.swordfish.radialgamepad.library.RadialGamePad
import com.swordfish.radialgamepad.library.config.ButtonConfig
import com.swordfish.radialgamepad.library.config.CrossConfig
import com.swordfish.radialgamepad.library.config.CrossContentDescription
import com.swordfish.radialgamepad.library.config.PrimaryDialConfig
import com.swordfish.radialgamepad.library.config.RadialGamePadConfig
import com.swordfish.radialgamepad.library.config.SecondaryDialConfig
import com.swordfish.radialgamepad.library.event.Event
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import org.kenjinx.android.viewmodels.MainViewModel
import org.kenjinx.android.viewmodels.QuickSettings
typealias GamePad = RadialGamePad
typealias GamePadConfig = RadialGamePadConfig
class GameController(var activity: Activity) {
companion object {
private fun Create(context: Context, controller: GameController): View {
val inflator = LayoutInflater.from(context)
val view = inflator.inflate(R.layout.game_layout, null)
view.findViewById<FrameLayout>(R.id.leftcontainer)!!.addView(controller.leftGamePad)
view.findViewById<FrameLayout>(R.id.rightcontainer)!!.addView(controller.rightGamePad)
return view
}
@Composable
fun Compose(viewModel: MainViewModel): Unit {
AndroidView(
modifier = Modifier.fillMaxSize(), factory = { context ->
val controller = GameController(viewModel.activity)
val c = Create(context, controller)
viewModel.activity.lifecycleScope.apply {
viewModel.activity.lifecycleScope.launch {
val events = merge(
controller.leftGamePad.events(),
controller.rightGamePad.events()
)
.shareIn(viewModel.activity.lifecycleScope, SharingStarted.Lazily)
events.safeCollect {
controller.handleEvent(it)
}
}
}
controller.controllerView = c
viewModel.setGameController(controller)
controller.setVisible(QuickSettings(viewModel.activity).useVirtualController)
c
})
}
}
private var controllerView: View? = null
var leftGamePad: GamePad
var rightGamePad: GamePad
var controllerId: Int = -1
val isVisible: Boolean
get() {
controllerView?.apply {
return this.isVisible
}
return false
}
init {
leftGamePad = GamePad(generateConfig(true), 16f, activity)
rightGamePad = GamePad(generateConfig(false), 16f, activity)
leftGamePad.primaryDialMaxSizeDp = 200f
rightGamePad.primaryDialMaxSizeDp = 200f
leftGamePad.gravityX = -1f
leftGamePad.gravityY = 1f
rightGamePad.gravityX = 1f
rightGamePad.gravityY = 1f
}
fun setVisible(isVisible: Boolean) {
controllerView?.apply {
this.isVisible = isVisible
if (isVisible)
connect()
}
}
fun connect() {
if (controllerId == -1)
controllerId = KenjinxNative.jnaInstance.inputConnectGamepad(0)
}
private fun handleEvent(ev: Event) {
if (controllerId == -1)
controllerId = KenjinxNative.jnaInstance.inputConnectGamepad(0)
controllerId.apply {
when (ev) {
is Event.Button -> {
val action = ev.action
when (action) {
KeyEvent.ACTION_UP -> {
KenjinxNative.jnaInstance.inputSetButtonReleased(ev.id, this)
}
KeyEvent.ACTION_DOWN -> {
KenjinxNative.jnaInstance.inputSetButtonPressed(ev.id, this)
}
}
}
is Event.Direction -> {
val direction = ev.id
when (direction) {
GamePadButtonInputId.DpadUp.ordinal -> {
if (ev.xAxis > 0) {
KenjinxNative.jnaInstance.inputSetButtonPressed(
GamePadButtonInputId.DpadRight.ordinal,
this
)
KenjinxNative.jnaInstance.inputSetButtonReleased(
GamePadButtonInputId.DpadLeft.ordinal,
this
)
} else if (ev.xAxis < 0) {
KenjinxNative.jnaInstance.inputSetButtonPressed(
GamePadButtonInputId.DpadLeft.ordinal,
this
)
KenjinxNative.jnaInstance.inputSetButtonReleased(
GamePadButtonInputId.DpadRight.ordinal,
this
)
} else {
KenjinxNative.jnaInstance.inputSetButtonReleased(
GamePadButtonInputId.DpadLeft.ordinal,
this
)
KenjinxNative.jnaInstance.inputSetButtonReleased(
GamePadButtonInputId.DpadRight.ordinal,
this
)
}
if (ev.yAxis < 0) {
KenjinxNative.jnaInstance.inputSetButtonPressed(
GamePadButtonInputId.DpadUp.ordinal,
this
)
KenjinxNative.jnaInstance.inputSetButtonReleased(
GamePadButtonInputId.DpadDown.ordinal,
this
)
} else if (ev.yAxis > 0) {
KenjinxNative.jnaInstance.inputSetButtonPressed(
GamePadButtonInputId.DpadDown.ordinal,
this
)
KenjinxNative.jnaInstance.inputSetButtonReleased(
GamePadButtonInputId.DpadUp.ordinal,
this
)
} else {
KenjinxNative.jnaInstance.inputSetButtonReleased(
GamePadButtonInputId.DpadDown.ordinal,
this
)
KenjinxNative.jnaInstance.inputSetButtonReleased(
GamePadButtonInputId.DpadUp.ordinal,
this
)
}
}
GamePadButtonInputId.LeftStick.ordinal -> {
val setting = QuickSettings(activity)
val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f)
val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f)
KenjinxNative.jnaInstance.inputSetStickAxis(
1,
x,
-y,
this
)
}
GamePadButtonInputId.RightStick.ordinal -> {
val setting = QuickSettings(activity)
val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f)
val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f)
KenjinxNative.jnaInstance.inputSetStickAxis(
2,
x,
-y,
this
)
}
}
}
}
}
}
}
suspend fun <T> Flow<T>.safeCollect(
block: suspend (T) -> Unit
) {
this.catch {}
.collect {
block(it)
}
}
private fun generateConfig(isLeft: Boolean): GamePadConfig {
val distance = 0.3f
val buttonScale = 1f
if (isLeft) {
return GamePadConfig(
12,
PrimaryDialConfig.Stick(
GamePadButtonInputId.LeftStick.ordinal,
GamePadButtonInputId.LeftStickButton.ordinal,
setOf(),
"LeftStick",
null
),
listOf(
SecondaryDialConfig.Cross(
10,
3,
2.5f,
distance,
CrossConfig(
GamePadButtonInputId.DpadUp.ordinal,
CrossConfig.Shape.STANDARD,
null,
setOf(),
CrossContentDescription(),
true,
null
),
SecondaryDialConfig.RotationProcessor()
),
SecondaryDialConfig.SingleButton(
1,
buttonScale,
distance,
ButtonConfig(
GamePadButtonInputId.Minus.ordinal,
"-",
true,
null,
"Minus",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
SecondaryDialConfig.DoubleButton(
2,
distance,
ButtonConfig(
GamePadButtonInputId.LeftShoulder.ordinal,
"L",
true,
null,
"LeftBumper",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
SecondaryDialConfig.SingleButton(
9,
buttonScale,
distance,
ButtonConfig(
GamePadButtonInputId.LeftTrigger.ordinal,
"ZL",
true,
null,
"LeftTrigger",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
)
)
} else {
return GamePadConfig(
12,
PrimaryDialConfig.PrimaryButtons(
listOf(
ButtonConfig(
GamePadButtonInputId.A.ordinal,
"A",
true,
null,
"A",
setOf(),
true,
null
),
ButtonConfig(
GamePadButtonInputId.X.ordinal,
"X",
true,
null,
"X",
setOf(),
true,
null
),
ButtonConfig(
GamePadButtonInputId.Y.ordinal,
"Y",
true,
null,
"Y",
setOf(),
true,
null
),
ButtonConfig(
GamePadButtonInputId.B.ordinal,
"B",
true,
null,
"B",
setOf(),
true,
null
)
),
null,
0f,
true,
null
),
listOf(
SecondaryDialConfig.Stick(
7,
2,
2f,
distance,
GamePadButtonInputId.RightStick.ordinal,
GamePadButtonInputId.RightStickButton.ordinal,
null,
setOf(),
"RightStick",
SecondaryDialConfig.RotationProcessor()
),
SecondaryDialConfig.SingleButton(
6,
buttonScale,
distance,
ButtonConfig(
GamePadButtonInputId.Plus.ordinal,
"+",
true,
null,
"Plus",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
SecondaryDialConfig.DoubleButton(
3,
distance,
ButtonConfig(
GamePadButtonInputId.RightShoulder.ordinal,
"R",
true,
null,
"RightBumper",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
SecondaryDialConfig.SingleButton(
9,
buttonScale,
distance,
ButtonConfig(
GamePadButtonInputId.RightTrigger.ordinal,
"ZR",
true,
null,
"RightTrigger",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
)
)
)
}
}
@@ -0,0 +1,180 @@
package org.kenjinx.android
import android.annotation.SuppressLint
import android.content.Context
import android.view.SurfaceHolder
import android.view.SurfaceView
import androidx.compose.runtime.MutableState
import org.kenjinx.android.viewmodels.GameModel
import org.kenjinx.android.viewmodels.MainViewModel
import kotlin.concurrent.thread
@SuppressLint("ViewConstructor")
class GameHost(context: Context?, private val mainViewModel: MainViewModel) : SurfaceView(context),
SurfaceHolder.Callback {
private var _currentWindow: Long = -1
private var isProgressHidden: Boolean = false
private var progress: MutableState<String>? = null
private var progressValue: MutableState<Float>? = null
private var showLoading: MutableState<Boolean>? = null
private var game: GameModel? = null
private var _isClosed: Boolean = false
private var _renderingThreadWatcher: Thread? = null
private var _height: Int = 0
private var _width: Int = 0
private var _updateThread: Thread? = null
private var _guestThread: Thread? = null
private var _isInit: Boolean = false
private var _isStarted: Boolean = false
private val _nativeWindow: NativeWindow
val currentSurface:Long
get() {
return _currentWindow
}
val currentWindowhandle: Long
get() {
return _nativeWindow.nativePointer
}
init {
holder.addCallback(this)
_nativeWindow = NativeWindow(this)
mainViewModel.gameHost = this
}
override fun surfaceCreated(holder: SurfaceHolder) {
// no-op
}
fun setProgress(info : String, progressVal: Float) {
showLoading?.apply {
progressValue?.apply {
this.value = progressVal
}
progress?.apply {
this.value = info
}
}
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
if (_isClosed)
return
if (_width != width || _height != height) {
_currentWindow = _nativeWindow.requeryWindowHandle()
_nativeWindow.swapInterval = 0
}
_width = width
_height = height
start(holder)
KenjinxNative.jnaInstance.graphicsRendererSetSize(
width,
height
)
if (_isStarted) {
KenjinxNative.jnaInstance.inputSetClientSize(width, height)
}
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
// no-op
}
fun close() {
_isClosed = true
_isInit = false
_isStarted = false
KenjinxNative.jnaInstance.uiHandlerSetResponse(false, "")
_updateThread?.join()
_renderingThreadWatcher?.join()
}
private fun start(surfaceHolder: SurfaceHolder) {
if (_isStarted)
return
_isStarted = true
game = if (mainViewModel.isMiiEditorLaunched) null else mainViewModel.gameModel
KenjinxNative.jnaInstance.inputInitialize(width, height)
val id = mainViewModel.physicalControllerManager?.connect()
mainViewModel.motionSensorManager?.setControllerId(id ?: -1)
KenjinxNative.jnaInstance.graphicsRendererSetSize(
surfaceHolder.surfaceFrame.width(),
surfaceHolder.surfaceFrame.height()
)
NativeHelpers.instance.setIsInitialOrientationFlipped(mainViewModel.activity.display?.rotation == 3)
_guestThread = thread(start = true) {
runGame()
}
_updateThread = thread(start = true) {
var c = 0
val helper = NativeHelpers.instance
while (_isStarted) {
KenjinxNative.jnaInstance.inputUpdate()
Thread.sleep(1)
c++
if (c >= 1000) {
if (progressValue?.value == -1f)
progress?.apply {
this.value =
"Loading ${if (mainViewModel.isMiiEditorLaunched) "Mii Editor" else game!!.titleName}"
}
c = 0
mainViewModel.updateStats(
KenjinxNative.jnaInstance.deviceGetGameFifo(),
KenjinxNative.jnaInstance.deviceGetGameFrameRate(),
KenjinxNative.jnaInstance.deviceGetGameFrameTime()
)
}
}
}
}
private fun runGame() {
KenjinxNative.jnaInstance.graphicsRendererRunLoop()
game?.close()
}
fun setProgressStates(
showLoading: MutableState<Boolean>?,
progressValue: MutableState<Float>?,
progress: MutableState<String>?
) {
this.showLoading = showLoading
this.progressValue = progressValue
this.progress = progress
showLoading?.apply {
showLoading.value = !isProgressHidden
}
}
fun hideProgressIndicator() {
isProgressHidden = true
showLoading?.apply {
if (value == isProgressHidden)
value = !isProgressHidden
}
}
}
@@ -0,0 +1,27 @@
package org.kenjinx.android
enum class GamePadButtonInputId {
None,
// Buttons
A,
B,
X,
Y,
LeftStickButton,
RightStickButton,
LeftShoulder,
RightShoulder,
LeftTrigger,
RightTrigger,
DpadUp,
DpadDown,
DpadLeft,
DpadRight,
Minus,
Plus,
// Stick
LeftStick,
RightStick
}
@@ -0,0 +1,169 @@
package org.kenjinx.android
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import androidx.compose.runtime.MutableState
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.SimpleStorageHelper
import com.anggrayudi.storage.callback.FileCallback
import com.anggrayudi.storage.file.copyFileTo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
class Helpers {
companion object {
fun getPath(context: Context, uri: Uri): String? {
// DocumentProvider
if (DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":".toRegex()).toTypedArray()
val type = split[0]
if ("primary".equals(type, ignoreCase = true)) {
return Environment.getExternalStorageDirectory().toString() + "/" + split[1]
}
} else if (isDownloadsDocument(uri)) {
val id = DocumentsContract.getDocumentId(uri)
val contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"),
java.lang.Long.valueOf(id)
)
return getDataColumn(context, contentUri, null, null)
} else if (isMediaDocument(uri)) {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":".toRegex()).toTypedArray()
val type = split[0]
var contentUri: Uri? = null
when (type) {
"image" -> {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
"video" -> {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
"audio" -> {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
}
val selection = "_id=?"
val selectionArgs = arrayOf(split[1])
return getDataColumn(context, contentUri, selection, selectionArgs)
}
} else if ("content".equals(uri.scheme, ignoreCase = true)) {
return getDataColumn(context, uri, null, null)
} else if ("file".equals(uri.scheme, ignoreCase = true)) {
return uri.path
}
return null
}
fun copyToData(
file: DocumentFile, path: String, storageHelper: SimpleStorageHelper,
isCopying: MutableState<Boolean>,
copyProgress: MutableState<Float>,
currentProgressName: MutableState<String>,
finish: () -> Unit
) {
var fPath = path + "/${file.name}"
var callback: FileCallback? = object : FileCallback() {
override fun onFailed(errorCode: ErrorCode) {
super.onFailed(errorCode)
File(fPath).delete()
finish()
}
override fun onStart(file: Any, workerThread: Thread): Long {
copyProgress.value = 0f
(file as DocumentFile).apply {
currentProgressName.value = "Copying ${file.name}"
}
return super.onStart(file, workerThread)
}
override fun onReport(report: Report) {
super.onReport(report)
if (!isCopying.value) {
Thread.currentThread().interrupt()
}
copyProgress.value = report.progress / 100f
}
override fun onCompleted(result: Any) {
super.onCompleted(result)
isCopying.value = false
finish()
}
}
val ioScope = CoroutineScope(Dispatchers.IO)
isCopying.value = true
File(fPath).delete()
file.apply {
val f = this
ioScope.launch {
f.copyFileTo(
storageHelper.storage.context,
File(path),
callback = callback!!
)
}
}
}
private fun getDataColumn(
context: Context,
uri: Uri?,
selection: String?,
selectionArgs: Array<String>?
): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(column)
try {
cursor = uri?.let {
context.contentResolver.query(
it,
projection,
selection,
selectionArgs,
null
)
}
if (cursor != null && cursor.moveToFirst()) {
val column_index: Int = cursor.getColumnIndexOrThrow(column)
return cursor.getString(column_index)
}
} finally {
cursor?.close()
}
return null
}
private fun isExternalStorageDocument(uri: Uri): Boolean {
return "com.android.externalstorage.documents" == uri.authority
}
private fun isDownloadsDocument(uri: Uri): Boolean {
return "com.android.providers.downloads.documents" == uri.authority
}
private fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
package org.kenjinx.android
import android.app.Application
import android.content.Context
import java.io.File
class KenjinxApplication : Application() {
init {
instance = this
}
fun getPublicFilesDir(): File = getExternalFilesDir(null) ?: filesDir
companion object {
lateinit var instance: KenjinxApplication
private set
val context: Context get() = instance.applicationContext
}
}
@@ -0,0 +1,163 @@
package org.kenjinx.android
import com.sun.jna.JNIEnv
import com.sun.jna.Library
import com.sun.jna.Native
import org.kenjinx.android.viewmodels.GameInfo
import java.util.Collections
interface KenjinxNativeJna : Library {
fun deviceInitialize(
memoryManagerMode: Int,
useNce: Boolean,
memoryConfiguration: Int,
systemLanguage: Int,
regionCode: Int,
vSyncMode: Int,
enableDockedMode: Boolean,
enablePptc: Boolean,
enableInternetAccess: Boolean,
enableFsIntegrityChecks: Boolean,
fsGlobalAccessLogMode: Int,
timeZone: String,
ignoreMissingServices: Boolean
): Boolean
fun graphicsInitialize(
rescale: Float = 1f,
maxAnisotropy: Float = 0f,
fastGpuTime: Boolean = true,
fast2DCopy: Boolean = true,
enableMacroJit: Boolean = false,
enableMacroHLE: Boolean = true,
enableShaderCache: Boolean = true,
enableTextureRecompression: Boolean = false,
backendThreading: Int = BackendThreading.Auto.ordinal
): Boolean
fun graphicsInitializeRenderer(
extensions: Array<String>,
extensionsLength: Int,
driver: Long
): Boolean
fun javaInitialize(appPath: String, env: JNIEnv): Boolean
fun deviceLaunchMiiEditor(): Boolean
fun deviceGetGameFrameRate(): Double
fun deviceGetGameFrameTime(): Double
fun deviceGetGameFifo(): Double
fun deviceLoadDescriptor(fileDescriptor: Int, gameType: Int, updateDescriptor: Int): Boolean
fun graphicsRendererSetSize(width: Int, height: Int)
fun graphicsRendererSetVsync(vSyncMode: Int)
fun graphicsRendererRunLoop()
fun deviceReloadFilesystem()
fun inputInitialize(width: Int, height: Int)
fun inputSetClientSize(width: Int, height: Int)
fun inputSetTouchPoint(x: Int, y: Int)
fun inputReleaseTouchPoint()
fun inputUpdate()
fun inputSetButtonPressed(button: Int, id: Int)
fun inputSetButtonReleased(button: Int, id: Int)
fun inputConnectGamepad(index: Int): Int
fun inputSetStickAxis(stick: Int, x: Float, y: Float, id: Int)
fun inputSetAccelerometerData(x: Float, y: Float, z: Float, id: Int)
fun inputSetGyroData(x: Float, y: Float, z: Float, id: Int)
fun deviceCloseEmulation()
fun deviceReinitEmulation()
fun deviceSignalEmulationClose()
fun userGetOpenedUser(): String
fun userGetUserPicture(userId: String): String
fun userSetUserPicture(userId: String, picture: String)
fun userGetUserName(userId: String): String
fun userSetUserName(userId: String, userName: String)
fun userAddUser(username: String, picture: String)
fun userDeleteUser(userId: String)
fun userOpenUser(userId: String)
fun userCloseUser(userId: String)
fun loggingSetEnabled(logLevel: Int, enabled: Boolean)
fun deviceVerifyFirmware(fileDescriptor: Int, isXci: Boolean): String
fun deviceInstallFirmware(fileDescriptor: Int, isXci: Boolean)
fun deviceGetInstalledFirmwareVersion(): String
fun uiHandlerSetup()
fun uiHandlerSetResponse(isOkPressed: Boolean, input: String)
fun deviceGetDlcTitleId(path: String, ncaPath: String): String
fun deviceGetGameInfo(fileDescriptor: Int, extension: String, info: GameInfo)
fun userGetAllUsers(): Array<String>
fun deviceGetDlcContentList(path: String, titleId: Long): Array<String>
fun loggingEnabledGraphicsLog(enabled: Boolean)
}
class KenjinxNative {
companion object {
val jnaInstance: KenjinxNativeJna = Native.load(
"kenjinx",
KenjinxNativeJna::class.java,
Collections.singletonMap(Library.OPTION_ALLOW_OBJECTS, true)
)
@JvmStatic
fun test()
{
val i = 0
}
@JvmStatic
fun frameEnded()
{
MainActivity.frameEnded()
}
@JvmStatic
fun getSurfacePtr() : Long
{
return MainActivity.mainViewModel?.gameHost?.currentSurface ?: -1
}
@JvmStatic
fun getWindowHandle() : Long
{
return MainActivity.mainViewModel?.gameHost?.currentWindowhandle ?: -1
}
@JvmStatic
fun updateProgress(infoPtr : Long, progress: Float)
{
val info = NativeHelpers.instance.getStringJava(infoPtr);
MainActivity.mainViewModel?.gameHost?.setProgress(info, progress)
}
@JvmStatic
fun updateUiHandler(
newTitlePointer: Long,
newMessagePointer: Long,
newWatermarkPointer: Long,
newType: Int,
min: Int,
max: Int,
nMode: Int,
newSubtitlePointer: Long,
newInitialTextPointer: Long
)
{
var uiHandler = MainActivity.mainViewModel?.activity?.uiHandler
uiHandler?.apply {
val newTitle = NativeHelpers.instance.getStringJava(newTitlePointer)
val newMessage = NativeHelpers.instance.getStringJava(newMessagePointer)
val newWatermark = NativeHelpers.instance.getStringJava(newWatermarkPointer)
val newSubtitle = NativeHelpers.instance.getStringJava(newSubtitlePointer)
val newInitialText = NativeHelpers.instance.getStringJava(newInitialTextPointer)
val newMode = KeyboardMode.entries[nMode]
update(newTitle,
newMessage,
newWatermark,
newType,
min,
max,
newMode,
newSubtitle,
newInitialText);
}
}
}
}
@@ -0,0 +1,63 @@
package org.kenjinx.android
import android.content.Intent
import androidx.core.content.FileProvider
import net.lingala.zip4j.ZipFile
import org.kenjinx.android.viewmodels.MainViewModel
import java.io.File
import java.net.URLConnection
class Logging(private var viewModel: MainViewModel) {
val logPath = MainActivity.AppPath + "/Logs"
init {
File(logPath).mkdirs()
}
fun requestExport() {
val files = File(logPath).listFiles()
files?.apply {
val zipExportPath = MainActivity.AppPath + "/log.zip"
File(zipExportPath).delete()
var count = 0
if (files.isNotEmpty()) {
val zipFile = ZipFile(zipExportPath)
for (file in files) {
if (file.isFile) {
zipFile.addFile(file)
count++
}
}
zipFile.close()
}
if (count > 0) {
val zip = File(zipExportPath)
val uri = FileProvider.getUriForFile(
viewModel.activity,
viewModel.activity.packageName + ".fileprovider",
zip
)
val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_STREAM, uri)
intent.setDataAndType(uri, URLConnection.guessContentTypeFromName(zip.name))
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val chooser = Intent.createChooser(intent, "Share logs")
viewModel.activity.startActivity(chooser)
} else {
File(zipExportPath).delete()
}
}
}
fun clearLogs() {
if (File(logPath).exists()) {
File(logPath).deleteRecursively()
}
File(logPath).mkdirs()
}
}
internal enum class LogLevel {
Debug, Stub, Info, Warning, Error, Guest, AccessLog, Notice, Trace
}
@@ -0,0 +1,253 @@
package org.kenjinx.android
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Environment
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.WindowManager
import android.content.Intent
import android.os.Handler
import android.os.Looper
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.SimpleStorageHelper
import com.sun.jna.JNIEnv
import org.kenjinx.android.ui.theme.KenjinxAndroidTheme
import org.kenjinx.android.viewmodels.MainViewModel
import org.kenjinx.android.viewmodels.QuickSettings
import org.kenjinx.android.viewmodels.GameModel
import org.kenjinx.android.views.MainView
class MainActivity : BaseActivity() {
private var physicalControllerManager: PhysicalControllerManager =
PhysicalControllerManager(this)
private lateinit var motionSensorManager: MotionSensorManager
private var _isInit: Boolean = false
private val handler = Handler(Looper.getMainLooper())
private val delayedHandleIntent = object : Runnable {
override fun run() {
handleIntent()
}
}
var storedIntent: Intent = Intent()
var isGameRunning = false
var isActive = false
var storageHelper: SimpleStorageHelper? = null
lateinit var uiHandler: UiHandler
companion object {
var mainViewModel: MainViewModel? = null
var AppPath: String = ""
var StorageHelper: SimpleStorageHelper? = null
val performanceMonitor = PerformanceMonitor()
@JvmStatic
fun frameEnded() {
mainViewModel?.activity?.apply {
if (isActive && QuickSettings(this).enablePerformanceMode) {
mainViewModel?.performanceManager?.setTurboMode(true)
}
}
mainViewModel?.gameHost?.hideProgressIndicator()
}
}
init {
storageHelper = SimpleStorageHelper(this)
StorageHelper = storageHelper
System.loadLibrary("kenjinxjni")
initVm()
}
private external fun initVm()
private fun initialize() {
if (_isInit)
return
val appPath: String = AppPath
var quickSettings = QuickSettings(this)
KenjinxNative.jnaInstance.loggingSetEnabled(
LogLevel.Info.ordinal,
quickSettings.enableInfoLogs
)
KenjinxNative.jnaInstance.loggingSetEnabled(
LogLevel.Stub.ordinal,
quickSettings.enableStubLogs
)
KenjinxNative.jnaInstance.loggingSetEnabled(
LogLevel.Warning.ordinal,
quickSettings.enableWarningLogs
)
KenjinxNative.jnaInstance.loggingSetEnabled(
LogLevel.Error.ordinal,
quickSettings.enableErrorLogs
)
KenjinxNative.jnaInstance.loggingSetEnabled(
LogLevel.AccessLog.ordinal,
quickSettings.enableFsAccessLogs
)
KenjinxNative.jnaInstance.loggingSetEnabled(
LogLevel.Guest.ordinal,
quickSettings.enableGuestLogs
)
KenjinxNative.jnaInstance.loggingSetEnabled(
LogLevel.Trace.ordinal,
quickSettings.enableTraceLogs
)
KenjinxNative.jnaInstance.loggingSetEnabled(
LogLevel.Debug.ordinal,
quickSettings.enableDebugLogs
)
KenjinxNative.jnaInstance.loggingEnabledGraphicsLog(
quickSettings.enableGraphicsLogs
)
val success =
KenjinxNative.jnaInstance.javaInitialize(appPath, JNIEnv.CURRENT)
_isInit = success
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
motionSensorManager = MotionSensorManager(this)
Thread.setDefaultUncaughtExceptionHandler(crashHandler)
if (
!Environment.isExternalStorageManager()
) {
storageHelper?.storage?.requestFullStorageAccess()
}
AppPath = this.getExternalFilesDir(null)!!.absolutePath
initialize()
window.attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
WindowCompat.setDecorFitsSystemWindows(window, false)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
controller.hide(WindowInsetsCompat.Type.systemBars())
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
mainViewModel = MainViewModel(this)
mainViewModel!!.physicalControllerManager = physicalControllerManager
mainViewModel!!.motionSensorManager = motionSensorManager
mainViewModel!!.refreshFirmwareVersion()
mainViewModel?.apply {
setContent {
KenjinxAndroidTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MainView.Main(mainViewModel = this)
}
}
}
}
storedIntent = intent
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
storedIntent = intent
}
override fun onSaveInstanceState(outState: Bundle) {
storageHelper?.onSaveInstanceState(outState)
super.onSaveInstanceState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
storageHelper?.onRestoreInstanceState(savedInstanceState)
}
@SuppressLint("RestrictedApi")
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
event.apply {
if (physicalControllerManager.onKeyEvent(this))
return true
}
return super.dispatchKeyEvent(event)
}
override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean {
ev?.apply {
physicalControllerManager.onMotionEvent(this)
}
return super.dispatchGenericMotionEvent(ev)
}
override fun onStop() {
super.onStop()
isActive = false
if (isGameRunning) {
mainViewModel?.performanceManager?.setTurboMode(false)
}
}
override fun onResume() {
super.onResume()
handler.postDelayed(delayedHandleIntent, 10)
isActive = true
if (isGameRunning) {
if (QuickSettings(this).enableMotion)
motionSensorManager.register()
}
}
override fun onPause() {
super.onPause()
isActive = true
if (isGameRunning) {
mainViewModel?.performanceManager?.setTurboMode(false)
}
motionSensorManager.unregister()
}
private fun handleIntent() {
when (storedIntent.action) {
Intent.ACTION_VIEW, "org.kenjinx.android.LAUNCH_GAME" -> {
val bootPath = storedIntent.getStringExtra("bootPath")
val forceNceAndPptc = storedIntent.getBooleanExtra("forceNceAndPptc",false)
if (bootPath != null) {
val uri = android.net.Uri.parse(bootPath)
val documentFile = DocumentFile.fromSingleUri(this, uri)
if (documentFile != null) {
val gameModel = GameModel(documentFile, this)
gameModel.getGameInfo()
mainViewModel?.loadGameModel?.value = gameModel
mainViewModel?.bootPath?.value = "gameItem_${gameModel.titleName}"
mainViewModel?.forceNceAndPptc?.value = forceNceAndPptc
}
}
}
}
}
}
@@ -0,0 +1,139 @@
package org.kenjinx.android
import android.app.Activity
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener2
import android.hardware.SensorManager
import android.view.OrientationEventListener
class MotionSensorManager(val activity: MainActivity) : SensorEventListener2 {
private var isRegistered: Boolean = false
private var gyro: Sensor?
private var accelerometer: Sensor?
private var sensorManager: SensorManager =
activity.getSystemService(Activity.SENSOR_SERVICE) as SensorManager
private var controllerId: Int = -1
private val motionGyroOrientation: FloatArray = FloatArray(3)
private val motionAcelOrientation: FloatArray = FloatArray(3)
init {
accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
gyro = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
setOrientation90()
var orientationListener = object : OrientationEventListener(activity) {
override fun onOrientationChanged(orientation: Int) {
when {
isWithinOrientationRange(orientation, 270) -> {
setOrientation270()
}
isWithinOrientationRange(orientation, 90) -> {
setOrientation90()
}
}
}
private fun isWithinOrientationRange(
currentOrientation: Int, targetOrientation: Int, epsilon: Int = 90
): Boolean {
return currentOrientation > targetOrientation - epsilon
&& currentOrientation < targetOrientation + epsilon
}
}
}
fun setOrientation270() {
motionGyroOrientation[0] = -1.0f
motionGyroOrientation[1] = 1.0f
motionGyroOrientation[2] = 1.0f
motionAcelOrientation[0] = 1.0f
motionAcelOrientation[1] = -1.0f
motionAcelOrientation[2] = -1.0f
}
fun setOrientation90() {
motionGyroOrientation[0] = 1.0f
motionGyroOrientation[1] = -1.0f
motionGyroOrientation[2] = 1.0f
motionAcelOrientation[0] = -1.0f
motionAcelOrientation[1] = 1.0f
motionAcelOrientation[2] = -1.0f
}
fun setControllerId(id: Int) {
controllerId = id
}
fun register() {
if (isRegistered)
return
gyro?.apply {
sensorManager.registerListener(
this@MotionSensorManager,
gyro,
SensorManager.SENSOR_DELAY_GAME
)
}
accelerometer?.apply {
sensorManager.registerListener(
this@MotionSensorManager,
accelerometer,
SensorManager.SENSOR_DELAY_GAME
)
}
isRegistered = true
}
fun unregister() {
sensorManager.unregisterListener(this)
isRegistered = false
if (controllerId != -1) {
KenjinxNative.jnaInstance.inputSetAccelerometerData(0.0F, 0.0F, 0.0F, controllerId)
KenjinxNative.jnaInstance.inputSetGyroData(0.0F, 0.0F, 0.0F, controllerId)
}
}
override fun onSensorChanged(event: SensorEvent?) {
if (controllerId != -1)
if (isRegistered)
event?.apply {
when (sensor.type) {
Sensor.TYPE_ACCELEROMETER -> {
val x = motionAcelOrientation[0] * event.values[1]
val y = motionAcelOrientation[1] * event.values[0]
val z = motionAcelOrientation[2] * event.values[2]
KenjinxNative.jnaInstance.inputSetAccelerometerData(
x,
y,
z,
controllerId
)
}
Sensor.TYPE_GYROSCOPE -> {
val x = motionGyroOrientation[0] * event.values[1]
val y = motionGyroOrientation[1] * event.values[0]
val z = motionGyroOrientation[2] * event.values[2]
KenjinxNative.jnaInstance.inputSetGyroData(x, y, z, controllerId)
}
}
}
else {
KenjinxNative.jnaInstance.inputSetAccelerometerData(0.0F, 0.0F, 0.0F, controllerId)
KenjinxNative.jnaInstance.inputSetGyroData(0.0F, 0.0F, 0.0F, controllerId)
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
// no-op
}
override fun onFlushCompleted(sensor: Sensor?) {
// no-op
}
}
@@ -0,0 +1,7 @@
package org.kenjinx.android
class NativeGraphicsInterop {
var VkCreateSurface: Long = 0
var SurfaceHandle: Long = 0
var VkRequiredExtensions: Array<String>? = null
}
@@ -0,0 +1,31 @@
package org.kenjinx.android
import android.view.Surface
class NativeHelpers {
companion object {
val instance = NativeHelpers()
init {
System.loadLibrary("kenjinxjni")
}
}
external fun releaseNativeWindow(window: Long)
external fun getCreateSurfacePtr(): Long
external fun getNativeWindow(surface: Surface): Long
external fun loadDriver(
nativeLibPath: String,
privateAppsPath: String,
driverName: String
): Long
external fun setTurboMode(enable: Boolean)
external fun getMaxSwapInterval(nativeWindow: Long): Int
external fun getMinSwapInterval(nativeWindow: Long): Int
external fun setSwapInterval(nativeWindow: Long, swapInterval: Int): Int
external fun getStringJava(ptr: Long): String
external fun setIsInitialOrientationFlipped(isFlipped: Boolean)
}
@@ -0,0 +1,42 @@
package org.kenjinx.android
import android.view.SurfaceView
class NativeWindow(val surface: SurfaceView) {
var nativePointer: Long
private val nativeHelpers: NativeHelpers = NativeHelpers.instance
private var _swapInterval: Int = 0
var maxSwapInterval: Int = 0
get() {
return if (nativePointer == -1L) 0 else nativeHelpers.getMaxSwapInterval(nativePointer)
}
var minSwapInterval: Int = 0
get() {
return if (nativePointer == -1L) 0 else nativeHelpers.getMinSwapInterval(nativePointer)
}
var swapInterval: Int
get() {
return _swapInterval
}
set(value) {
if (nativePointer == -1L || nativeHelpers.setSwapInterval(nativePointer, value) == 0)
_swapInterval = value
}
init {
nativePointer = nativeHelpers.getNativeWindow(surface.holder.surface)
swapInterval = maxOf(1, minSwapInterval)
}
fun requeryWindowHandle(): Long {
nativePointer = nativeHelpers.getNativeWindow(surface.holder.surface)
swapInterval = swapInterval
return nativePointer
}
}
@@ -0,0 +1,32 @@
package org.kenjinx.android
import android.content.Intent
import kotlin.math.abs
class PerformanceManager(private val activity: MainActivity) {
companion object {
fun force60HzRefreshRate(enable: Boolean, activity: MainActivity) {
// Hack for MIUI devices since they don't support the standard Android APIs
try {
val setFpsIntent = Intent("com.miui.powerkeeper.SET_ACTIVITY_FPS").apply {
putExtra("package_name", "org.kenjinx.android")
putExtra("isEnter", enable)
}
activity.sendBroadcast(setFpsIntent)
} catch (_: Exception) {
}
if (enable)
activity.display?.supportedModes?.minByOrNull { abs(it.refreshRate - 60f) }
?.let { activity.window.attributes.preferredDisplayModeId = it.modeId }
else
activity.display?.supportedModes?.maxByOrNull { it.refreshRate }
?.let { activity.window.attributes.preferredDisplayModeId = it.modeId }
}
}
fun setTurboMode(enable: Boolean) {
NativeHelpers.instance.setTurboMode(enable)
force60HzRefreshRate(enable, activity)
}
}
@@ -0,0 +1,43 @@
package org.kenjinx.android
import android.app.ActivityManager
import android.content.Context.ACTIVITY_SERVICE
import android.util.Log
import androidx.compose.runtime.MutableState
import java.io.RandomAccessFile
class PerformanceMonitor {
val numberOfCores = Runtime.getRuntime().availableProcessors()
fun getFrequencies(frequencies: MutableList<Double>){
frequencies.clear()
for (i in 0 until numberOfCores) {
var freq = 0.0
try {
RandomAccessFile("/sys/devices/system/cpu/cpu${i}/cpufreq/scaling_cur_freq", "r").use { reader->
val f = reader.readLine()
freq = f.toDouble() / 1000.0
}
} catch (e: Exception) {
Log.e("Performance Monitor", "Failed to read CPU freq", e);
}
frequencies.add(freq)
}
}
fun getMemoryUsage(
usedMem: MutableState<Int>,
totalMem: MutableState<Int>) {
MainActivity.mainViewModel?.activity?.apply {
val actManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
val memInfo = ActivityManager.MemoryInfo()
actManager.getMemoryInfo(memInfo)
val availMemory = memInfo.availMem.toDouble() / (1024 * 1024)
val totalMemory = memInfo.totalMem.toDouble() / (1024 * 1024)
usedMem.value = (totalMemory - availMemory).toInt()
totalMem.value = totalMemory.toInt()
}
}
}
@@ -0,0 +1,158 @@
package org.kenjinx.android
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import org.kenjinx.android.viewmodels.QuickSettings
class PhysicalControllerManager(val activity: MainActivity) {
private var controllerId: Int = -1
fun onKeyEvent(event: KeyEvent): Boolean {
val id = getGamePadButtonInputId(event.keyCode)
if (id != GamePadButtonInputId.None) {
val isNotFallback = (event.flags and KeyEvent.FLAG_FALLBACK) == 0
if (/*controllerId != -1 &&*/ isNotFallback) {
when (event.action) {
KeyEvent.ACTION_UP -> {
KenjinxNative.jnaInstance.inputSetButtonReleased(id.ordinal, controllerId)
}
KeyEvent.ACTION_DOWN -> {
KenjinxNative.jnaInstance.inputSetButtonPressed(id.ordinal, controllerId)
}
}
return true
} //else if (!isNotFallback) {
return true
// }
}
return false
}
fun onMotionEvent(ev: MotionEvent) {
// if (true) {
if (ev.action == MotionEvent.ACTION_MOVE) {
val leftStickX = ev.getAxisValue(MotionEvent.AXIS_X)
val leftStickY = ev.getAxisValue(MotionEvent.AXIS_Y)
val rightStickX = ev.getAxisValue(MotionEvent.AXIS_Z)
val rightStickY = ev.getAxisValue(MotionEvent.AXIS_RZ)
KenjinxNative.jnaInstance.inputSetStickAxis(
1,
leftStickX,
-leftStickY,
controllerId
)
KenjinxNative.jnaInstance.inputSetStickAxis(
2,
rightStickX,
-rightStickY,
controllerId
)
ev.device?.apply {
if (sources and InputDevice.SOURCE_DPAD != InputDevice.SOURCE_DPAD) {
// Controller uses HAT
val dPadHor = ev.getAxisValue(MotionEvent.AXIS_HAT_X)
val dPadVert = ev.getAxisValue(MotionEvent.AXIS_HAT_Y)
if (dPadVert == 0.0f) {
KenjinxNative.jnaInstance.inputSetButtonReleased(
GamePadButtonInputId.DpadUp.ordinal,
controllerId
)
KenjinxNative.jnaInstance.inputSetButtonReleased(
GamePadButtonInputId.DpadDown.ordinal,
controllerId
)
}
if (dPadHor == 0.0f) {
KenjinxNative.jnaInstance.inputSetButtonReleased(
GamePadButtonInputId.DpadLeft.ordinal,
controllerId
)
KenjinxNative.jnaInstance.inputSetButtonReleased(
GamePadButtonInputId.DpadRight.ordinal,
controllerId
)
}
if (dPadVert < 0.0f) {
KenjinxNative.jnaInstance.inputSetButtonPressed(
GamePadButtonInputId.DpadUp.ordinal,
controllerId
)
KenjinxNative.jnaInstance.inputSetButtonReleased(
GamePadButtonInputId.DpadDown.ordinal,
controllerId
)
}
if (dPadHor < 0.0f) {
KenjinxNative.jnaInstance.inputSetButtonPressed(
GamePadButtonInputId.DpadLeft.ordinal,
controllerId
)
KenjinxNative.jnaInstance.inputSetButtonReleased(
GamePadButtonInputId.DpadRight.ordinal,
controllerId
)
}
if (dPadVert > 0.0f) {
KenjinxNative.jnaInstance.inputSetButtonReleased(
GamePadButtonInputId.DpadUp.ordinal,
controllerId
)
KenjinxNative.jnaInstance.inputSetButtonPressed(
GamePadButtonInputId.DpadDown.ordinal,
controllerId
)
}
if (dPadHor > 0.0f) {
KenjinxNative.jnaInstance.inputSetButtonReleased(
GamePadButtonInputId.DpadLeft.ordinal,
controllerId
)
KenjinxNative.jnaInstance.inputSetButtonPressed(
GamePadButtonInputId.DpadRight.ordinal,
controllerId
)
}
}
}
}
// }
}
fun connect(): Int {
controllerId = KenjinxNative.jnaInstance.inputConnectGamepad(0)
return controllerId
}
fun disconnect() {
controllerId = -1
}
private fun getGamePadButtonInputId(keycode: Int): GamePadButtonInputId {
val quickSettings = QuickSettings(activity)
return when (keycode) {
KeyEvent.KEYCODE_BUTTON_A -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.A else GamePadButtonInputId.B
KeyEvent.KEYCODE_BUTTON_B -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.B else GamePadButtonInputId.A
KeyEvent.KEYCODE_BUTTON_X -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.X else GamePadButtonInputId.Y
KeyEvent.KEYCODE_BUTTON_Y -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.Y else GamePadButtonInputId.X
KeyEvent.KEYCODE_BUTTON_L1 -> GamePadButtonInputId.LeftShoulder
KeyEvent.KEYCODE_BUTTON_L2 -> GamePadButtonInputId.LeftTrigger
KeyEvent.KEYCODE_BUTTON_R1 -> GamePadButtonInputId.RightShoulder
KeyEvent.KEYCODE_BUTTON_R2 -> GamePadButtonInputId.RightTrigger
KeyEvent.KEYCODE_BUTTON_THUMBL -> GamePadButtonInputId.LeftStick
KeyEvent.KEYCODE_BUTTON_THUMBR -> GamePadButtonInputId.RightStick
KeyEvent.KEYCODE_DPAD_UP -> GamePadButtonInputId.DpadUp
KeyEvent.KEYCODE_DPAD_DOWN -> GamePadButtonInputId.DpadDown
KeyEvent.KEYCODE_DPAD_LEFT -> GamePadButtonInputId.DpadLeft
KeyEvent.KEYCODE_DPAD_RIGHT -> GamePadButtonInputId.DpadRight
KeyEvent.KEYCODE_BUTTON_START -> GamePadButtonInputId.Plus
KeyEvent.KEYCODE_BUTTON_SELECT -> GamePadButtonInputId.Minus
else -> GamePadButtonInputId.None
}
}
}
@@ -0,0 +1,11 @@
package org.kenjinx.android
enum class RegionCode {
Japan,
USA,
Europe,
Australia,
China,
Korea,
Taiwan,
}
@@ -0,0 +1,22 @@
package org.kenjinx.android
enum class SystemLanguage {
Japanese,
AmericanEnglish,
French,
German,
Italian,
Spanish,
Chinese,
Korean,
Dutch,
Portuguese,
Russian,
Taiwanese,
BritishEnglish,
CanadianFrench,
LatinAmericanSpanish,
SimplifiedChinese,
TraditionalChinese,
BrazilianPortuguese
}
@@ -0,0 +1,220 @@
package org.kenjinx.android
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.material3.RichText
import org.kenjinx.android.widgets.SimpleAlertDialog
enum class KeyboardMode {
Default, Numeric, ASCII, FullLatin, Alphabet, SimplifiedChinese, TraditionalChinese, Korean, LanguageSet2, LanguageSet2Latin
}
class UiHandler {
private var initialText: String = ""
private var subtitle: String = ""
private var maxLength: Int = 0
private var minLength: Int = 0
private var watermark: String = ""
private var type: Int = -1
private var mode: KeyboardMode = KeyboardMode.Default
val showMessage = mutableStateOf(false)
val inputText = mutableStateOf("")
var title: String = ""
var message: String = ""
init {
KenjinxNative.jnaInstance.uiHandlerSetup()
}
fun update(
newTitle: String,
newMessage: String,
newWatermark: String,
newType: Int,
min: Int,
max: Int,
newMode: KeyboardMode,
newSubtitle: String,
newInitialText: String
)
{
title = newTitle
message = newMessage
watermark = newWatermark
type = newType
minLength = min
maxLength = max
mode = newMode
subtitle = newSubtitle
initialText = newInitialText
inputText.value = initialText
showMessage.value = type > 0
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Compose() {
val showMessageListener = remember {
showMessage
}
val inputListener = remember {
inputText
}
val validation = remember {
mutableStateOf("")
}
fun validate(): Boolean {
if (inputText.value.isEmpty()) {
validation.value = "Must be between ${minLength} and ${maxLength} characters"
} else {
return inputText.value.length < minLength || inputText.value.length > maxLength
}
return false
}
fun getInputType(): KeyboardType {
return when (mode) {
KeyboardMode.Default -> KeyboardType.Text
KeyboardMode.Numeric -> KeyboardType.Decimal
KeyboardMode.ASCII -> KeyboardType.Ascii
else -> {
KeyboardType.Text
}
}
}
fun submit() {
if (type == 2) {
if (inputListener.value.length < minLength || inputListener.value.length > maxLength)
return
}
KenjinxNative.jnaInstance.uiHandlerSetResponse(
true,
if (type == 2) inputListener.value else ""
)
showMessageListener.value = false
}
SimpleAlertDialog.Custom(
showDialog = showMessageListener,
onDismissRequest = { },
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false)
) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = title,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
textAlign = TextAlign.Center
)
Column(
modifier = Modifier
.height(128.dp)
.verticalScroll(rememberScrollState())
.padding(8.dp),
verticalArrangement = Arrangement.Center
) {
RichText {
Markdown(content = message)
}
if (type == 2) {
validate()
if (watermark.isNotEmpty()) {
TextField(
value = inputListener.value,
onValueChange = { inputListener.value = it },
modifier = Modifier
.fillMaxWidth()
.padding(4.dp),
label = {
Text(text = watermark)
},
keyboardOptions = KeyboardOptions(keyboardType = getInputType()),
isError = validate()
)
} else {
TextField(
value = inputListener.value,
onValueChange = { inputListener.value = it },
modifier = Modifier
.fillMaxWidth()
.padding(4.dp),
keyboardOptions = KeyboardOptions(
keyboardType = getInputType(),
imeAction = ImeAction.Done
),
isError = validate(),
singleLine = true,
keyboardActions = KeyboardActions(onDone = { submit() })
)
}
if (subtitle.isNotEmpty()) {
Text(text = subtitle)
}
Text(text = validation.value)
}
}
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
) {
Button(
onClick = { submit() },
modifier = Modifier
.weight(1f)
.padding(horizontal = 8.dp)
) {
Text(text = "Ok")
}
}
}
}
}
}
@@ -0,0 +1,300 @@
package org.kenjinx.android.providers
import android.database.Cursor
import android.database.MatrixCursor
import android.os.CancellationSignal
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract
import android.provider.DocumentsProvider
import android.webkit.MimeTypeMap
import org.kenjinx.android.BuildConfig
import org.kenjinx.android.R
import org.kenjinx.android.KenjinxApplication
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
class DocumentProvider : DocumentsProvider() {
private val baseDirectory = File(KenjinxApplication.instance.getPublicFilesDir().canonicalPath)
private val applicationName = "Kenji-NX"
companion object {
private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf(
DocumentsContract.Root.COLUMN_ROOT_ID,
DocumentsContract.Root.COLUMN_MIME_TYPES,
DocumentsContract.Root.COLUMN_FLAGS,
DocumentsContract.Root.COLUMN_ICON,
DocumentsContract.Root.COLUMN_TITLE,
DocumentsContract.Root.COLUMN_SUMMARY,
DocumentsContract.Root.COLUMN_DOCUMENT_ID,
DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
)
private val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf(
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_MIME_TYPE,
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
DocumentsContract.Document.COLUMN_FLAGS,
DocumentsContract.Document.COLUMN_SIZE
)
const val AUTHORITY: String = BuildConfig.APPLICATION_ID + ".providers"
const val ROOT_ID: String = "root"
}
override fun onCreate(): Boolean {
return true
}
/**
* @return The [File] that corresponds to the document ID supplied by [getDocumentId]
*/
private fun getFile(documentId: String): File {
if (documentId.startsWith(ROOT_ID)) {
val file = baseDirectory.resolve(documentId.drop(ROOT_ID.length + 1))
if (!file.exists()) throw FileNotFoundException("${file.absolutePath} ($documentId) not found")
return file
} else {
throw FileNotFoundException("'$documentId' is not in any known root")
}
}
/**
* @return A unique ID for the provided [File]
*/
private fun getDocumentId(file: File): String {
return "$ROOT_ID/${file.toRelativeString(baseDirectory)}"
}
override fun queryRoots(projection: Array<out String>?): Cursor {
val cursor = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION)
cursor.newRow().apply {
add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT_ID)
add(DocumentsContract.Root.COLUMN_SUMMARY, null)
add(
DocumentsContract.Root.COLUMN_FLAGS,
DocumentsContract.Root.FLAG_SUPPORTS_CREATE or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD
)
add(DocumentsContract.Root.COLUMN_TITLE, applicationName)
add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocumentId(baseDirectory))
add(DocumentsContract.Root.COLUMN_MIME_TYPES, "*/*")
add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDirectory.freeSpace)
add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_launcher_foreground)
}
return cursor
}
override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor {
val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
return includeFile(cursor, documentId, null)
}
override fun isChildDocument(parentDocumentId: String?, documentId: String?): Boolean {
return documentId?.startsWith(parentDocumentId!!) ?: false
}
/**
* @return A new [File] with a unique name based off the supplied [name], not conflicting with any existing file
*/
fun File.resolveWithoutConflict(name: String): File {
var file = resolve(name)
if (file.exists()) {
var noConflictId =
1 // Makes sure two files don't have the same name by adding a number to the end
val extension = name.substringAfterLast('.')
val baseName = name.substringBeforeLast('.')
while (file.exists())
file = resolve("$baseName (${noConflictId++}).$extension")
}
return file
}
override fun createDocument(
parentDocumentId: String?,
mimeType: String?,
displayName: String
): String {
val parentFile = getFile(parentDocumentId!!)
val newFile = parentFile.resolveWithoutConflict(displayName)
try {
if (DocumentsContract.Document.MIME_TYPE_DIR == mimeType) {
if (!newFile.mkdir())
throw IOException("Failed to create directory")
} else {
if (!newFile.createNewFile())
throw IOException("Failed to create file")
}
} catch (e: IOException) {
throw FileNotFoundException("Couldn't create document '${newFile.path}': ${e.message}")
}
return getDocumentId(newFile)
}
override fun deleteDocument(documentId: String?) {
val file = getFile(documentId!!)
if (!file.delete())
throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
}
override fun removeDocument(documentId: String, parentDocumentId: String?) {
val parent = getFile(parentDocumentId!!)
val file = getFile(documentId)
if (parent == file || file.parentFile == null || file.parentFile!! == parent) {
if (!file.delete())
throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
} else {
throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
}
}
override fun renameDocument(documentId: String?, displayName: String?): String {
if (displayName == null)
throw FileNotFoundException("Couldn't rename document '$documentId' as the new name is null")
val sourceFile = getFile(documentId!!)
val sourceParentFile = sourceFile.parentFile
?: throw FileNotFoundException("Couldn't rename document '$documentId' as it has no parent")
val destFile = sourceParentFile.resolve(displayName)
try {
if (!sourceFile.renameTo(destFile))
throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}'")
} catch (e: Exception) {
throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}': ${e.message}")
}
return getDocumentId(destFile)
}
private fun copyDocument(
sourceDocumentId: String, sourceParentDocumentId: String,
targetParentDocumentId: String?
): String? {
if (!isChildDocument(sourceParentDocumentId, sourceDocumentId))
throw FileNotFoundException("Couldn't copy document '$sourceDocumentId' as its parent is not '$sourceParentDocumentId'")
return copyDocument(sourceDocumentId, targetParentDocumentId)
}
override fun copyDocument(sourceDocumentId: String, targetParentDocumentId: String?): String {
val parent = getFile(targetParentDocumentId!!)
val oldFile = getFile(sourceDocumentId)
val newFile = parent.resolveWithoutConflict(oldFile.name)
try {
if (!(newFile.createNewFile() && newFile.setWritable(true) && newFile.setReadable(true)))
throw IOException("Couldn't create new file")
FileInputStream(oldFile).use { inStream ->
FileOutputStream(newFile).use { outStream ->
inStream.copyTo(outStream)
}
}
} catch (e: IOException) {
throw FileNotFoundException("Couldn't copy document '$sourceDocumentId': ${e.message}")
}
return getDocumentId(newFile)
}
override fun moveDocument(
sourceDocumentId: String, sourceParentDocumentId: String?,
targetParentDocumentId: String?
): String? {
try {
val newDocumentId = copyDocument(
sourceDocumentId, sourceParentDocumentId!!,
targetParentDocumentId
)
removeDocument(sourceDocumentId, sourceParentDocumentId)
return newDocumentId
} catch (e: FileNotFoundException) {
throw FileNotFoundException("Couldn't move document '$sourceDocumentId'")
}
}
private fun includeFile(cursor: MatrixCursor, documentId: String?, file: File?): MatrixCursor {
val localDocumentId = documentId ?: file?.let { getDocumentId(it) }
val localFile = file ?: getFile(documentId!!)
var flags = 0
if (localFile.isDirectory && localFile.canWrite()) {
flags = DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE
} else if (localFile.canWrite()) {
flags = DocumentsContract.Document.FLAG_SUPPORTS_WRITE
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_DELETE
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_REMOVE
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_MOVE
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_COPY
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_RENAME
}
cursor.newRow().apply {
add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, localDocumentId)
add(
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
if (localFile == baseDirectory) applicationName else localFile.name
)
add(DocumentsContract.Document.COLUMN_SIZE, localFile.length())
add(DocumentsContract.Document.COLUMN_MIME_TYPE, getTypeForFile(localFile))
add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, localFile.lastModified())
add(DocumentsContract.Document.COLUMN_FLAGS, flags)
if (localFile == baseDirectory)
add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_launcher_foreground)
}
return cursor
}
private fun getTypeForFile(file: File): Any? {
return if (file.isDirectory)
DocumentsContract.Document.MIME_TYPE_DIR
else
getTypeForName(file.name)
}
private fun getTypeForName(name: String): Any {
val lastDot = name.lastIndexOf('.')
if (lastDot >= 0) {
val extension = name.substring(lastDot + 1)
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
if (mime != null)
return mime
}
return "application/octect-stream"
}
override fun queryChildDocuments(
parentDocumentId: String?,
projection: Array<out String>?,
sortOrder: String?
): Cursor {
var cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
val parent = getFile(parentDocumentId!!)
for (file in parent.listFiles()!!)
cursor = includeFile(cursor, null, file)
return cursor
}
override fun openDocument(
documentId: String?,
mode: String?,
signal: CancellationSignal?
): ParcelFileDescriptor {
val file = documentId?.let { getFile(it) }
val accessMode = ParcelFileDescriptor.parseMode(mode)
return ParcelFileDescriptor.open(file, accessMode)
}
}
@@ -0,0 +1,13 @@
package org.kenjinx.android.ui.theme
import androidx.compose.ui.graphics.Color
val Gray10 = Color(0xFF1A1A1A)
val Gray20 = Color(0xFF333333)
val Gray40 = Color(0xFF666666)
val Gray60 = Color(0xFF999999)
val Gray80 = Color(0xFFCCCCCC)
val Gray90 = Color(0xFFE6E6E6)
val Red40 = Color(0xFFE75C8B)
val AquaGreen60 = Color(0xFF7EDFD9)
@@ -0,0 +1,86 @@
package org.kenjinx.android.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Shapes
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = AquaGreen60,
secondary = Gray60,
tertiary = Red40,
background = Gray10,
surface = Gray20,
onPrimary = Gray10,
onSecondary = Gray90,
onTertiary = Gray10,
onBackground = Gray90,
onSurface = Gray80
)
private val LightColorScheme = lightColorScheme(
primary = Red40,
secondary = Gray40,
tertiary = AquaGreen60,
background = Color(0xFFF8F8F8),
surface = Color(0xFFFFFFFF),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Gray20,
onSurface = Gray10,
)
@Composable
fun KenjinxAndroidTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = false, // Cambiato da true a false per usare i nostri colori personalizzati
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val shapes = Shapes(
extraSmall = RoundedCornerShape(4.dp),
small = RoundedCornerShape(8.dp),
medium = RoundedCornerShape(12.dp),
large = RoundedCornerShape(16.dp),
extraLarge = RoundedCornerShape(24.dp)
)
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content,
shapes = shapes
)
}
@@ -0,0 +1,34 @@
package org.kenjinx.android.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
@@ -0,0 +1,254 @@
package org.kenjinx.android.viewmodels
import android.content.Intent
import android.net.Uri
import android.os.Environment
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.toLowerCase
import com.anggrayudi.storage.SimpleStorageHelper
import com.anggrayudi.storage.file.extension
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import org.kenjinx.android.MainActivity
import org.kenjinx.android.KenjinxNative
import java.io.File
class DlcViewModel(val titleId: String) {
private var canClose: MutableState<Boolean>? = null
private var storageHelper: SimpleStorageHelper
private var dlcItemsState: SnapshotStateList<DlcItem>? = null
companion object {
const val UpdateRequestCode = 1002
}
fun remove(item: DlcItem) {
data?.apply {
this.removeAll { it.path == item.containerPath }
refreshDlcItems()
saveChanges()
canClose?.let {
it.value = false
it.value = true
}
}
}
fun add() {
val callBack = storageHelper.onFileSelected
storageHelper.onFileSelected = { requestCode, files ->
run {
storageHelper.onFileSelected = callBack
if (requestCode == UpdateRequestCode) {
val file = files.firstOrNull()
file?.apply {
if (file.extension == "nsp" || file.extension == "xci") {
storageHelper.storage.context.contentResolver.takePersistableUriPermission(
file.uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
val uri = file.uri
var filePath: String? = null
var path = uri.pathSegments.joinToString("/")
if (path.startsWith("document/")) {
val relativePath = Uri.decode(path.substring("document/".length))
if (relativePath.startsWith("root/")) {
val rootRelativePath = relativePath.substring("root/".length)
val baseDirectories = listOf(
storageHelper.storage.context.filesDir,
storageHelper.storage.context.getExternalFilesDir(null),
Environment.getExternalStorageDirectory()
)
for (baseDir in baseDirectories) {
val potentialFile = File(baseDir, rootRelativePath)
if (potentialFile.exists()) {
filePath = potentialFile.absolutePath
break
}
}
} else if (relativePath.startsWith("primary:")) {
val rootRelativePath = relativePath.substring("primary:".length)
val baseDirectories = listOf(
storageHelper.storage.context.filesDir,
storageHelper.storage.context.getExternalFilesDir(null),
Environment.getExternalStorageDirectory()
)
for (baseDir in baseDirectories) {
val potentialFile = File(baseDir, rootRelativePath)
if (potentialFile.exists()) {
filePath = potentialFile.absolutePath
break
}
}
}
}
path = filePath!!
if (path.isNotEmpty()) {
data?.apply {
val isDuplicate = this.any { it.path == path }
if (!isDuplicate) {
val contents =
KenjinxNative.jnaInstance.deviceGetDlcContentList(
path,
titleId.toLong(16)
)
if (contents.isNotEmpty()) {
val contentPath = path
val container = DlcContainerList(contentPath)
for (content in contents)
container.dlc_nca_list.add(
DlcContainer(
true,
KenjinxNative.jnaInstance.deviceGetDlcTitleId(
contentPath,
content
).toLong(16),
content
)
)
this.add(container)
}
}
}
}
}
}
}
refreshDlcItems()
saveChanges()
}
}
storageHelper.openFilePicker(UpdateRequestCode)
}
fun save(openDialog: MutableState<Boolean>) {
saveChanges()
openDialog.value = false
}
fun setDlcItems(items: SnapshotStateList<DlcItem>, canClose: MutableState<Boolean>) {
dlcItemsState = items
this.canClose = canClose
refreshDlcItems()
}
private fun refreshDlcItems() {
val items = mutableListOf<DlcItem>()
data?.apply {
for (container in this) {
val containerPath = container.path
if (!File(containerPath).exists())
continue
for (dlc in container.dlc_nca_list) {
val enabled = mutableStateOf(dlc.is_enabled)
items.add(
DlcItem(
File(containerPath).name,
enabled,
containerPath,
dlc.path,
KenjinxNative.jnaInstance.deviceGetDlcTitleId(
containerPath,
dlc.path
)
)
)
}
}
}
dlcItemsState?.clear()
dlcItemsState?.addAll(items)
canClose?.apply {
value = true
}
}
private fun saveChanges() {
data?.apply {
dlcItemsState?.forEach { item ->
for (container in this) {
if (container.path == item.containerPath) {
for (dlc in container.dlc_nca_list) {
if (dlc.path == item.fullPath) {
dlc.is_enabled = item.isEnabled.value
break
}
}
}
}
}
val gson = Gson()
val json = gson.toJson(this)
val savePath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current)
File(savePath).mkdirs()
File("$savePath/dlc.json").writeText(json)
}
}
var data: MutableList<DlcContainerList>? = null
private var jsonPath: String
init {
jsonPath =
MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current) + "/dlc.json"
storageHelper = MainActivity.StorageHelper!!
reloadFromDisk()
}
private fun reloadFromDisk() {
data = mutableListOf()
if (File(jsonPath).exists()) {
val gson = Gson()
val typeToken = object : TypeToken<MutableList<DlcContainerList>>() {}.type
data =
gson.fromJson<MutableList<DlcContainerList>>(File(jsonPath).readText(), typeToken)
}
}
}
data class DlcContainerList(
var path: String = "",
var dlc_nca_list: MutableList<DlcContainer> = mutableListOf()
)
data class DlcContainer(
var is_enabled: Boolean = false,
var title_id: Long = 0,
var path: String = ""
)
data class DlcItem(
var name: String = "",
var isEnabled: MutableState<Boolean> = mutableStateOf(false),
var containerPath: String = "",
var fullPath: String = "",
var titleId: String = ""
)
@@ -0,0 +1,27 @@
package org.kenjinx.android.viewmodels;
import com.sun.jna.Structure;
import java.util.List;
public class GameInfo extends Structure {
public double FileSize = 0.0;
public String TitleName;
public String TitleId;
public String Developer;
public String Version;
public String Icon;
@Override
protected List<String> getFieldOrder() {
return List.of("FileSize", "TitleName", "TitleId",
"Developer", "Version", "Icon");
}
}
@@ -0,0 +1,95 @@
package org.kenjinx.android.viewmodels
import android.content.Context
import android.net.Uri
import android.os.ParcelFileDescriptor
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.file.extension
import org.kenjinx.android.KenjinxNative
class GameModel(var file: DocumentFile, val context: Context) {
private var updateDescriptor: ParcelFileDescriptor? = null
var type: FileType
var descriptor: ParcelFileDescriptor? = null
var fileName: String?
var fileBaseName: String?
var fileSize = 0.0
var titleName: String? = null
var titleId: String? = null
var developer: String? = null
var version: String? = null
var icon: String? = null
init {
fileName = file.name
fileBaseName = fileName?.substringBeforeLast(".", fileName!!)
type = when {
(file.extension == "xci") -> FileType.Xci
(file.extension == "nsp") -> FileType.Nsp
(file.extension == "nro") -> FileType.Nro
else -> FileType.None
}
}
fun getGameInfo() {
val pid = open()
val gameInfo = GameInfo()
KenjinxNative.jnaInstance.deviceGetGameInfo(pid, file.extension, gameInfo)
close()
fileSize = gameInfo.FileSize
titleId = gameInfo.TitleId
titleName = gameInfo.TitleName
developer = gameInfo.Developer
version = gameInfo.Version
icon = gameInfo.Icon
if (type == FileType.Nro && (titleName.isNullOrEmpty() || titleName == "Unknown")) {
titleName = file.name
}
}
fun open(): Int {
descriptor = context.contentResolver.openFileDescriptor(file.uri, "rw")
return descriptor?.fd ?: 0
}
fun openUpdate(): Int {
if (titleId?.isNotEmpty() == true) {
val vm = TitleUpdateViewModel(titleId ?: "")
if (vm.data?.selected?.isNotEmpty() == true) {
val uri = Uri.parse(vm.data?.selected)
val file = DocumentFile.fromSingleUri(context, uri)
if (file?.exists() == true) {
try {
updateDescriptor =
context.contentResolver.openFileDescriptor(file.uri, "rw")
return updateDescriptor?.fd ?: -1
} catch (e: Exception) {
return -2
}
}
}
}
return -1
}
fun close() {
descriptor?.close()
descriptor = null
updateDescriptor?.close()
updateDescriptor = null
}
}
enum class FileType {
None,
Nsp,
Xci,
Nro
}
@@ -0,0 +1,119 @@
package org.kenjinx.android.viewmodels
import android.content.SharedPreferences
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager
import com.anggrayudi.storage.file.DocumentFileCompat
import com.anggrayudi.storage.file.DocumentFileType
import com.anggrayudi.storage.file.extension
import com.anggrayudi.storage.file.search
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.kenjinx.android.MainActivity
import java.util.Locale
import kotlin.concurrent.thread
class HomeViewModel(
val activity: MainActivity? = null,
val mainViewModel: MainViewModel? = null
) {
private var shouldReload: Boolean = false
private var savedFolder: String = ""
private var loadedCache: MutableList<GameModel> = mutableListOf()
private var gameFolderPath: DocumentFile? = null
private var sharedPref: SharedPreferences? = null
val gameList: SnapshotStateList<GameModel> = SnapshotStateList()
val isLoading: MutableState<Boolean> = mutableStateOf(false)
init {
if (activity != null) {
sharedPref = PreferenceManager.getDefaultSharedPreferences(activity)
}
}
fun ensureReloadIfNecessary() {
val oldFolder = savedFolder
savedFolder = sharedPref?.getString("gameFolder", "") ?: ""
if (savedFolder.isNotEmpty() && (shouldReload || savedFolder != oldFolder)) {
gameFolderPath = DocumentFileCompat.fromFullPath(
mainViewModel?.activity!!,
savedFolder,
documentType = DocumentFileType.FOLDER,
requiresWriteAccess = true
)
reloadGameList()
}
}
fun filter(query: String) {
gameList.clear()
gameList.addAll(loadedCache.filter {
it.titleName != null && it.titleName!!.isNotEmpty() && (query.trim()
.isEmpty() || it.titleName!!.lowercase(Locale.getDefault())
.contains(query))
})
}
fun requestReload() {
shouldReload = true
}
@OptIn(DelicateCoroutinesApi::class)
private fun reloadGameList() {
activity?.storageHelper ?: return
val folder = gameFolderPath ?: return
shouldReload = false
if (isLoading.value)
return
gameList.clear()
loadedCache.clear()
isLoading.value = true
thread {
try {
for (file in folder.search(true, DocumentFileType.FILE)) {
if (file.extension == "xci" || file.extension == "nsp" || file.extension == "nro")
activity.let {
loadedCache.add(GameModel(file, it))
}
}
loadedCache.sortWith { itemA, itemB ->
val strA = itemA.fileBaseName.toString().lowercase(Locale.ROOT)
val strB = itemB.fileBaseName.toString().lowercase(Locale.ROOT)
val minLength = minOf(strA.length, strB.length)
for (i in 0 until minLength) {
val charA = strA[i]
val charB = strB[i]
if (charA != charB) {
return@sortWith charA.code - charB.code
}
}
return@sortWith strA.length - strB.length
}
for(game in loadedCache)
{
game.getGameInfo()
}
} finally {
isLoading.value = false
GlobalScope.launch(Dispatchers.Main){
filter("")
}
}
}
}
}
@@ -0,0 +1,432 @@
package org.kenjinx.android.viewmodels
import android.annotation.SuppressLint
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.navigation.NavHostController
import com.anggrayudi.storage.extension.launchOnUiThread
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore
import org.kenjinx.android.GameController
import org.kenjinx.android.GameHost
import org.kenjinx.android.Logging
import org.kenjinx.android.MainActivity
import org.kenjinx.android.MotionSensorManager
import org.kenjinx.android.NativeGraphicsInterop
import org.kenjinx.android.NativeHelpers
import org.kenjinx.android.PerformanceManager
import org.kenjinx.android.PhysicalControllerManager
import org.kenjinx.android.RegionCode
import org.kenjinx.android.KenjinxNative
import org.kenjinx.android.SystemLanguage
import org.kenjinx.android.UiHandler
import java.io.File
@SuppressLint("WrongConstant")
class MainViewModel(val activity: MainActivity) {
var physicalControllerManager: PhysicalControllerManager? = null
var motionSensorManager: MotionSensorManager? = null
var gameModel: GameModel? = null
var controller: GameController? = null
var performanceManager: PerformanceManager? = null
var selected: GameModel? = null
val loadGameModel: MutableState<GameModel?> = mutableStateOf<GameModel?>(null)
val bootPath: MutableState<String?> = mutableStateOf<String?>(null)
val forceNceAndPptc: MutableState<Boolean> = mutableStateOf<Boolean>(false)
var isMiiEditorLaunched = false
val userViewModel = UserViewModel()
val logging = Logging(this)
var firmwareVersion = ""
private var gameTimeState: MutableState<Double>? = null
private var gameFpsState: MutableState<Double>? = null
private var fifoState: MutableState<Double>? = null
private var usedMemState: MutableState<Int>? = null
private var totalMemState: MutableState<Int>? = null
private var frequenciesState: MutableList<Double>? = null
private var progress: MutableState<String>? = null
private var progressValue: MutableState<Float>? = null
private var showLoading: MutableState<Boolean>? = null
private var refreshUser: MutableState<Boolean>? = null
var gameHost: GameHost? = null
set(value) {
field = value
field?.setProgressStates(showLoading, progressValue, progress)
}
var navController: NavHostController? = null
var homeViewModel: HomeViewModel = HomeViewModel(activity, this)
init {
performanceManager = PerformanceManager(activity)
}
fun refreshFirmwareVersion() {
firmwareVersion = KenjinxNative.jnaInstance.deviceGetInstalledFirmwareVersion()
}
fun closeGame() {
KenjinxNative.jnaInstance.deviceSignalEmulationClose()
gameHost?.close()
KenjinxNative.jnaInstance.deviceCloseEmulation()
motionSensorManager?.unregister()
physicalControllerManager?.disconnect()
motionSensorManager?.setControllerId(-1)
}
fun loadGame(game: GameModel, overrideSettings: Boolean? = false, forceNceAndPptc: Boolean? = false): Int {
KenjinxNative.jnaInstance.deviceReinitEmulation()
MainActivity.mainViewModel?.activity?.uiHandler = UiHandler()
val descriptor = game.open()
if (descriptor == 0)
return 0
val update = game.openUpdate()
if(update == -2)
{
return -2
}
gameModel = game
isMiiEditorLaunched = false
val settings = QuickSettings(activity)
if(overrideSettings == true)
{
settings.overrideSettings(forceNceAndPptc);
}
var success = KenjinxNative.jnaInstance.graphicsInitialize(
enableMacroHLE = settings.enableMacroHLE,
enableShaderCache = settings.enableShaderCache,
enableTextureRecompression = settings.enableTextureRecompression,
rescale = settings.resScale,
maxAnisotropy = settings.maxAnisotropy,
backendThreading = org.kenjinx.android.BackendThreading.Auto.ordinal
)
if (!success)
return 0
val nativeHelpers = NativeHelpers.instance
val nativeInterop = NativeGraphicsInterop()
nativeInterop.VkRequiredExtensions = arrayOf("VK_KHR_surface", "VK_KHR_android_surface")
nativeInterop.VkCreateSurface = nativeHelpers.getCreateSurfacePtr()
nativeInterop.SurfaceHandle = 0
val driverViewModel = VulkanDriverViewModel(activity)
val drivers = driverViewModel.getAvailableDrivers()
var driverHandle = 0L
if (driverViewModel.selected.isNotEmpty()) {
val metaData = drivers.find { it.driverPath == driverViewModel.selected }
metaData?.apply {
val privatePath = activity.filesDir
val privateDriverPath = privatePath.canonicalPath + "/driver/"
val pD = File(privateDriverPath)
if (pD.exists())
pD.deleteRecursively()
pD.mkdirs()
val driver = File(driverViewModel.selected)
val parent = driver.parentFile
if (parent != null) {
for (file in parent.walkTopDown()) {
if (file.absolutePath == parent.absolutePath)
continue
file.copyTo(File(privateDriverPath + file.name), true)
}
}
driverHandle = NativeHelpers.instance.loadDriver(
activity.applicationInfo.nativeLibraryDir!! + "/",
privateDriverPath,
this.libraryName
)
}
}
val extensions = nativeInterop.VkRequiredExtensions
success = KenjinxNative.jnaInstance.graphicsInitializeRenderer(
extensions!!,
extensions.size,
driverHandle
)
if (!success)
return 0
val semaphore = Semaphore(1, 0)
runBlocking {
semaphore.acquire()
launchOnUiThread {
// We are only able to initialize the emulation context on the main thread
success = KenjinxNative.jnaInstance.deviceInitialize(
settings.memoryManagerMode.ordinal,
settings.useNce,
settings.memoryConfiguration.ordinal,
SystemLanguage.AmericanEnglish.ordinal,
RegionCode.USA.ordinal,
settings.vSyncMode.ordinal,
settings.enableDocked,
settings.enablePptc,
false,
settings.enableFsIntegrityChecks,
settings.fsGlobalAccessLogMode,
"UTC",
settings.ignoreMissingServices
)
semaphore.release()
}
semaphore.acquire()
semaphore.release()
}
if (!success)
return 0
success = KenjinxNative.jnaInstance.deviceLoadDescriptor(descriptor, game.type.ordinal, update)
return if (success) 1 else 0
}
fun loadMiiEditor(): Boolean {
gameModel = null
isMiiEditorLaunched = true
val settings = QuickSettings(activity)
var success = KenjinxNative.jnaInstance.graphicsInitialize(
enableMacroHLE = settings.enableMacroHLE,
enableShaderCache = settings.enableShaderCache,
enableTextureRecompression = settings.enableTextureRecompression,
rescale = settings.resScale,
maxAnisotropy = settings.maxAnisotropy,
backendThreading = org.kenjinx.android.BackendThreading.Auto.ordinal
)
if (!success)
return false
val nativeHelpers = NativeHelpers.instance
val nativeInterop = NativeGraphicsInterop()
nativeInterop.VkRequiredExtensions = arrayOf("VK_KHR_surface", "VK_KHR_android_surface")
nativeInterop.VkCreateSurface = nativeHelpers.getCreateSurfacePtr()
nativeInterop.SurfaceHandle = 0
val driverViewModel = VulkanDriverViewModel(activity)
val drivers = driverViewModel.getAvailableDrivers()
var driverHandle = 0L
if (driverViewModel.selected.isNotEmpty()) {
val metaData = drivers.find { it.driverPath == driverViewModel.selected }
metaData?.apply {
val privatePath = activity.filesDir
val privateDriverPath = privatePath.canonicalPath + "/driver/"
val pD = File(privateDriverPath)
if (pD.exists())
pD.deleteRecursively()
pD.mkdirs()
val driver = File(driverViewModel.selected)
val parent = driver.parentFile
if (parent != null) {
for (file in parent.walkTopDown()) {
if (file.absolutePath == parent.absolutePath)
continue
file.copyTo(File(privateDriverPath + file.name), true)
}
}
driverHandle = NativeHelpers.instance.loadDriver(
activity.applicationInfo.nativeLibraryDir!! + "/",
privateDriverPath,
this.libraryName
)
}
}
val extensions = nativeInterop.VkRequiredExtensions
success = KenjinxNative.jnaInstance.graphicsInitializeRenderer(
extensions!!,
extensions.size,
driverHandle
)
if (!success)
return false
val semaphore = Semaphore(1, 0)
runBlocking {
semaphore.acquire()
launchOnUiThread {
// We are only able to initialize the emulation context on the main thread
success = KenjinxNative.jnaInstance.deviceInitialize(
settings.memoryManagerMode.ordinal,
settings.useNce,
settings.memoryConfiguration.ordinal,
SystemLanguage.AmericanEnglish.ordinal,
RegionCode.USA.ordinal,
settings.vSyncMode.ordinal,
settings.enableDocked,
settings.enablePptc,
false,
settings.enableFsIntegrityChecks,
settings.fsGlobalAccessLogMode,
"UTC",
settings.ignoreMissingServices
)
semaphore.release()
}
semaphore.acquire()
semaphore.release()
}
if (!success)
return false
success = KenjinxNative.jnaInstance.deviceLaunchMiiEditor()
return success
}
fun clearPptcCache(titleId: String) {
if (titleId.isNotEmpty()) {
val basePath = MainActivity.AppPath + "/games/$titleId/cache/cpu"
if (File(basePath).exists()) {
var caches = mutableListOf<String>()
val mainCache = basePath + "${File.separator}0"
File(mainCache).listFiles()?.forEach {
if (it.isFile && it.name.endsWith(".cache"))
caches.add(it.absolutePath)
}
val backupCache = basePath + "${File.separator}1"
File(backupCache).listFiles()?.forEach {
if (it.isFile && it.name.endsWith(".cache"))
caches.add(it.absolutePath)
}
for (path in caches)
File(path).delete()
}
}
}
fun purgeShaderCache(titleId: String) {
if (titleId.isNotEmpty()) {
val basePath = MainActivity.AppPath + "/games/$titleId/cache/shader"
if (File(basePath).exists()) {
var caches = mutableListOf<String>()
File(basePath).listFiles()?.forEach {
if (!it.isFile)
it.delete()
else {
if (it.name.endsWith(".toc") || it.name.endsWith(".data"))
caches.add(it.absolutePath)
}
}
for (path in caches)
File(path).delete()
}
}
}
fun deleteCache(titleId: String) {
fun deleteDirectory(directory: File) {
if (directory.exists() && directory.isDirectory) {
directory.listFiles()?.forEach { file ->
if (file.isDirectory) {
deleteDirectory(file)
} else {
file.delete()
}
}
directory.delete()
}
}
if (titleId.isNotEmpty()) {
val basePath = MainActivity.AppPath + "/games/$titleId/cache"
if (File(basePath).exists()) {
deleteDirectory(File(basePath))
}
}
}
fun setStatStates(
fifo: MutableState<Double>,
gameFps: MutableState<Double>,
gameTime: MutableState<Double>,
usedMem: MutableState<Int>,
totalMem: MutableState<Int>,
frequencies: MutableList<Double>
) {
fifoState = fifo
gameFpsState = gameFps
gameTimeState = gameTime
usedMemState = usedMem
totalMemState = totalMem
frequenciesState = frequencies
}
fun updateStats(
fifo: Double,
gameFps: Double,
gameTime: Double
) {
fifoState?.apply {
this.value = fifo
}
gameFpsState?.apply {
this.value = gameFps
}
gameTimeState?.apply {
this.value = gameTime
}
usedMemState?.let { usedMem ->
totalMemState?.let { totalMem ->
MainActivity.performanceMonitor.getMemoryUsage(
usedMem,
totalMem
)
}
}
frequenciesState?.let { MainActivity.performanceMonitor.getFrequencies(it) }
}
fun setGameController(controller: GameController) {
this.controller = controller
}
fun navigateToGame() {
navController?.navigate("game")
activity.isGameRunning = true
if (QuickSettings(activity).enableMotion)
motionSensorManager?.register()
}
fun setProgressStates(
showLoading: MutableState<Boolean>,
progressValue: MutableState<Float>,
progress: MutableState<String>
) {
this.showLoading = showLoading
this.progressValue = progressValue
this.progress = progress
gameHost?.setProgressStates(showLoading, progressValue, progress)
}
}
@@ -0,0 +1,9 @@
package org.kenjinx.android.viewmodels
enum class MemoryConfiguration {
MemoryConfiguration4GiB,
MemoryConfiguration6GiB,
MemoryConfiguration8GiB,
MemoryConfiguration10GiB,
MemoryConfiguration12GiB,
}
@@ -0,0 +1,7 @@
package org.kenjinx.android.viewmodels
enum class MemoryManagerMode {
SoftwarePageTable,
HostMapped,
HostMappedUnsafe
}
@@ -0,0 +1,120 @@
package org.kenjinx.android.viewmodels
import android.app.Activity
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
class QuickSettings(val activity: Activity) {
var ignoreMissingServices: Boolean
var enablePptc: Boolean
var enableFsIntegrityChecks: Boolean
var fsGlobalAccessLogMode: Int
var enableDocked: Boolean
var vSyncMode: VSyncMode
var useNce: Boolean
var memoryConfiguration: MemoryConfiguration
var useVirtualController: Boolean
var memoryManagerMode: MemoryManagerMode
var enableShaderCache: Boolean
var enableTextureRecompression: Boolean
var enableMacroHLE: Boolean
var resScale: Float
var maxAnisotropy: Float
var isGrid: Boolean
var useSwitchLayout: Boolean
var enableMotion: Boolean
var enablePerformanceMode: Boolean
var controllerStickSensitivity: Float
var enableStubLogs: Boolean
var enableInfoLogs: Boolean
var enableWarningLogs: Boolean
var enableErrorLogs: Boolean
var enableGuestLogs: Boolean
var enableFsAccessLogs: Boolean
var enableTraceLogs: Boolean
var enableDebugLogs: Boolean
var enableGraphicsLogs: Boolean
private var sharedPref: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
init {
memoryManagerMode = MemoryManagerMode.values()[sharedPref.getInt("memoryManagerMode", MemoryManagerMode.HostMappedUnsafe.ordinal)]
useNce = sharedPref.getBoolean("useNce", true)
memoryConfiguration = MemoryConfiguration.values()[sharedPref.getInt("memoryConfiguration", MemoryConfiguration.MemoryConfiguration4GiB.ordinal)]
vSyncMode = VSyncMode.values()[sharedPref.getInt("vSyncMode", VSyncMode.Switch.ordinal)]
enableDocked = sharedPref.getBoolean("enableDocked", true)
enablePptc = sharedPref.getBoolean("enablePptc", true)
enableFsIntegrityChecks = sharedPref.getBoolean("enableFsIntegrityChecks", false)
fsGlobalAccessLogMode = sharedPref.getInt("fsGlobalAccessLogMode", 0)
ignoreMissingServices = sharedPref.getBoolean("ignoreMissingServices", false)
enableShaderCache = sharedPref.getBoolean("enableShaderCache", true)
enableTextureRecompression = sharedPref.getBoolean("enableTextureRecompression", false)
enableMacroHLE = sharedPref.getBoolean("enableMacroHLE", true)
resScale = sharedPref.getFloat("resScale", 1f)
maxAnisotropy = sharedPref.getFloat("maxAnisotropy", 0f)
useVirtualController = sharedPref.getBoolean("useVirtualController", true)
isGrid = sharedPref.getBoolean("isGrid", true)
useSwitchLayout = sharedPref.getBoolean("useSwitchLayout", true)
enableMotion = sharedPref.getBoolean("enableMotion", true)
enablePerformanceMode = sharedPref.getBoolean("enablePerformanceMode", true)
controllerStickSensitivity = sharedPref.getFloat("controllerStickSensitivity", 1.0f)
enableStubLogs = sharedPref.getBoolean("enableStubLogs", false)
enableInfoLogs = sharedPref.getBoolean("enableInfoLogs", true)
enableWarningLogs = sharedPref.getBoolean("enableWarningLogs", true)
enableErrorLogs = sharedPref.getBoolean("enableErrorLogs", true)
enableGuestLogs = sharedPref.getBoolean("enableGuestLogs", true)
enableFsAccessLogs = sharedPref.getBoolean("enableFsAccessLogs", false)
enableTraceLogs = sharedPref.getBoolean("enableStubLogs", false)
enableDebugLogs = sharedPref.getBoolean("enableDebugLogs", false)
enableGraphicsLogs = sharedPref.getBoolean("enableGraphicsLogs", false)
}
fun save() {
val editor = sharedPref.edit()
editor.putInt("memoryManagerMode", memoryManagerMode.ordinal)
editor.putBoolean("useNce", useNce)
editor.putInt("memoryConfiguration", memoryConfiguration.ordinal)
editor.putInt("vSyncMode", vSyncMode.ordinal)
editor.putBoolean("enableDocked", enableDocked)
editor.putBoolean("enablePptc", enablePptc)
editor.putBoolean("enableFsIntegrityChecks", enableFsIntegrityChecks)
editor.putInt("fsGlobalAccessLogMode", fsGlobalAccessLogMode)
editor.putBoolean("ignoreMissingServices", ignoreMissingServices)
editor.putBoolean("enableShaderCache", enableShaderCache)
editor.putBoolean("enableTextureRecompression", enableTextureRecompression)
editor.putBoolean("enableMacroHLE",enableMacroHLE)
editor.putFloat("resScale", resScale)
editor.putFloat("maxAnisotropy", maxAnisotropy)
editor.putBoolean("useVirtualController", useVirtualController)
editor.putBoolean("isGrid", isGrid)
editor.putBoolean("useSwitchLayout", useSwitchLayout)
editor.putBoolean("enableMotion", enableMotion)
editor.putBoolean("enablePerformanceMode", enablePerformanceMode)
editor.putFloat("controllerStickSensitivity", controllerStickSensitivity)
editor.putBoolean("enableStubLogs", enableStubLogs)
editor.putBoolean("enableInfoLogs", enableInfoLogs)
editor.putBoolean("enableWarningLogs", enableWarningLogs)
editor.putBoolean("enableErrorLogs", enableErrorLogs)
editor.putBoolean("enableGuestLogs", enableGuestLogs)
editor.putBoolean("enableFsAccessLogs", enableFsAccessLogs)
editor.putBoolean("enableTraceLogs", enableTraceLogs)
editor.putBoolean("enableDebugLogs", enableDebugLogs)
editor.putBoolean("enableGraphicsLogs", enableGraphicsLogs)
editor.apply()
}
fun overrideSettings(forceNceAndPptc: Boolean?)
{
if(forceNceAndPptc == true)
{
enablePptc = true;
useNce = true;
}
else
{
enablePptc = false;
useNce = false;
}
}
}
@@ -0,0 +1,392 @@
package org.kenjinx.android.viewmodels
import android.content.SharedPreferences
import androidx.compose.runtime.MutableState
import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager
import com.anggrayudi.storage.callback.FileCallback
import com.anggrayudi.storage.file.FileFullPath
import com.anggrayudi.storage.file.copyFileTo
import com.anggrayudi.storage.file.extension
import com.anggrayudi.storage.file.getAbsolutePath
import com.anggrayudi.storage.file.openInputStream
import net.lingala.zip4j.io.inputstream.ZipInputStream
import org.kenjinx.android.LogLevel
import org.kenjinx.android.MainActivity
import org.kenjinx.android.KenjinxNative
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import kotlin.concurrent.thread
class SettingsViewModel(val activity: MainActivity) {
var selectedFirmwareVersion: String = ""
private var previousFileCallback: ((requestCode: Int, files: List<DocumentFile>) -> Unit)?
private var previousFolderCallback: ((requestCode: Int, folder: DocumentFile) -> Unit)?
private var sharedPref: SharedPreferences
var selectedKeyFile: DocumentFile? = null
var selectedFirmwareFile: DocumentFile? = null
init {
sharedPref = getPreferences()
previousFolderCallback = activity.storageHelper!!.onFolderSelected
previousFileCallback = activity.storageHelper!!.onFileSelected
activity.storageHelper!!.onFolderSelected = { _, folder ->
run {
val p = folder.getAbsolutePath(activity)
val editor = sharedPref.edit()
editor?.putString("gameFolder", p)
editor?.apply()
}
}
}
private fun getPreferences(): SharedPreferences {
return PreferenceManager.getDefaultSharedPreferences(activity)
}
fun initializeState(
memoryManagerMode: MutableState<MemoryManagerMode>,
useNce: MutableState<Boolean>,
memoryConfiguration: MutableState<MemoryConfiguration>,
vSyncMode: MutableState<VSyncMode>,
enableDocked: MutableState<Boolean>,
enablePptc: MutableState<Boolean>,
enableFsIntegrityChecks: MutableState<Boolean>,
fsGlobalAccessLogMode: MutableState<Int>,
ignoreMissingServices: MutableState<Boolean>,
enableShaderCache: MutableState<Boolean>,
enableTextureRecompression: MutableState<Boolean>,
enableMacroHLE: MutableState<Boolean>,
resScale: MutableState<Float>,
maxAnisotropy: MutableState<Float>,
useVirtualController: MutableState<Boolean>,
isGrid: MutableState<Boolean>,
useSwitchLayout: MutableState<Boolean>,
enableMotion: MutableState<Boolean>,
enablePerformanceMode: MutableState<Boolean>,
controllerStickSensitivity: MutableState<Float>,
enableStubLogs: MutableState<Boolean>,
enableInfoLogs: MutableState<Boolean>,
enableWarningLogs: MutableState<Boolean>,
enableErrorLogs: MutableState<Boolean>,
enableGuestLogs: MutableState<Boolean>,
enableFsAccessLogs: MutableState<Boolean>,
enableTraceLogs: MutableState<Boolean>,
enableDebugLogs: MutableState<Boolean>,
enableGraphicsLogs: MutableState<Boolean>
) {
memoryManagerMode.value = MemoryManagerMode.values()[sharedPref.getInt("memoryManagerMode", MemoryManagerMode.HostMappedUnsafe.ordinal)]
useNce.value = sharedPref.getBoolean("useNce", true)
memoryConfiguration.value = MemoryConfiguration.values()[sharedPref.getInt("memoryConfiguration", MemoryConfiguration.MemoryConfiguration4GiB.ordinal)]
vSyncMode.value = VSyncMode.values()[sharedPref.getInt("vSyncMode", VSyncMode.Switch.ordinal)]
enableDocked.value = sharedPref.getBoolean("enableDocked", true)
enablePptc.value = sharedPref.getBoolean("enablePptc", true)
enableFsIntegrityChecks.value = sharedPref.getBoolean("enableFsIntegrityChecks", false)
fsGlobalAccessLogMode.value = sharedPref.getInt("fsGlobalAccessLogMode", 0)
ignoreMissingServices.value = sharedPref.getBoolean("ignoreMissingServices", false)
enableShaderCache.value = sharedPref.getBoolean("enableShaderCache", true)
enableTextureRecompression.value = sharedPref.getBoolean("enableTextureRecompression", false)
enableMacroHLE.value = sharedPref.getBoolean("enableMacroHLE", false)
resScale.value = sharedPref.getFloat("resScale", 1f)
maxAnisotropy.value = sharedPref.getFloat("maxAnisotropy", 0f)
useVirtualController.value = sharedPref.getBoolean("useVirtualController", true)
isGrid.value = sharedPref.getBoolean("isGrid", true)
useSwitchLayout.value = sharedPref.getBoolean("useSwitchLayout", true)
enableMotion.value = sharedPref.getBoolean("enableMotion", true)
enablePerformanceMode.value = sharedPref.getBoolean("enablePerformanceMode", false)
controllerStickSensitivity.value = sharedPref.getFloat("controllerStickSensitivity", 1.0f)
enableStubLogs.value = sharedPref.getBoolean("enableStubLogs", false)
enableInfoLogs.value = sharedPref.getBoolean("enableInfoLogs", true)
enableWarningLogs.value = sharedPref.getBoolean("enableWarningLogs", true)
enableErrorLogs.value = sharedPref.getBoolean("enableErrorLogs", true)
enableGuestLogs.value = sharedPref.getBoolean("enableGuestLogs", true)
enableFsAccessLogs.value = sharedPref.getBoolean("enableFsAccessLogs", false)
enableTraceLogs.value = sharedPref.getBoolean("enableStubLogs", false)
enableDebugLogs.value = sharedPref.getBoolean("enableDebugLogs", false)
enableGraphicsLogs.value = sharedPref.getBoolean("enableGraphicsLogs", false)
}
fun save(
memoryManagerMode: MutableState<MemoryManagerMode>,
useNce: MutableState<Boolean>,
memoryConfiguration: MutableState<MemoryConfiguration>,
vSyncMode: MutableState<VSyncMode>,
enableDocked: MutableState<Boolean>,
enablePptc: MutableState<Boolean>,
enableFsIntegrityChecks: MutableState<Boolean>,
fsGlobalAccessLogMode: MutableState<Int>,
ignoreMissingServices: MutableState<Boolean>,
enableShaderCache: MutableState<Boolean>,
enableTextureRecompression: MutableState<Boolean>,
enableMacroHLE: MutableState<Boolean>,
resScale: MutableState<Float>,
maxAnisotropy: MutableState<Float>,
useVirtualController: MutableState<Boolean>,
isGrid: MutableState<Boolean>,
useSwitchLayout: MutableState<Boolean>,
enableMotion: MutableState<Boolean>,
enablePerformanceMode: MutableState<Boolean>,
controllerStickSensitivity: MutableState<Float>,
enableStubLogs: MutableState<Boolean>,
enableInfoLogs: MutableState<Boolean>,
enableWarningLogs: MutableState<Boolean>,
enableErrorLogs: MutableState<Boolean>,
enableGuestLogs: MutableState<Boolean>,
enableFsAccessLogs: MutableState<Boolean>,
enableTraceLogs: MutableState<Boolean>,
enableDebugLogs: MutableState<Boolean>,
enableGraphicsLogs: MutableState<Boolean>
) {
val editor = sharedPref.edit()
editor.putInt("memoryManagerMode", memoryManagerMode.value.ordinal)
editor.putBoolean("useNce", useNce.value)
editor.putInt("memoryConfiguration", memoryConfiguration.value.ordinal)
editor.putInt("vSyncMode", vSyncMode.value.ordinal)
editor.putBoolean("enableDocked", enableDocked.value)
editor.putBoolean("enablePptc", enablePptc.value)
editor.putBoolean("enableFsIntegrityChecks", enableFsIntegrityChecks.value)
editor.putInt("fsGlobalAccessLogMode", fsGlobalAccessLogMode.value)
editor.putBoolean("ignoreMissingServices", ignoreMissingServices.value)
editor.putBoolean("enableShaderCache", enableShaderCache.value)
editor.putBoolean("enableTextureRecompression", enableTextureRecompression.value)
editor.putBoolean("enableMacroHLE", enableMacroHLE.value)
editor.putFloat("resScale", resScale.value)
editor.putFloat("maxAnisotropy", maxAnisotropy.value)
editor.putBoolean("useVirtualController", useVirtualController.value)
editor.putBoolean("isGrid", isGrid.value)
editor.putBoolean("useSwitchLayout", useSwitchLayout.value)
editor.putBoolean("enableMotion", enableMotion.value)
editor.putBoolean("enablePerformanceMode", enablePerformanceMode.value)
editor.putFloat("controllerStickSensitivity", controllerStickSensitivity.value)
editor.putBoolean("enableStubLogs", enableStubLogs.value)
editor.putBoolean("enableInfoLogs", enableInfoLogs.value)
editor.putBoolean("enableWarningLogs", enableWarningLogs.value)
editor.putBoolean("enableErrorLogs", enableErrorLogs.value)
editor.putBoolean("enableGuestLogs", enableGuestLogs.value)
editor.putBoolean("enableFsAccessLogs", enableFsAccessLogs.value)
editor.putBoolean("enableTraceLogs", enableTraceLogs.value)
editor.putBoolean("enableDebugLogs", enableDebugLogs.value)
editor.putBoolean("enableGraphicsLogs", enableGraphicsLogs.value)
editor.apply()
activity.storageHelper!!.onFolderSelected = previousFolderCallback
KenjinxNative.jnaInstance.loggingSetEnabled(LogLevel.Info.ordinal, enableInfoLogs.value)
KenjinxNative.jnaInstance.loggingSetEnabled(LogLevel.Stub.ordinal, enableStubLogs.value)
KenjinxNative.jnaInstance.loggingSetEnabled(LogLevel.Warning.ordinal, enableWarningLogs.value)
KenjinxNative.jnaInstance.loggingSetEnabled(LogLevel.Error.ordinal, enableErrorLogs.value)
KenjinxNative.jnaInstance.loggingSetEnabled(LogLevel.AccessLog.ordinal, enableFsAccessLogs.value)
KenjinxNative.jnaInstance.loggingSetEnabled(LogLevel.Guest.ordinal, enableGuestLogs.value)
KenjinxNative.jnaInstance.loggingSetEnabled(LogLevel.Trace.ordinal, enableTraceLogs.value)
KenjinxNative.jnaInstance.loggingSetEnabled(LogLevel.Debug.ordinal, enableDebugLogs.value)
KenjinxNative.jnaInstance.loggingEnabledGraphicsLog(enableGraphicsLogs.value)
}
fun openGameFolder() {
val path = sharedPref.getString("gameFolder", "") ?: ""
if (path.isEmpty())
activity.storageHelper?.storage?.openFolderPicker()
else
activity.storageHelper?.storage?.openFolderPicker(
activity.storageHelper!!.storage.requestCodeFolderPicker,
FileFullPath(activity, path)
)
}
fun selectKey(installState: MutableState<KeyInstallState>) {
if (installState.value != KeyInstallState.None)
return
activity.storageHelper!!.onFileSelected = { _, files ->
run {
activity.storageHelper!!.onFileSelected = previousFileCallback
val file = files.firstOrNull()
file?.apply {
if (name == "prod.keys") {
selectedKeyFile = file
installState.value = KeyInstallState.Query
}
else {
installState.value = KeyInstallState.Cancelled
}
}
}
}
activity.storageHelper?.storage?.openFilePicker()
}
fun installKey(installState: MutableState<KeyInstallState>) {
if (installState.value != KeyInstallState.Query)
return
if (selectedKeyFile == null) {
installState.value = KeyInstallState.None
return
}
selectedKeyFile?.apply {
val outputFolder = File(MainActivity.AppPath + "/system")
val outputFile = File(MainActivity.AppPath + "/system/" + name)
outputFile.delete()
installState.value = KeyInstallState.Install
thread {
Thread.sleep(1000)
this.copyFileTo(
activity,
outputFolder,
callback = object : FileCallback() {
override fun onCompleted(result: Any) {
KenjinxNative.jnaInstance.deviceReloadFilesystem()
installState.value = KeyInstallState.Done
}
}
)
}
}
}
fun clearKeySelection(installState: MutableState<KeyInstallState>) {
selectedKeyFile = null
installState.value = KeyInstallState.None
}
fun selectFirmware(installState: MutableState<FirmwareInstallState>) {
if (installState.value != FirmwareInstallState.None)
return
activity.storageHelper!!.onFileSelected = { _, files ->
run {
activity.storageHelper!!.onFileSelected = previousFileCallback
val file = files.firstOrNull()
file?.apply {
if (extension == "xci" || extension == "zip") {
installState.value = FirmwareInstallState.Verifying
thread {
Thread.sleep(1000)
val descriptor = activity.contentResolver.openFileDescriptor(file.uri, "rw")
descriptor?.use { d ->
selectedFirmwareVersion = KenjinxNative.jnaInstance.deviceVerifyFirmware(d.fd, extension == "xci")
selectedFirmwareFile = file
if (!selectedFirmwareVersion.isEmpty()) {
installState.value = FirmwareInstallState.Query
} else {
installState.value = FirmwareInstallState.Cancelled
}
}
}
} else {
installState.value = FirmwareInstallState.Cancelled
}
}
}
}
activity.storageHelper?.storage?.openFilePicker()
}
fun installFirmware(installState: MutableState<FirmwareInstallState>) {
if (installState.value != FirmwareInstallState.Query)
return
if (selectedFirmwareFile == null) {
installState.value = FirmwareInstallState.None
return
}
selectedFirmwareFile?.apply {
val descriptor = activity.contentResolver.openFileDescriptor(uri, "rw")
if(descriptor != null)
{
installState.value = FirmwareInstallState.Install
thread {
Thread.sleep(1000)
try {
KenjinxNative.jnaInstance.deviceInstallFirmware(descriptor.fd,extension == "xci")
} finally {
MainActivity.mainViewModel?.refreshFirmwareVersion()
installState.value = FirmwareInstallState.Done
}
}
}
}
}
fun clearFirmwareSelection(installState: MutableState<FirmwareInstallState>) {
selectedFirmwareFile = null
selectedFirmwareVersion = ""
installState.value = FirmwareInstallState.None
}
fun importAppData(
file: DocumentFile,
dataImportState: MutableState<DataImportState>
) {
dataImportState.value = DataImportState.Import
try {
MainActivity.StorageHelper?.apply {
val stream = file.openInputStream(storage.context)
stream?.apply {
val folders = listOf("bis", "games", "profiles", "system")
for (f in folders) {
val dir = File(MainActivity.AppPath + "${File.separator}${f}")
if (dir.exists()) {
dir.deleteRecursively()
}
dir.mkdirs()
}
ZipInputStream(stream).use { zip ->
while (true) {
val header = zip.nextEntry ?: break
if (!folders.any { header.fileName.startsWith(it) }) {
continue
}
val filePath =
MainActivity.AppPath + File.separator + header.fileName
if (!header.isDirectory) {
val bos = BufferedOutputStream(FileOutputStream(filePath))
val bytesIn = ByteArray(4096)
var read: Int = 0
while (zip.read(bytesIn).also { read = it } > 0) {
bos.write(bytesIn, 0, read)
}
bos.close()
} else {
val dir = File(filePath)
dir.mkdir()
}
}
}
stream.close()
}
}
} finally {
dataImportState.value = DataImportState.Done
KenjinxNative.jnaInstance.deviceReloadFilesystem()
MainActivity.mainViewModel?.refreshFirmwareVersion()
}
}
}
enum class KeyInstallState {
None,
Cancelled,
Query,
Install,
Done
}
enum class FirmwareInstallState {
None,
Cancelled,
Verifying,
Query,
Install,
Done
}
enum class DataImportState {
None,
Query,
Import,
Done
}
@@ -0,0 +1,212 @@
package org.kenjinx.android.viewmodels
import android.content.Intent
import android.net.Uri
import android.os.Environment
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.toLowerCase
import com.anggrayudi.storage.SimpleStorageHelper
import com.anggrayudi.storage.file.extension
import com.google.gson.Gson
import org.kenjinx.android.MainActivity
import java.io.File
class TitleUpdateViewModel(val titleId: String) {
private var canClose: MutableState<Boolean>? = null
private var basePath: String
private var updateJsonName = "updates.json"
private var storageHelper: SimpleStorageHelper
private var currentPaths: MutableList<String> = mutableListOf()
private var pathsState: SnapshotStateList<String>? = null
companion object {
const val UpdateRequestCode = 1002
}
fun remove(index: Int) {
if (index <= 0) {
return
}
val updatesData = data
if (updatesData != null && updatesData.paths.isNotEmpty() && index - 1 < updatesData.paths.size) {
val str = updatesData.paths.removeAt(index - 1)
currentPaths = ArrayList(updatesData.paths)
pathsState?.clear()
pathsState?.addAll(updatesData.paths)
Uri.parse(str)?.let { uri ->
try {
storageHelper.storage.context.contentResolver.releasePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
} catch (securityException: SecurityException) {
}
}
saveChanges()
canClose?.let {
it.value = false
it.value = true
}
}
}
fun add() {
val callBack = storageHelper.onFileSelected
storageHelper.onFileSelected = { requestCode, files ->
run {
storageHelper.onFileSelected = callBack
if (requestCode == UpdateRequestCode) {
val file = files.firstOrNull()
file?.apply {
if (file.extension == "nsp" || file.extension == "xci") {
storageHelper.storage.context.contentResolver.takePersistableUriPermission(
file.uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
val uri = file.uri
var filePath: String? = null
var path = uri.pathSegments.joinToString("/")
if (path.startsWith("document/")) {
val relativePath = Uri.decode(path.substring("document/".length))
if (relativePath.startsWith("root/")) {
val rootRelativePath = relativePath.substring("root/".length)
val baseDirectories = listOf(
storageHelper.storage.context.filesDir,
storageHelper.storage.context.getExternalFilesDir(null),
Environment.getExternalStorageDirectory()
)
for (baseDir in baseDirectories) {
val potentialFile = File(baseDir, rootRelativePath)
if (potentialFile.exists()) {
filePath = potentialFile.absolutePath
break
}
}
}
else if(relativePath.startsWith("primary:")) {
val rootRelativePath = relativePath.substring("primary:".length)
val baseDirectories = listOf(
storageHelper.storage.context.filesDir,
storageHelper.storage.context.getExternalFilesDir(null),
Environment.getExternalStorageDirectory()
)
for (baseDir in baseDirectories) {
val potentialFile = File(baseDir, rootRelativePath)
if (potentialFile.exists()) {
filePath = potentialFile.absolutePath
break
}
}
}
}
path = filePath!!
if (path.isNotEmpty()) {
val isDuplicate = currentPaths.contains(path) || data?.paths?.contains(path) == true
if (!isDuplicate) {
currentPaths.add(path)
}
}
}
}
refreshPaths()
saveChanges()
}
}
}
storageHelper.openFilePicker(UpdateRequestCode)
}
fun save(index: Int,openDialog: MutableState<Boolean>) {
data?.apply {
this.selected = ""
if (paths.isNotEmpty() && index > 0) {
val ind = index - 1
this.selected = paths[ind]
}
saveChanges()
openDialog.value = false
}
}
fun setPaths(paths: SnapshotStateList<String>, canClose: MutableState<Boolean>) {
pathsState = paths
this.canClose = canClose
refreshPaths()
}
private fun refreshPaths() {
data?.apply {
val existingPaths = mutableListOf<String>()
currentPaths.forEach {
existingPaths.add(it)
}
if (!existingPaths.contains(selected)) {
selected = ""
}
pathsState?.clear()
pathsState?.addAll(existingPaths)
paths = existingPaths
canClose?.apply {
value = true
}
}
}
private fun saveChanges() {
val metadata = data ?: TitleUpdateMetadata()
val gson = Gson()
File(basePath).mkdirs()
val json = gson.toJson(metadata)
File("$basePath/$updateJsonName").writeText(json)
}
var data: TitleUpdateMetadata? = null
private var jsonPath: String
init {
basePath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current)
jsonPath = "${basePath}/${updateJsonName}"
data = TitleUpdateMetadata()
if (File(jsonPath).exists()) {
val gson = Gson()
data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java)
}
currentPaths = data?.paths ?: mutableListOf()
storageHelper = MainActivity.StorageHelper!!
refreshPaths()
File("$basePath/update").deleteRecursively()
}
}
data class TitleUpdateMetadata(
var selected: String = "",
var paths: MutableList<String> = mutableListOf()
)
@@ -0,0 +1,77 @@
package org.kenjinx.android.viewmodels
import org.kenjinx.android.KenjinxNative
import java.util.Base64
class UserViewModel {
var openedUser = UserModel()
val userList = mutableListOf<UserModel>()
init {
refreshUsers()
}
fun refreshUsers() {
userList.clear()
val decoder = Base64.getDecoder()
openedUser = UserModel()
openedUser.id = KenjinxNative.jnaInstance.userGetOpenedUser()
if (openedUser.id.isNotEmpty()) {
openedUser.username = KenjinxNative.jnaInstance.userGetUserName(openedUser.id)
openedUser.userPicture = decoder.decode(
KenjinxNative.jnaInstance.userGetUserPicture(
openedUser.id
)
)
}
val users = KenjinxNative.jnaInstance.userGetAllUsers()
for (user in users) {
userList.add(
UserModel(
user,
KenjinxNative.jnaInstance.userGetUserName(user),
decoder.decode(
KenjinxNative.jnaInstance.userGetUserPicture(user)
)
)
)
}
}
fun openUser(userModel: UserModel) {
KenjinxNative.jnaInstance.userOpenUser(userModel.id)
refreshUsers()
}
}
data class UserModel(
var id: String = "",
var username: String = "",
var userPicture: ByteArray? = null
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as UserModel
if (id != other.id) return false
if (username != other.username) return false
if (userPicture != null) {
if (other.userPicture == null) return false
if (!userPicture.contentEquals(other.userPicture)) return false
} else if (other.userPicture != null) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + username.hashCode()
result = 31 * result + (userPicture?.contentHashCode() ?: 0)
return result
}
}
@@ -0,0 +1,6 @@
package org.kenjinx.android.viewmodels
enum class VSyncMode {
Switch,
Unbounded
}
@@ -0,0 +1,167 @@
package org.kenjinx.android.viewmodels
import androidx.compose.runtime.MutableState
import com.anggrayudi.storage.file.extension
import com.anggrayudi.storage.file.openInputStream
import com.google.gson.Gson
import org.kenjinx.android.MainActivity
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.util.zip.ZipInputStream
class VulkanDriverViewModel(val activity: MainActivity) {
var selected: String = ""
companion object {
const val DriverRequestCode: Int = 1003
const val DriverFolder: String = "drivers"
}
private fun getAppPath(): String {
var appPath =
MainActivity.AppPath
appPath += "/"
return appPath
}
fun ensureDriverPath(): File {
val driverPath = getAppPath() + DriverFolder
val driverFolder = File(driverPath)
if (!driverFolder.exists())
driverFolder.mkdirs()
return driverFolder
}
fun getAvailableDrivers(): MutableList<DriverMetadata> {
val driverFolder = ensureDriverPath()
val folders = driverFolder.walkTopDown()
val drivers = mutableListOf<DriverMetadata>()
val selectedDriverFile = File(driverFolder.absolutePath + "/selected")
if (selectedDriverFile.exists()) {
selected = selectedDriverFile.readText()
if (!File(selected).exists()) {
selected = ""
saveSelected()
}
}
val gson = Gson()
for (folder in folders) {
if (folder.isDirectory && folder.parent == driverFolder.absolutePath) {
val meta = File(folder.absolutePath + "/meta.json")
if (meta.exists()) {
val metadata = gson.fromJson(meta.readText(), DriverMetadata::class.java)
if (metadata.name.isNotEmpty()) {
val driver = folder.absolutePath + "/${metadata.libraryName}"
metadata.driverPath = driver
if (File(driver).exists())
drivers.add(metadata)
}
}
}
}
return drivers
}
fun saveSelected() {
val driverFolder = ensureDriverPath()
val selectedDriverFile = File(driverFolder.absolutePath + "/selected")
selectedDriverFile.writeText(selected)
}
fun removeSelected() {
if (selected.isNotEmpty()) {
val sel = File(selected)
if (sel.exists()) {
sel.parentFile?.deleteRecursively()
}
selected = ""
saveSelected()
}
}
fun add(refresh: MutableState<Boolean>) {
activity.storageHelper?.apply {
val callBack = this.onFileSelected
onFileSelected = { requestCode, files ->
run {
onFileSelected = callBack
if (requestCode == DriverRequestCode) {
val file = files.firstOrNull()
file?.apply {
val stream = file.openInputStream(storage.context)
stream?.apply {
val name = file.name?.removeSuffix("." + file.extension) ?: ""
val driverFolder = ensureDriverPath()
val extractionFolder = File(driverFolder.absolutePath + "/${name}")
extractionFolder.deleteRecursively()
extractionFolder.mkdirs()
ZipInputStream(stream).use { zip ->
var entry = zip.nextEntry
while (entry != null) {
val filePath =
extractionFolder.absolutePath + File.separator + entry.name
if (!entry.isDirectory) {
File(filePath).delete()
val bos =
BufferedOutputStream(FileOutputStream(filePath))
val bytesIn = ByteArray(4096)
var read: Int
while (zip.read(bytesIn)
.also { read = it } != -1
) {
bos.write(bytesIn, 0, read)
}
bos.close()
} else {
val dir = File(filePath)
dir.mkdir()
}
entry = zip.nextEntry
}
}
}
}
refresh.value = true
}
}
}
openFilePicker(
DriverRequestCode,
filterMimeTypes = arrayOf("application/zip")
)
}
}
}
data class DriverMetadata(
var schemaVersion: Int = 0,
var name: String = "",
var description: String = "",
var author: String = "",
var packageVersion: String = "",
var vendor: String = "",
var driverVersion: String = "",
var minApi: Int = 0,
var libraryName: String = "",
var driverPath: String = ""
)
@@ -0,0 +1,170 @@
package org.kenjinx.android.views
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.kenjinx.android.viewmodels.DlcItem
import org.kenjinx.android.viewmodels.DlcViewModel
class DlcViews {
companion object {
@Composable
fun Main(titleId: String, name: String, openDialog: MutableState<Boolean>, canClose: MutableState<Boolean>) {
val viewModel = DlcViewModel(titleId)
val dlcItems = remember { SnapshotStateList<DlcItem>() }
Column(modifier = Modifier.padding(16.dp)) {
Row(modifier = Modifier.align(Alignment.Start)) {
IconButton(
modifier = Modifier.padding(4.dp),
onClick = {
viewModel.add()
},
) {
Icon(
Icons.Filled.Add,
contentDescription = "Add"
)
}
IconButton(
modifier = Modifier.padding(4.dp),
onClick = {
canClose.value = true
viewModel.save(openDialog)
},
) {
Icon(
Icons.Filled.Edit,
contentDescription = "Save"
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Column {
Text(text = "DLC for ${name}", textAlign = TextAlign.Center)
Box(
modifier = Modifier
.padding(8.dp)
.height(150.dp)
) {
val scrollState = rememberScrollState()
val needsScrollbar by remember {
derivedStateOf {
scrollState.maxValue > 0
}
}
viewModel.setDlcItems(dlcItems, canClose)
Surface(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(end = if (needsScrollbar) 16.dp else 0.dp),
color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.medium
) {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(scrollState)
.padding(8.dp)
) {
dlcItems.forEach { dlcItem ->
Row(
modifier = Modifier
.padding(vertical = 4.dp)
.fillMaxWidth()
) {
Checkbox(
checked = dlcItem.isEnabled.value,
onCheckedChange = { dlcItem.isEnabled.value = it }
)
Text(
text = dlcItem.name,
modifier = Modifier
.align(Alignment.CenterVertically)
.weight(1f)
)
IconButton(
onClick = {
viewModel.remove(dlcItem)
}
) {
Icon(
Icons.Filled.Delete,
contentDescription = "remove"
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
}
}
if (needsScrollbar) {
Box(
modifier = Modifier
.align(Alignment.CenterEnd)
.width(6.dp)
.fillMaxHeight()
.background(Color.LightGray.copy(alpha = 0.3f))
) {
val thumbRatio = 150.dp.value / (150.dp.value + scrollState.maxValue)
val thumbSize = (150.dp.value * thumbRatio).coerceAtLeast(40f)
val scrollRatio = scrollState.value.toFloat() / scrollState.maxValue.toFloat()
val maxOffset = 150.dp.value - (thumbSize * 0.7f)
val thumbOffset = maxOffset * scrollRatio
Box(
modifier = Modifier
.width(6.dp)
.height((thumbSize * 0.7f).dp)
.offset(y = thumbOffset.dp)
.background(
color = MaterialTheme.colorScheme.tertiary,
shape = MaterialTheme.shapes.small
)
)
}
}
}
}
}
}
}
}
@@ -0,0 +1,354 @@
package org.kenjinx.android.views
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Popup
import compose.icons.CssGgIcons
import compose.icons.cssggicons.ToolbarBottom
import org.kenjinx.android.GameController
import org.kenjinx.android.GameHost
import org.kenjinx.android.Icons
import org.kenjinx.android.MainActivity
import org.kenjinx.android.KenjinxNative
import org.kenjinx.android.viewmodels.MainViewModel
import org.kenjinx.android.viewmodels.QuickSettings
import org.kenjinx.android.viewmodels.VSyncMode
import org.kenjinx.android.widgets.SimpleAlertDialog
import kotlin.math.roundToInt
class GameViews {
companion object {
@Composable
fun Main() {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
GameView(mainViewModel = MainActivity.mainViewModel!!)
}
}
@Composable
fun GameView(mainViewModel: MainViewModel) {
Box(modifier = Modifier.fillMaxSize()) {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
GameHost(context, mainViewModel)
}
)
GameOverlay(mainViewModel)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GameOverlay(mainViewModel: MainViewModel) {
Box(modifier = Modifier.fillMaxSize()) {
val showStats = remember {
mutableStateOf(false)
}
val showController = remember {
mutableStateOf(QuickSettings(mainViewModel.activity).useVirtualController)
}
val vSyncMode = remember {
mutableStateOf(QuickSettings(mainViewModel.activity).vSyncMode)
}
val enableMotion = remember {
mutableStateOf(QuickSettings(mainViewModel.activity).enableMotion)
}
val showMore = remember {
mutableStateOf(false)
}
val showLoading = remember {
mutableStateOf(true)
}
val progressValue = remember {
mutableStateOf(0.0f)
}
val progress = remember {
mutableStateOf("Loading")
}
if (showStats.value) {
GameStats(mainViewModel)
}
mainViewModel.setProgressStates(showLoading, progressValue, progress)
// touch surface
Surface(color = Color.Transparent, modifier = Modifier
.fillMaxSize()
.padding(0.dp)
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
if (showController.value)
continue
val change = event
.component1()
.firstOrNull()
change?.apply {
val position = this.position
when (event.type) {
PointerEventType.Press -> {
KenjinxNative.jnaInstance.inputSetTouchPoint(
position.x.roundToInt(),
position.y.roundToInt()
)
}
PointerEventType.Release -> {
KenjinxNative.jnaInstance.inputReleaseTouchPoint()
}
PointerEventType.Move -> {
KenjinxNative.jnaInstance.inputSetTouchPoint(
position.x.roundToInt(),
position.y.roundToInt()
)
}
}
}
}
}
}) {
}
if (!showLoading.value) {
GameController.Compose(mainViewModel)
Row(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(8.dp)
) {
IconButton(modifier = Modifier.padding(4.dp), onClick = {
showMore.value = true
}) {
Icon(
imageVector = CssGgIcons.ToolbarBottom,
contentDescription = "Open Panel"
)
}
}
if (showMore.value) {
Popup(
alignment = Alignment.BottomCenter,
onDismissRequest = { showMore.value = false }) {
Surface(
modifier = Modifier.padding(16.dp),
shape = MaterialTheme.shapes.medium
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Row(
modifier = Modifier.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
IconButton(modifier = Modifier.padding(4.dp), onClick = {
showMore.value = false
showController.value = !showController.value
KenjinxNative.jnaInstance.inputReleaseTouchPoint()
mainViewModel.controller?.setVisible(showController.value)
}) {
Icon(
imageVector = Icons.videoGame(),
tint = if (showController.value) Color.Green else Color.Red,
contentDescription = "Toggle Virtual Pad"
)
}
IconButton(modifier = Modifier.padding(4.dp), onClick = {
showMore.value = false
if(vSyncMode.value == VSyncMode.Switch)
{
vSyncMode.value= VSyncMode.Unbounded
}
else
{
vSyncMode.value= VSyncMode.Switch
}
KenjinxNative.jnaInstance.graphicsRendererSetVsync(
vSyncMode.value.ordinal
)
}) {
Icon(
imageVector = Icons.vSync(),
tint = if (vSyncMode.value == VSyncMode.Switch) Color.Green else Color.Red,
contentDescription = "Toggle VSync"
)
}
IconButton(modifier = Modifier.padding(4.dp), onClick = {
showMore.value = false
enableMotion.value = !enableMotion.value
val settings = QuickSettings(mainViewModel.activity)
settings.enableMotion = enableMotion.value
settings.save()
if (enableMotion.value)
mainViewModel.motionSensorManager?.register()
else
mainViewModel.motionSensorManager?.unregister()
}) {
Icon(
imageVector = Icons.motionSensor(),
tint = if (enableMotion.value) Color.Green else Color.Red,
contentDescription = "Toggle Motion Sensor"
)
}
IconButton(modifier = Modifier.padding(4.dp), onClick = {
showMore.value = false
showStats.value = !showStats.value
}) {
Icon(
imageVector = Icons.barChart(),
tint = if (showStats.value) Color.Green else Color.Red,
contentDescription = "Toggle Game Stats"
)
}
}
}
}
}
}
}
val showBackNotice = remember {
mutableStateOf(false)
}
BackHandler {
showBackNotice.value = true
}
SimpleAlertDialog.Progress(
showDialog = showLoading,
progressText = progress.value,
progressValue = progressValue.value
)
SimpleAlertDialog.Confirmation(
showDialog = showBackNotice,
title = "Exit Game",
message = "Are you sure you want to exit the game? All unsaved data will be lost!",
confirmText = "Exit Game",
dismissText = "Dismiss",
onConfirm = {
mainViewModel.closeGame()
mainViewModel.navController?.popBackStack()
mainViewModel.activity.isGameRunning = false
}
)
mainViewModel.activity.uiHandler.Compose()
}
}
@Composable
fun GameStats(mainViewModel: MainViewModel) {
val fifo = remember {
mutableDoubleStateOf(0.0)
}
val gameFps = remember {
mutableDoubleStateOf(0.0)
}
val gameTime = remember {
mutableDoubleStateOf(0.0)
}
val usedMem = remember {
mutableIntStateOf(0)
}
val totalMem = remember {
mutableIntStateOf(0)
}
val frequencies = remember {
mutableListOf<Double>()
}
Surface(
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.background.copy(0.4f)
) {
CompositionLocalProvider(LocalTextStyle provides TextStyle(fontSize = 10.sp)) {
Column {
var gameTimeVal = 0.0
if (!gameTime.value.isInfinite())
gameTimeVal = gameTime.value
Text(text = "${String.format("%.3f", fifo.value)} %")
Text(text = "${String.format("%.3f", gameFps.value)} FPS")
Text(text = "${String.format("%.3f", gameTimeVal)} ms")
Box(modifier = Modifier.width(96.dp)) {
Column {
LazyColumn {
items(count = frequencies.size) { i ->
if (i < frequencies.size) {
val t = frequencies[i]
Row {
Text(
modifier = Modifier.padding(2.dp),
text = "CPU $i"
)
Spacer(Modifier.weight(1f))
Text(text = "$t MHz")
}
}
}
}
Row {
Text(modifier = Modifier.padding(2.dp), text = "Used")
Spacer(Modifier.weight(1f))
Text(text = "${usedMem.value} MB")
}
Row {
Text(modifier = Modifier.padding(2.dp), text = "Total")
Spacer(Modifier.weight(1f))
Text(text = "${totalMem.value} MB")
}
}
}
}
}
}
mainViewModel.setStatStates(fifo, gameFps, gameTime, usedMem, totalMem, frequencies)
}
}
}
@@ -0,0 +1,719 @@
package org.kenjinx.android.views
import android.content.res.Resources
import android.graphics.BitmapFactory
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.anggrayudi.storage.extension.launchOnUiThread
import java.util.Base64
import java.util.Locale
import kotlin.concurrent.thread
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.kenjinx.android.R
import org.kenjinx.android.viewmodels.FileType
import org.kenjinx.android.viewmodels.GameModel
import org.kenjinx.android.viewmodels.HomeViewModel
import org.kenjinx.android.viewmodels.QuickSettings
import org.kenjinx.android.widgets.SimpleAlertDialog
class HomeViews {
companion object {
const val ListImageSize = 150
const val GridImageSize = 300
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun Home(
viewModel: HomeViewModel = HomeViewModel(),
navController: NavHostController? = null,
isPreview: Boolean = false
) {
viewModel.ensureReloadIfNecessary()
val showAppActions = remember { mutableStateOf(false) }
val showLoading = remember { mutableStateOf(false) }
val openTitleUpdateDialog = remember { mutableStateOf(false) }
val canClose = remember { mutableStateOf(true) }
val openDlcDialog = remember { mutableStateOf(false) }
val showError = remember { mutableStateOf("") }
val selectedModel = remember { mutableStateOf(viewModel.mainViewModel?.selected) }
val query = remember { mutableStateOf("") }
var refreshUser by remember { mutableStateOf(true) }
var isFabVisible by remember { mutableStateOf(true)}
val isNavigating = remember { mutableStateOf(false) }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (available.y < -1) {
isFabVisible = false
}
if (available.y > 1) {
isFabVisible = true
}
return Offset.Zero
}
}
}
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp,vertical = 8.dp)
.background(MaterialTheme.colorScheme.surface),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(end = 8.dp)
) {
Box(
modifier = Modifier
.padding(end = 8.dp)
.size(56.dp)
.clip(RoundedCornerShape(8.dp))
.background(
if (refreshUser && viewModel.mainViewModel?.userViewModel?.openedUser?.userPicture?.isNotEmpty() == true) {
Color.Transparent
} else {
MaterialTheme.colorScheme.surface
}
)
.border(
width = 1.dp,
color = MaterialTheme.colorScheme.outline,
shape = RoundedCornerShape(8.dp)
)
.clickable {
if (!isNavigating.value) {
isNavigating.value = true
val currentRoute = navController?.currentDestination?.route
if (currentRoute != "user") {
navController?.navigate("user") {
launchSingleTop = true
restoreState = true
}
}
CoroutineScope(Dispatchers.Main).launch {
delay(500)
isNavigating.value = false
}
}
},
contentAlignment = Alignment.Center
) {
if (refreshUser && viewModel.mainViewModel?.userViewModel?.openedUser?.userPicture?.isNotEmpty() == true) {
val pic = viewModel.mainViewModel.userViewModel.openedUser.userPicture
Image(
bitmap = BitmapFactory.decodeByteArray(
pic,
0,
pic?.size ?: 0
).asImageBitmap(),
contentDescription = "user image",
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
} else {
Icon(
Icons.Filled.Person,
contentDescription = "User",
modifier = Modifier.size(28.dp),
tint = MaterialTheme.colorScheme.onSurface
)
}
}
IconButton(
onClick = {
if (!isNavigating.value) {
isNavigating.value = true
val currentRoute = navController?.currentDestination?.route
if (currentRoute != "settings") {
navController?.navigate("settings") {
launchSingleTop = true
restoreState = true
}
}
CoroutineScope(Dispatchers.Main).launch {
delay(500)
isNavigating.value = false
}
}
},
modifier = Modifier
.size(56.dp)
.clip(RoundedCornerShape(8.dp))
.border(
width = 1.dp,
color = MaterialTheme.colorScheme.outline,
shape = RoundedCornerShape(8.dp)
)
) {
Icon(
Icons.Filled.Settings,
contentDescription = "Settings"
)
}
}
OutlinedTextField(
value = query.value,
onValueChange = {
query.value = it
},
modifier = Modifier
.weight(1f)
.height(56.dp),
placeholder = {
Text(
"Search...",
modifier = Modifier.padding(bottom = 4.dp)
)
},
leadingIcon = {
Icon(
Icons.Filled.Search,
contentDescription = "Search"
)
},
singleLine = true,
shape = RoundedCornerShape(8.dp),
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline
)
)
}
},
floatingActionButton = {
AnimatedVisibility(visible = isFabVisible) {
FloatingActionButton(
onClick = {
viewModel.requestReload()
viewModel.ensureReloadIfNecessary()
},
shape = MaterialTheme.shapes.small,
containerColor = MaterialTheme.colorScheme.tertiary
) {
Icon(Icons.Default.Refresh, contentDescription = "refresh")
}
}
},
floatingActionButtonPosition = FabPosition.End
) { contentPadding ->
Column(modifier = Modifier.padding(contentPadding)) {
Box {
val list = remember { viewModel.gameList }
val isLoading = remember { viewModel.isLoading }
viewModel.filter(query.value)
if (!isPreview) {
var settings = QuickSettings(viewModel.activity!!)
if (isLoading.value) {
Box(modifier = Modifier.fillMaxSize())
{
CircularProgressIndicator(
modifier = Modifier
.width(64.dp)
.align(Alignment.Center),
color = MaterialTheme.colorScheme.secondary,
trackColor = MaterialTheme.colorScheme.surfaceVariant
)
}
} else {
if (settings.isGrid) {
val size =
GridImageSize / Resources.getSystem().displayMetrics.density
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = (size + 4).dp),
modifier = Modifier
.fillMaxSize()
.padding(4.dp)
.nestedScroll(nestedScrollConnection),
horizontalArrangement = Arrangement.SpaceEvenly
) {
items(list) {
it.titleName?.apply {
if (this.isNotEmpty() && (query.value.trim()
.isEmpty() || this.lowercase(Locale.getDefault())
.contains(query.value))
)
GridGameItem(
it,
viewModel,
showAppActions,
showLoading,
selectedModel,
showError
)
}
}
}
} else {
LazyColumn(Modifier.fillMaxSize()) {
items(list) {
it.titleName?.apply {
if (this.isNotEmpty() && (query.value.trim()
.isEmpty() || this.lowercase(
Locale.getDefault()
)
.contains(query.value))
)
Box(modifier = Modifier.animateItemPlacement()) {
ListGameItem(
it,
viewModel,
showAppActions,
showLoading,
selectedModel,
showError
)
}
}
}
}
}
}
}
}
}
SimpleAlertDialog.Loading(showDialog = showLoading)
SimpleAlertDialog.Custom(
showDialog = openTitleUpdateDialog,
onDismissRequest = { openTitleUpdateDialog.value = false },
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
val titleId = viewModel.mainViewModel?.selected?.titleId ?: ""
val name = viewModel.mainViewModel?.selected?.titleName ?: ""
TitleUpdateViews.Main(titleId, name, openTitleUpdateDialog, canClose)
}
SimpleAlertDialog.Custom(
showDialog = openDlcDialog,
onDismissRequest = { openDlcDialog.value = false },
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
val titleId = viewModel.mainViewModel?.selected?.titleId ?: ""
val name = viewModel.mainViewModel?.selected?.titleName ?: ""
DlcViews.Main(titleId, name, openDlcDialog, canClose)
}
}
if(viewModel.mainViewModel?.loadGameModel?.value != null)
LaunchedEffect(viewModel.mainViewModel.loadGameModel.value) {
if (viewModel.mainViewModel.bootPath.value == "gameItem_${viewModel.mainViewModel.loadGameModel.value!!.titleName}") {
viewModel.mainViewModel.bootPath.value = null
thread {
showLoading.value = true
val success =
viewModel.mainViewModel.loadGame(
viewModel.mainViewModel.loadGameModel.value!!,
true,
viewModel.mainViewModel.forceNceAndPptc.value
) ?: false
if (success == 1) {
launchOnUiThread {
viewModel.mainViewModel.navigateToGame()
}
} else {
if (success == -2)
showError.value =
"Error loading update. Please re-add update file"
viewModel.mainViewModel.loadGameModel.value!!.close()
}
showLoading.value = false
}
}
}
if (showAppActions.value)
ModalBottomSheet(
content = {
Row(
modifier = Modifier.padding(8.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
if (showAppActions.value) {
IconButton(onClick = {
if (viewModel.mainViewModel?.selected != null) {
thread {
showLoading.value = true
val success =
viewModel.mainViewModel.loadGame(viewModel.mainViewModel.selected!!)
if (success == 1) {
launchOnUiThread {
viewModel.mainViewModel.navigateToGame()
}
} else {
if (success == -2)
showError.value =
"Error loading update. Please re-add update file"
viewModel.mainViewModel.selected!!.close()
}
showLoading.value = false
}
}
}) {
Icon(
org.kenjinx.android.Icons.playArrow(MaterialTheme.colorScheme.onSurface),
contentDescription = "Run"
)
}
val showAppMenu = remember { mutableStateOf(false) }
Box {
IconButton(onClick = {
showAppMenu.value = true
}) {
Icon(
Icons.Filled.Menu,
contentDescription = "Menu"
)
}
DropdownMenu(
expanded = showAppMenu.value,
onDismissRequest = { showAppMenu.value = false }) {
DropdownMenuItem(text = {
Text(text = "Clear PPTC Cache")
}, onClick = {
showAppMenu.value = false
viewModel.mainViewModel?.clearPptcCache(
viewModel.mainViewModel.selected?.titleId ?: ""
)
})
DropdownMenuItem(text = {
Text(text = "Purge Shader Cache")
}, onClick = {
showAppMenu.value = false
viewModel.mainViewModel?.purgeShaderCache(
viewModel.mainViewModel.selected?.titleId ?: ""
)
})
DropdownMenuItem(text = {
Text(text = "Delete All Cache")
}, onClick = {
showAppMenu.value = false
viewModel.mainViewModel?.deleteCache(
viewModel.mainViewModel.selected?.titleId ?: ""
)
})
DropdownMenuItem(text = {
Text(text = "Manage Updates")
}, onClick = {
showAppMenu.value = false
openTitleUpdateDialog.value = true
})
DropdownMenuItem(text = {
Text(text = "Manage DLC")
}, onClick = {
showAppMenu.value = false
openDlcDialog.value = true
})
}
}
}
}
},
onDismissRequest = {
showAppActions.value = false
selectedModel.value = null
}
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ListGameItem(
gameModel: GameModel,
viewModel: HomeViewModel,
showAppActions: MutableState<Boolean>,
showLoading: MutableState<Boolean>,
selectedModel: MutableState<GameModel?>,
showError: MutableState<String>
) {
remember {
selectedModel
}
val color =
if (selectedModel.value == gameModel) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface
val decoder = Base64.getDecoder()
Surface(
shape = MaterialTheme.shapes.medium,
color = color,
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.combinedClickable(
onClick = {
if (viewModel.mainViewModel?.selected != null) {
showAppActions.value = false
viewModel.mainViewModel.apply {
selected = null
}
selectedModel.value = null
} else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro) {
thread {
showLoading.value = true
val success =
viewModel.mainViewModel?.loadGame(gameModel) ?: false
if (success == 1) {
launchOnUiThread {
viewModel.mainViewModel?.navigateToGame()
}
} else {
if (success == -2)
showError.value =
"Error loading update. Please re-add update file"
gameModel.close()
}
showLoading.value = false
}
}
},
onLongClick = {
viewModel.mainViewModel?.selected = gameModel
showAppActions.value = true
selectedModel.value = gameModel
})
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Row {
if (!gameModel.titleId.isNullOrEmpty() && (gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro)) {
if (gameModel.icon?.isNotEmpty() == true) {
val pic = decoder.decode(gameModel.icon)
val size =
ListImageSize / Resources.getSystem().displayMetrics.density
Image(
bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size)
.asImageBitmap(),
contentDescription = gameModel.titleName + " icon",
modifier = Modifier
.padding(end = 8.dp)
.width(size.roundToInt().dp)
.height(size.roundToInt().dp)
)
} else if (gameModel.type == FileType.Nro)
NROIcon()
else NotAvailableIcon()
} else NotAvailableIcon()
Column {
Text(text = gameModel.titleName ?: "")
Text(text = gameModel.developer ?: "")
Text(text = gameModel.titleId ?: "")
}
}
Column {
Text(text = gameModel.version ?: "")
Text(text = String.format("%.3f", gameModel.fileSize))
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun GridGameItem(
gameModel: GameModel,
viewModel: HomeViewModel,
showAppActions: MutableState<Boolean>,
showLoading: MutableState<Boolean>,
selectedModel: MutableState<GameModel?>,
showError: MutableState<String>
) {
remember {
selectedModel
}
val color =
if (selectedModel.value == gameModel) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface
val decoder = Base64.getDecoder()
Surface(
shape = MaterialTheme.shapes.medium,
color = color,
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.combinedClickable(
onClick = {
if (viewModel.mainViewModel?.selected != null) {
showAppActions.value = false
viewModel.mainViewModel.apply {
selected = null
}
selectedModel.value = null
} else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro) {
thread {
showLoading.value = true
val success =
viewModel.mainViewModel?.loadGame(gameModel) ?: false
if (success == 1) {
launchOnUiThread {
viewModel.mainViewModel?.navigateToGame()
}
} else {
if (success == -2)
showError.value =
"Error loading update. Please re-add update file"
gameModel.close()
}
showLoading.value = false
}
}
},
onLongClick = {
viewModel.mainViewModel?.selected = gameModel
showAppActions.value = true
selectedModel.value = gameModel
})
) {
Column(modifier = Modifier.padding(4.dp)) {
if (!gameModel.titleId.isNullOrEmpty() && (gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro)) {
if (gameModel.icon?.isNotEmpty() == true) {
val pic = decoder.decode(gameModel.icon)
Image(
bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size)
.asImageBitmap(),
contentDescription = gameModel.titleName + " icon",
modifier = Modifier
.padding(0.dp)
.clip(RoundedCornerShape(16.dp))
.align(Alignment.CenterHorizontally)
)
} else if (gameModel.type == FileType.Nro)
NROIcon()
else NotAvailableIcon()
} else NotAvailableIcon()
Text(
text = gameModel.titleName ?: "N/A",
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(vertical = 4.dp)
.basicMarquee()
)
}
}
}
@Composable
fun NotAvailableIcon() {
val size = ListImageSize / Resources.getSystem().displayMetrics.density
Icon(
Icons.Filled.Add,
contentDescription = "N/A",
modifier = Modifier
.padding(end = 8.dp)
.width(size.roundToInt().dp)
.height(size.roundToInt().dp)
)
}
@Composable
fun NROIcon() {
val size = ListImageSize / Resources.getSystem().displayMetrics.density
Image(
painter = painterResource(id = R.drawable.icon_nro),
contentDescription = "NRO",
modifier = Modifier
.padding(end = 8.dp)
.width(size.roundToInt().dp)
.height(size.roundToInt().dp)
)
}
}
@Preview
@Composable
fun HomePreview() {
Home(isPreview = true)
}
}
@@ -0,0 +1,25 @@
package org.kenjinx.android.views
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import org.kenjinx.android.viewmodels.MainViewModel
import org.kenjinx.android.viewmodels.SettingsViewModel
class MainView {
companion object {
@Composable
fun Main(mainViewModel: MainViewModel) {
val navController = rememberNavController()
mainViewModel.navController = navController
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) }
composable("user") { UserViews.Main(mainViewModel) }
composable("game") { GameViews.Main() }
composable("settings") { SettingViews.Main(SettingsViewModel(mainViewModel.activity), mainViewModel) }
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,179 @@
package org.kenjinx.android.views
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.kenjinx.android.viewmodels.TitleUpdateViewModel
import java.io.File
class TitleUpdateViews {
companion object {
@Composable
fun Main(titleId: String, name: String, openDialog: MutableState<Boolean>, canClose: MutableState<Boolean>) {
val viewModel = TitleUpdateViewModel(titleId)
var selectedIndex by remember { mutableStateOf(0) }
viewModel.data?.apply {
selectedIndex = paths.indexOf(this.selected) + 1
}
Column(modifier = Modifier.padding(16.dp)) {
Row(modifier = Modifier.align(Alignment.Start)) {
IconButton(
onClick = {
viewModel.remove(selectedIndex)
}
) {
Icon(
Icons.Filled.Delete,
contentDescription = "Remove"
)
}
IconButton(
onClick = {
viewModel.add()
}
) {
Icon(
Icons.Filled.Add,
contentDescription = "Add"
)
}
IconButton(
onClick = {
canClose.value = true
viewModel.save(selectedIndex, openDialog)
},
) {
Icon(
Icons.Filled.Edit,
contentDescription = "Save"
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Column {
Text(text = "Updates for ${name}", textAlign = TextAlign.Center)
Box(
modifier = Modifier
.padding(8.dp)
.height(150.dp)
) {
val scrollState = rememberScrollState()
val paths = remember { mutableStateListOf<String>() }
viewModel.setPaths(paths, canClose)
Surface(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(end = 16.dp),
color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.medium
) {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(scrollState)
.padding(8.dp)
) {
Row(modifier = Modifier.padding(vertical = 4.dp)) {
RadioButton(
selected = (selectedIndex == 0),
onClick = { selectedIndex = 0 }
)
Text(
text = "None",
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterVertically)
)
}
paths.forEachIndexed { index, path ->
val itemIndex = index + 1
val file = File(path)
if (file.exists()) {
Row(modifier = Modifier.padding(vertical = 4.dp)) {
RadioButton(
selected = (selectedIndex == itemIndex),
onClick = { selectedIndex = itemIndex }
)
Text(
text = file.name,
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterVertically)
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
}
}
if (scrollState.maxValue > 0) {
Box(
modifier = Modifier
.align(Alignment.CenterEnd)
.width(6.dp)
.fillMaxHeight()
.background(Color.LightGray.copy(alpha = 0.3f))
) {
val thumbRatio = 150.dp.value / (150.dp.value + scrollState.maxValue)
val thumbSize = (150.dp.value * thumbRatio).coerceAtLeast(40f)
val scrollRatio = scrollState.value.toFloat() / scrollState.maxValue.toFloat()
val maxOffset = 150.dp.value - (thumbSize * 0.7f)
val thumbOffset = maxOffset * scrollRatio
Box(
modifier = Modifier
.width(6.dp)
.height((thumbSize * 0.7f).dp)
.offset(y = thumbOffset.dp)
.background(
color = MaterialTheme.colorScheme.tertiary,
shape = MaterialTheme.shapes.small
)
)
}
}
}
}
}
}
}
}
@@ -0,0 +1,179 @@
package org.kenjinx.android.views
import android.graphics.BitmapFactory
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.kenjinx.android.viewmodels.MainViewModel
class UserViews {
companion object {
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun Main(viewModel: MainViewModel) {
val reload = remember { mutableStateOf(true) }
val isNavigating = remember { mutableStateOf(false) }
fun refresh() {
viewModel?.userViewModel?.refreshUsers()
reload.value = true
}
LaunchedEffect(reload.value) {
reload.value = false
}
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(title = {
Text(text = "Users")
},
navigationIcon = {
IconButton(onClick = {
if (!isNavigating.value) {
isNavigating.value = true
viewModel.navController?.popBackStack()
CoroutineScope(Dispatchers.Main).launch {
delay(500)
isNavigating.value = false
}
}
}) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
})
}) { contentPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(contentPadding)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(text = "Selected user")
Row(
modifier = Modifier
.fillMaxWidth()
.padding(4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
if (viewModel.userViewModel.openedUser.id?.isNotEmpty() == true) {
val openUser = viewModel.userViewModel.openedUser
Image(
bitmap = BitmapFactory.decodeByteArray(
openUser.userPicture,
0,
openUser.userPicture?.size ?: 0
).asImageBitmap(),
contentDescription = "selected image",
contentScale = ContentScale.Crop,
modifier = Modifier
.padding(4.dp)
.size(48.dp)
.clip(CircleShape)
)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(text = openUser.username)
Text(text = openUser.id)
}
}
}
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = "Available Users")
IconButton(onClick = {
refresh()
}) {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = "refresh users"
)
}
}
LazyHorizontalGrid(
rows = GridCells.Fixed(1),
modifier = Modifier
.height(104.dp)
.fillMaxSize()
) {
if (viewModel.userViewModel.userList.isNotEmpty() == true) {
items(viewModel.userViewModel.userList) { user ->
Box(
contentAlignment = Alignment.Center
){
Image(
bitmap = BitmapFactory.decodeByteArray(
user.userPicture,
0,
user.userPicture?.size ?: 0
)
.asImageBitmap(),
contentDescription = "selected image",
contentScale = ContentScale.Crop,
modifier = Modifier
.padding(4.dp)
.clip(CircleShape)
.size(96.dp)
.combinedClickable(
onClick = {
viewModel.userViewModel.openUser(user)
reload.value = true
})
)
}
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,234 @@
package org.kenjinx.android.views
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.kenjinx.android.MainActivity
import org.kenjinx.android.viewmodels.VulkanDriverViewModel
class VulkanDriverViews {
companion object {
@Composable
fun Main(activity: MainActivity, openDialog: MutableState<Boolean>) {
var driverViewModel = VulkanDriverViewModel(activity)
var isChanged = remember { mutableStateOf(false) }
var refresh = remember { mutableStateOf(false) }
var drivers = driverViewModel.getAvailableDrivers()
var selectedDriver = remember { mutableStateOf(0) }
if (refresh.value) {
isChanged.value = true
refresh.value = false
}
if (!isChanged.value) {
selectedDriver.value =
drivers.indexOfFirst { it.driverPath == driverViewModel.selected } + 1
isChanged.value = true
}
Column(modifier = Modifier.padding(16.dp)) {
Row(modifier = Modifier.align(Alignment.Start)) {
IconButton(
onClick = {
driverViewModel.removeSelected()
refresh.value = true
}
) {
Icon(
Icons.Filled.Delete,
contentDescription = "Remove"
)
}
IconButton(
onClick = {
driverViewModel.add(refresh)
refresh.value = true
selectedDriver.value = 0
driverViewModel.selected = ""
}
) {
Icon(
Icons.Filled.Add,
contentDescription = "Add"
)
}
IconButton(
onClick = {
driverViewModel.saveSelected()
openDialog.value = false
},
) {
Icon(
Icons.Filled.Edit,
contentDescription = "Save"
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Column {
Text(text = "Driver List", textAlign = TextAlign.Center)
Box(
modifier = Modifier
.padding(8.dp)
.height(150.dp)
) {
val scrollState = rememberScrollState()
val needsScrollbar by remember {
derivedStateOf {
scrollState.maxValue > 0
}
}
Surface(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(end = if (needsScrollbar) 16.dp else 0.dp),
color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.medium
) {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(
scrollState
)
.padding(8.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.clickable {
selectedDriver.value = 0
isChanged.value = true
driverViewModel.selected = ""
},
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedDriver.value == 0 || driverViewModel.selected.isEmpty(),
onClick = {
selectedDriver.value = 0
isChanged.value = true
driverViewModel.selected = ""
}
)
Text(
text = "Default",
modifier = Modifier
.fillMaxWidth()
.padding(start = 8.dp)
)
}
var driverIndex = 1
for (driver in drivers) {
var ind = driverIndex
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
vertical = 4.dp
)
.clickable {
selectedDriver.value = ind
isChanged.value = true
driverViewModel.selected = driver.driverPath
},
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedDriver.value == ind,
onClick = {
selectedDriver.value = ind
isChanged.value = true
driverViewModel.selected = driver.driverPath
}
)
Column(
modifier = Modifier
.weight(1f)
.padding(
start = 8.dp
)
) {
Text(text = driver.libraryName)
Text(text = driver.driverVersion)
Text(text = driver.description)
}
}
driverIndex++
}
Spacer(
modifier = Modifier.height(
8.dp
)
)
}
}
if (needsScrollbar) {
Box(
modifier = Modifier
.align(Alignment.CenterEnd)
.width(6.dp)
.fillMaxHeight()
.background(Color.LightGray.copy(alpha = 0.3f))
) {
val thumbRatio = 150.dp.value / (150.dp.value + scrollState.maxValue)
val thumbSize = (150.dp.value * thumbRatio).coerceAtLeast(40f)
val scrollRatio = scrollState.value.toFloat() / scrollState.maxValue.toFloat()
val maxOffset = 150.dp.value - (thumbSize * 0.7f)
val thumbOffset = maxOffset * scrollRatio
Box(
modifier = Modifier
.width(6.dp)
.height((thumbSize * 0.7f).dp)
.offset(y = thumbOffset.dp)
.background(
color = MaterialTheme.colorScheme.tertiary,
shape = MaterialTheme.shapes.small
)
)
}
}
}
}
}
}
}
}
@@ -0,0 +1,58 @@
package org.kenjinx.android.widgets
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
@Composable
fun ActionButton(
onClick: () -> Unit,
text: String,
icon: ImageVector? = null,
modifier: Modifier = Modifier,
isFullWidth: Boolean = false,
) {
val buttonModifier = if (isFullWidth) {
modifier.fillMaxWidth()
} else {
modifier
}
Button(
onClick = onClick,
modifier = buttonModifier.height(36.dp),
shape = MaterialTheme.shapes.small,
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
if (icon != null) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(4.dp))
}
Text(
text = text
)
}
}
}
@@ -0,0 +1,79 @@
package org.kenjinx.android.widgets
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun <T> DropdownSelector(
label: String,
selectedValue: T,
options: List<T>,
getDisplayText: (T) -> String,
onOptionSelected: (T) -> Unit,
modifier: Modifier = Modifier
) {
val expanded = remember { mutableStateOf(false) }
Row(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
.padding(horizontal = 8.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = label,
modifier = Modifier.weight(1f)
)
Box {
Button(
onClick = { expanded.value = true },
modifier = Modifier.height(36.dp),
shape = MaterialTheme.shapes.small
) {
Text(getDisplayText(selectedValue))
Icon(
Icons.Filled.KeyboardArrowDown,
contentDescription = "Expand",
modifier = Modifier.padding(start = 4.dp)
)
}
DropdownMenu(
expanded = expanded.value,
onDismissRequest = { expanded.value = false }
) {
options.forEach { option ->
DropdownMenuItem(
text = { Text(getDisplayText(option)) },
onClick = {
onOptionSelected(option)
expanded.value = false
}
)
}
}
}
}
}
@@ -0,0 +1,179 @@
package org.kenjinx.android.widgets
import android.annotation.SuppressLint
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import org.kenjinx.android.views.SettingViews.Companion.EXPANSTION_TRANSITION_DURATION
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@SuppressLint("UnusedTransitionTargetStateParameter")
fun ExpandableView(
onCardArrowClick: () -> Unit,
title: String,
icon: ImageVector,
isFirst: Boolean = false,
content: @Composable () -> Unit
) {
val expanded = false
val mutableExpanded = remember { mutableStateOf(expanded) }
val transitionState = remember {
MutableTransitionState(expanded).apply {
targetState = !mutableExpanded.value
}
}
val transition = updateTransition(transitionState, label = "transition")
val arrowRotationDegree = transition.animateFloat(
transitionSpec = {
tween(durationMillis = EXPANSTION_TRANSITION_DURATION)
},
label = "rotationDegreeTransition"
) {
if (mutableExpanded.value) 0f else 180f
}.value
Card(
shape = MaterialTheme.shapes.medium,
modifier = Modifier
.fillMaxWidth()
.padding(
top = if (isFirst) 8.dp else 4.dp,
bottom = 4.dp,
start = 12.dp,
end = 12.dp
)
) {
Column {
Card(
onClick = {
mutableExpanded.value = !mutableExpanded.value
onCardArrowClick()
},
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
if (icon != null) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
androidx.compose.foundation.layout.Spacer(modifier = Modifier.padding(end = 8.dp))
}
Text(
text = title,
style = MaterialTheme.typography.titleMedium
)
}
Icon(
Icons.Filled.KeyboardArrowUp,
contentDescription = "Expandable Arrow",
modifier = Modifier.rotate(arrowRotationDegree)
)
}
}
AnimatedVisibility(
visible = mutableExpanded.value,
enter = expandVertically(
expandFrom = Alignment.Top,
animationSpec = tween(EXPANSTION_TRANSITION_DURATION)
) + fadeIn(
initialAlpha = 0.3f,
animationSpec = tween(EXPANSTION_TRANSITION_DURATION)
),
exit = shrinkVertically(
shrinkTowards = Alignment.Top,
animationSpec = tween(EXPANSTION_TRANSITION_DURATION)
) + fadeOut(
animationSpec = tween(EXPANSTION_TRANSITION_DURATION)
)
) {
Column(modifier = Modifier.padding(
start = 8.dp,
end = 8.dp,
top = 0.dp,
bottom = 8.dp
)) {
content()
}
}
}
}
}
@Composable
fun ExpandableContent(
visible: Boolean = true,
content: @Composable () -> Unit
) {
val enterTransition = remember {
expandVertically(
expandFrom = Alignment.Top,
animationSpec = tween(EXPANSTION_TRANSITION_DURATION)
) + fadeIn(
initialAlpha = 0.3f,
animationSpec = tween(EXPANSTION_TRANSITION_DURATION)
)
}
val exitTransition = remember {
shrinkVertically(
// Expand from the top.
shrinkTowards = Alignment.Top,
animationSpec = tween(EXPANSTION_TRANSITION_DURATION)
) + fadeOut(
// Fade in with the initial alpha of 0.3f.
animationSpec = tween(EXPANSTION_TRANSITION_DURATION)
)
}
AnimatedVisibility(
visible = visible,
enter = enterTransition,
exit = exitTransition
) {
Column(modifier = Modifier.padding(8.dp)) {
content()
}
}
}
@@ -0,0 +1,227 @@
package org.kenjinx.android.widgets
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
@OptIn(ExperimentalMaterial3Api::class)
object SimpleAlertDialog {
@Composable
fun Loading(
showDialog: MutableState<Boolean>,
title: String = "Loading",
isDeterminate: Boolean = false,
progress: Float = 0f,
widthFraction: Float = 0.6f,
onDismissRequest: (() -> Unit)? = null
) {
val screenWidth = LocalConfiguration.current.screenWidthDp
if (showDialog.value) {
Dialog(
onDismissRequest = {
onDismissRequest?.invoke() ?: run { showDialog.value = false }
},
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Card(
modifier = Modifier
.width((screenWidth * widthFraction).dp)
.wrapContentHeight()
.padding(8.dp),
shape = MaterialTheme.shapes.medium
) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Text(
text = title,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
textAlign = TextAlign.Center
)
if (isDeterminate) {
LinearProgressIndicator(
progress = { progress },
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
)
} else {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
)
}
}
}
}
}
}
}
@Composable
fun Confirmation(
showDialog: MutableState<Boolean>,
title: String,
message: String,
confirmText: String = "Yes",
dismissText: String = "No",
widthFraction: Float = 0.6f,
onConfirm: () -> Unit,
onDismiss: () -> Unit = { showDialog.value = false }
) {
val screenWidth = LocalConfiguration.current.screenWidthDp
if (showDialog.value) {
Dialog(
onDismissRequest = { showDialog.value = false },
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Card(
modifier = Modifier
.width((screenWidth * widthFraction).dp)
.wrapContentHeight()
.padding(8.dp),
shape = MaterialTheme.shapes.medium
) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Text(
text = title,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
textAlign = TextAlign.Center
)
Text(
text = message,
modifier = Modifier
.fillMaxWidth()
.padding(start = 8.dp, bottom = 8.dp),
textAlign = TextAlign.Start
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
Button(
onClick = {
showDialog.value = false
onDismiss()
},
modifier = Modifier
.weight(1f)
.padding(end = 8.dp)
) {
Text(text = dismissText)
}
Button(
onClick = {
showDialog.value = false
onConfirm()
},
modifier = Modifier
.weight(1f)
.padding(start = 8.dp)
) {
Text(text = confirmText)
}
}
}
}
}
}
}
}
@Composable
fun Custom(
showDialog: MutableState<Boolean>,
onDismissRequest: () -> Unit = { showDialog.value = false },
properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false),
widthFraction: Float = 0.6f,
content: @Composable () -> Unit
) {
val screenWidth = LocalConfiguration.current.screenWidthDp
if (showDialog.value) {
Dialog(
onDismissRequest = onDismissRequest,
properties = properties
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Surface(
modifier = Modifier
.width((screenWidth * widthFraction).dp)
.wrapContentHeight(),
shape = MaterialTheme.shapes.large,
tonalElevation = AlertDialogDefaults.TonalElevation
) {
content()
}
}
}
}
}
@Composable
fun Progress(
showDialog: MutableState<Boolean>,
progressText: String,
progressValue: Float
) {
val isDeterminate = progressValue > -1
Loading(
showDialog = showDialog,
title = progressText,
isDeterminate = isDeterminate,
progress = if (isDeterminate) progressValue else 0f,
onDismissRequest = null
)
}
}
@@ -0,0 +1,68 @@
package org.kenjinx.android.widgets
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
/**
* A reusable switch component with consistent styling across the app.
*
* @param text Main text label for the switch
* @param checked Current state of the switch
* @param onCheckedChange Callback for when the switch is toggled
* @param description Optional descriptive text to display below the main label
* @param secondaryDescription Optional additional description text (displays as third line)
* @param modifier Modifier for customizing the layout
*/
@Composable
fun SwitchSelector(
label: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.padding(horizontal = 8.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = label,
modifier = Modifier.weight(1f)
)
Switch(
checked = checked,
onCheckedChange = onCheckedChange,
)
}
}
@Composable
fun MutableState<Boolean>.SwitchSelector(
label: String,
modifier: Modifier = Modifier
) {
SwitchSelector(
label = label,
checked = this.value,
onCheckedChange = { this.value = it },
modifier = modifier
)
}
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M10.5,3A2.5,2.5 0,0 1,13 5.5L13,11h5.5a2.5,2.5 0,0 1,2.5 2.5v5a2.5,2.5 0,0 1,-2.5 2.5h-13A2.5,2.5 0,0 1,3 18.5v-13A2.5,2.5 0,0 1,5.5 3h5ZM11,13L5,13v5.5a0.5,0.5 0,0 0,0.5 0.5L11,19v-6ZM18.5,13L13,13v6h5.5a0.5,0.5 0,0 0,0.5 -0.5v-5a0.5,0.5 0,0 0,-0.5 -0.5ZM10.5,5h-5a0.5,0.5 0,0 0,-0.5 0.5L5,11h6L11,5.5a0.5,0.5 0,0 0,-0.5 -0.5ZM17.883,2.007L18,2a1,1 0,0 1,0.993 0.883L19,3v2h2a1,1 0,0 1,0.993 0.883L22,6a1,1 0,0 1,-0.883 0.993L21,7h-2v2a1,1 0,0 1,-0.883 0.993L18,10a1,1 0,0 1,-0.993 -0.883L17,9L17,7h-2a1,1 0,0 1,-0.993 -0.883L14,6a1,1 0,0 1,0.883 -0.993L15,5h2L17,3a1,1 0,0 1,0.883 -0.993L18,2l-0.117,0.007Z"
android:fillColor="#ffffff"/>
</vector>
@@ -0,0 +1,35 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="255.76"
android:viewportHeight="255.76">
<group android:scaleX="0.44"
android:scaleY="0.44"
android:translateX="71.6128"
android:translateY="71.6128">
<path
android:pathData="M80.63,0V220.39H44.37c-14,0 -35.74,-20.74 -35.74,-39.13V40.13C8.63,19.19 31.36,0 49.06,0Z"
android:fillColor="#02c5e5"/>
<path
android:pathData="M175.13,35.37V255.76h36.26c14,0 35.74,-20.74 35.74,-39.13V75.5c0,-20.94 -22.73,-40.13 -40.43,-40.13Z"
android:fillColor="#ff5f55"/>
<path
android:pathData="M124.34,137.96l-1.76,7.61l-31.94,0l2.25,-7.61l31.45,0z"
android:fillColor="#02c5e5"/>
<path
android:pathData="M160.29,137.96l-2.45,7.61l-35.26,0l1.76,-7.61l35.95,0z"
android:fillColor="#ff5f55"/>
<path
android:pathData="M130.39,111.86l-1.77,7.61l-33.48,0l2.25,-7.61l33,0z"
android:fillColor="#02c5e5"/>
<path
android:pathData="M164.79,111.86l-2.45,7.61l-33.72,0l1.77,-7.61l34.4,0z"
android:fillColor="#ff5f55"/>
<path
android:pathData="M104.24,167.99l18.59,-80.22l6.95,0l-18.59,80.22l-6.95,0z"
android:fillColor="#02c5e5"/>
<path
android:pathData="M128.18,167.99l18.59,-80.22l7.12,0l-18.59,80.22l-7.12,0z"
android:fillColor="#ff5f55"/>
</group>
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/constraint"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/leftcontainer"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<FrameLayout
android:id="@+id/rightcontainer"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5"
app:layout_constraintGuide_begin="20dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Kenji-NX</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.KenjinxAndroid" parent="android:Theme.Material.Light.NoActionBar" />
</resources>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="external"
path="/" />
<external-files-path
name="external_files"
path="/" />
<files-path
name="files"
path="/" />
</paths>

Some files were not shown because too many files have changed in this diff Show More