Compare commits
223 Commits
Author | SHA1 | Date | |
---|---|---|---|
7b59fcb4f1 | |||
a261b4edc0 | |||
60a4940be8 | |||
94262d3bc1 | |||
532c187d83 | |||
17cb07a366 | |||
ce35eb4122 | |||
d9bdbbd307 | |||
5db6593abd | |||
35ea452ff5 | |||
3820aa20eb | |||
c047d31989 | |||
cd18cab6d3 | |||
9057310551 | |||
c94e9cdb9a | |||
c8c06eaae5 | |||
|
59b2c7be48 | ||
9a733f881b | |||
fd02603860 | |||
ee85c709c9 | |||
f20c739c48 | |||
f2f8f2c92e | |||
|
f8bbed282e | ||
7a9b79565b | |||
8eb09a5aa1 | |||
bc3f43705c | |||
6b22510752 | |||
8d4b921749 | |||
67bd44f9fb | |||
|
1eb6b1dcb0 | ||
|
062f2e3613 | ||
|
0eb18780e4 | ||
|
1538e5bc99 | ||
a6f200ff21 | |||
f0d4e9bede | |||
64ddc4065a | |||
bf186d81b1 | |||
|
f783bdb170 | ||
|
80d583e219 | ||
d78e41a0f0 | |||
005debcd8a | |||
b7ad037647 | |||
5ee92bc1ee | |||
1f161e2217 | |||
284a473584 | |||
|
e21a1a6e62 | ||
5bf0190771 | |||
|
9866b07654 | ||
fbdf65c57b | |||
fb8401098e | |||
e8265931bc | |||
dbf529eed4 | |||
e71ba6db9e | |||
a532068c86 | |||
f266a5138e | |||
4171d9cc87 | |||
c0f4c4de3e | |||
0181518c79 | |||
ac78553119 | |||
|
8e5c2c4897 | ||
|
db11d3ec7b | ||
|
23a14cf674 | ||
dc28963b18 | |||
43070ef584 | |||
aab13f7dd2 | |||
af558a6b41 | |||
cbcfc5347e | |||
7ab51bcd78 | |||
|
d4d5ee3154 | ||
|
40c42e3b8a | ||
|
0f989e37ee | ||
|
3c26abba0e | ||
3c1e1a20da | |||
|
4b622b058a | ||
|
b116381d28 | ||
|
575f130854 | ||
880579ea15 | |||
4fb563aed3 | |||
7083b0fb09 | |||
bd244fb402 | |||
|
9cf737dde1 | ||
a25ed78f6c | |||
|
592f225798 | ||
|
0785a0064a | ||
|
91784d6d04 | ||
|
9db28e0daa | ||
|
365c63e99e | ||
|
714de48c75 | ||
|
413367631f | ||
|
2714e5d0f6 | ||
|
4157a8c8e1 | ||
|
9b03f0a82e | ||
|
4c4b00d719 | ||
|
c0f745994c | ||
|
e2726757f7 | ||
|
d833004edd | ||
|
c16954e407 | ||
|
478a488348 | ||
|
6fbceface0 | ||
|
e7d88cca42 | ||
|
035e35c962 | ||
|
70df6b60c2 | ||
|
3219db4e89 | ||
|
732281da4a | ||
|
8c5e0cb61f | ||
|
371c3d3115 | ||
|
46c24e6c47 | ||
|
fe35c3a31c | ||
|
1f26bfa168 | ||
|
c3f00dedc2 | ||
|
1150926130 | ||
|
f69818097b | ||
|
8cd48bd34f | ||
|
f58cf228ae | ||
|
6156f4b0a8 | ||
|
4237754178 | ||
|
e3b85d1207 | ||
|
a1a38c20af | ||
|
25a36b9291 | ||
|
eded502651 | ||
|
28f62eccab | ||
|
6da47b485c | ||
|
c99e7bd5f0 | ||
|
0db262d8be | ||
|
0db84b59d0 | ||
|
5e88542be6 | ||
|
192f52f7c9 | ||
|
b92aa05c33 | ||
|
2f9c3e791e | ||
|
cf99ce6b30 | ||
|
8d38855874 | ||
93785b726a | |||
|
311c059bf8 | ||
|
16a9de2fe2 | ||
|
eb122a4e80 | ||
|
e4cb11e84b | ||
|
0df81d3e20 | ||
|
22443daf2c | ||
|
23844d8836 | ||
|
b5b0917f26 | ||
|
af36361f42 | ||
|
141559689e | ||
|
93232becc0 | ||
|
8c205f5bed | ||
|
1f4ff9cea6 | ||
|
2a38a33ff8 | ||
|
e8dc878d1f | ||
|
c820cb204a | ||
|
9985ffb2fc | ||
|
ccf20998f4 | ||
|
13c4b3bd20 | ||
|
ebe6a07c9a | ||
|
6c5022f692 | ||
|
c7bb8e5baf | ||
|
da679d619e | ||
|
9e83c219e7 | ||
|
78d9ec0f2a | ||
|
3f4adfd6bb | ||
|
75a5c0876e | ||
|
b7a313a574 | ||
|
4b4946ca1e | ||
|
3d33abf1cd | ||
|
3ce839dc11 | ||
|
a254814f11 | ||
|
df9e57bf05 | ||
|
ecc472ab14 | ||
|
8535658791 | ||
|
3ecb0895aa | ||
|
e9157515b9 | ||
|
8f74118a43 | ||
|
dd5407e0cb | ||
|
1c703d231c | ||
|
cc93ab60b4 | ||
|
b8f3c0c177 | ||
|
a6b126e91f | ||
|
48e8b205ec | ||
|
7f4a384c69 | ||
|
ef72016f28 | ||
|
9d894bc80e | ||
|
d051f3a1b3 | ||
|
443332f33b | ||
|
caa7ae51bb | ||
|
bfd46c88c8 | ||
|
6befa9c8e9 | ||
|
bd51d690ab | ||
|
cbb0bc2f53 | ||
|
4918f6a50f | ||
|
087a56cf06 | ||
|
fe5495288e | ||
|
6e76ef4320 | ||
|
7624c29d96 | ||
|
ee2ebbfb91 | ||
|
a6299e1cb5 | ||
|
6f87b3855d | ||
|
d006cb54f5 | ||
|
0810698049 | ||
|
bd4b420ccd | ||
|
c658140b4b | ||
|
17f710bfd5 | ||
|
fdd2efbbbe | ||
|
7f4d379e02 | ||
|
735b01bd5f | ||
|
36883505da | ||
|
7f85835a8f | ||
|
fb78a8a0cd | ||
|
0fed6077fe | ||
|
65dc4b42d3 | ||
|
1c19f6069f | ||
|
e22b748ce9 | ||
|
d31d2387f3 | ||
|
17ebe562a3 | ||
|
f9d97b5d05 | ||
|
357d3aaf8e | ||
|
7b0794d243 | ||
|
0bfc98fe26 | ||
|
f09359eb58 | ||
|
d4f6bf31c2 | ||
|
3cf9c06ae4 | ||
|
3991e90bbc | ||
|
fbd1c6b88d | ||
|
d6c82c3f7c | ||
|
e621d5b02c | ||
|
e66d7fd516 |
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -6,9 +6,9 @@ labels: Unconfirmed bug
|
|||||||
assignees: ''
|
assignees: ''
|
||||||
---
|
---
|
||||||
|
|
||||||
##### Minetest version
|
##### MultiCraft version
|
||||||
<!--
|
<!--
|
||||||
Paste Minetest version between quotes below
|
Paste MultiCraft version between quotes below
|
||||||
If you are on a devel version, please add git commit hash
|
If you are on a devel version, please add git commit hash
|
||||||
You can use `minetest --version` to find it.
|
You can use `minetest --version` to find it.
|
||||||
-->
|
-->
|
||||||
|
14
.github/workflows/android.yml
vendored
@ -8,7 +8,7 @@ on:
|
|||||||
- 'lib/**.cpp'
|
- 'lib/**.cpp'
|
||||||
- 'src/**.[ch]'
|
- 'src/**.[ch]'
|
||||||
- 'src/**.cpp'
|
- 'src/**.cpp'
|
||||||
- 'build/android/**'
|
- 'Android/**'
|
||||||
- '.github/workflows/android.yml'
|
- '.github/workflows/android.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
@ -16,7 +16,7 @@ on:
|
|||||||
- 'lib/**.cpp'
|
- 'lib/**.cpp'
|
||||||
- 'src/**.[ch]'
|
- 'src/**.[ch]'
|
||||||
- 'src/**.cpp'
|
- 'src/**.cpp'
|
||||||
- 'build/android/**'
|
- 'Android/**'
|
||||||
- '.github/workflows/android.yml'
|
- '.github/workflows/android.yml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@ -24,22 +24,22 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '11'
|
java-version: '17'
|
||||||
- name: Install GNU gettext
|
- name: Install GNU gettext
|
||||||
run: sudo apt install gettext
|
run: sudo apt install gettext
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: cd build/android; ./gradlew assemblerelease
|
run: cd Android; ./gradlew assemblerelease
|
||||||
- name: Save armeabi artifact
|
- name: Save armeabi artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: MultiCraft-armeabi-v7a.apk
|
name: MultiCraft-armeabi-v7a.apk
|
||||||
path: build/android/app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned.apk
|
path: Android/app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned.apk
|
||||||
- name: Save arm64 artifact
|
- name: Save arm64 artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: MultiCraft-arm64-v8a.apk
|
name: MultiCraft-arm64-v8a.apk
|
||||||
path: build/android/app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
|
path: Android/app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
|
||||||
|
105
.github/workflows/build.yml
vendored
@ -48,22 +48,22 @@ jobs:
|
|||||||
# run: |
|
# run: |
|
||||||
# ./bin/multicraft --run-unittests
|
# ./bin/multicraft --run-unittests
|
||||||
|
|
||||||
# This is the current gcc compiler (available in bionic)
|
# This is the current gcc compiler (available in jammy)
|
||||||
gcc_8:
|
gcc_10:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: |
|
run: |
|
||||||
source ./util/ci/common.sh
|
source ./util/ci/common.sh
|
||||||
install_linux_deps g++-8
|
install_linux_deps g++-10
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
./util/ci/build.sh
|
./util/ci/build.sh
|
||||||
env:
|
env:
|
||||||
CC: gcc-8
|
CC: gcc-10
|
||||||
CXX: g++-8
|
CXX: g++-10
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: |
|
run: |
|
||||||
@ -92,30 +92,30 @@ jobs:
|
|||||||
# ./bin/multicraft --run-unittests
|
# ./bin/multicraft --run-unittests
|
||||||
|
|
||||||
# This is the current clang version
|
# This is the current clang version
|
||||||
clang_9:
|
clang_11:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: |
|
run: |
|
||||||
source ./util/ci/common.sh
|
source ./util/ci/common.sh
|
||||||
install_linux_deps clang-9 valgrind libluajit-5.1-dev
|
install_linux_deps clang-11 valgrind libluajit-5.1-dev
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
./util/ci/build.sh
|
./util/ci/build.sh
|
||||||
env:
|
env:
|
||||||
CC: clang-9
|
CC: clang-11
|
||||||
CXX: clang++-9
|
CXX: clang++-11
|
||||||
CMAKE_FLAGS: "-DREQUIRE_LUAJIT=1"
|
CMAKE_FLAGS: "-DREQUIRE_LUAJIT=1"
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: |
|
run: |
|
||||||
./bin/multicraft --run-unittests
|
./bin/multicraft --run-unittests
|
||||||
|
|
||||||
- name: Valgrind
|
# - name: Valgrind
|
||||||
run: |
|
# run: |
|
||||||
valgrind --leak-check=full --leak-check-heuristics=all --undef-value-errors=no --error-exitcode=9 ./bin/multicraft --run-unittests
|
# valgrind --leak-check=full --leak-check-heuristics=all --undef-value-errors=no --error-exitcode=9 ./bin/multicraft --run-unittests
|
||||||
|
|
||||||
# Build with prometheus-cpp (server-only)
|
# Build with prometheus-cpp (server-only)
|
||||||
# clang_9_prometheus:
|
# clang_9_prometheus:
|
||||||
@ -171,18 +171,18 @@ jobs:
|
|||||||
|
|
||||||
docker:
|
docker:
|
||||||
name: "Docker image"
|
name: "Docker image"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Build docker image
|
- name: Build docker image
|
||||||
run: |
|
run: |
|
||||||
docker build .
|
docker build .
|
||||||
|
|
||||||
win32:
|
win32:
|
||||||
name: "MinGW cross-compiler (32-bit)"
|
name: "MinGW cross-compiler (32-bit)"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Install compiler
|
- name: Install compiler
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update -q && sudo apt-get install gettext -qyy
|
sudo apt-get update -q && sudo apt-get install gettext -qyy
|
||||||
@ -198,9 +198,9 @@ jobs:
|
|||||||
|
|
||||||
win64:
|
win64:
|
||||||
name: "MinGW cross-compiler (64-bit)"
|
name: "MinGW cross-compiler (64-bit)"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Install compiler
|
- name: Install compiler
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update -q && sudo apt-get install gettext -qyy
|
sudo apt-get update -q && sudo apt-get install gettext -qyy
|
||||||
@ -214,25 +214,67 @@ jobs:
|
|||||||
NO_MINETEST_GAME: 1
|
NO_MINETEST_GAME: 1
|
||||||
NO_PACKAGE: 1
|
NO_PACKAGE: 1
|
||||||
|
|
||||||
|
msys2-mingw64:
|
||||||
|
name: "MSYS2-MinGW (64-bit)"
|
||||||
|
runs-on: windows-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: msys2 {0}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install MSYS2
|
||||||
|
uses: msys2/setup-msys2@v2
|
||||||
|
with:
|
||||||
|
msystem: MINGW64
|
||||||
|
update: true
|
||||||
|
install: mingw-w64-x86_64-clang mingw-w64-x86_64-cmake mingw-w64-x86_64-autotools mingw-w64-x86_64-tcl git zip
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
cd ./Windows
|
||||||
|
./Start.sh
|
||||||
|
cmake --build . -j
|
||||||
|
cd -
|
||||||
|
strip -s ./bin/multicraft.exe
|
||||||
|
|
||||||
|
- name: Create ZIP
|
||||||
|
run: |
|
||||||
|
mkdir MultiCraft
|
||||||
|
cp -r ./bin MultiCraft/
|
||||||
|
cp -r ./builtin MultiCraft/
|
||||||
|
cp -r ./client MultiCraft/
|
||||||
|
cp -r ./fonts MultiCraft/
|
||||||
|
cp -r ./locale MultiCraft/
|
||||||
|
cp -r ./textures MultiCraft/
|
||||||
|
zip -r MultiCraft.zip MultiCraft
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: MultiCraft-msys2-mingw64
|
||||||
|
path: ./MultiCraft.zip
|
||||||
|
|
||||||
msvc:
|
msvc:
|
||||||
name: VS 2019 ${{ matrix.config.arch }}-${{ matrix.type }}
|
name: VS 2022 ${{ matrix.config.arch }}-${{ matrix.type }}
|
||||||
runs-on: windows-2019
|
runs-on: windows-2022
|
||||||
env:
|
env:
|
||||||
VCPKG_VERSION: af2287382b1991dbdcb7e5112d236f3323b9dd7a
|
VCPKG_VERSION: a42af01b72c28a8e1d7b48107b33e4f286a55ef6
|
||||||
# 2022.03.10
|
# 2023.11.20
|
||||||
vcpkg_packages: irrlicht zlib curl[winssl] openal-soft libvorbis libogg sqlite3 freetype luajit libiconv gettext jsoncpp
|
vcpkg_packages: irrlicht zlib curl[winssl] openal-soft libvorbis libogg sqlite3 freetype luajit gmp jsoncpp
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
config:
|
config:
|
||||||
- {
|
- {
|
||||||
arch: x86,
|
arch: x86,
|
||||||
generator: "-G'Visual Studio 16 2019' -A Win32",
|
generator: "-G'Visual Studio 17 2022' -A Win32",
|
||||||
vcpkg_triplet: x86-windows
|
vcpkg_triplet: x86-windows
|
||||||
}
|
}
|
||||||
- {
|
- {
|
||||||
arch: x64,
|
arch: x64,
|
||||||
generator: "-G'Visual Studio 16 2019' -A x64",
|
generator: "-G'Visual Studio 17 2022' -A x64",
|
||||||
vcpkg_triplet: x64-windows
|
vcpkg_triplet: x64-windows
|
||||||
}
|
}
|
||||||
type: [portable]
|
type: [portable]
|
||||||
@ -242,7 +284,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Restore from cache and run vcpkg
|
- name: Restore from cache and run vcpkg
|
||||||
uses: lukka/run-vcpkg@v7
|
uses: lukka/run-vcpkg@v7
|
||||||
@ -259,7 +301,6 @@ jobs:
|
|||||||
-DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}\vcpkg\scripts\buildsystems\vcpkg.cmake" `
|
-DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}\vcpkg\scripts\buildsystems\vcpkg.cmake" `
|
||||||
-DCMAKE_BUILD_TYPE=Release `
|
-DCMAKE_BUILD_TYPE=Release `
|
||||||
-DENABLE_POSTGRESQL=OFF `
|
-DENABLE_POSTGRESQL=OFF `
|
||||||
-DENABLE_SYSTEM_JSONCPP=ON `
|
|
||||||
-DRUN_IN_PLACE=${{ contains(matrix.type, 'portable') }} .
|
-DRUN_IN_PLACE=${{ contains(matrix.type, 'portable') }} .
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
@ -283,5 +324,5 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: msvc-${{ matrix.config.arch }}-${{ matrix.type }}
|
name: MultiCraft-msvc-${{ matrix.config.arch }}-${{ matrix.type }}
|
||||||
path: .\Package\
|
path: .\Package\
|
||||||
|
14
.github/workflows/cpp_lint.yml
vendored
@ -25,27 +25,27 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
clang_format:
|
clang_format:
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Install clang-format
|
- name: Install clang-format
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install clang-format-9 -qyy
|
sudo apt-get install clang-format-11 -qyy
|
||||||
|
|
||||||
- name: Run clang-format
|
- name: Run clang-format
|
||||||
run: |
|
run: |
|
||||||
source ./util/ci/lint.sh
|
source ./util/ci/lint.sh
|
||||||
perform_lint
|
perform_lint
|
||||||
env:
|
env:
|
||||||
CLANG_FORMAT: clang-format-9
|
CLANG_FORMAT: clang-format-11
|
||||||
|
|
||||||
clang_tidy:
|
clang_tidy:
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install clang-tidy-9 -qyy
|
sudo apt-get install clang-tidy-11 -qyy
|
||||||
source ./util/ci/common.sh
|
source ./util/ci/common.sh
|
||||||
install_linux_deps
|
install_linux_deps
|
||||||
|
|
||||||
|
2
.github/workflows/lua_lint.yml
vendored
@ -14,7 +14,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
luacheck:
|
luacheck:
|
||||||
name: "Builtin Luacheck and Unit Tests"
|
name: "Builtin Luacheck and Unit Tests"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: leafo/gh-actions-lua@v9
|
- uses: leafo/gh-actions-lua@v9
|
||||||
|
3
.gitignore
vendored
@ -41,6 +41,7 @@ build/.cmake/
|
|||||||
/bin/
|
/bin/
|
||||||
/games/*
|
/games/*
|
||||||
!/games/devtest/
|
!/games/devtest/
|
||||||
|
!/games/minetest
|
||||||
/cache
|
/cache
|
||||||
/textures/*
|
/textures/*
|
||||||
!/textures/base/
|
!/textures/base/
|
||||||
@ -55,8 +56,6 @@ build/.cmake/
|
|||||||
/clientmods/*
|
/clientmods/*
|
||||||
!/clientmods/preview/
|
!/clientmods/preview/
|
||||||
/client/mod_storage/
|
/client/mod_storage/
|
||||||
/builtin/mainmenu/hosting/
|
|
||||||
/textures/base/pack/hosting/
|
|
||||||
|
|
||||||
## Configuration/log files
|
## Configuration/log files
|
||||||
multicraft.conf
|
multicraft.conf
|
||||||
|
411
.gitlab-ci.yml
@ -1,293 +1,216 @@
|
|||||||
---
|
---
|
||||||
# Github repository is cloned every day on Gitlab.com
|
# Github repository is really at minetest.org using the poikilos git.minetest.io
|
||||||
# https://gitlab.com/minetest/minetest
|
# https://gitlab.com/minenux/minetest-engine-minetest
|
||||||
# Pipelines URL: https://gitlab.com/minetest/minetest/pipelines
|
# Pipelines URL: https://gitlab.com/minenux/minetest-engine-minetest/pipelines
|
||||||
|
# packages moved to https://build.opensuse.org/project/show/home:venenux:minenux
|
||||||
|
# in future we only build here, or made apk packs for alpine
|
||||||
|
|
||||||
|
variables:
|
||||||
|
GIT_SUBMODULE_STRATEGY: recursive
|
||||||
|
GIT_SUBMODULE_DEPTH: 1
|
||||||
|
GIT_STRATEGY: clone
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- build
|
- build
|
||||||
- package
|
|
||||||
- deploy
|
|
||||||
|
|
||||||
variables:
|
.build_template: &build_definition
|
||||||
MINETEST_GAME_REPO: "https://github.com/minetest/minetest_game.git"
|
|
||||||
CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH
|
|
||||||
|
|
||||||
.build_template:
|
|
||||||
stage: build
|
stage: build
|
||||||
script:
|
script:
|
||||||
- mkdir cmakebuild
|
- mkdir cmakebuild
|
||||||
- mkdir -p artifact/minetest/usr/
|
- mkdir -p artifact/multicraft/usr/
|
||||||
- cd cmakebuild
|
- cd cmakebuild
|
||||||
- cmake -DCMAKE_INSTALL_PREFIX=../artifact/minetest/usr/ -DCMAKE_BUILD_TYPE=Release -DRUN_IN_PLACE=FALSE -DENABLE_GETTEXT=TRUE -DENABLE_SYSTEM_JSONCPP=TRUE -DBUILD_SERVER=TRUE ..
|
- cmake -DCMAKE_INSTALL_PREFIX=../artifact/multicraft/usr/ -DBUILD_SERVER=ON -DBUILD_CLIENT=ON -DRUN_IN_PLACE=OFF -DENABLE_CURL=ON -DENABLE_SOUND=ON -DENABLE_LUAJIT=ON -DENABLE_GETTEXT=ON -DENABLE_FREETYPE=ON -DENABLE_SYSTEM_GMP=ON -DENABLE_SYSTEM_JSONCPP=ON -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo -DENABLE_POSTGRESQL=ON ..
|
||||||
- make -j2
|
- make -j$(nproc)
|
||||||
- make install
|
- make install
|
||||||
artifacts:
|
artifacts:
|
||||||
when: on_success
|
when: on_success
|
||||||
expire_in: 1h
|
expire_in: 1y
|
||||||
paths:
|
paths:
|
||||||
- artifact/*
|
- artifact/*
|
||||||
|
|
||||||
.debpkg_template:
|
##
|
||||||
stage: package
|
## Alpine the limited distro for nonsocial geeks
|
||||||
before_script:
|
##
|
||||||
- apt-get update -y
|
|
||||||
- apt-get install -y git
|
|
||||||
- mkdir -p build/deb/minetest/DEBIAN/
|
|
||||||
- cp misc/debpkg-control build/deb/minetest/DEBIAN/control
|
|
||||||
- cp -a artifact/minetest/usr build/deb/minetest/
|
|
||||||
script:
|
|
||||||
- git clone $MINETEST_GAME_REPO build/deb/minetest/usr/share/minetest/games/minetest_game
|
|
||||||
- rm -rf build/deb/minetest/usr/share/minetest/games/minetest/.git
|
|
||||||
- sed -i 's/DATEPLACEHOLDER/'$(date +%y.%m.%d)'/g' build/deb/minetest/DEBIAN/control
|
|
||||||
- sed -i 's/LEVELDB_PLACEHOLDER/'$LEVELDB_PKG'/g' build/deb/minetest/DEBIAN/control
|
|
||||||
- cd build/deb/ && dpkg-deb -b minetest/ && mv minetest.deb ../../
|
|
||||||
artifacts:
|
|
||||||
expire_in: 90 day
|
|
||||||
paths:
|
|
||||||
- ./*.deb
|
|
||||||
|
|
||||||
.debpkg_install:
|
build:alpine-312:
|
||||||
stage: deploy
|
extends: .build_template
|
||||||
|
image: alpine:3.12
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update -y
|
- apk update
|
||||||
script:
|
- apk add build-base cmake pkgconf gettext-dev bzip2-dev curl-dev libnl3-dev rtmpdump-dev libidn2-dev ncurses-dev freetype-dev mesa-dev gmp-dev irrlicht-dev libjpeg-turbo-dev jsoncpp-dev leveldb-dev luajit-dev lua5.1-dev libogg-dev openal-soft-dev libpng-dev postgresql-dev hiredis-dev sqlite-dev libvorbis-dev libxi-dev zlib-dev doxygen libxrandr-dev libx11-dev zstd-dev openssl-dev samurai
|
||||||
- apt-get install -y ./*.deb
|
|
||||||
- minetest --version
|
build:alpine-314:
|
||||||
|
extends: .build_template
|
||||||
|
image: alpine:3.14
|
||||||
|
before_script:
|
||||||
|
- apk update
|
||||||
|
- apk add build-base cmake pkgconf gettext-dev bzip2-dev curl-dev libnl3-dev rtmpdump-dev libidn2-dev ncurses-dev freetype-dev mesa-dev gmp-dev irrlicht-dev libjpeg-turbo-dev jsoncpp-dev leveldb-dev luajit-dev lua5.1-dev libogg-dev openal-soft-dev libpng-dev postgresql-dev hiredis-dev sqlite-dev libvorbis-dev libxi-dev zlib-dev doxygen libxrandr-dev libx11-dev zstd-dev openssl-dev samurai
|
||||||
|
|
||||||
|
build:alpine-316:
|
||||||
|
extends: .build_template
|
||||||
|
image: alpine:3.16
|
||||||
|
before_script:
|
||||||
|
- apk update
|
||||||
|
- apk add build-base cmake pkgconf gettext-dev bzip2-dev curl-dev libnl3-dev rtmpdump-dev libidn2-dev ncurses-dev freetype-dev mesa-dev gmp-dev irrlicht-dev libjpeg-turbo-dev jsoncpp-dev leveldb-dev luajit-dev lua5.1-dev libogg-dev openal-soft-dev libpng-dev postgresql-dev hiredis-dev sqlite-dev libvorbis-dev libxi-dev zlib-dev doxygen libxrandr-dev libx11-dev zstd-dev openssl-dev samurai
|
||||||
|
|
||||||
|
build:alpine-319:
|
||||||
|
extends: .build_template
|
||||||
|
image: alpine:3.19
|
||||||
|
before_script:
|
||||||
|
- apk update
|
||||||
|
- apk add build-base cmake pkgconf gettext-dev bzip2-dev curl-dev libnl3-dev rtmpdump-dev libidn2-dev ncurses-dev freetype-dev mesa-dev gmp-dev irrlicht-dev libjpeg-turbo-dev jsoncpp-dev leveldb-dev luajit-dev lua5.1-dev libogg-dev openal-soft-dev libpng-dev postgresql-dev hiredis-dev sqlite-dev libvorbis-dev libxi-dev zlib-dev doxygen libxrandr-dev libx11-dev zstd-dev openssl-dev samurai
|
||||||
|
|
||||||
|
# error - need to enable testing repo due spatial index
|
||||||
|
#build:alpine-edge:
|
||||||
|
# extends: .build_template
|
||||||
|
# image: alpine:edge
|
||||||
|
# before_script:
|
||||||
|
# - apk update
|
||||||
|
# - apk add build-base git cmake pkgconf gettext-dev bzip2-dev curl-dev libnl3-dev rtmpdump-dev libidn2-dev ncurses-dev freetype-dev mesa-dev gmp-dev irrlicht-dev libjpeg-turbo-dev jsoncpp-dev leveldb-dev luajit-dev lua5.1-dev libogg-dev openal-soft-dev libpng-dev postgresql-dev hiredis-dev sqlite-dev libvorbis-dev libxi-dev zlib-dev doxygen libxrandr-dev libx11-dev zstd-dev openssl1.1-compat-dev samurai libspatialindex-dev
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## Debian
|
## Debian mother of many distros
|
||||||
##
|
##
|
||||||
|
|
||||||
|
# Jessie
|
||||||
|
|
||||||
|
build:debian-8-64:
|
||||||
|
<<: *build_definition
|
||||||
|
image: amd64/debian:8
|
||||||
|
before_script:
|
||||||
|
- echo "" > /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
|
- echo "APT::Get::AllowUnauthenticated \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
|
- echo "Acquire::AllowInsecureRepositories \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
|
- echo "Acquire::AllowDowngradeToInsecureRepositories \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
|
- echo "Acquire::AllowReleaseInfoChange::Suite \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
|
- echo "Acquire::Check-Valid-Until \"false\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
|
- echo "Acquire::Languages \"en\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
|
- echo "Aptitude::CmdLine::Ignore-Trust-Violations \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
|
- rm -rf /etc/apt/sources.list
|
||||||
|
- echo "deb http://archive.debian.org/debian/ jessie main contrib" > /etc/apt/sources.list.d/50debianoficial.list
|
||||||
|
- echo "deb http://archive.debian.org/debian/ jessie-backports main contrib non-free" >> /etc/apt/sources.list.d/50debianoficial.list
|
||||||
|
- echo "deb http://deb.freexian.com/extended-lts jessie main contrib non-free" >> /etc/apt/sources.list.d/50debianoficial.list
|
||||||
|
- DEBIAN_FRONTEND=noninteractive apt-get update -y || true
|
||||||
|
- DEBIAN_FRONTEND=noninteractive apt-get -y --force-yes install build-essential cmake pkg-config cmake-data debhelper lsb-release gettext libbz2-dev libcurl4-gnutls-dev libnl-genl-3-dev libnl-3-dev librtmp-dev libidn11-dev libncurses-dev libfreetype6-dev libglu1-mesa-dev libgmp-dev libirrlicht-dev libjpeg-dev libleveldb-dev libluajit-5.1-dev liblua5.1-dev libogg-dev libopenal-dev libpng-dev libpq-dev libhiredis-dev libspatialindex-dev libsqlite3-dev libvorbis-dev libx11-dev libxxf86vm-dev libpq-dev postgresql-server-dev-all libhiredis-dev zlib1g-dev doxygen libxrandr-dev x11proto-xf86vidmode-dev
|
||||||
|
- DEBIAN_FRONTEND=noninteractive apt-get -y --force-yes -t jessie-backports install libjsoncpp-dev
|
||||||
|
|
||||||
|
build:debian-8-32:
|
||||||
|
<<: *build_definition
|
||||||
|
image: i386/debian:8
|
||||||
|
before_script:
|
||||||
|
- echo "" > /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
|
- echo "APT::Get::AllowUnauthenticated \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
|
- echo "Acquire::AllowInsecureRepositories \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
|
- echo "Acquire::AllowDowngradeToInsecureRepositories \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
|
- echo "Acquire::AllowReleaseInfoChange::Suite \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
|
- echo "Acquire::Check-Valid-Until \"false\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
|
- echo "Acquire::Languages \"en\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
|
- echo "Aptitude::CmdLine::Ignore-Trust-Violations \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
|
- rm -rf /etc/apt/sources.list
|
||||||
|
- echo "deb http://archive.debian.org/debian/ jessie main contrib" > /etc/apt/sources.list.d/50debianoficial.list
|
||||||
|
- echo "deb http://archive.debian.org/debian/ jessie-backports main contrib non-free" >> /etc/apt/sources.list.d/50debianoficial.list
|
||||||
|
- echo "deb http://deb.freexian.com/extended-lts jessie main contrib non-free" >> /etc/apt/sources.list.d/50debianoficial.list
|
||||||
|
- DEBIAN_FRONTEND=noninteractive apt-get update -y || true
|
||||||
|
- DEBIAN_FRONTEND=noninteractive apt-get -y --force-yes install build-essential cmake pkg-config cmake-data debhelper lsb-release gettext libbz2-dev libcurl4-gnutls-dev libnl-genl-3-dev libnl-3-dev librtmp-dev libidn11-dev libncurses-dev libfreetype6-dev libglu1-mesa-dev libgmp-dev libirrlicht-dev libjpeg-dev libleveldb-dev libluajit-5.1-dev liblua5.1-dev libogg-dev libopenal-dev libpng-dev libpq-dev libhiredis-dev libspatialindex-dev libsqlite3-dev libvorbis-dev libx11-dev libxxf86vm-dev libpq-dev postgresql-server-dev-all libhiredis-dev zlib1g-dev doxygen libxrandr-dev x11proto-xf86vidmode-dev
|
||||||
|
- DEBIAN_FRONTEND=noninteractive apt-get -y --force-yes -t jessie-backports install libjsoncpp-dev
|
||||||
|
|
||||||
# Stretch
|
# Stretch
|
||||||
|
|
||||||
build:debian-9:
|
build:debian-9-32:
|
||||||
extends: .build_template
|
<<: *build_definition
|
||||||
image: debian:9
|
image: i386/debian:9
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update -y
|
- echo "" > /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
- apt-get -y install build-essential libirrlicht-dev cmake libbz2-dev libpng-dev libjpeg-dev libxxf86vm-dev libgl1-mesa-dev libsqlite3-dev libogg-dev libvorbis-dev libopenal-dev libcurl4-gnutls-dev libfreetype6-dev zlib1g-dev libgmp-dev libjsoncpp-dev
|
- echo "APT::Get::AllowUnauthenticated \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
|
- echo "Acquire::AllowInsecureRepositories \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
|
- echo "deb http://archive.debian.org/debian/ stretch main contrib" > /etc/apt/sources.list.d/50debianoficial.list
|
||||||
|
- echo "deb http://deb.freexian.com/extended-lts stretch main contrib non-free" > /etc/apt/sources.list
|
||||||
|
- DEBIAN_FRONTEND=noninteractive apt-get update -y || true
|
||||||
|
- DEBIAN_FRONTEND=noninteractive apt-get -y --force-yes install build-essential cmake pkg-config debhelper lsb-release gettext libbz2-dev libcurl4-gnutls-dev libnl-genl-3-dev libnl-3-dev librtmp-dev libidn11-dev libncurses-dev libfreetype6-dev libglu1-mesa-dev libgmp-dev libirrlicht-dev libjpeg-dev libjsoncpp-dev libleveldb-dev libluajit-5.1-dev liblua5.1-dev libogg-dev libopenal-dev libpng-dev libpq-dev libhiredis-dev libspatialindex-dev libsqlite3-dev libvorbis-dev libx11-dev libxxf86vm-dev postgresql-server-dev-all libpq-dev libhiredis-dev zlib1g-dev doxygen libxrandr-dev mesa-common-dev x11proto-xf86vidmode-dev libzstd-dev
|
||||||
|
|
||||||
package:debian-9:
|
# Bullseye
|
||||||
extends: .debpkg_template
|
|
||||||
image: debian:9
|
|
||||||
needs:
|
|
||||||
- build:debian-9
|
|
||||||
variables:
|
|
||||||
LEVELDB_PKG: libleveldb1v5
|
|
||||||
|
|
||||||
deploy:debian-9:
|
build:debian-11:
|
||||||
extends: .debpkg_install
|
<<: *build_definition
|
||||||
image: debian:9
|
image: debian:11
|
||||||
needs:
|
|
||||||
- package:debian-9
|
|
||||||
|
|
||||||
# Buster
|
|
||||||
|
|
||||||
build:debian-10:
|
|
||||||
extends: .build_template
|
|
||||||
image: debian:10
|
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update -y
|
- apt-get update -y || true
|
||||||
- apt-get -y install build-essential libirrlicht-dev cmake libbz2-dev libpng-dev libjpeg-dev libxxf86vm-dev libgl1-mesa-dev libsqlite3-dev libogg-dev libvorbis-dev libopenal-dev libcurl4-gnutls-dev libfreetype6-dev zlib1g-dev libgmp-dev libjsoncpp-dev
|
- apt-get -y install build-essential cmake pkg-config debhelper lsb-release gettext libbz2-dev libcurl4-gnutls-dev libnl-genl-3-dev libnl-3-dev librtmp-dev libidn11-dev libncurses-dev libfreetype6-dev libglu1-mesa-dev libgmp-dev libirrlicht-dev libjpeg-dev libjsoncpp-dev libleveldb-dev libluajit-5.1-dev liblua5.1-dev libogg-dev libopenal-dev libpng-dev libpq-dev postgresql-server-dev-all libhiredis-dev libspatialindex-dev libsqlite3-dev libvorbis-dev libx11-dev libxxf86vm-dev postgresql-server-dev-all libpq-dev libhiredis-dev zlib1g-dev doxygen libxrandr-dev mesa-common-dev x11proto-xf86vidmode-dev libzstd-dev
|
||||||
|
|
||||||
package:debian-10:
|
# Bookworm
|
||||||
extends: .debpkg_template
|
|
||||||
image: debian:10
|
|
||||||
needs:
|
|
||||||
- build:debian-10
|
|
||||||
variables:
|
|
||||||
LEVELDB_PKG: libleveldb1d
|
|
||||||
|
|
||||||
deploy:debian-10:
|
build:debian-12:
|
||||||
extends: .debpkg_install
|
<<: *build_definition
|
||||||
image: debian:10
|
image: debian:12
|
||||||
needs:
|
|
||||||
- package:debian-10
|
|
||||||
|
|
||||||
##
|
|
||||||
## Ubuntu
|
|
||||||
##
|
|
||||||
|
|
||||||
# Xenial
|
|
||||||
|
|
||||||
build:ubuntu-16.04:
|
|
||||||
extends: .build_template
|
|
||||||
image: ubuntu:xenial
|
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update -y
|
- apt-get update -y || true
|
||||||
- apt-get -y install build-essential libirrlicht-dev cmake libbz2-dev libpng-dev libjpeg-dev libxxf86vm-dev libgl1-mesa-dev libsqlite3-dev libogg-dev libvorbis-dev libopenal-dev libcurl4-gnutls-dev libfreetype6-dev zlib1g-dev libgmp-dev libjsoncpp-dev
|
- apt-get -y install build-essential cmake pkg-config debhelper lsb-release gettext libbz2-dev libcurl4-gnutls-dev libnl-genl-3-dev libnl-3-dev librtmp-dev libidn11-dev libncurses-dev libfreetype6-dev libglu1-mesa-dev libgmp-dev libirrlicht-dev libjpeg-dev libjsoncpp-dev libleveldb-dev libluajit-5.1-dev liblua5.1-dev libogg-dev libopenal-dev libpng-dev libpq-dev libhiredis-dev libspatialindex-dev libsqlite3-dev libvorbis-dev libx11-dev libxxf86vm-dev postgresql-server-dev-all libpq-dev libhiredis-dev zlib1g-dev doxygen libxrandr-dev mesa-common-dev x11proto-xf86vidmode-dev libzstd-dev
|
||||||
|
|
||||||
package:ubuntu-16.04:
|
##
|
||||||
extends: .debpkg_template
|
## winbuntu the distro for stupid users
|
||||||
image: ubuntu:xenial
|
##
|
||||||
needs:
|
|
||||||
- build:ubuntu-16.04
|
|
||||||
variables:
|
|
||||||
LEVELDB_PKG: libleveldb1v5
|
|
||||||
|
|
||||||
deploy:ubuntu-16.04:
|
build:ubuntu-22.04:
|
||||||
extends: .debpkg_install
|
<<: *build_definition
|
||||||
image: ubuntu:xenial
|
image: ubuntu:jammy
|
||||||
needs:
|
|
||||||
- package:ubuntu-16.04
|
|
||||||
|
|
||||||
# Bionic
|
|
||||||
|
|
||||||
build:ubuntu-18.04:
|
|
||||||
extends: .build_template
|
|
||||||
image: ubuntu:bionic
|
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update -y
|
- DEBIAN_FRONTEND=noninteractive apt-get update -y
|
||||||
- apt-get -y install build-essential libirrlicht-dev cmake libbz2-dev libpng-dev libjpeg-dev libxxf86vm-dev libgl1-mesa-dev libsqlite3-dev libogg-dev libvorbis-dev libopenal-dev libcurl4-gnutls-dev libfreetype6-dev zlib1g-dev libgmp-dev libjsoncpp-dev
|
- DEBIAN_FRONTEND=noninteractive apt-get -y install build-essential cmake pkg-config debhelper lsb-release gettext libbz2-dev libcurl4-gnutls-dev libnl-genl-3-dev librtmp-dev libidn11-dev libncurses-dev libfreetype6-dev libglu1-mesa-dev libgmp-dev libirrlicht-dev libjpeg-dev libjsoncpp-dev libleveldb-dev libluajit-5.1-dev liblua5.1-dev libogg-dev libopenal-dev libpng-dev libpq-dev libhiredis-dev libspatialindex-dev libsqlite3-dev libvorbis-dev libx11-dev libxxf86vm-dev postgresql-server-dev-all zlib1g-dev doxygen libxrandr-dev mesa-common-dev x11proto-xf86vidmode-dev libzstd-dev
|
||||||
|
|
||||||
package:ubuntu-18.04:
|
# Focal most close to bullseye
|
||||||
extends: .debpkg_template
|
|
||||||
image: ubuntu:bionic
|
|
||||||
needs:
|
|
||||||
- build:ubuntu-18.04
|
|
||||||
variables:
|
|
||||||
LEVELDB_PKG: libleveldb1v5
|
|
||||||
|
|
||||||
deploy:ubuntu-18.04:
|
build:ubuntu-20.04:
|
||||||
extends: .debpkg_install
|
<<: *build_definition
|
||||||
image: ubuntu:bionic
|
image: ubuntu:focal
|
||||||
needs:
|
|
||||||
- package:ubuntu-18.04
|
|
||||||
|
|
||||||
##
|
|
||||||
## Fedora
|
|
||||||
##
|
|
||||||
|
|
||||||
# Fedora 28 <-> RHEL 8
|
|
||||||
build:fedora-28:
|
|
||||||
extends: .build_template
|
|
||||||
image: fedora:28
|
|
||||||
before_script:
|
before_script:
|
||||||
- dnf -y install make automake gcc gcc-c++ kernel-devel cmake libcurl-devel openal-soft-devel libvorbis-devel libXxf86vm-devel libogg-devel freetype-devel mesa-libGL-devel zlib-devel jsoncpp-devel irrlicht-devel gmp-devel sqlite-devel luajit-devel leveldb-devel ncurses-devel spatialindex-devel
|
- DEBIAN_FRONTEND=noninteractive apt-get update -y
|
||||||
|
- DEBIAN_FRONTEND=noninteractive apt-get -y install build-essential cmake pkg-config debhelper lsb-release gettext libbz2-dev libcurl4-gnutls-dev libnl-genl-3-dev librtmp-dev libidn11-dev libncurses-dev libfreetype6-dev libglu1-mesa-dev libgmp-dev libirrlicht-dev libjpeg-dev libjsoncpp-dev libleveldb-dev libluajit-5.1-dev liblua5.1-dev libogg-dev libopenal-dev libpng-dev libpq-dev libhiredis-dev libspatialindex-dev libsqlite3-dev libvorbis-dev libx11-dev libxxf86vm-dev postgresql-server-dev-all zlib1g-dev doxygen libxrandr-dev mesa-common-dev x11proto-xf86vidmode-dev libzstd-dev
|
||||||
|
|
||||||
##
|
# yakkety most close to jessie
|
||||||
## MinGW for Windows
|
|
||||||
##
|
|
||||||
|
|
||||||
.generic_win_template:
|
build:ubuntu-16.10:
|
||||||
image: ubuntu:bionic
|
<<: *build_definition
|
||||||
|
image: ubuntu:yakkety
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update -y
|
- echo "" > /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
- apt-get install -y wget xz-utils unzip git cmake gettext
|
- echo "APT::Get::AllowUnauthenticated \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
- wget -nv http://minetest.kitsunemimi.pw/mingw-w64-${WIN_ARCH}_9.2.0_ubuntu18.04.tar.xz -O mingw.tar.xz
|
- echo "Acquire::AllowInsecureRepositories \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
- tar -xaf mingw.tar.xz -C /usr
|
- rm -rf /etc/apt/sources.list
|
||||||
|
- echo "deb http://old-releases.ubuntu.com/ubuntu/ yakkety main restricted universe multiverse" > /etc/apt/sources.list.d/50debianoficial.list
|
||||||
|
- echo "deb http://old-releases.ubuntu.com/ubuntu/ yakkety-updates main restricted universe multiverse" >> /etc/apt/sources.list.d/50debianoficial.list
|
||||||
|
- echo "deb http://old-releases.ubuntu.com/ubuntu yakkety-security main restricted universe multiverse" >> /etc/apt/sources.list.d/50debianoficial.list
|
||||||
|
- apt-get update -y || true
|
||||||
|
- apt-get -y --force-yes install build-essential cmake pkg-config debhelper lsb-release gettext libbz2-dev libcurl4-gnutls-dev libnl-genl-3-dev libnl-3-dev librtmp-dev libidn11-dev libncurses-dev libfreetype6-dev libglu1-mesa-dev libgmp-dev libirrlicht-dev libjpeg-dev libjsoncpp-dev libleveldb-dev libluajit-5.1-dev liblua5.1-dev libogg-dev libopenal-dev libpng-dev libpq-dev libhiredis-dev libsqlite3-dev libvorbis-dev libx11-dev libxxf86vm-dev libpq-dev libhiredis-dev zlib1g-dev doxygen libxrandr-dev mesa-common-dev x11proto-xf86vidmode-dev
|
||||||
|
|
||||||
.build_win_template:
|
# Zesty most close to stretch
|
||||||
extends: .generic_win_template
|
|
||||||
stage: build
|
|
||||||
artifacts:
|
|
||||||
expire_in: 1h
|
|
||||||
paths:
|
|
||||||
- build/minetest/_build/*
|
|
||||||
|
|
||||||
.package_win_template:
|
build:ubuntu-17.04:
|
||||||
extends: .generic_win_template
|
<<: *build_definition
|
||||||
stage: package
|
image: ubuntu:zesty
|
||||||
script:
|
|
||||||
- unzip build/minetest/_build/minetest-*.zip
|
|
||||||
- cp -p /usr/${WIN_ARCH}-w64-mingw32/bin/libgcc*.dll minetest-*-win*/bin/
|
|
||||||
- cp -p /usr/${WIN_ARCH}-w64-mingw32/bin/libstdc++*.dll minetest-*-win*/bin/
|
|
||||||
- cp -p /usr/${WIN_ARCH}-w64-mingw32/bin/libwinpthread*.dll minetest-*-win*/bin/
|
|
||||||
artifacts:
|
|
||||||
expire_in: 90 day
|
|
||||||
paths:
|
|
||||||
- minetest-*-win*/*
|
|
||||||
|
|
||||||
build:win32:
|
|
||||||
extends: .build_win_template
|
|
||||||
script:
|
|
||||||
- ./util/buildbot/buildwin32.sh build
|
|
||||||
variables:
|
|
||||||
WIN_ARCH: "i686"
|
|
||||||
|
|
||||||
package:win32:
|
|
||||||
extends: .package_win_template
|
|
||||||
needs:
|
|
||||||
- build:win32
|
|
||||||
variables:
|
|
||||||
WIN_ARCH: "i686"
|
|
||||||
|
|
||||||
|
|
||||||
build:win64:
|
|
||||||
extends: .build_win_template
|
|
||||||
script:
|
|
||||||
- ./util/buildbot/buildwin64.sh build
|
|
||||||
variables:
|
|
||||||
WIN_ARCH: "x86_64"
|
|
||||||
|
|
||||||
package:win64:
|
|
||||||
extends: .package_win_template
|
|
||||||
needs:
|
|
||||||
- build:win64
|
|
||||||
variables:
|
|
||||||
WIN_ARCH: "x86_64"
|
|
||||||
|
|
||||||
##
|
|
||||||
## Docker
|
|
||||||
##
|
|
||||||
|
|
||||||
package:docker:
|
|
||||||
stage: package
|
|
||||||
image: docker:stable
|
|
||||||
services:
|
|
||||||
- docker:dind
|
|
||||||
before_script:
|
before_script:
|
||||||
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
|
- echo "" > /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
script:
|
- echo "APT::Get::AllowUnauthenticated \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
- docker build . -t ${CONTAINER_IMAGE}/server:$CI_COMMIT_SHA -t ${CONTAINER_IMAGE}/server:$CI_COMMIT_REF_NAME -t ${CONTAINER_IMAGE}/server:latest
|
- echo "Acquire::AllowInsecureRepositories \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
|
||||||
- docker push ${CONTAINER_IMAGE}/server:$CI_COMMIT_SHA
|
- rm -rf /etc/apt/sources.list
|
||||||
- docker push ${CONTAINER_IMAGE}/server:$CI_COMMIT_REF_NAME
|
- echo "deb http://old-releases.ubuntu.com/ubuntu/ zesty main restricted universe multiverse" > /etc/apt/sources.list.d/50debianoficial.list
|
||||||
- docker push ${CONTAINER_IMAGE}/server:latest
|
- echo "deb http://old-releases.ubuntu.com/ubuntu/ zesty-updates main restricted universe multiverse" >> /etc/apt/sources.list.d/50debianoficial.list
|
||||||
|
- echo "deb http://old-releases.ubuntu.com/ubuntu zesty-security main restricted universe multiverse" >> /etc/apt/sources.list.d/50debianoficial.list
|
||||||
|
- apt-get update -y || true
|
||||||
|
- apt-get -y --force-yes install build-essential cmake pkg-config debhelper dh-systemd dh-autoreconf lsb-release gettext libbz2-dev libcurl4-gnutls-dev libnl-genl-3-dev libnl-3-dev librtmp-dev libidn11-dev libncurses-dev libfreetype6-dev libglu1-mesa-dev libgmp-dev libirrlicht-dev libjpeg-dev libjsoncpp-dev libleveldb-dev libluajit-5.1-dev liblua5.1-dev libogg-dev libopenal-dev libpng-dev libpq-dev libhiredis-dev libspatialindex-dev libsqlite3-dev libvorbis-dev libx11-dev libxxf86vm-dev postgresql-server-dev-all libpq-dev libhiredis-dev zlib1g-dev doxygen libxrandr-dev mesa-common-dev x11proto-xf86vidmode-dev libzstd-dev
|
||||||
|
|
||||||
##
|
##
|
||||||
## Gitlab Pages (Lua API documentation)
|
## Feladora shit distro
|
||||||
##
|
##
|
||||||
|
|
||||||
pages:
|
build:fedora-36:
|
||||||
stage: deploy
|
<<: *build_definition
|
||||||
image: python:3.8
|
image: fedora:36
|
||||||
before_script:
|
before_script:
|
||||||
- pip install git+https://github.com/Python-Markdown/markdown.git
|
- dnf -y install make automake gcc gcc-c++ kernel-devel cmake pkgconfig bzip2-devel gettext-devel sqlite-devel zlib-devel libpng-devel libjpeg-turbo-devel libXxf86vm-devel mesa-libGL-devel irrlicht-devel desktop-file-utils systemd openal* libvorbis* jsoncpp-devel libcurl-devel libcurl luajit-devel leveldb-devel gmp-devel libappstream-glib freetype-devel spatialindex-devel openssl-devel libogg-devel hiredis-devel libzstd-devel libXi-devel ncurses-devel doxygen
|
||||||
- pip install git+https://github.com/mkdocs/mkdocs.git
|
|
||||||
- pip install pygments
|
|
||||||
script:
|
|
||||||
- cd doc/mkdocs && ./build.sh
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- public
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
|
|
||||||
##
|
build:fedora-37:
|
||||||
## AppImage
|
<<: *build_definition
|
||||||
##
|
image: fedora:37
|
||||||
|
|
||||||
package:appimage-client:
|
|
||||||
stage: package
|
|
||||||
image: appimagecrafters/appimage-builder
|
|
||||||
needs:
|
|
||||||
- build:ubuntu-18.04
|
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update -y
|
- dnf -y install make automake gcc gcc-c++ kernel-devel cmake pkgconfig bzip2-devel gettext-devel sqlite-devel zlib-devel libpng-devel libjpeg-turbo-devel libXxf86vm-devel mesa-libGL-devel irrlicht-devel desktop-file-utils systemd openal* libvorbis* jsoncpp-devel libcurl-devel libcurl luajit-devel leveldb-devel gmp-devel libappstream-glib freetype-devel spatialindex-devel openssl-devel libogg-devel hiredis-devel libzstd-devel libXi-devel ncurses-devel doxygen
|
||||||
- apt-get install -y git wget
|
|
||||||
# Collect files
|
|
||||||
- mkdir AppDir
|
|
||||||
- cp -a artifact/minetest/usr/ AppDir/usr/
|
|
||||||
- rm AppDir/usr/bin/minetestserver
|
|
||||||
- cp -a clientmods AppDir/usr/share/minetest
|
|
||||||
script:
|
|
||||||
- git clone $MINETEST_GAME_REPO AppDir/usr/share/minetest/games/minetest_game
|
|
||||||
- rm -rf AppDir/usr/share/minetest/games/minetest/.git
|
|
||||||
- export VERSION=$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA
|
|
||||||
# Remove PrefersNonDefaultGPU property due to validation errors
|
|
||||||
- sed -i '/PrefersNonDefaultGPU/d' AppDir/usr/share/applications/net.minetest.minetest.desktop
|
|
||||||
- appimage-builder --skip-test
|
|
||||||
artifacts:
|
|
||||||
expire_in: 90 day
|
|
||||||
paths:
|
|
||||||
- ./*.AppImage
|
|
||||||
|
4
.gitmodules
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[submodule "games/minetest"]
|
||||||
|
path = games/minetest
|
||||||
|
url = https://codeberg.org/minenux/minetest-game-minetest
|
||||||
|
branch = stable-5.2
|
@ -20,7 +20,7 @@ read_globals = {
|
|||||||
|
|
||||||
string = {fields = {"split", "trim"}},
|
string = {fields = {"split", "trim"}},
|
||||||
table = {fields = {"copy", "getn", "indexof", "insert_all"}},
|
table = {fields = {"copy", "getn", "indexof", "insert_all"}},
|
||||||
math = {fields = {"hypot"}},
|
math = {fields = {"hypot", "round"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
globals = {
|
globals = {
|
||||||
|
@ -2,13 +2,13 @@ apply plugin: 'com.android.application'
|
|||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 32
|
compileSdkVersion 34
|
||||||
buildToolsVersion '32.0.0'
|
buildToolsVersion '34.0.0'
|
||||||
ndkVersion '23.1.7779620'
|
ndkVersion '25.2.9519653'
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'com.multicraft.game'
|
applicationId 'com.multicraft.game'
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 32
|
targetSdkVersion 34
|
||||||
versionName "${versionMajor}.${versionMinor}.${versionPatch}"
|
versionName "${versionMajor}.${versionMinor}.${versionPatch}"
|
||||||
versionCode project.versionCode
|
versionCode project.versionCode
|
||||||
}
|
}
|
||||||
@ -47,26 +47,29 @@ android {
|
|||||||
abi {
|
abi {
|
||||||
enable true
|
enable true
|
||||||
reset()
|
reset()
|
||||||
include 'armeabi-v7a', 'arm64-v8a'
|
//noinspection ChromeOsAbiSupport
|
||||||
|
include 'armeabi-v7a', 'arm64-v8a', 'x86_64'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlinOptions.jvmTarget = "17"
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
}
|
}
|
||||||
|
namespace = "com.multicraft.game"
|
||||||
}
|
}
|
||||||
|
|
||||||
import com.android.build.OutputFile
|
|
||||||
import org.apache.tools.ant.taskdefs.condition.Os
|
import org.apache.tools.ant.taskdefs.condition.Os
|
||||||
|
|
||||||
task prepareAssetsFiles() {
|
tasks.register('prepareAssetsFiles') {
|
||||||
def assetsFolder = "build/assets/Files"
|
def assetsFolder = "build/assets/Files"
|
||||||
def projRoot = "../../.."
|
def projRoot = "../.."
|
||||||
|
|
||||||
copy {
|
copy {
|
||||||
from "${projRoot}/builtin" into "${assetsFolder}/builtin" exclude '*.txt'
|
from "${projRoot}/builtin" into "${assetsFolder}/builtin" exclude '*.txt'
|
||||||
@ -75,7 +78,7 @@ task prepareAssetsFiles() {
|
|||||||
from "${projRoot}/client/shaders" into "${assetsFolder}/client/shaders"
|
from "${projRoot}/client/shaders" into "${assetsFolder}/client/shaders"
|
||||||
}
|
}
|
||||||
copy {
|
copy {
|
||||||
from "../native/deps/Android/Irrlicht/shaders" into "${assetsFolder}/client/shaders/Irrlicht"
|
from "../native/deps/irrlicht/shaders" into "${assetsFolder}/client/shaders/Irrlicht"
|
||||||
}
|
}
|
||||||
copy {
|
copy {
|
||||||
from "${projRoot}/fonts/MultiCraftFont.ttf" into "${assetsFolder}/fonts"
|
from "${projRoot}/fonts/MultiCraftFont.ttf" into "${assetsFolder}/fonts"
|
||||||
@ -92,40 +95,25 @@ task prepareAssetsFiles() {
|
|||||||
copy {
|
copy {
|
||||||
from "${projRoot}/textures" into "${assetsFolder}/textures" exclude '*.txt'
|
from "${projRoot}/textures" into "${assetsFolder}/textures" exclude '*.txt'
|
||||||
}
|
}
|
||||||
|
|
||||||
task zipAssetsFiles(type: Zip) {
|
|
||||||
archiveFileName = "Files.zip"
|
|
||||||
destinationDirectory = file("src/main/assets/data")
|
|
||||||
|
|
||||||
from "${assetsFolder}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
task prepareAssetsGames() {
|
tasks.register('zipAssetsFiles', Zip) {
|
||||||
def assetsFolder = "build/assets/games"
|
dependsOn prepareAssetsFiles
|
||||||
def projRoot = "../../.."
|
archiveFileName = 'assets.zip'
|
||||||
def gameToCopy = "default"
|
destinationDirectory = file('src/main/assets')
|
||||||
|
from('build/assets/Files')
|
||||||
copy {
|
|
||||||
from "${projRoot}/games/${gameToCopy}" into "${assetsFolder}/games/${gameToCopy}"
|
|
||||||
}
|
|
||||||
|
|
||||||
task zipAssetsGames(type: Zip) {
|
|
||||||
archiveFileName = "games.zip"
|
|
||||||
destinationDirectory = file("src/main/assets/data")
|
|
||||||
from "${assetsFolder}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
preBuild.dependsOn zipAssetsFiles
|
tasks.named("preBuild") {
|
||||||
preBuild.dependsOn zipAssetsGames
|
dependsOn(zipAssetsFiles)
|
||||||
|
}
|
||||||
|
|
||||||
// Map for the version code that gives each ABI a value.
|
// Map for the version code that gives each ABI a value.
|
||||||
def abiCodes = ['armeabi-v7a': 0, 'arm64-v8a': 1]
|
def abiCodes = ['armeabi-v7a': 0, 'arm64-v8a': 1, 'x86_64': 2]
|
||||||
android.applicationVariants.all { variant ->
|
android.applicationVariants.configureEach { variant ->
|
||||||
variant.outputs.each {
|
variant.outputs.each {
|
||||||
output ->
|
output ->
|
||||||
def abiName = output.getFilter(OutputFile.ABI)
|
def abiName = output.filters[0].identifier
|
||||||
output.versionCodeOverride = abiCodes.get(abiName, 0) + variant.versionCode
|
output.versionCodeOverride = abiCodes.get(abiName, 0) + variant.versionCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -135,9 +123,10 @@ dependencies {
|
|||||||
implementation project(':native')
|
implementation project(':native')
|
||||||
|
|
||||||
/* Third-party libraries */
|
/* Third-party libraries */
|
||||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.appcompat:appcompat-resources:1.4.1'
|
implementation 'androidx.appcompat:appcompat-resources:1.6.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
|
implementation("androidx.browser:browser:1.6.0")
|
||||||
implementation 'androidx.work:work-runtime-ktx:2.7.1'
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
|
||||||
implementation 'com.google.android.material:material:1.5.0'
|
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
||||||
|
implementation 'com.google.android.material:material:1.10.0'
|
||||||
}
|
}
|
@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.multicraft.game"
|
|
||||||
android:installLocation="auto">
|
android:installLocation="auto">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
@ -28,6 +27,7 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
@ -41,8 +41,8 @@
|
|||||||
android:value="3.0" />
|
android:value="3.0" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.multicraft.game.MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="orientation|keyboardHidden|navigation|screenSize"
|
android:configChanges="orientation|keyboardHidden|navigation|screenSize|screenLayout"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:maxAspectRatio="3.0"
|
android:maxAspectRatio="3.0"
|
||||||
android:screenOrientation="sensorLandscape"
|
android:screenOrientation="sensorLandscape"
|
||||||
@ -54,8 +54,8 @@
|
|||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.multicraft.game.GameActivity"
|
android:name=".GameActivity"
|
||||||
android:configChanges="orientation|keyboard|keyboardHidden|navigation|screenSize|smallestScreenSize"
|
android:configChanges="orientation|keyboard|keyboardHidden|navigation|screenSize|smallestScreenSize|screenLayout|uiMode"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
@ -71,6 +71,19 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".dialogs.ConnectionDialog"
|
||||||
|
android:configChanges="orientation|keyboardHidden|navigation|screenSize"
|
||||||
|
android:exported="false"
|
||||||
|
android:screenOrientation="sensorLandscape"
|
||||||
|
android:theme="@style/CustomDialog" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".dialogs.RestartDialog"
|
||||||
|
android:configChanges="orientation|keyboardHidden|navigation|screenSize"
|
||||||
|
android:exported="false"
|
||||||
|
android:screenOrientation="sensorLandscape"
|
||||||
|
android:theme="@style/CustomDialog" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
MultiCraft
|
MultiCraft
|
||||||
Copyright (C) 2014-2021 MoNTE48, Maksim Gamarnik <MoNTE48@mail.ua>
|
Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
|
||||||
Copyright (C) 2014-2021 ubulem, Bektur Mambetov <berkut87@gmail.com>
|
Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
This program is free software; you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Lesser General Public License as published by
|
it under the terms of the GNU Lesser General Public License as published by
|
307
Android/app/src/main/java/com/multicraft/game/GameActivity.kt
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
/*
|
||||||
|
MultiCraft
|
||||||
|
Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
|
||||||
|
Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation; either version 3.0 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.multicraft.game
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.*
|
||||||
|
import android.text.InputType
|
||||||
|
import android.view.*
|
||||||
|
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN
|
||||||
|
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
|
import androidx.browser.customtabs.CustomTabsIntent.SHARE_STATE_OFF
|
||||||
|
import com.multicraft.game.MainActivity.Companion.radius
|
||||||
|
import com.multicraft.game.databinding.*
|
||||||
|
import com.multicraft.game.helpers.*
|
||||||
|
import com.multicraft.game.helpers.ApiLevelHelper.isOreo
|
||||||
|
import org.libsdl.app.SDLActivity
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
class GameActivity : SDLActivity() {
|
||||||
|
companion object {
|
||||||
|
var isMultiPlayer = false
|
||||||
|
var isInputActive = false
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
external fun pauseGame()
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
external fun keyboardEvent(keyboard: Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var messageReturnValue = ""
|
||||||
|
private var hasKeyboard = false
|
||||||
|
override fun getLibraries() = arrayOf("MultiCraft")
|
||||||
|
|
||||||
|
override fun getMainSharedObject() =
|
||||||
|
"${getContext().applicationInfo.nativeLibraryDir}/libMultiCraft.so"
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
try {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
} catch (e: Error) {
|
||||||
|
exitProcess(0)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
hasKeyboard = hasHardKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||||
|
super.onWindowFocusChanged(hasFocus)
|
||||||
|
if (hasFocus) window.makeFullScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun onBackPressed() {
|
||||||
|
// Ignore the back press so MultiCraft can handle it
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
pauseGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
if (hasKeyboard) keyboardEvent(true)
|
||||||
|
window.makeFullScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
val statusKeyboard = hasHardKeyboard()
|
||||||
|
if (hasKeyboard != statusKeyboard) {
|
||||||
|
hasKeyboard = statusKeyboard
|
||||||
|
keyboardEvent(hasKeyboard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun showDialog(hint: String?, current: String?, editType: Int) {
|
||||||
|
isInputActive = true
|
||||||
|
messageReturnValue = ""
|
||||||
|
if (editType == 1)
|
||||||
|
runOnUiThread { showMultiLineDialog(hint, current) }
|
||||||
|
else
|
||||||
|
runOnUiThread { showSingleDialog(hint, current, editType) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSingleDialog(hint: String?, current: String?, editType: Int) {
|
||||||
|
val builder = AlertDialog.Builder(this, R.style.FullScreenDialogStyle)
|
||||||
|
val binding = InputTextBinding.inflate(layoutInflater)
|
||||||
|
var hintText: String = hint?.ifEmpty {
|
||||||
|
resources.getString(if (editType == 3) R.string.input_password else R.string.input_text)
|
||||||
|
}.toString()
|
||||||
|
hintText = hintText.replace(":$".toRegex(), "")
|
||||||
|
binding.input.hint = hintText
|
||||||
|
builder.setView(binding.root)
|
||||||
|
val alertDialog = builder.create()
|
||||||
|
val editText = binding.editText
|
||||||
|
editText.requestFocus()
|
||||||
|
editText.setText(current.toString())
|
||||||
|
editText.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN
|
||||||
|
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
var inputType = InputType.TYPE_CLASS_TEXT
|
||||||
|
if (editType == 3) {
|
||||||
|
inputType = inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||||
|
if (isOreo())
|
||||||
|
editText.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO
|
||||||
|
}
|
||||||
|
editText.inputType = inputType
|
||||||
|
editText.setSelection(editText.text?.length ?: 0)
|
||||||
|
// for Android OS
|
||||||
|
editText.setOnEditorActionListener { _: TextView?, keyCode: Int, _: KeyEvent? ->
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_ENDCALL) {
|
||||||
|
imm.hideSoftInputFromWindow(editText.windowToken, 0)
|
||||||
|
messageReturnValue = editText.text.toString()
|
||||||
|
alertDialog.dismiss()
|
||||||
|
isInputActive = false
|
||||||
|
return@setOnEditorActionListener true
|
||||||
|
}
|
||||||
|
return@setOnEditorActionListener false
|
||||||
|
}
|
||||||
|
if (isChromebook()) {
|
||||||
|
editText.setOnKeyListener { _: View?, keyCode: Int, _: KeyEvent? ->
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_ENDCALL) {
|
||||||
|
imm.hideSoftInputFromWindow(editText.windowToken, 0)
|
||||||
|
messageReturnValue = editText.text.toString()
|
||||||
|
alertDialog.dismiss()
|
||||||
|
isInputActive = false
|
||||||
|
return@setOnKeyListener true
|
||||||
|
}
|
||||||
|
return@setOnKeyListener false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.input.setEndIconOnClickListener {
|
||||||
|
imm.hideSoftInputFromWindow(editText.windowToken, 0)
|
||||||
|
messageReturnValue = editText.text.toString()
|
||||||
|
alertDialog.dismiss()
|
||||||
|
isInputActive = false
|
||||||
|
}
|
||||||
|
binding.rl.setOnClickListener {
|
||||||
|
window.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN)
|
||||||
|
messageReturnValue = current.toString()
|
||||||
|
alertDialog.dismiss()
|
||||||
|
isInputActive = false
|
||||||
|
}
|
||||||
|
val alertWindow = alertDialog.window!!
|
||||||
|
// should be above `show()`
|
||||||
|
alertWindow.setSoftInputMode(SOFT_INPUT_STATE_VISIBLE)
|
||||||
|
alertDialog.show()
|
||||||
|
if (!isTablet())
|
||||||
|
alertWindow.makeFullScreenAlert()
|
||||||
|
alertDialog.setOnCancelListener {
|
||||||
|
window.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN)
|
||||||
|
messageReturnValue = current.toString()
|
||||||
|
isInputActive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showMultiLineDialog(hint: String?, current: String?) {
|
||||||
|
val builder = AlertDialog.Builder(this, R.style.FullScreenDialogStyle)
|
||||||
|
val binding = MultilineInputBinding.inflate(layoutInflater)
|
||||||
|
var hintText: String = hint?.ifEmpty {
|
||||||
|
resources.getString(R.string.input_text)
|
||||||
|
}.toString()
|
||||||
|
hintText = hintText.replace(":$".toRegex(), "")
|
||||||
|
binding.multiInput.hint = hintText
|
||||||
|
builder.setView(binding.root)
|
||||||
|
val alertDialog = builder.create()
|
||||||
|
val editText = binding.multiEditText
|
||||||
|
editText.requestFocus()
|
||||||
|
editText.setText(current.toString())
|
||||||
|
editText.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN
|
||||||
|
editText.setSelection(editText.text?.length ?: 0)
|
||||||
|
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
// for Android OS
|
||||||
|
editText.setOnEditorActionListener { _: TextView?, keyCode: Int, _: KeyEvent? ->
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_ENDCALL) {
|
||||||
|
imm.hideSoftInputFromWindow(editText.windowToken, 0)
|
||||||
|
messageReturnValue = editText.text.toString()
|
||||||
|
alertDialog.dismiss()
|
||||||
|
isInputActive = false
|
||||||
|
return@setOnEditorActionListener true
|
||||||
|
}
|
||||||
|
return@setOnEditorActionListener false
|
||||||
|
}
|
||||||
|
if (isChromebook()) {
|
||||||
|
editText.setOnKeyListener { _: View?, keyCode: Int, _: KeyEvent? ->
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_ENDCALL) {
|
||||||
|
imm.hideSoftInputFromWindow(editText.windowToken, 0)
|
||||||
|
messageReturnValue = editText.text.toString()
|
||||||
|
alertDialog.dismiss()
|
||||||
|
isInputActive = false
|
||||||
|
return@setOnKeyListener true
|
||||||
|
}
|
||||||
|
return@setOnKeyListener false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.multiInput.setEndIconOnClickListener {
|
||||||
|
imm.hideSoftInputFromWindow(editText.windowToken, 0)
|
||||||
|
messageReturnValue = editText.text.toString()
|
||||||
|
alertDialog.dismiss()
|
||||||
|
isInputActive = false
|
||||||
|
}
|
||||||
|
binding.multiRl.setOnClickListener {
|
||||||
|
window.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN)
|
||||||
|
messageReturnValue = current.toString()
|
||||||
|
alertDialog.dismiss()
|
||||||
|
isInputActive = false
|
||||||
|
}
|
||||||
|
// should be above `show()`
|
||||||
|
val alertWindow = alertDialog.window!!
|
||||||
|
alertWindow.setSoftInputMode(SOFT_INPUT_STATE_VISIBLE)
|
||||||
|
alertDialog.show()
|
||||||
|
if (!isTablet())
|
||||||
|
alertWindow.makeFullScreenAlert()
|
||||||
|
alertDialog.setOnCancelListener {
|
||||||
|
window.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN)
|
||||||
|
messageReturnValue = current.toString()
|
||||||
|
isInputActive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun isDialogActive() = isInputActive
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun getDialogValue(): String {
|
||||||
|
val value = messageReturnValue
|
||||||
|
messageReturnValue = ""
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun getDensity() = resources.displayMetrics.density
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun notifyServerConnect(multiplayer: Boolean) {
|
||||||
|
isMultiPlayer = multiplayer
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun notifyExitGame() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun openURI(uri: String?) {
|
||||||
|
val builder = CustomTabsIntent.Builder()
|
||||||
|
builder.setShareState(SHARE_STATE_OFF)
|
||||||
|
.setStartAnimations(this, R.anim.slide_in_bottom, R.anim.slide_out_top)
|
||||||
|
.setExitAnimations(this, R.anim.slide_in_top, R.anim.slide_out_bottom)
|
||||||
|
val customTabsIntent = builder.build()
|
||||||
|
try {
|
||||||
|
customTabsIntent.launchUrl(this, Uri.parse(uri))
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun finishGame(exc: String?) {
|
||||||
|
finishApp(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun handleError(exc: String?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun upgrade(item: String) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun getSecretKey(key: String): String {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun getRoundScreen(): Int {
|
||||||
|
return radius
|
||||||
|
}
|
||||||
|
}
|
235
Android/app/src/main/java/com/multicraft/game/MainActivity.kt
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
/*
|
||||||
|
MultiCraft
|
||||||
|
Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
|
||||||
|
Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation; either version 3.0 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.multicraft.game
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.graphics.drawable.AnimationDrawable
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings.ACTION_WIFI_SETTINGS
|
||||||
|
import android.provider.Settings.ACTION_WIRELESS_SETTINGS
|
||||||
|
import android.view.RoundedCorner
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.work.WorkInfo
|
||||||
|
import com.multicraft.game.databinding.ActivityMainBinding
|
||||||
|
import com.multicraft.game.dialogs.ConnectionDialog
|
||||||
|
import com.multicraft.game.helpers.*
|
||||||
|
import com.multicraft.game.helpers.ApiLevelHelper.isAndroid12
|
||||||
|
import com.multicraft.game.helpers.ApiLevelHelper.isPie
|
||||||
|
import com.multicraft.game.helpers.PreferenceHelper.TAG_BUILD_VER
|
||||||
|
import com.multicraft.game.helpers.PreferenceHelper.getStringValue
|
||||||
|
import com.multicraft.game.helpers.PreferenceHelper.set
|
||||||
|
import com.multicraft.game.workmanager.UnzipWorker.Companion.PROGRESS
|
||||||
|
import com.multicraft.game.workmanager.WorkerViewModel
|
||||||
|
import com.multicraft.game.workmanager.WorkerViewModelFactory
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
private lateinit var binding: ActivityMainBinding
|
||||||
|
private var externalStorage: File? = null
|
||||||
|
private val sep = File.separator
|
||||||
|
private lateinit var prefs: SharedPreferences
|
||||||
|
private lateinit var restartStartForResult: ActivityResultLauncher<Intent>
|
||||||
|
private lateinit var connStartForResult: ActivityResultLauncher<Intent>
|
||||||
|
private val versionCode = BuildConfig.VERSION_CODE
|
||||||
|
private val versionName = "${BuildConfig.VERSION_NAME}+$versionCode"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var radius = 0
|
||||||
|
const val NO_SPACE_LEFT = "ENOSPC"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
connStartForResult = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
checkAppVersion()
|
||||||
|
}
|
||||||
|
restartStartForResult = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
if (it.resultCode == RESULT_OK)
|
||||||
|
finishApp(true)
|
||||||
|
else
|
||||||
|
finishApp(false)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
prefs = PreferenceHelper.init(this)
|
||||||
|
externalStorage = getExternalFilesDir(null)
|
||||||
|
listOf(filesDir, cacheDir, externalStorage).requireNoNulls()
|
||||||
|
checkConnection()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val isRestart = e.message?.contains(NO_SPACE_LEFT) != true
|
||||||
|
showRestartDialog(restartStartForResult, isRestart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
window.makeFullScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||||
|
super.onWindowFocusChanged(hasFocus)
|
||||||
|
if (hasFocus) window.makeFullScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
if (isPie()) {
|
||||||
|
val cutout = window.decorView.rootWindowInsets.displayCutout
|
||||||
|
if (cutout != null) {
|
||||||
|
radius = 40
|
||||||
|
}
|
||||||
|
if (isAndroid12()) {
|
||||||
|
val insets = window.decorView.rootWindowInsets
|
||||||
|
if (insets != null) {
|
||||||
|
val tl = insets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT)
|
||||||
|
radius = tl?.radius ?: if (cutout != null) 40 else 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val animation = binding.loadingAnim.drawable as AnimationDrawable
|
||||||
|
animation.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startNative() {
|
||||||
|
val initLua = File(filesDir, "builtin${sep}mainmenu${sep}init.lua")
|
||||||
|
if (initLua.exists() && initLua.canRead()) {
|
||||||
|
val intent = Intent(this, GameActivity::class.java)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
startActivity(intent)
|
||||||
|
} else {
|
||||||
|
prefs[TAG_BUILD_VER] = "0"
|
||||||
|
showRestartDialog(restartStartForResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prepareToRun() {
|
||||||
|
val filesList = mutableListOf<File>().apply {
|
||||||
|
addAll(listOf(
|
||||||
|
"builtin",
|
||||||
|
"client${sep}shaders",
|
||||||
|
"fonts",
|
||||||
|
"games${sep}default",
|
||||||
|
"textures${sep}base"
|
||||||
|
).map { File(filesDir, it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
val zips = mutableListOf("assets.zip")
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
filesList.forEach { it.deleteRecursively() }
|
||||||
|
zips.forEach {
|
||||||
|
try {
|
||||||
|
assets.open(it).use { input ->
|
||||||
|
File(cacheDir, it).copyInputStreamToFile(input)
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
val isNotEnoughSpace = e.message!!.contains(NO_SPACE_LEFT)
|
||||||
|
runOnUiThread { showRestartDialog(restartStartForResult, !isNotEnoughSpace) }
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
startUnzipWorker(zips.toTypedArray())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
runOnUiThread { showRestartDialog(restartStartForResult) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkAppVersion() {
|
||||||
|
val prefVersion = prefs.getStringValue(TAG_BUILD_VER)
|
||||||
|
if (prefVersion == versionName)
|
||||||
|
startNative()
|
||||||
|
else
|
||||||
|
prepareToRun()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showConnectionDialog() {
|
||||||
|
val intent = Intent(this, ConnectionDialog::class.java)
|
||||||
|
val startForResult = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
when (it.resultCode) {
|
||||||
|
RESULT_OK -> connStartForResult.launch(Intent(ACTION_WIFI_SETTINGS))
|
||||||
|
RESULT_FIRST_USER -> connStartForResult.launch(Intent(ACTION_WIRELESS_SETTINGS))
|
||||||
|
else -> checkAppVersion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startForResult.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check connection available
|
||||||
|
private fun checkConnection() = lifecycleScope.launch {
|
||||||
|
if (isConnected()) checkAppVersion()
|
||||||
|
else try {
|
||||||
|
showConnectionDialog()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
checkAppVersion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startUnzipWorker(file: Array<String>) {
|
||||||
|
val viewModelFactory = WorkerViewModelFactory(application, file)
|
||||||
|
val viewModel = ViewModelProvider(this, viewModelFactory)[WorkerViewModel::class.java]
|
||||||
|
viewModel.unzippingWorkObserver
|
||||||
|
.observe(this, Observer { workInfo ->
|
||||||
|
if (workInfo == null)
|
||||||
|
return@Observer
|
||||||
|
val progress = workInfo.progress.getInt(PROGRESS, 0)
|
||||||
|
if (progress > 0) {
|
||||||
|
val progressMessage = "${getString(R.string.loading)} $progress%"
|
||||||
|
binding.tvProgress.text = progressMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workInfo.state.isFinished) {
|
||||||
|
if (workInfo.state == WorkInfo.State.FAILED) {
|
||||||
|
val isRestart = workInfo.outputData.getBoolean("restart", true)
|
||||||
|
showRestartDialog(restartStartForResult, isRestart)
|
||||||
|
} else if (workInfo.state == WorkInfo.State.SUCCEEDED) {
|
||||||
|
prefs[TAG_BUILD_VER] = versionName
|
||||||
|
startNative()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
viewModel.startOneTimeWorkRequest()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
MultiCraft
|
||||||
|
Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
|
||||||
|
Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation; either version 3.0 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.multicraft.game.dialogs
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.telephony.TelephonyManager
|
||||||
|
import android.telephony.TelephonyManager.SIM_STATE_READY
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.multicraft.game.databinding.ConnectionDialogBinding
|
||||||
|
import com.multicraft.game.helpers.ApiLevelHelper.isOreo
|
||||||
|
import com.multicraft.game.helpers.makeFullScreen
|
||||||
|
import org.libsdl.app.SDLActivity
|
||||||
|
|
||||||
|
class ConnectionDialog : AppCompatActivity() {
|
||||||
|
private fun isSimCardPresent(): Boolean {
|
||||||
|
val telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
|
||||||
|
|
||||||
|
if (!isOreo())
|
||||||
|
return telephonyManager.simState == SIM_STATE_READY
|
||||||
|
|
||||||
|
val isFirstSimPresent = telephonyManager.getSimState(0) == SIM_STATE_READY
|
||||||
|
val isSecondSimPresent = telephonyManager.getSimState(1) == SIM_STATE_READY
|
||||||
|
|
||||||
|
return isFirstSimPresent || isSecondSimPresent
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val binding = ConnectionDialogBinding.inflate(layoutInflater)
|
||||||
|
if (SDLActivity.isTablet()) {
|
||||||
|
val param = LinearLayout.LayoutParams(
|
||||||
|
0,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
0.5f
|
||||||
|
)
|
||||||
|
binding.connRoot.layoutParams = param
|
||||||
|
}
|
||||||
|
if (isSimCardPresent())
|
||||||
|
binding.mobile.visibility = View.VISIBLE
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.wifi.setOnClickListener {
|
||||||
|
setResult(Activity.RESULT_OK)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
binding.mobile.setOnClickListener {
|
||||||
|
setResult(Activity.RESULT_FIRST_USER)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
binding.ignore.setOnClickListener {
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
window.makeFullScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context?) {
|
||||||
|
val configuration = Configuration(base?.resources?.configuration)
|
||||||
|
configuration.fontScale = 1.0f
|
||||||
|
applyOverrideConfiguration(configuration)
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
MultiCraft
|
||||||
|
Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
|
||||||
|
Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation; either version 3.0 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.multicraft.game.dialogs
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.multicraft.game.databinding.RestartDialogBinding
|
||||||
|
import com.multicraft.game.helpers.makeFullScreen
|
||||||
|
import org.libsdl.app.SDLActivity
|
||||||
|
|
||||||
|
class RestartDialog : AppCompatActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val binding = RestartDialogBinding.inflate(layoutInflater)
|
||||||
|
if (SDLActivity.isTablet()) {
|
||||||
|
val param = LinearLayout.LayoutParams(
|
||||||
|
0,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
0.5f
|
||||||
|
)
|
||||||
|
binding.restartRoot.layoutParams = param
|
||||||
|
}
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
val message = intent.getStringExtra("message")!!
|
||||||
|
binding.errorDesc.text = message
|
||||||
|
binding.restart.setOnClickListener {
|
||||||
|
setResult(Activity.RESULT_OK)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
binding.close.setOnClickListener {
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
window.makeFullScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context?) {
|
||||||
|
val configuration = Configuration(base?.resources?.configuration)
|
||||||
|
configuration.fontScale = 1.0f
|
||||||
|
applyOverrideConfiguration(configuration)
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
MultiCraft
|
MultiCraft
|
||||||
Copyright (C) 2014-2021 MoNTE48, Maksim Gamarnik <MoNTE48@mail.ua>
|
Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
|
||||||
Copyright (C) 2014-2021 ubulem, Bektur Mambetov <berkut87@gmail.com>
|
Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
This program is free software; you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Lesser General Public License as published by
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
@ -30,5 +30,7 @@ object ApiLevelHelper {
|
|||||||
|
|
||||||
fun isOreo() = isGreaterOrEqual(O)
|
fun isOreo() = isGreaterOrEqual(O)
|
||||||
|
|
||||||
|
fun isPie() = isGreaterOrEqual(P)
|
||||||
|
|
||||||
fun isAndroid12() = isGreaterOrEqual(S)
|
fun isAndroid12() = isGreaterOrEqual(S)
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
MultiCraft
|
MultiCraft
|
||||||
Copyright (C) 2014-2021 MoNTE48, Maksim Gamarnik <MoNTE48@mail.ua>
|
Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
|
||||||
Copyright (C) 2014-2021 ubulem, Bektur Mambetov <berkut87@gmail.com>
|
Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
This program is free software; you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Lesser General Public License as published by
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
@ -24,9 +24,7 @@ import android.content.Context
|
|||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
object PreferenceHelper {
|
object PreferenceHelper {
|
||||||
const val TAG_SHORTCUT_EXIST = "createShortcut"
|
|
||||||
const val TAG_BUILD_VER = "buildVer"
|
const val TAG_BUILD_VER = "buildVer"
|
||||||
const val TAG_LAUNCH_TIMES = "launchTimes"
|
|
||||||
|
|
||||||
fun init(context: Context): SharedPreferences =
|
fun init(context: Context): SharedPreferences =
|
||||||
context.getSharedPreferences("MultiCraftSettings", Context.MODE_PRIVATE)
|
context.getSharedPreferences("MultiCraftSettings", Context.MODE_PRIVATE)
|
||||||
@ -46,16 +44,6 @@ object PreferenceHelper {
|
|||||||
else -> throw UnsupportedOperationException("Not yet implemented")
|
else -> throw UnsupportedOperationException("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun SharedPreferences.getBoolValue(key: String): Boolean = when (key) {
|
|
||||||
TAG_SHORTCUT_EXIST -> getBoolean(key, false)
|
|
||||||
else -> throw UnsupportedOperationException("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun SharedPreferences.getIntValue(key: String) = when (key) {
|
|
||||||
TAG_LAUNCH_TIMES -> getInt(key, 0)
|
|
||||||
else -> throw UnsupportedOperationException("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun SharedPreferences.getStringValue(key: String) = when (key) {
|
fun SharedPreferences.getStringValue(key: String) = when (key) {
|
||||||
TAG_BUILD_VER -> getString(key, "0") as String
|
TAG_BUILD_VER -> getString(key, "0") as String
|
||||||
else -> throw UnsupportedOperationException("Not yet implemented")
|
else -> throw UnsupportedOperationException("Not yet implemented")
|
@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
MultiCraft
|
||||||
|
Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
|
||||||
|
Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
the Free Software Foundation; either version 3.0 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.multicraft.game.helpers
|
||||||
|
|
||||||
|
import android.app.*
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.net.*
|
||||||
|
import android.os.*
|
||||||
|
import android.view.Window
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.*
|
||||||
|
import com.multicraft.game.*
|
||||||
|
import com.multicraft.game.databinding.*
|
||||||
|
import com.multicraft.game.dialogs.RestartDialog
|
||||||
|
import com.multicraft.game.helpers.ApiLevelHelper.isAndroid12
|
||||||
|
import com.multicraft.game.helpers.ApiLevelHelper.isMarshmallow
|
||||||
|
import java.io.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
// Activity extensions
|
||||||
|
fun Activity.finishApp(restart: Boolean) {
|
||||||
|
if (restart) {
|
||||||
|
val intent = Intent(this, this::class.java)
|
||||||
|
val mPendingIntentId = 1337
|
||||||
|
val flag =
|
||||||
|
if (isAndroid12()) PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
val mgr = getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
|
mgr.set(
|
||||||
|
AlarmManager.RTC, System.currentTimeMillis(), PendingIntent.getActivity(
|
||||||
|
this, mPendingIntentId, intent, flag
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Activity.isConnected(): Boolean {
|
||||||
|
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
if (isMarshmallow()) {
|
||||||
|
val activeNetwork = cm.activeNetwork ?: return false
|
||||||
|
val capabilities = cm.getNetworkCapabilities(activeNetwork) ?: return false
|
||||||
|
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||||
|
} else @Suppress("DEPRECATION") {
|
||||||
|
val activeNetworkInfo = cm.activeNetworkInfo ?: return false
|
||||||
|
return activeNetworkInfo.isConnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AppCompatActivity.showRestartDialog(
|
||||||
|
startForResult: ActivityResultLauncher<Intent>,
|
||||||
|
isRestart: Boolean = true
|
||||||
|
) {
|
||||||
|
val message =
|
||||||
|
if (isRestart) getString(R.string.restart) else getString(R.string.no_space)
|
||||||
|
val intent = Intent(this, RestartDialog::class.java)
|
||||||
|
intent.putExtra("message", message)
|
||||||
|
startForResult.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Activity.hasHardKeyboard() =
|
||||||
|
resources.configuration.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO
|
||||||
|
|
||||||
|
// Other extensions
|
||||||
|
fun File.copyInputStreamToFile(inputStream: InputStream) =
|
||||||
|
outputStream().use { fileOut -> inputStream.copyTo(fileOut, 8192) }
|
||||||
|
|
||||||
|
fun Window.makeFullScreen() {
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(this, false)
|
||||||
|
WindowInsetsControllerCompat(this, decorView).let {
|
||||||
|
it.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
|
||||||
|
it.systemBarsBehavior =
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Window.makeFullScreenAlert() {
|
||||||
|
WindowInsetsControllerCompat(this, decorView).let {
|
||||||
|
it.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
|
||||||
|
it.systemBarsBehavior =
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
MultiCraft
|
MultiCraft
|
||||||
Copyright (C) 2014-2021 MoNTE48, Maksim Gamarnik <MoNTE48@mail.ua>
|
Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
|
||||||
Copyright (C) 2014-2021 ubulem, Bektur Mambetov <berkut87@gmail.com>
|
Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
This program is free software; you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Lesser General Public License as published by
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
@ -25,13 +25,11 @@ import android.app.NotificationManager
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.NOTIFICATION_SERVICE
|
import android.content.Context.NOTIFICATION_SERVICE
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.*
|
||||||
import androidx.work.ForegroundInfo
|
import com.multicraft.game.MainActivity.Companion.NO_SPACE_LEFT
|
||||||
import androidx.work.WorkerParameters
|
|
||||||
import androidx.work.workDataOf
|
|
||||||
import com.multicraft.game.R
|
import com.multicraft.game.R
|
||||||
import com.multicraft.game.helpers.ApiLevelHelper.isOreo
|
import com.multicraft.game.helpers.ApiLevelHelper.isOreo
|
||||||
import com.multicraft.game.helpers.Utilities.copyInputStreamToFile
|
import com.multicraft.game.helpers.copyInputStreamToFile
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
@ -86,7 +84,11 @@ class UnzipWorker(private val appContext: Context, workerParams: WorkerParameter
|
|||||||
}
|
}
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Result.failure()
|
val isNotEnoughSpace = e.localizedMessage!!.contains(NO_SPACE_LEFT)
|
||||||
|
val out = Data.Builder()
|
||||||
|
.putBoolean("restart", !isNotEnoughSpace)
|
||||||
|
.build()
|
||||||
|
Result.failure(out)
|
||||||
} finally {
|
} finally {
|
||||||
zips.forEach { File(appContext.cacheDir, it).delete() }
|
zips.forEach { File(appContext.cacheDir, it).delete() }
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
MultiCraft
|
MultiCraft
|
||||||
Copyright (C) 2014-2021 MoNTE48, Maksim Gamarnik <MoNTE48@mail.ua>
|
Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
|
||||||
Copyright (C) 2014-2021 ubulem, Bektur Mambetov <berkut87@gmail.com>
|
Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
This program is free software; you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Lesser General Public License as published by
|
it under the terms of the GNU Lesser General Public License as published by
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
MultiCraft
|
MultiCraft
|
||||||
Copyright (C) 2014-2021 MoNTE48, Maksim Gamarnik <MoNTE48@mail.ua>
|
Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
|
||||||
Copyright (C) 2014-2021 ubulem, Bektur Mambetov <berkut87@gmail.com>
|
Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
This program is free software; you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Lesser General Public License as published by
|
it under the terms of the GNU Lesser General Public License as published by
|
||||||
@ -30,7 +30,7 @@ class WorkerViewModelFactory(
|
|||||||
private val zips: Array<String>
|
private val zips: Array<String>
|
||||||
) :
|
) :
|
||||||
ViewModelProvider.Factory {
|
ViewModelProvider.Factory {
|
||||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
if (modelClass.isAssignableFrom(WorkerViewModel::class.java)) {
|
if (modelClass.isAssignableFrom(WorkerViewModel::class.java)) {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
return WorkerViewModel(application, zips) as T
|
return WorkerViewModel(application, zips) as T
|
22
Android/app/src/main/java/org/libsdl/app/HIDDevice.java
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package org.libsdl.app;
|
||||||
|
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
|
||||||
|
interface HIDDevice
|
||||||
|
{
|
||||||
|
public int getId();
|
||||||
|
public int getVendorId();
|
||||||
|
public int getProductId();
|
||||||
|
public String getSerialNumber();
|
||||||
|
public int getVersion();
|
||||||
|
public String getManufacturerName();
|
||||||
|
public String getProductName();
|
||||||
|
public UsbDevice getDevice();
|
||||||
|
public boolean open();
|
||||||
|
public int sendFeatureReport(byte[] report);
|
||||||
|
public int sendOutputReport(byte[] report);
|
||||||
|
public boolean getFeatureReport(byte[] report);
|
||||||
|
public void setFrozen(boolean frozen);
|
||||||
|
public void close();
|
||||||
|
public void shutdown();
|
||||||
|
}
|
@ -0,0 +1,650 @@
|
|||||||
|
package org.libsdl.app;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.bluetooth.BluetoothDevice;
|
||||||
|
import android.bluetooth.BluetoothGatt;
|
||||||
|
import android.bluetooth.BluetoothGattCallback;
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic;
|
||||||
|
import android.bluetooth.BluetoothGattDescriptor;
|
||||||
|
import android.bluetooth.BluetoothManager;
|
||||||
|
import android.bluetooth.BluetoothProfile;
|
||||||
|
import android.bluetooth.BluetoothGattService;
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.os.*;
|
||||||
|
|
||||||
|
//import com.android.internal.util.HexDump;
|
||||||
|
|
||||||
|
import java.lang.Runnable;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice {
|
||||||
|
|
||||||
|
private static final String TAG = "hidapi";
|
||||||
|
private HIDDeviceManager mManager;
|
||||||
|
private BluetoothDevice mDevice;
|
||||||
|
private int mDeviceId;
|
||||||
|
private BluetoothGatt mGatt;
|
||||||
|
private boolean mIsRegistered = false;
|
||||||
|
private boolean mIsConnected = false;
|
||||||
|
private boolean mIsChromebook = false;
|
||||||
|
private boolean mIsReconnecting = false;
|
||||||
|
private boolean mFrozen = false;
|
||||||
|
private LinkedList<GattOperation> mOperations;
|
||||||
|
GattOperation mCurrentOperation = null;
|
||||||
|
private Handler mHandler;
|
||||||
|
|
||||||
|
private static final int TRANSPORT_AUTO = 0;
|
||||||
|
private static final int TRANSPORT_BREDR = 1;
|
||||||
|
private static final int TRANSPORT_LE = 2;
|
||||||
|
|
||||||
|
private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000;
|
||||||
|
|
||||||
|
static public final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3");
|
||||||
|
static public final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3");
|
||||||
|
static public final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3");
|
||||||
|
static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 };
|
||||||
|
|
||||||
|
static class GattOperation {
|
||||||
|
private enum Operation {
|
||||||
|
CHR_READ,
|
||||||
|
CHR_WRITE,
|
||||||
|
ENABLE_NOTIFICATION
|
||||||
|
}
|
||||||
|
|
||||||
|
Operation mOp;
|
||||||
|
UUID mUuid;
|
||||||
|
byte[] mValue;
|
||||||
|
BluetoothGatt mGatt;
|
||||||
|
boolean mResult = true;
|
||||||
|
|
||||||
|
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) {
|
||||||
|
mGatt = gatt;
|
||||||
|
mOp = operation;
|
||||||
|
mUuid = uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) {
|
||||||
|
mGatt = gatt;
|
||||||
|
mOp = operation;
|
||||||
|
mUuid = uuid;
|
||||||
|
mValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
// This is executed in main thread
|
||||||
|
BluetoothGattCharacteristic chr;
|
||||||
|
|
||||||
|
switch (mOp) {
|
||||||
|
case CHR_READ:
|
||||||
|
chr = getCharacteristic(mUuid);
|
||||||
|
//Log.v(TAG, "Reading characteristic " + chr.getUuid());
|
||||||
|
if (!mGatt.readCharacteristic(chr)) {
|
||||||
|
Log.e(TAG, "Unable to read characteristic " + mUuid.toString());
|
||||||
|
mResult = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
mResult = true;
|
||||||
|
break;
|
||||||
|
case CHR_WRITE:
|
||||||
|
chr = getCharacteristic(mUuid);
|
||||||
|
//Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value));
|
||||||
|
chr.setValue(mValue);
|
||||||
|
if (!mGatt.writeCharacteristic(chr)) {
|
||||||
|
Log.e(TAG, "Unable to write characteristic " + mUuid.toString());
|
||||||
|
mResult = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
mResult = true;
|
||||||
|
break;
|
||||||
|
case ENABLE_NOTIFICATION:
|
||||||
|
chr = getCharacteristic(mUuid);
|
||||||
|
//Log.v(TAG, "Writing descriptor of " + chr.getUuid());
|
||||||
|
if (chr != null) {
|
||||||
|
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
|
||||||
|
if (cccd != null) {
|
||||||
|
int properties = chr.getProperties();
|
||||||
|
byte[] value;
|
||||||
|
if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) {
|
||||||
|
value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
|
||||||
|
} else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) {
|
||||||
|
value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Unable to start notifications on input characteristic");
|
||||||
|
mResult = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mGatt.setCharacteristicNotification(chr, true);
|
||||||
|
cccd.setValue(value);
|
||||||
|
if (!mGatt.writeDescriptor(cccd)) {
|
||||||
|
Log.e(TAG, "Unable to write descriptor " + mUuid.toString());
|
||||||
|
mResult = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mResult = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean finish() {
|
||||||
|
return mResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
|
||||||
|
BluetoothGattService valveService = mGatt.getService(steamControllerService);
|
||||||
|
if (valveService == null)
|
||||||
|
return null;
|
||||||
|
return valveService.getCharacteristic(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) {
|
||||||
|
return new GattOperation(gatt, Operation.CHR_READ, uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) {
|
||||||
|
return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) {
|
||||||
|
return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) {
|
||||||
|
mManager = manager;
|
||||||
|
mDevice = device;
|
||||||
|
mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier());
|
||||||
|
mIsRegistered = false;
|
||||||
|
mIsChromebook = mManager.getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
|
||||||
|
mOperations = new LinkedList<GattOperation>();
|
||||||
|
mHandler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
mGatt = connectGatt();
|
||||||
|
// final HIDDeviceBLESteamController finalThis = this;
|
||||||
|
// mHandler.postDelayed(new Runnable() {
|
||||||
|
// @Override
|
||||||
|
// public void run() {
|
||||||
|
// finalThis.checkConnectionForChromebookIssue();
|
||||||
|
// }
|
||||||
|
// }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIdentifier() {
|
||||||
|
return String.format("SteamController.%s", mDevice.getAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
public BluetoothGatt getGatt() {
|
||||||
|
return mGatt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead
|
||||||
|
// of TRANSPORT_LE. Let's force ourselves to connect low energy.
|
||||||
|
private BluetoothGatt connectGatt(boolean managed) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) {
|
||||||
|
try {
|
||||||
|
return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return mDevice.connectGatt(mManager.getContext(), managed, this);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return mDevice.connectGatt(mManager.getContext(), managed, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BluetoothGatt connectGatt() {
|
||||||
|
return connectGatt(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getConnectionState() {
|
||||||
|
|
||||||
|
Context context = mManager.getContext();
|
||||||
|
if (context == null) {
|
||||||
|
// We are lacking any context to get our Bluetooth information. We'll just assume disconnected.
|
||||||
|
return BluetoothProfile.STATE_DISCONNECTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
|
||||||
|
if (btManager == null) {
|
||||||
|
// This device doesn't support Bluetooth. We should never be here, because how did
|
||||||
|
// we instantiate a device to start with?
|
||||||
|
return BluetoothProfile.STATE_DISCONNECTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reconnect() {
|
||||||
|
|
||||||
|
if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
|
||||||
|
mGatt.disconnect();
|
||||||
|
mGatt = connectGatt();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void checkConnectionForChromebookIssue() {
|
||||||
|
if (!mIsChromebook) {
|
||||||
|
// We only do this on Chromebooks, because otherwise it's really annoying to just attempt
|
||||||
|
// over and over.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int connectionState = getConnectionState();
|
||||||
|
|
||||||
|
switch (connectionState) {
|
||||||
|
case BluetoothProfile.STATE_CONNECTED:
|
||||||
|
if (!mIsConnected) {
|
||||||
|
// We are in the Bad Chromebook Place. We can force a disconnect
|
||||||
|
// to try to recover.
|
||||||
|
Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect.");
|
||||||
|
mIsReconnecting = true;
|
||||||
|
mGatt.disconnect();
|
||||||
|
mGatt = connectGatt(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (!isRegistered()) {
|
||||||
|
if (mGatt.getServices().size() > 0) {
|
||||||
|
Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover.");
|
||||||
|
probeService(this);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover.");
|
||||||
|
mIsReconnecting = true;
|
||||||
|
mGatt.disconnect();
|
||||||
|
mGatt = connectGatt(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BluetoothProfile.STATE_DISCONNECTED:
|
||||||
|
Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover.");
|
||||||
|
|
||||||
|
mIsReconnecting = true;
|
||||||
|
mGatt.disconnect();
|
||||||
|
mGatt = connectGatt(false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BluetoothProfile.STATE_CONNECTING:
|
||||||
|
Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
final HIDDeviceBLESteamController finalThis = this;
|
||||||
|
mHandler.postDelayed(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
finalThis.checkConnectionForChromebookIssue();
|
||||||
|
}
|
||||||
|
}, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRegistered() {
|
||||||
|
return mIsRegistered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setRegistered() {
|
||||||
|
mIsRegistered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean probeService(HIDDeviceBLESteamController controller) {
|
||||||
|
|
||||||
|
if (isRegistered()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mIsConnected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.v(TAG, "probeService controller=" + controller);
|
||||||
|
|
||||||
|
for (BluetoothGattService service : mGatt.getServices()) {
|
||||||
|
if (service.getUuid().equals(steamControllerService)) {
|
||||||
|
Log.v(TAG, "Found Valve steam controller service " + service.getUuid());
|
||||||
|
|
||||||
|
for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
|
||||||
|
if (chr.getUuid().equals(inputCharacteristic)) {
|
||||||
|
Log.v(TAG, "Found input characteristic");
|
||||||
|
// Start notifications
|
||||||
|
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
|
||||||
|
if (cccd != null) {
|
||||||
|
enableNotification(chr.getUuid());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) {
|
||||||
|
Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us.");
|
||||||
|
mIsConnected = false;
|
||||||
|
mIsReconnecting = true;
|
||||||
|
mGatt.disconnect();
|
||||||
|
mGatt = connectGatt(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private void finishCurrentGattOperation() {
|
||||||
|
GattOperation op = null;
|
||||||
|
synchronized (mOperations) {
|
||||||
|
if (mCurrentOperation != null) {
|
||||||
|
op = mCurrentOperation;
|
||||||
|
mCurrentOperation = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (op != null) {
|
||||||
|
boolean result = op.finish(); // TODO: Maybe in main thread as well?
|
||||||
|
|
||||||
|
// Our operation failed, let's add it back to the beginning of our queue.
|
||||||
|
if (!result) {
|
||||||
|
mOperations.addFirst(op);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
executeNextGattOperation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeNextGattOperation() {
|
||||||
|
synchronized (mOperations) {
|
||||||
|
if (mCurrentOperation != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (mOperations.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
mCurrentOperation = mOperations.removeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run in main thread
|
||||||
|
mHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
synchronized (mOperations) {
|
||||||
|
if (mCurrentOperation == null) {
|
||||||
|
Log.e(TAG, "Current operation null in executor?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mCurrentOperation.run();
|
||||||
|
// now wait for the GATT callback and when it comes, finish this operation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void queueGattOperation(GattOperation op) {
|
||||||
|
synchronized (mOperations) {
|
||||||
|
mOperations.add(op);
|
||||||
|
}
|
||||||
|
executeNextGattOperation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enableNotification(UUID chrUuid) {
|
||||||
|
GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid);
|
||||||
|
queueGattOperation(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeCharacteristic(UUID uuid, byte[] value) {
|
||||||
|
GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value);
|
||||||
|
queueGattOperation(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void readCharacteristic(UUID uuid) {
|
||||||
|
GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid);
|
||||||
|
queueGattOperation(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////////// BluetoothGattCallback overridden methods
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public void onConnectionStateChange(BluetoothGatt g, int status, int newState) {
|
||||||
|
//Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState);
|
||||||
|
mIsReconnecting = false;
|
||||||
|
if (newState == 2) {
|
||||||
|
mIsConnected = true;
|
||||||
|
// Run directly, without GattOperation
|
||||||
|
if (!isRegistered()) {
|
||||||
|
mHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
mGatt.discoverServices();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (newState == 0) {
|
||||||
|
mIsConnected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent.
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
|
||||||
|
//Log.v(TAG, "onServicesDiscovered status=" + status);
|
||||||
|
if (status == 0) {
|
||||||
|
if (gatt.getServices().size() == 0) {
|
||||||
|
Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack.");
|
||||||
|
mIsReconnecting = true;
|
||||||
|
mIsConnected = false;
|
||||||
|
gatt.disconnect();
|
||||||
|
mGatt = connectGatt(false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
probeService(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||||
|
//Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid());
|
||||||
|
|
||||||
|
if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) {
|
||||||
|
mManager.HIDDeviceFeatureReport(getId(), characteristic.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
finishCurrentGattOperation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||||
|
//Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid());
|
||||||
|
|
||||||
|
if (characteristic.getUuid().equals(reportCharacteristic)) {
|
||||||
|
// Only register controller with the native side once it has been fully configured
|
||||||
|
if (!isRegistered()) {
|
||||||
|
Log.v(TAG, "Registering Steam Controller with ID: " + getId());
|
||||||
|
mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0);
|
||||||
|
setRegistered();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finishCurrentGattOperation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
|
||||||
|
// Enable this for verbose logging of controller input reports
|
||||||
|
//Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue()));
|
||||||
|
|
||||||
|
if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) {
|
||||||
|
mManager.HIDDeviceInputReport(getId(), characteristic.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
|
||||||
|
//Log.v(TAG, "onDescriptorRead status=" + status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
|
||||||
|
BluetoothGattCharacteristic chr = descriptor.getCharacteristic();
|
||||||
|
//Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid());
|
||||||
|
|
||||||
|
if (chr.getUuid().equals(inputCharacteristic)) {
|
||||||
|
boolean hasWrittenInputDescriptor = true;
|
||||||
|
BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic);
|
||||||
|
if (reportChr != null) {
|
||||||
|
Log.v(TAG, "Writing report characteristic to enter valve mode");
|
||||||
|
reportChr.setValue(enterValveMode);
|
||||||
|
gatt.writeCharacteristic(reportChr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finishCurrentGattOperation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
|
||||||
|
//Log.v(TAG, "onReliableWriteCompleted status=" + status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
|
||||||
|
//Log.v(TAG, "onReadRemoteRssi status=" + status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
|
||||||
|
//Log.v(TAG, "onMtuChanged status=" + status);
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//////// Public API
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getId() {
|
||||||
|
return mDeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getVendorId() {
|
||||||
|
// Valve Corporation
|
||||||
|
final int VALVE_USB_VID = 0x28DE;
|
||||||
|
return VALVE_USB_VID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getProductId() {
|
||||||
|
// We don't have an easy way to query from the Bluetooth device, but we know what it is
|
||||||
|
final int D0G_BLE2_PID = 0x1106;
|
||||||
|
return D0G_BLE2_PID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSerialNumber() {
|
||||||
|
// This will be read later via feature report by Steam
|
||||||
|
return "12345";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getVersion() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getManufacturerName() {
|
||||||
|
return "Valve Corporation";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProductName() {
|
||||||
|
return "Steam Controller";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UsbDevice getDevice() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean open() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int sendFeatureReport(byte[] report) {
|
||||||
|
if (!isRegistered()) {
|
||||||
|
Log.e(TAG, "Attempted sendFeatureReport before Steam Controller is registered!");
|
||||||
|
if (mIsConnected) {
|
||||||
|
probeService(this);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to skip the first byte, as that doesn't go over the air
|
||||||
|
byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
|
||||||
|
//Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(actual_report));
|
||||||
|
writeCharacteristic(reportCharacteristic, actual_report);
|
||||||
|
return report.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int sendOutputReport(byte[] report) {
|
||||||
|
if (!isRegistered()) {
|
||||||
|
Log.e(TAG, "Attempted sendOutputReport before Steam Controller is registered!");
|
||||||
|
if (mIsConnected) {
|
||||||
|
probeService(this);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report));
|
||||||
|
writeCharacteristic(reportCharacteristic, report);
|
||||||
|
return report.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getFeatureReport(byte[] report) {
|
||||||
|
if (!isRegistered()) {
|
||||||
|
Log.e(TAG, "Attempted getFeatureReport before Steam Controller is registered!");
|
||||||
|
if (mIsConnected) {
|
||||||
|
probeService(this);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Log.v(TAG, "getFeatureReport");
|
||||||
|
readCharacteristic(reportCharacteristic);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setFrozen(boolean frozen) {
|
||||||
|
mFrozen = frozen;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
close();
|
||||||
|
|
||||||
|
BluetoothGatt g = mGatt;
|
||||||
|
if (g != null) {
|
||||||
|
g.disconnect();
|
||||||
|
g.close();
|
||||||
|
mGatt = null;
|
||||||
|
}
|
||||||
|
mManager = null;
|
||||||
|
mIsRegistered = false;
|
||||||
|
mIsConnected = false;
|
||||||
|
mOperations.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
683
Android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java
Normal file
@ -0,0 +1,683 @@
|
|||||||
|
package org.libsdl.app;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.bluetooth.BluetoothAdapter;
|
||||||
|
import android.bluetooth.BluetoothDevice;
|
||||||
|
import android.bluetooth.BluetoothManager;
|
||||||
|
import android.bluetooth.BluetoothProfile;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.hardware.usb.*;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class HIDDeviceManager {
|
||||||
|
private static final String TAG = "hidapi";
|
||||||
|
private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION";
|
||||||
|
|
||||||
|
private static HIDDeviceManager sManager;
|
||||||
|
private static int sManagerRefCount = 0;
|
||||||
|
|
||||||
|
public static HIDDeviceManager acquire(Context context) {
|
||||||
|
if (sManagerRefCount == 0) {
|
||||||
|
sManager = new HIDDeviceManager(context);
|
||||||
|
}
|
||||||
|
++sManagerRefCount;
|
||||||
|
return sManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void release(HIDDeviceManager manager) {
|
||||||
|
if (manager == sManager) {
|
||||||
|
--sManagerRefCount;
|
||||||
|
if (sManagerRefCount == 0) {
|
||||||
|
sManager.close();
|
||||||
|
sManager = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Context mContext;
|
||||||
|
private HashMap<Integer, HIDDevice> mDevicesById = new HashMap<Integer, HIDDevice>();
|
||||||
|
private HashMap<BluetoothDevice, HIDDeviceBLESteamController> mBluetoothDevices = new HashMap<BluetoothDevice, HIDDeviceBLESteamController>();
|
||||||
|
private int mNextDeviceId = 0;
|
||||||
|
private SharedPreferences mSharedPreferences = null;
|
||||||
|
private boolean mIsChromebook = false;
|
||||||
|
private UsbManager mUsbManager;
|
||||||
|
private Handler mHandler;
|
||||||
|
private BluetoothManager mBluetoothManager;
|
||||||
|
private List<BluetoothDevice> mLastBluetoothDevices;
|
||||||
|
|
||||||
|
private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() {
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
String action = intent.getAction();
|
||||||
|
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
|
||||||
|
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||||
|
handleUsbDeviceAttached(usbDevice);
|
||||||
|
} else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
|
||||||
|
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||||
|
handleUsbDeviceDetached(usbDevice);
|
||||||
|
} else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) {
|
||||||
|
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||||
|
handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() {
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
String action = intent.getAction();
|
||||||
|
// Bluetooth device was connected. If it was a Steam Controller, handle it
|
||||||
|
if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) {
|
||||||
|
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||||
|
Log.d(TAG, "Bluetooth device connected: " + device);
|
||||||
|
|
||||||
|
if (isSteamController(device)) {
|
||||||
|
connectBluetoothDevice(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bluetooth device was disconnected, remove from controller manager (if any)
|
||||||
|
if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
|
||||||
|
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||||
|
Log.d(TAG, "Bluetooth device disconnected: " + device);
|
||||||
|
|
||||||
|
disconnectBluetoothDevice(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private HIDDeviceManager(final Context context) {
|
||||||
|
mContext = context;
|
||||||
|
|
||||||
|
HIDDeviceRegisterCallback();
|
||||||
|
|
||||||
|
mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE);
|
||||||
|
mIsChromebook = mContext.getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
|
||||||
|
|
||||||
|
// if (shouldClear) {
|
||||||
|
// SharedPreferences.Editor spedit = mSharedPreferences.edit();
|
||||||
|
// spedit.clear();
|
||||||
|
// spedit.commit();
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
{
|
||||||
|
mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Context getContext() {
|
||||||
|
return mContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDeviceIDForIdentifier(String identifier) {
|
||||||
|
SharedPreferences.Editor spedit = mSharedPreferences.edit();
|
||||||
|
|
||||||
|
int result = mSharedPreferences.getInt(identifier, 0);
|
||||||
|
if (result == 0) {
|
||||||
|
result = mNextDeviceId++;
|
||||||
|
spedit.putInt("next_device_id", mNextDeviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
spedit.putInt(identifier, result);
|
||||||
|
spedit.commit();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeUSB() {
|
||||||
|
mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE);
|
||||||
|
if (mUsbManager == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Logging
|
||||||
|
for (UsbDevice device : mUsbManager.getDeviceList().values()) {
|
||||||
|
Log.i(TAG,"Path: " + device.getDeviceName());
|
||||||
|
Log.i(TAG,"Manufacturer: " + device.getManufacturerName());
|
||||||
|
Log.i(TAG,"Product: " + device.getProductName());
|
||||||
|
Log.i(TAG,"ID: " + device.getDeviceId());
|
||||||
|
Log.i(TAG,"Class: " + device.getDeviceClass());
|
||||||
|
Log.i(TAG,"Protocol: " + device.getDeviceProtocol());
|
||||||
|
Log.i(TAG,"Vendor ID " + device.getVendorId());
|
||||||
|
Log.i(TAG,"Product ID: " + device.getProductId());
|
||||||
|
Log.i(TAG,"Interface count: " + device.getInterfaceCount());
|
||||||
|
Log.i(TAG,"---------------------------------------");
|
||||||
|
|
||||||
|
// Get interface details
|
||||||
|
for (int index = 0; index < device.getInterfaceCount(); index++) {
|
||||||
|
UsbInterface mUsbInterface = device.getInterface(index);
|
||||||
|
Log.i(TAG," ***** *****");
|
||||||
|
Log.i(TAG," Interface index: " + index);
|
||||||
|
Log.i(TAG," Interface ID: " + mUsbInterface.getId());
|
||||||
|
Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass());
|
||||||
|
Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass());
|
||||||
|
Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol());
|
||||||
|
Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount());
|
||||||
|
|
||||||
|
// Get endpoint details
|
||||||
|
for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++)
|
||||||
|
{
|
||||||
|
UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi);
|
||||||
|
Log.i(TAG," ++++ ++++ ++++");
|
||||||
|
Log.i(TAG," Endpoint index: " + epi);
|
||||||
|
Log.i(TAG," Attributes: " + mEndpoint.getAttributes());
|
||||||
|
Log.i(TAG," Direction: " + mEndpoint.getDirection());
|
||||||
|
Log.i(TAG," Number: " + mEndpoint.getEndpointNumber());
|
||||||
|
Log.i(TAG," Interval: " + mEndpoint.getInterval());
|
||||||
|
Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize());
|
||||||
|
Log.i(TAG," Type: " + mEndpoint.getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i(TAG," No more devices connected.");
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Register for USB broadcasts and permission completions
|
||||||
|
IntentFilter filter = new IntentFilter();
|
||||||
|
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
|
||||||
|
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
|
||||||
|
filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION);
|
||||||
|
mContext.registerReceiver(mUsbBroadcast, filter);
|
||||||
|
|
||||||
|
for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
|
||||||
|
handleUsbDeviceAttached(usbDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UsbManager getUSBManager() {
|
||||||
|
return mUsbManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void shutdownUSB() {
|
||||||
|
try {
|
||||||
|
mContext.unregisterReceiver(mUsbBroadcast);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// We may not have registered, that's okay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) {
|
||||||
|
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) {
|
||||||
|
final int XB360_IFACE_SUBCLASS = 93;
|
||||||
|
final int XB360_IFACE_PROTOCOL = 1; // Wired
|
||||||
|
final int XB360W_IFACE_PROTOCOL = 129; // Wireless
|
||||||
|
final int[] SUPPORTED_VENDORS = {
|
||||||
|
0x0079, // GPD Win 2
|
||||||
|
0x044f, // Thrustmaster
|
||||||
|
0x045e, // Microsoft
|
||||||
|
0x046d, // Logitech
|
||||||
|
0x056e, // Elecom
|
||||||
|
0x06a3, // Saitek
|
||||||
|
0x0738, // Mad Catz
|
||||||
|
0x07ff, // Mad Catz
|
||||||
|
0x0e6f, // PDP
|
||||||
|
0x0f0d, // Hori
|
||||||
|
0x1038, // SteelSeries
|
||||||
|
0x11c9, // Nacon
|
||||||
|
0x12ab, // Unknown
|
||||||
|
0x1430, // RedOctane
|
||||||
|
0x146b, // BigBen
|
||||||
|
0x1532, // Razer Sabertooth
|
||||||
|
0x15e4, // Numark
|
||||||
|
0x162e, // Joytech
|
||||||
|
0x1689, // Razer Onza
|
||||||
|
0x1949, // Lab126, Inc.
|
||||||
|
0x1bad, // Harmonix
|
||||||
|
0x20d6, // PowerA
|
||||||
|
0x24c6, // PowerA
|
||||||
|
0x2c22, // Qanba
|
||||||
|
0x2dc8, // 8BitDo
|
||||||
|
0x9886, // ASTRO Gaming
|
||||||
|
};
|
||||||
|
|
||||||
|
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||||
|
usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
|
||||||
|
(usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL ||
|
||||||
|
usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) {
|
||||||
|
int vendor_id = usbDevice.getVendorId();
|
||||||
|
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||||
|
if (vendor_id == supportedVid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) {
|
||||||
|
final int XB1_IFACE_SUBCLASS = 71;
|
||||||
|
final int XB1_IFACE_PROTOCOL = 208;
|
||||||
|
final int[] SUPPORTED_VENDORS = {
|
||||||
|
0x044f, // Thrustmaster
|
||||||
|
0x045e, // Microsoft
|
||||||
|
0x0738, // Mad Catz
|
||||||
|
0x0e6f, // PDP
|
||||||
|
0x0f0d, // Hori
|
||||||
|
0x10f5, // Turtle Beach
|
||||||
|
0x1532, // Razer Wildcat
|
||||||
|
0x20d6, // PowerA
|
||||||
|
0x24c6, // PowerA
|
||||||
|
0x2dc8, // 8BitDo
|
||||||
|
0x2e24, // Hyperkin
|
||||||
|
};
|
||||||
|
|
||||||
|
if (usbInterface.getId() == 0 &&
|
||||||
|
usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||||
|
usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
|
||||||
|
usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
|
||||||
|
int vendor_id = usbDevice.getVendorId();
|
||||||
|
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||||
|
if (vendor_id == supportedVid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleUsbDeviceAttached(UsbDevice usbDevice) {
|
||||||
|
connectHIDDeviceUSB(usbDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleUsbDeviceDetached(UsbDevice usbDevice) {
|
||||||
|
List<Integer> devices = new ArrayList<Integer>();
|
||||||
|
for (HIDDevice device : mDevicesById.values()) {
|
||||||
|
if (usbDevice.equals(device.getDevice())) {
|
||||||
|
devices.add(device.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int id : devices) {
|
||||||
|
HIDDevice device = mDevicesById.get(id);
|
||||||
|
mDevicesById.remove(id);
|
||||||
|
device.shutdown();
|
||||||
|
HIDDeviceDisconnected(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) {
|
||||||
|
for (HIDDevice device : mDevicesById.values()) {
|
||||||
|
if (usbDevice.equals(device.getDevice())) {
|
||||||
|
boolean opened = false;
|
||||||
|
if (permission_granted) {
|
||||||
|
opened = device.open();
|
||||||
|
}
|
||||||
|
HIDDeviceOpenResult(device.getId(), opened);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void connectHIDDeviceUSB(UsbDevice usbDevice) {
|
||||||
|
synchronized (this) {
|
||||||
|
int interface_mask = 0;
|
||||||
|
for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) {
|
||||||
|
UsbInterface usbInterface = usbDevice.getInterface(interface_index);
|
||||||
|
if (isHIDDeviceInterface(usbDevice, usbInterface)) {
|
||||||
|
// Check to see if we've already added this interface
|
||||||
|
// This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive
|
||||||
|
int interface_id = usbInterface.getId();
|
||||||
|
if ((interface_mask & (1 << interface_id)) != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
interface_mask |= (1 << interface_id);
|
||||||
|
|
||||||
|
HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index);
|
||||||
|
int id = device.getId();
|
||||||
|
mDevicesById.put(id, device);
|
||||||
|
HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeBluetooth() {
|
||||||
|
Log.d(TAG, "Initializing Bluetooth");
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT <= 30 /* Android 11.0 (R) */ &&
|
||||||
|
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) || (Build.VERSION.SDK_INT < 18 /* Android 4.3 (JELLY_BEAN_MR2) */)) {
|
||||||
|
Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find bonded bluetooth controllers and create SteamControllers for them
|
||||||
|
mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
|
||||||
|
if (mBluetoothManager == null) {
|
||||||
|
// This device doesn't support Bluetooth.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothAdapter btAdapter = mBluetoothManager.getAdapter();
|
||||||
|
if (btAdapter == null) {
|
||||||
|
// This device has Bluetooth support in the codebase, but has no available adapters.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get our bonded devices.
|
||||||
|
for (BluetoothDevice device : btAdapter.getBondedDevices()) {
|
||||||
|
|
||||||
|
Log.d(TAG, "Bluetooth device available: " + device);
|
||||||
|
if (isSteamController(device)) {
|
||||||
|
connectBluetoothDevice(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: These don't work on Chromebooks, to my undying dismay.
|
||||||
|
IntentFilter filter = new IntentFilter();
|
||||||
|
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
|
||||||
|
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
|
||||||
|
mContext.registerReceiver(mBluetoothBroadcast, filter);
|
||||||
|
|
||||||
|
if (mIsChromebook) {
|
||||||
|
mHandler = new Handler(Looper.getMainLooper());
|
||||||
|
mLastBluetoothDevices = new ArrayList<BluetoothDevice>();
|
||||||
|
|
||||||
|
// final HIDDeviceManager finalThis = this;
|
||||||
|
// mHandler.postDelayed(new Runnable() {
|
||||||
|
// @Override
|
||||||
|
// public void run() {
|
||||||
|
// finalThis.chromebookConnectionHandler();
|
||||||
|
// }
|
||||||
|
// }, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void shutdownBluetooth() {
|
||||||
|
try {
|
||||||
|
mContext.unregisterReceiver(mBluetoothBroadcast);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// We may not have registered, that's okay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly.
|
||||||
|
// This function provides a sort of dummy version of that, watching for changes in the
|
||||||
|
// connected devices and attempting to add controllers as things change.
|
||||||
|
public void chromebookConnectionHandler() {
|
||||||
|
if (!mIsChromebook) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<BluetoothDevice> disconnected = new ArrayList<BluetoothDevice>();
|
||||||
|
ArrayList<BluetoothDevice> connected = new ArrayList<BluetoothDevice>();
|
||||||
|
|
||||||
|
List<BluetoothDevice> currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
|
||||||
|
|
||||||
|
for (BluetoothDevice bluetoothDevice : currentConnected) {
|
||||||
|
if (!mLastBluetoothDevices.contains(bluetoothDevice)) {
|
||||||
|
connected.add(bluetoothDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) {
|
||||||
|
if (!currentConnected.contains(bluetoothDevice)) {
|
||||||
|
disconnected.add(bluetoothDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mLastBluetoothDevices = currentConnected;
|
||||||
|
|
||||||
|
for (BluetoothDevice bluetoothDevice : disconnected) {
|
||||||
|
disconnectBluetoothDevice(bluetoothDevice);
|
||||||
|
}
|
||||||
|
for (BluetoothDevice bluetoothDevice : connected) {
|
||||||
|
connectBluetoothDevice(bluetoothDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
final HIDDeviceManager finalThis = this;
|
||||||
|
mHandler.postDelayed(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
finalThis.chromebookConnectionHandler();
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) {
|
||||||
|
Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice);
|
||||||
|
synchronized (this) {
|
||||||
|
if (mBluetoothDevices.containsKey(bluetoothDevice)) {
|
||||||
|
Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect");
|
||||||
|
|
||||||
|
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
|
||||||
|
device.reconnect();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice);
|
||||||
|
int id = device.getId();
|
||||||
|
mBluetoothDevices.put(bluetoothDevice, device);
|
||||||
|
mDevicesById.put(id, device);
|
||||||
|
|
||||||
|
// The Steam Controller will mark itself connected once initialization is complete
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) {
|
||||||
|
synchronized (this) {
|
||||||
|
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
|
||||||
|
if (device == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int id = device.getId();
|
||||||
|
mBluetoothDevices.remove(bluetoothDevice);
|
||||||
|
mDevicesById.remove(id);
|
||||||
|
device.shutdown();
|
||||||
|
HIDDeviceDisconnected(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSteamController(BluetoothDevice bluetoothDevice) {
|
||||||
|
// Sanity check. If you pass in a null device, by definition it is never a Steam Controller.
|
||||||
|
if (bluetoothDevice == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the device has no local name, we really don't want to try an equality check against it.
|
||||||
|
if (bluetoothDevice.getName() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void close() {
|
||||||
|
shutdownUSB();
|
||||||
|
shutdownBluetooth();
|
||||||
|
synchronized (this) {
|
||||||
|
for (HIDDevice device : mDevicesById.values()) {
|
||||||
|
device.shutdown();
|
||||||
|
}
|
||||||
|
mDevicesById.clear();
|
||||||
|
mBluetoothDevices.clear();
|
||||||
|
HIDDeviceReleaseCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFrozen(boolean frozen) {
|
||||||
|
synchronized (this) {
|
||||||
|
for (HIDDevice device : mDevicesById.values()) {
|
||||||
|
device.setFrozen(frozen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private HIDDevice getDevice(int id) {
|
||||||
|
synchronized (this) {
|
||||||
|
HIDDevice result = mDevicesById.get(id);
|
||||||
|
if (result == null) {
|
||||||
|
Log.v(TAG, "No device for id: " + id);
|
||||||
|
Log.v(TAG, "Available devices: " + mDevicesById.keySet());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////// JNI interface functions
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public boolean initialize(boolean usb, boolean bluetooth) {
|
||||||
|
Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")");
|
||||||
|
|
||||||
|
if (usb) {
|
||||||
|
initializeUSB();
|
||||||
|
}
|
||||||
|
if (bluetooth) {
|
||||||
|
initializeBluetooth();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean openDevice(int deviceID) {
|
||||||
|
Log.v(TAG, "openDevice deviceID=" + deviceID);
|
||||||
|
HIDDevice device = getDevice(deviceID);
|
||||||
|
if (device == null) {
|
||||||
|
HIDDeviceDisconnected(deviceID);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look to see if this is a USB device and we have permission to access it
|
||||||
|
UsbDevice usbDevice = device.getDevice();
|
||||||
|
if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) {
|
||||||
|
HIDDeviceOpenPending(deviceID);
|
||||||
|
try {
|
||||||
|
final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31
|
||||||
|
int flags;
|
||||||
|
if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) {
|
||||||
|
flags = FLAG_MUTABLE;
|
||||||
|
} else {
|
||||||
|
flags = 0;
|
||||||
|
}
|
||||||
|
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), flags));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.v(TAG, "Couldn't request permission for USB device " + usbDevice);
|
||||||
|
HIDDeviceOpenResult(deviceID, false);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return device.open();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int sendOutputReport(int deviceID, byte[] report) {
|
||||||
|
try {
|
||||||
|
//Log.v(TAG, "sendOutputReport deviceID=" + deviceID + " length=" + report.length);
|
||||||
|
HIDDevice device;
|
||||||
|
device = getDevice(deviceID);
|
||||||
|
if (device == null) {
|
||||||
|
HIDDeviceDisconnected(deviceID);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return device.sendOutputReport(report);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int sendFeatureReport(int deviceID, byte[] report) {
|
||||||
|
try {
|
||||||
|
//Log.v(TAG, "sendFeatureReport deviceID=" + deviceID + " length=" + report.length);
|
||||||
|
HIDDevice device;
|
||||||
|
device = getDevice(deviceID);
|
||||||
|
if (device == null) {
|
||||||
|
HIDDeviceDisconnected(deviceID);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return device.sendFeatureReport(report);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getFeatureReport(int deviceID, byte[] report) {
|
||||||
|
try {
|
||||||
|
//Log.v(TAG, "getFeatureReport deviceID=" + deviceID);
|
||||||
|
HIDDevice device;
|
||||||
|
device = getDevice(deviceID);
|
||||||
|
if (device == null) {
|
||||||
|
HIDDeviceDisconnected(deviceID);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return device.getFeatureReport(report);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void closeDevice(int deviceID) {
|
||||||
|
try {
|
||||||
|
Log.v(TAG, "closeDevice deviceID=" + deviceID);
|
||||||
|
HIDDevice device;
|
||||||
|
device = getDevice(deviceID);
|
||||||
|
if (device == null) {
|
||||||
|
HIDDeviceDisconnected(deviceID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
device.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/////////////// Native methods
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private native void HIDDeviceRegisterCallback();
|
||||||
|
private native void HIDDeviceReleaseCallback();
|
||||||
|
|
||||||
|
native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol);
|
||||||
|
native void HIDDeviceOpenPending(int deviceID);
|
||||||
|
native void HIDDeviceOpenResult(int deviceID, boolean opened);
|
||||||
|
native void HIDDeviceDisconnected(int deviceID);
|
||||||
|
|
||||||
|
native void HIDDeviceInputReport(int deviceID, byte[] report);
|
||||||
|
native void HIDDeviceFeatureReport(int deviceID, byte[] report);
|
||||||
|
}
|
309
Android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
package org.libsdl.app;
|
||||||
|
|
||||||
|
import android.hardware.usb.*;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
class HIDDeviceUSB implements HIDDevice {
|
||||||
|
|
||||||
|
private static final String TAG = "hidapi";
|
||||||
|
|
||||||
|
protected HIDDeviceManager mManager;
|
||||||
|
protected UsbDevice mDevice;
|
||||||
|
protected int mInterfaceIndex;
|
||||||
|
protected int mInterface;
|
||||||
|
protected int mDeviceId;
|
||||||
|
protected UsbDeviceConnection mConnection;
|
||||||
|
protected UsbEndpoint mInputEndpoint;
|
||||||
|
protected UsbEndpoint mOutputEndpoint;
|
||||||
|
protected InputThread mInputThread;
|
||||||
|
protected boolean mRunning;
|
||||||
|
protected boolean mFrozen;
|
||||||
|
|
||||||
|
public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) {
|
||||||
|
mManager = manager;
|
||||||
|
mDevice = usbDevice;
|
||||||
|
mInterfaceIndex = interface_index;
|
||||||
|
mInterface = mDevice.getInterface(mInterfaceIndex).getId();
|
||||||
|
mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier());
|
||||||
|
mRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIdentifier() {
|
||||||
|
return String.format("%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getId() {
|
||||||
|
return mDeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getVendorId() {
|
||||||
|
return mDevice.getVendorId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getProductId() {
|
||||||
|
return mDevice.getProductId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSerialNumber() {
|
||||||
|
String result = null;
|
||||||
|
if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) {
|
||||||
|
try {
|
||||||
|
result = mDevice.getSerialNumber();
|
||||||
|
}
|
||||||
|
catch (SecurityException exception) {
|
||||||
|
//Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result == null) {
|
||||||
|
result = "";
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getVersion() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getManufacturerName() {
|
||||||
|
String result = null;
|
||||||
|
if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) {
|
||||||
|
result = mDevice.getManufacturerName();
|
||||||
|
}
|
||||||
|
if (result == null) {
|
||||||
|
result = String.format("%x", getVendorId());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProductName() {
|
||||||
|
String result = null;
|
||||||
|
if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) {
|
||||||
|
result = mDevice.getProductName();
|
||||||
|
}
|
||||||
|
if (result == null) {
|
||||||
|
result = String.format("%x", getProductId());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UsbDevice getDevice() {
|
||||||
|
return mDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDeviceName() {
|
||||||
|
return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean open() {
|
||||||
|
mConnection = mManager.getUSBManager().openDevice(mDevice);
|
||||||
|
if (mConnection == null) {
|
||||||
|
Log.w(TAG, "Unable to open USB device " + getDeviceName());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force claim our interface
|
||||||
|
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
|
||||||
|
if (!mConnection.claimInterface(iface, true)) {
|
||||||
|
Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName());
|
||||||
|
close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the endpoints
|
||||||
|
for (int j = 0; j < iface.getEndpointCount(); j++) {
|
||||||
|
UsbEndpoint endpt = iface.getEndpoint(j);
|
||||||
|
switch (endpt.getDirection()) {
|
||||||
|
case UsbConstants.USB_DIR_IN:
|
||||||
|
if (mInputEndpoint == null) {
|
||||||
|
mInputEndpoint = endpt;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case UsbConstants.USB_DIR_OUT:
|
||||||
|
if (mOutputEndpoint == null) {
|
||||||
|
mOutputEndpoint = endpt;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the required endpoints were present
|
||||||
|
if (mInputEndpoint == null || mOutputEndpoint == null) {
|
||||||
|
Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName());
|
||||||
|
close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start listening for input
|
||||||
|
mRunning = true;
|
||||||
|
mInputThread = new InputThread();
|
||||||
|
mInputThread.start();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int sendFeatureReport(byte[] report) {
|
||||||
|
int res = -1;
|
||||||
|
int offset = 0;
|
||||||
|
int length = report.length;
|
||||||
|
boolean skipped_report_id = false;
|
||||||
|
byte report_number = report[0];
|
||||||
|
|
||||||
|
if (report_number == 0x0) {
|
||||||
|
++offset;
|
||||||
|
--length;
|
||||||
|
skipped_report_id = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
res = mConnection.controlTransfer(
|
||||||
|
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT,
|
||||||
|
0x09/*HID set_report*/,
|
||||||
|
(3/*HID feature*/ << 8) | report_number,
|
||||||
|
mInterface,
|
||||||
|
report, offset, length,
|
||||||
|
1000/*timeout millis*/);
|
||||||
|
|
||||||
|
if (res < 0) {
|
||||||
|
Log.w(TAG, "sendFeatureReport() returned " + res + " on device " + getDeviceName());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipped_report_id) {
|
||||||
|
++length;
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int sendOutputReport(byte[] report) {
|
||||||
|
int r = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000);
|
||||||
|
if (r != report.length) {
|
||||||
|
Log.w(TAG, "sendOutputReport() returned " + r + " on device " + getDeviceName());
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getFeatureReport(byte[] report) {
|
||||||
|
int res = -1;
|
||||||
|
int offset = 0;
|
||||||
|
int length = report.length;
|
||||||
|
boolean skipped_report_id = false;
|
||||||
|
byte report_number = report[0];
|
||||||
|
|
||||||
|
if (report_number == 0x0) {
|
||||||
|
/* Offset the return buffer by 1, so that the report ID
|
||||||
|
will remain in byte 0. */
|
||||||
|
++offset;
|
||||||
|
--length;
|
||||||
|
skipped_report_id = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
res = mConnection.controlTransfer(
|
||||||
|
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN,
|
||||||
|
0x01/*HID get_report*/,
|
||||||
|
(3/*HID feature*/ << 8) | report_number,
|
||||||
|
mInterface,
|
||||||
|
report, offset, length,
|
||||||
|
1000/*timeout millis*/);
|
||||||
|
|
||||||
|
if (res < 0) {
|
||||||
|
Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipped_report_id) {
|
||||||
|
++res;
|
||||||
|
++length;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] data;
|
||||||
|
if (res == length) {
|
||||||
|
data = report;
|
||||||
|
} else {
|
||||||
|
data = Arrays.copyOfRange(report, 0, res);
|
||||||
|
}
|
||||||
|
mManager.HIDDeviceFeatureReport(mDeviceId, data);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
mRunning = false;
|
||||||
|
if (mInputThread != null) {
|
||||||
|
while (mInputThread.isAlive()) {
|
||||||
|
mInputThread.interrupt();
|
||||||
|
try {
|
||||||
|
mInputThread.join();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Keep trying until we're done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mInputThread = null;
|
||||||
|
}
|
||||||
|
if (mConnection != null) {
|
||||||
|
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
|
||||||
|
mConnection.releaseInterface(iface);
|
||||||
|
mConnection.close();
|
||||||
|
mConnection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
close();
|
||||||
|
mManager = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setFrozen(boolean frozen) {
|
||||||
|
mFrozen = frozen;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected class InputThread extends Thread {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
int packetSize = mInputEndpoint.getMaxPacketSize();
|
||||||
|
byte[] packet = new byte[packetSize];
|
||||||
|
while (mRunning) {
|
||||||
|
int r;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (r < 0) {
|
||||||
|
// Could be a timeout or an I/O error
|
||||||
|
}
|
||||||
|
if (r > 0) {
|
||||||
|
byte[] data;
|
||||||
|
if (r == packetSize) {
|
||||||
|
data = packet;
|
||||||
|
} else {
|
||||||
|
data = Arrays.copyOfRange(packet, 0, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mFrozen) {
|
||||||
|
mManager.HIDDeviceInputReport(mDeviceId, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
86
Android/app/src/main/java/org/libsdl/app/SDL.java
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package org.libsdl.app;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import java.lang.Class;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
/**
|
||||||
|
SDL library initialization
|
||||||
|
*/
|
||||||
|
public class SDL {
|
||||||
|
|
||||||
|
// This function should be called first and sets up the native code
|
||||||
|
// so it can call into the Java classes
|
||||||
|
public static void setupJNI() {
|
||||||
|
SDLActivity.nativeSetupJNI();
|
||||||
|
SDLAudioManager.nativeSetupJNI();
|
||||||
|
SDLControllerManager.nativeSetupJNI();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function should be called each time the activity is started
|
||||||
|
public static void initialize() {
|
||||||
|
setContext(null);
|
||||||
|
|
||||||
|
SDLActivity.initialize();
|
||||||
|
SDLAudioManager.initialize();
|
||||||
|
SDLControllerManager.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function stores the current activity (SDL or not)
|
||||||
|
public static void setContext(Context context) {
|
||||||
|
SDLAudioManager.setContext(context);
|
||||||
|
mContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Context getContext() {
|
||||||
|
return mContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
|
||||||
|
|
||||||
|
if (libraryName == null) {
|
||||||
|
throw new NullPointerException("No library name provided.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Let's see if we have ReLinker available in the project. This is necessary for
|
||||||
|
// some projects that have huge numbers of local libraries bundled, and thus may
|
||||||
|
// trip a bug in Android's native library loader which ReLinker works around. (If
|
||||||
|
// loadLibrary works properly, ReLinker will simply use the normal Android method
|
||||||
|
// internally.)
|
||||||
|
//
|
||||||
|
// To use ReLinker, just add it as a dependency. For more information, see
|
||||||
|
// https://github.com/KeepSafe/ReLinker for ReLinker's repository.
|
||||||
|
//
|
||||||
|
Class<?> relinkClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker");
|
||||||
|
Class<?> relinkListenerClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener");
|
||||||
|
Class<?> contextClass = mContext.getClassLoader().loadClass("android.content.Context");
|
||||||
|
Class<?> stringClass = mContext.getClassLoader().loadClass("java.lang.String");
|
||||||
|
|
||||||
|
// Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if
|
||||||
|
// they've changed during updates.
|
||||||
|
Method forceMethod = relinkClass.getDeclaredMethod("force");
|
||||||
|
Object relinkInstance = forceMethod.invoke(null);
|
||||||
|
Class<?> relinkInstanceClass = relinkInstance.getClass();
|
||||||
|
|
||||||
|
// Actually load the library!
|
||||||
|
Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass);
|
||||||
|
loadMethod.invoke(relinkInstance, mContext, libraryName, null, null);
|
||||||
|
}
|
||||||
|
catch (final Throwable e) {
|
||||||
|
// Fall back
|
||||||
|
try {
|
||||||
|
System.loadLibrary(libraryName);
|
||||||
|
}
|
||||||
|
catch (final UnsatisfiedLinkError ule) {
|
||||||
|
throw ule;
|
||||||
|
}
|
||||||
|
catch (final SecurityException se) {
|
||||||
|
throw se;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Context mContext;
|
||||||
|
}
|
2119
Android/app/src/main/java/org/libsdl/app/SDLActivity.java
Normal file
514
Android/app/src/main/java/org/libsdl/app/SDLAudioManager.java
Normal file
@ -0,0 +1,514 @@
|
|||||||
|
package org.libsdl.app;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.media.AudioDeviceCallback;
|
||||||
|
import android.media.AudioDeviceInfo;
|
||||||
|
import android.media.AudioFormat;
|
||||||
|
import android.media.AudioManager;
|
||||||
|
import android.media.AudioRecord;
|
||||||
|
import android.media.AudioTrack;
|
||||||
|
import android.media.MediaRecorder;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class SDLAudioManager {
|
||||||
|
protected static final String TAG = "SDLAudio";
|
||||||
|
|
||||||
|
protected static AudioTrack mAudioTrack;
|
||||||
|
protected static AudioRecord mAudioRecord;
|
||||||
|
protected static Context mContext;
|
||||||
|
|
||||||
|
private static final int[] NO_DEVICES = {};
|
||||||
|
|
||||||
|
private static AudioDeviceCallback mAudioDeviceCallback;
|
||||||
|
|
||||||
|
public static void initialize() {
|
||||||
|
mAudioTrack = null;
|
||||||
|
mAudioRecord = null;
|
||||||
|
mAudioDeviceCallback = null;
|
||||||
|
|
||||||
|
if(Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */)
|
||||||
|
{
|
||||||
|
mAudioDeviceCallback = new AudioDeviceCallback() {
|
||||||
|
@Override
|
||||||
|
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
|
||||||
|
Arrays.stream(addedDevices).forEach(deviceInfo -> addAudioDevice(deviceInfo.isSink(), deviceInfo.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
|
||||||
|
Arrays.stream(removedDevices).forEach(deviceInfo -> removeAudioDevice(deviceInfo.isSink(), deviceInfo.getId()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setContext(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
if (context != null) {
|
||||||
|
registerAudioDeviceCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void release(Context context) {
|
||||||
|
unregisterAudioDeviceCallback(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio
|
||||||
|
|
||||||
|
protected static String getAudioFormatString(int audioFormat) {
|
||||||
|
switch (audioFormat) {
|
||||||
|
case AudioFormat.ENCODING_PCM_8BIT:
|
||||||
|
return "8-bit";
|
||||||
|
case AudioFormat.ENCODING_PCM_16BIT:
|
||||||
|
return "16-bit";
|
||||||
|
case AudioFormat.ENCODING_PCM_FLOAT:
|
||||||
|
return "float";
|
||||||
|
default:
|
||||||
|
return Integer.toString(audioFormat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) {
|
||||||
|
int channelConfig;
|
||||||
|
int sampleSize;
|
||||||
|
int frameSize;
|
||||||
|
|
||||||
|
Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", requested " + desiredFrames + " frames of " + desiredChannels + " channel " + getAudioFormatString(audioFormat) + " audio at " + sampleRate + " Hz");
|
||||||
|
|
||||||
|
/* On older devices let's use known good settings */
|
||||||
|
if (Build.VERSION.SDK_INT < 21 /* Android 5.0 (LOLLIPOP) */) {
|
||||||
|
if (desiredChannels > 2) {
|
||||||
|
desiredChannels = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AudioTrack has sample rate limitation of 48000 (fixed in 5.0.2) */
|
||||||
|
if (Build.VERSION.SDK_INT < 22 /* Android 5.1 (LOLLIPOP_MR1) */) {
|
||||||
|
if (sampleRate < 8000) {
|
||||||
|
sampleRate = 8000;
|
||||||
|
} else if (sampleRate > 48000) {
|
||||||
|
sampleRate = 48000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioFormat == AudioFormat.ENCODING_PCM_FLOAT) {
|
||||||
|
int minSDKVersion = (isCapture ? 23 /* Android 6.0 (M) */ : 21 /* Android 5.0 (LOLLIPOP) */);
|
||||||
|
if (Build.VERSION.SDK_INT < minSDKVersion) {
|
||||||
|
audioFormat = AudioFormat.ENCODING_PCM_16BIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (audioFormat)
|
||||||
|
{
|
||||||
|
case AudioFormat.ENCODING_PCM_8BIT:
|
||||||
|
sampleSize = 1;
|
||||||
|
break;
|
||||||
|
case AudioFormat.ENCODING_PCM_16BIT:
|
||||||
|
sampleSize = 2;
|
||||||
|
break;
|
||||||
|
case AudioFormat.ENCODING_PCM_FLOAT:
|
||||||
|
sampleSize = 4;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.v(TAG, "Requested format " + audioFormat + ", getting ENCODING_PCM_16BIT");
|
||||||
|
audioFormat = AudioFormat.ENCODING_PCM_16BIT;
|
||||||
|
sampleSize = 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCapture) {
|
||||||
|
switch (desiredChannels) {
|
||||||
|
case 1:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_IN_MONO;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo");
|
||||||
|
desiredChannels = 2;
|
||||||
|
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (desiredChannels) {
|
||||||
|
case 1:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
|
||||||
|
break;
|
||||||
|
case 8:
|
||||||
|
if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) {
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
|
||||||
|
} else {
|
||||||
|
Log.v(TAG, "Requested " + desiredChannels + " channels, getting 5.1 surround");
|
||||||
|
desiredChannels = 6;
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo");
|
||||||
|
desiredChannels = 2;
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Log.v(TAG, "Speaker configuration (and order of channels):");
|
||||||
|
|
||||||
|
if ((channelConfig & 0x00000004) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00000008) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00000010) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_FRONT_CENTER");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00000020) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_LOW_FREQUENCY");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00000040) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_BACK_LEFT");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00000080) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_BACK_RIGHT");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00000100) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT_OF_CENTER");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00000200) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT_OF_CENTER");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00000400) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_BACK_CENTER");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00000800) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_SIDE_LEFT");
|
||||||
|
}
|
||||||
|
if ((channelConfig & 0x00001000) != 0) {
|
||||||
|
Log.v(TAG, " CHANNEL_OUT_SIDE_RIGHT");
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
frameSize = (sampleSize * desiredChannels);
|
||||||
|
|
||||||
|
// Let the user pick a larger buffer if they really want -- but ye
|
||||||
|
// gods they probably shouldn't, the minimums are horrifyingly high
|
||||||
|
// latency already
|
||||||
|
int minBufferSize;
|
||||||
|
if (isCapture) {
|
||||||
|
minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
|
||||||
|
} else {
|
||||||
|
minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);
|
||||||
|
}
|
||||||
|
desiredFrames = Math.max(desiredFrames, (minBufferSize + frameSize - 1) / frameSize);
|
||||||
|
|
||||||
|
int[] results = new int[4];
|
||||||
|
|
||||||
|
if (isCapture) {
|
||||||
|
if (mAudioRecord == null) {
|
||||||
|
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate,
|
||||||
|
channelConfig, audioFormat, desiredFrames * frameSize);
|
||||||
|
|
||||||
|
// see notes about AudioTrack state in audioOpen(), above. Probably also applies here.
|
||||||
|
if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
|
||||||
|
Log.e(TAG, "Failed during initialization of AudioRecord");
|
||||||
|
mAudioRecord.release();
|
||||||
|
mAudioRecord = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) {
|
||||||
|
mAudioRecord.setPreferredDevice(getOutputAudioDeviceInfo(deviceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
mAudioRecord.startRecording();
|
||||||
|
}
|
||||||
|
|
||||||
|
results[0] = mAudioRecord.getSampleRate();
|
||||||
|
results[1] = mAudioRecord.getAudioFormat();
|
||||||
|
results[2] = mAudioRecord.getChannelCount();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (mAudioTrack == null) {
|
||||||
|
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM);
|
||||||
|
|
||||||
|
// Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid
|
||||||
|
// Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java
|
||||||
|
// Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState()
|
||||||
|
if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
|
||||||
|
/* Try again, with safer values */
|
||||||
|
|
||||||
|
Log.e(TAG, "Failed during initialization of Audio Track");
|
||||||
|
mAudioTrack.release();
|
||||||
|
mAudioTrack = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) {
|
||||||
|
mAudioTrack.setPreferredDevice(getInputAudioDeviceInfo(deviceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
mAudioTrack.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
results[0] = mAudioTrack.getSampleRate();
|
||||||
|
results[1] = mAudioTrack.getAudioFormat();
|
||||||
|
results[2] = mAudioTrack.getChannelCount();
|
||||||
|
}
|
||||||
|
results[3] = desiredFrames;
|
||||||
|
|
||||||
|
Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", got " + results[3] + " frames of " + results[2] + " channel " + getAudioFormatString(results[1]) + " audio at " + results[0] + " Hz");
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AudioDeviceInfo getInputAudioDeviceInfo(int deviceId) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||||
|
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||||
|
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS))
|
||||||
|
.filter(deviceInfo -> deviceInfo.getId() == deviceId)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AudioDeviceInfo getOutputAudioDeviceInfo(int deviceId) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||||
|
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||||
|
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS))
|
||||||
|
.filter(deviceInfo -> deviceInfo.getId() == deviceId)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void registerAudioDeviceCallback() {
|
||||||
|
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||||
|
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||||
|
audioManager.registerAudioDeviceCallback(mAudioDeviceCallback, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void unregisterAudioDeviceCallback(Context context) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||||
|
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||||
|
audioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static int[] getAudioOutputDevices() {
|
||||||
|
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||||
|
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||||
|
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)).mapToInt(AudioDeviceInfo::getId).toArray();
|
||||||
|
} else {
|
||||||
|
return NO_DEVICES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static int[] getAudioInputDevices() {
|
||||||
|
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||||
|
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||||
|
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).mapToInt(AudioDeviceInfo::getId).toArray();
|
||||||
|
} else {
|
||||||
|
return NO_DEVICES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) {
|
||||||
|
return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames, deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static void audioWriteFloatBuffer(float[] buffer) {
|
||||||
|
if (mAudioTrack == null) {
|
||||||
|
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (android.os.Build.VERSION.SDK_INT < 21 /* Android 5.0 (LOLLIPOP) */) {
|
||||||
|
Log.e(TAG, "Attempted to make an incompatible audio call with uninitialized audio! (floating-point output is supported since Android 5.0 Lollipop)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < buffer.length;) {
|
||||||
|
int result = mAudioTrack.write(buffer, i, buffer.length - i, AudioTrack.WRITE_BLOCKING);
|
||||||
|
if (result > 0) {
|
||||||
|
i += result;
|
||||||
|
} else if (result == 0) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(1);
|
||||||
|
} catch(InterruptedException e) {
|
||||||
|
// Nom nom
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "SDL audio: error return from write(float)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static void audioWriteShortBuffer(short[] buffer) {
|
||||||
|
if (mAudioTrack == null) {
|
||||||
|
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < buffer.length;) {
|
||||||
|
int result = mAudioTrack.write(buffer, i, buffer.length - i);
|
||||||
|
if (result > 0) {
|
||||||
|
i += result;
|
||||||
|
} else if (result == 0) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(1);
|
||||||
|
} catch(InterruptedException e) {
|
||||||
|
// Nom nom
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "SDL audio: error return from write(short)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static void audioWriteByteBuffer(byte[] buffer) {
|
||||||
|
if (mAudioTrack == null) {
|
||||||
|
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < buffer.length; ) {
|
||||||
|
int result = mAudioTrack.write(buffer, i, buffer.length - i);
|
||||||
|
if (result > 0) {
|
||||||
|
i += result;
|
||||||
|
} else if (result == 0) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(1);
|
||||||
|
} catch(InterruptedException e) {
|
||||||
|
// Nom nom
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "SDL audio: error return from write(byte)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) {
|
||||||
|
return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames, deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This method is called by SDL using JNI. */
|
||||||
|
public static int captureReadFloatBuffer(float[] buffer, boolean blocking) {
|
||||||
|
if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This method is called by SDL using JNI. */
|
||||||
|
public static int captureReadShortBuffer(short[] buffer, boolean blocking) {
|
||||||
|
if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) {
|
||||||
|
return mAudioRecord.read(buffer, 0, buffer.length);
|
||||||
|
} else {
|
||||||
|
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This method is called by SDL using JNI. */
|
||||||
|
public static int captureReadByteBuffer(byte[] buffer, boolean blocking) {
|
||||||
|
if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) {
|
||||||
|
return mAudioRecord.read(buffer, 0, buffer.length);
|
||||||
|
} else {
|
||||||
|
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This method is called by SDL using JNI. */
|
||||||
|
public static void audioClose() {
|
||||||
|
if (mAudioTrack != null) {
|
||||||
|
mAudioTrack.stop();
|
||||||
|
mAudioTrack.release();
|
||||||
|
mAudioTrack = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This method is called by SDL using JNI. */
|
||||||
|
public static void captureClose() {
|
||||||
|
if (mAudioRecord != null) {
|
||||||
|
mAudioRecord.stop();
|
||||||
|
mAudioRecord.release();
|
||||||
|
mAudioRecord = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This method is called by SDL using JNI. */
|
||||||
|
public static void audioSetThreadPriority(boolean iscapture, int device_id) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
/* Set thread name */
|
||||||
|
if (iscapture) {
|
||||||
|
Thread.currentThread().setName("SDLAudioC" + device_id);
|
||||||
|
} else {
|
||||||
|
Thread.currentThread().setName("SDLAudioP" + device_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set thread priority */
|
||||||
|
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.v(TAG, "modify thread properties failed " + e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static native int nativeSetupJNI();
|
||||||
|
|
||||||
|
public static native void removeAudioDevice(boolean isCapture, int deviceId);
|
||||||
|
|
||||||
|
public static native void addAudioDevice(boolean isCapture, int deviceId);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,854 @@
|
|||||||
|
package org.libsdl.app;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.VibrationEffect;
|
||||||
|
import android.os.Vibrator;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.InputDevice;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
|
||||||
|
public class SDLControllerManager
|
||||||
|
{
|
||||||
|
|
||||||
|
public static native int nativeSetupJNI();
|
||||||
|
|
||||||
|
public static native int nativeAddJoystick(int device_id, String name, String desc,
|
||||||
|
int vendor_id, int product_id,
|
||||||
|
boolean is_accelerometer, int button_mask,
|
||||||
|
int naxes, int axis_mask, int nhats, int nballs);
|
||||||
|
public static native int nativeRemoveJoystick(int device_id);
|
||||||
|
public static native int nativeAddHaptic(int device_id, String name);
|
||||||
|
public static native int nativeRemoveHaptic(int device_id);
|
||||||
|
public static native int onNativePadDown(int device_id, int keycode);
|
||||||
|
public static native int onNativePadUp(int device_id, int keycode);
|
||||||
|
public static native void onNativeJoy(int device_id, int axis,
|
||||||
|
float value);
|
||||||
|
public static native void onNativeHat(int device_id, int hat_id,
|
||||||
|
int x, int y);
|
||||||
|
|
||||||
|
protected static SDLJoystickHandler mJoystickHandler;
|
||||||
|
protected static SDLHapticHandler mHapticHandler;
|
||||||
|
|
||||||
|
private static final String TAG = "SDLControllerManager";
|
||||||
|
|
||||||
|
public static void initialize() {
|
||||||
|
if (mJoystickHandler == null) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) {
|
||||||
|
mJoystickHandler = new SDLJoystickHandler_API19();
|
||||||
|
} else {
|
||||||
|
mJoystickHandler = new SDLJoystickHandler_API16();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mHapticHandler == null) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) {
|
||||||
|
mHapticHandler = new SDLHapticHandler_API26();
|
||||||
|
} else {
|
||||||
|
mHapticHandler = new SDLHapticHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance
|
||||||
|
public static boolean handleJoystickMotionEvent(MotionEvent event) {
|
||||||
|
return mJoystickHandler.handleMotionEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static void pollInputDevices() {
|
||||||
|
mJoystickHandler.pollInputDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static void pollHapticDevices() {
|
||||||
|
mHapticHandler.pollHapticDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static void hapticRun(int device_id, float intensity, int length) {
|
||||||
|
mHapticHandler.run(device_id, intensity, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by SDL using JNI.
|
||||||
|
*/
|
||||||
|
public static void hapticStop(int device_id)
|
||||||
|
{
|
||||||
|
mHapticHandler.stop(device_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a given device is considered a possible SDL joystick
|
||||||
|
public static boolean isDeviceSDLJoystick(int deviceId) {
|
||||||
|
InputDevice device = InputDevice.getDevice(deviceId);
|
||||||
|
// We cannot use InputDevice.isVirtual before API 16, so let's accept
|
||||||
|
// only nonnegative device ids (VIRTUAL_KEYBOARD equals -1)
|
||||||
|
if ((device == null) || (deviceId < 0)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int sources = device.getSources();
|
||||||
|
|
||||||
|
/* This is called for every button press, so let's not spam the logs */
|
||||||
|
/*
|
||||||
|
if ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
|
||||||
|
Log.v(TAG, "Input device " + device.getName() + " has class joystick.");
|
||||||
|
}
|
||||||
|
if ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) {
|
||||||
|
Log.v(TAG, "Input device " + device.getName() + " is a dpad.");
|
||||||
|
}
|
||||||
|
if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
|
||||||
|
Log.v(TAG, "Input device " + device.getName() + " is a gamepad.");
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
return ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 ||
|
||||||
|
((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) ||
|
||||||
|
((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SDLJoystickHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles given MotionEvent.
|
||||||
|
* @param event the event to be handled.
|
||||||
|
* @return if given event was processed.
|
||||||
|
*/
|
||||||
|
public boolean handleMotionEvent(MotionEvent event) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles adding and removing of input devices.
|
||||||
|
*/
|
||||||
|
public void pollInputDevices() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actual joystick functionality available for API >= 12 devices */
|
||||||
|
class SDLJoystickHandler_API16 extends SDLJoystickHandler {
|
||||||
|
|
||||||
|
static class SDLJoystick {
|
||||||
|
public int device_id;
|
||||||
|
public String name;
|
||||||
|
public String desc;
|
||||||
|
public ArrayList<InputDevice.MotionRange> axes;
|
||||||
|
public ArrayList<InputDevice.MotionRange> hats;
|
||||||
|
}
|
||||||
|
static class RangeComparator implements Comparator<InputDevice.MotionRange> {
|
||||||
|
@Override
|
||||||
|
public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
|
||||||
|
// Some controllers, like the Moga Pro 2, return AXIS_GAS (22) for right trigger and AXIS_BRAKE (23) for left trigger - swap them so they're sorted in the right order for SDL
|
||||||
|
int arg0Axis = arg0.getAxis();
|
||||||
|
int arg1Axis = arg1.getAxis();
|
||||||
|
if (arg0Axis == MotionEvent.AXIS_GAS) {
|
||||||
|
arg0Axis = MotionEvent.AXIS_BRAKE;
|
||||||
|
} else if (arg0Axis == MotionEvent.AXIS_BRAKE) {
|
||||||
|
arg0Axis = MotionEvent.AXIS_GAS;
|
||||||
|
}
|
||||||
|
if (arg1Axis == MotionEvent.AXIS_GAS) {
|
||||||
|
arg1Axis = MotionEvent.AXIS_BRAKE;
|
||||||
|
} else if (arg1Axis == MotionEvent.AXIS_BRAKE) {
|
||||||
|
arg1Axis = MotionEvent.AXIS_GAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the AXIS_Z is sorted between AXIS_RY and AXIS_RZ.
|
||||||
|
// This is because the usual pairing are:
|
||||||
|
// - AXIS_X + AXIS_Y (left stick).
|
||||||
|
// - AXIS_RX, AXIS_RY (sometimes the right stick, sometimes triggers).
|
||||||
|
// - AXIS_Z, AXIS_RZ (sometimes the right stick, sometimes triggers).
|
||||||
|
// This sorts the axes in the above order, which tends to be correct
|
||||||
|
// for Xbox-ish game pads that have the right stick on RX/RY and the
|
||||||
|
// triggers on Z/RZ.
|
||||||
|
//
|
||||||
|
// Gamepads that don't have AXIS_Z/AXIS_RZ but use
|
||||||
|
// AXIS_LTRIGGER/AXIS_RTRIGGER are unaffected by this.
|
||||||
|
//
|
||||||
|
// References:
|
||||||
|
// - https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input
|
||||||
|
// - https://www.kernel.org/doc/html/latest/input/gamepad.html
|
||||||
|
if (arg0Axis == MotionEvent.AXIS_Z) {
|
||||||
|
arg0Axis = MotionEvent.AXIS_RZ - 1;
|
||||||
|
} else if (arg0Axis > MotionEvent.AXIS_Z && arg0Axis < MotionEvent.AXIS_RZ) {
|
||||||
|
--arg0Axis;
|
||||||
|
}
|
||||||
|
if (arg1Axis == MotionEvent.AXIS_Z) {
|
||||||
|
arg1Axis = MotionEvent.AXIS_RZ - 1;
|
||||||
|
} else if (arg1Axis > MotionEvent.AXIS_Z && arg1Axis < MotionEvent.AXIS_RZ) {
|
||||||
|
--arg1Axis;
|
||||||
|
}
|
||||||
|
|
||||||
|
return arg0Axis - arg1Axis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ArrayList<SDLJoystick> mJoysticks;
|
||||||
|
|
||||||
|
public SDLJoystickHandler_API16() {
|
||||||
|
|
||||||
|
mJoysticks = new ArrayList<SDLJoystick>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void pollInputDevices() {
|
||||||
|
int[] deviceIds = InputDevice.getDeviceIds();
|
||||||
|
|
||||||
|
for (int device_id : deviceIds) {
|
||||||
|
if (SDLControllerManager.isDeviceSDLJoystick(device_id)) {
|
||||||
|
SDLJoystick joystick = getJoystick(device_id);
|
||||||
|
if (joystick == null) {
|
||||||
|
InputDevice joystickDevice = InputDevice.getDevice(device_id);
|
||||||
|
joystick = new SDLJoystick();
|
||||||
|
joystick.device_id = device_id;
|
||||||
|
joystick.name = joystickDevice.getName();
|
||||||
|
joystick.desc = getJoystickDescriptor(joystickDevice);
|
||||||
|
joystick.axes = new ArrayList<InputDevice.MotionRange>();
|
||||||
|
joystick.hats = new ArrayList<InputDevice.MotionRange>();
|
||||||
|
|
||||||
|
List<InputDevice.MotionRange> ranges = joystickDevice.getMotionRanges();
|
||||||
|
Collections.sort(ranges, new RangeComparator());
|
||||||
|
for (InputDevice.MotionRange range : ranges) {
|
||||||
|
if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
|
||||||
|
if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) {
|
||||||
|
joystick.hats.add(range);
|
||||||
|
} else {
|
||||||
|
joystick.axes.add(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mJoysticks.add(joystick);
|
||||||
|
SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc,
|
||||||
|
getVendorId(joystickDevice), getProductId(joystickDevice), false,
|
||||||
|
getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check removed devices */
|
||||||
|
ArrayList<Integer> removedDevices = null;
|
||||||
|
for (SDLJoystick joystick : mJoysticks) {
|
||||||
|
int device_id = joystick.device_id;
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < deviceIds.length; i++) {
|
||||||
|
if (device_id == deviceIds[i]) break;
|
||||||
|
}
|
||||||
|
if (i == deviceIds.length) {
|
||||||
|
if (removedDevices == null) {
|
||||||
|
removedDevices = new ArrayList<Integer>();
|
||||||
|
}
|
||||||
|
removedDevices.add(device_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedDevices != null) {
|
||||||
|
for (int device_id : removedDevices) {
|
||||||
|
SDLControllerManager.nativeRemoveJoystick(device_id);
|
||||||
|
for (int i = 0; i < mJoysticks.size(); i++) {
|
||||||
|
if (mJoysticks.get(i).device_id == device_id) {
|
||||||
|
mJoysticks.remove(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected SDLJoystick getJoystick(int device_id) {
|
||||||
|
for (SDLJoystick joystick : mJoysticks) {
|
||||||
|
if (joystick.device_id == device_id) {
|
||||||
|
return joystick;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handleMotionEvent(MotionEvent event) {
|
||||||
|
int actionPointerIndex = event.getActionIndex();
|
||||||
|
int action = event.getActionMasked();
|
||||||
|
if (action == MotionEvent.ACTION_MOVE) {
|
||||||
|
SDLJoystick joystick = getJoystick(event.getDeviceId());
|
||||||
|
if (joystick != null) {
|
||||||
|
for (int i = 0; i < joystick.axes.size(); i++) {
|
||||||
|
InputDevice.MotionRange range = joystick.axes.get(i);
|
||||||
|
/* Normalize the value to -1...1 */
|
||||||
|
float value = (event.getAxisValue(range.getAxis(), actionPointerIndex) - range.getMin()) / range.getRange() * 2.0f - 1.0f;
|
||||||
|
SDLControllerManager.onNativeJoy(joystick.device_id, i, value);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < joystick.hats.size() / 2; i++) {
|
||||||
|
int hatX = Math.round(event.getAxisValue(joystick.hats.get(2 * i).getAxis(), actionPointerIndex));
|
||||||
|
int hatY = Math.round(event.getAxisValue(joystick.hats.get(2 * i + 1).getAxis(), actionPointerIndex));
|
||||||
|
SDLControllerManager.onNativeHat(joystick.device_id, i, hatX, hatY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getJoystickDescriptor(InputDevice joystickDevice) {
|
||||||
|
String desc = joystickDevice.getDescriptor();
|
||||||
|
|
||||||
|
if (desc != null && !desc.isEmpty()) {
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return joystickDevice.getName();
|
||||||
|
}
|
||||||
|
public int getProductId(InputDevice joystickDevice) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
public int getVendorId(InputDevice joystickDevice) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
public int getAxisMask(List<InputDevice.MotionRange> ranges) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
public int getButtonMask(InputDevice joystickDevice) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SDLJoystickHandler_API19 extends SDLJoystickHandler_API16 {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getProductId(InputDevice joystickDevice) {
|
||||||
|
return joystickDevice.getProductId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getVendorId(InputDevice joystickDevice) {
|
||||||
|
return joystickDevice.getVendorId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAxisMask(List<InputDevice.MotionRange> ranges) {
|
||||||
|
// For compatibility, keep computing the axis mask like before,
|
||||||
|
// only really distinguishing 2, 4 and 6 axes.
|
||||||
|
int axis_mask = 0;
|
||||||
|
if (ranges.size() >= 2) {
|
||||||
|
// ((1 << SDL_GAMEPAD_AXIS_LEFTX) | (1 << SDL_GAMEPAD_AXIS_LEFTY))
|
||||||
|
axis_mask |= 0x0003;
|
||||||
|
}
|
||||||
|
if (ranges.size() >= 4) {
|
||||||
|
// ((1 << SDL_GAMEPAD_AXIS_RIGHTX) | (1 << SDL_GAMEPAD_AXIS_RIGHTY))
|
||||||
|
axis_mask |= 0x000c;
|
||||||
|
}
|
||||||
|
if (ranges.size() >= 6) {
|
||||||
|
// ((1 << SDL_GAMEPAD_AXIS_LEFT_TRIGGER) | (1 << SDL_GAMEPAD_AXIS_RIGHT_TRIGGER))
|
||||||
|
axis_mask |= 0x0030;
|
||||||
|
}
|
||||||
|
// Also add an indicator bit for whether the sorting order has changed.
|
||||||
|
// This serves to disable outdated gamecontrollerdb.txt mappings.
|
||||||
|
boolean have_z = false;
|
||||||
|
boolean have_past_z_before_rz = false;
|
||||||
|
for (InputDevice.MotionRange range : ranges) {
|
||||||
|
int axis = range.getAxis();
|
||||||
|
if (axis == MotionEvent.AXIS_Z) {
|
||||||
|
have_z = true;
|
||||||
|
} else if (axis > MotionEvent.AXIS_Z && axis < MotionEvent.AXIS_RZ) {
|
||||||
|
have_past_z_before_rz = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (have_z && have_past_z_before_rz) {
|
||||||
|
// If both these exist, the compare() function changed sorting order.
|
||||||
|
// Set a bit to indicate this fact.
|
||||||
|
axis_mask |= 0x8000;
|
||||||
|
}
|
||||||
|
return axis_mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getButtonMask(InputDevice joystickDevice) {
|
||||||
|
int button_mask = 0;
|
||||||
|
int[] keys = new int[] {
|
||||||
|
KeyEvent.KEYCODE_BUTTON_A,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_B,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_X,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_Y,
|
||||||
|
KeyEvent.KEYCODE_BACK,
|
||||||
|
KeyEvent.KEYCODE_MENU,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_MODE,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_START,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_THUMBL,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_THUMBR,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_L1,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_R1,
|
||||||
|
KeyEvent.KEYCODE_DPAD_UP,
|
||||||
|
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||||
|
KeyEvent.KEYCODE_DPAD_LEFT,
|
||||||
|
KeyEvent.KEYCODE_DPAD_RIGHT,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_SELECT,
|
||||||
|
KeyEvent.KEYCODE_DPAD_CENTER,
|
||||||
|
|
||||||
|
// These don't map into any SDL controller buttons directly
|
||||||
|
KeyEvent.KEYCODE_BUTTON_L2,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_R2,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_C,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_Z,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_1,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_2,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_3,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_4,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_5,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_6,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_7,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_8,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_9,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_10,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_11,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_12,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_13,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_14,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_15,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_16,
|
||||||
|
};
|
||||||
|
int[] masks = new int[] {
|
||||||
|
(1 << 0), // A -> A
|
||||||
|
(1 << 1), // B -> B
|
||||||
|
(1 << 2), // X -> X
|
||||||
|
(1 << 3), // Y -> Y
|
||||||
|
(1 << 4), // BACK -> BACK
|
||||||
|
(1 << 6), // MENU -> START
|
||||||
|
(1 << 5), // MODE -> GUIDE
|
||||||
|
(1 << 6), // START -> START
|
||||||
|
(1 << 7), // THUMBL -> LEFTSTICK
|
||||||
|
(1 << 8), // THUMBR -> RIGHTSTICK
|
||||||
|
(1 << 9), // L1 -> LEFTSHOULDER
|
||||||
|
(1 << 10), // R1 -> RIGHTSHOULDER
|
||||||
|
(1 << 11), // DPAD_UP -> DPAD_UP
|
||||||
|
(1 << 12), // DPAD_DOWN -> DPAD_DOWN
|
||||||
|
(1 << 13), // DPAD_LEFT -> DPAD_LEFT
|
||||||
|
(1 << 14), // DPAD_RIGHT -> DPAD_RIGHT
|
||||||
|
(1 << 4), // SELECT -> BACK
|
||||||
|
(1 << 0), // DPAD_CENTER -> A
|
||||||
|
(1 << 15), // L2 -> ??
|
||||||
|
(1 << 16), // R2 -> ??
|
||||||
|
(1 << 17), // C -> ??
|
||||||
|
(1 << 18), // Z -> ??
|
||||||
|
(1 << 20), // 1 -> ??
|
||||||
|
(1 << 21), // 2 -> ??
|
||||||
|
(1 << 22), // 3 -> ??
|
||||||
|
(1 << 23), // 4 -> ??
|
||||||
|
(1 << 24), // 5 -> ??
|
||||||
|
(1 << 25), // 6 -> ??
|
||||||
|
(1 << 26), // 7 -> ??
|
||||||
|
(1 << 27), // 8 -> ??
|
||||||
|
(1 << 28), // 9 -> ??
|
||||||
|
(1 << 29), // 10 -> ??
|
||||||
|
(1 << 30), // 11 -> ??
|
||||||
|
(1 << 31), // 12 -> ??
|
||||||
|
// We're out of room...
|
||||||
|
0xFFFFFFFF, // 13 -> ??
|
||||||
|
0xFFFFFFFF, // 14 -> ??
|
||||||
|
0xFFFFFFFF, // 15 -> ??
|
||||||
|
0xFFFFFFFF, // 16 -> ??
|
||||||
|
};
|
||||||
|
boolean[] has_keys = joystickDevice.hasKeys(keys);
|
||||||
|
for (int i = 0; i < keys.length; ++i) {
|
||||||
|
if (has_keys[i]) {
|
||||||
|
button_mask |= masks[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return button_mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SDLHapticHandler_API26 extends SDLHapticHandler {
|
||||||
|
@Override
|
||||||
|
public void run(int device_id, float intensity, int length) {
|
||||||
|
SDLHaptic haptic = getHaptic(device_id);
|
||||||
|
if (haptic != null) {
|
||||||
|
Log.d("SDL", "Rtest: Vibe with intensity " + intensity + " for " + length);
|
||||||
|
if (intensity == 0.0f) {
|
||||||
|
stop(device_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vibeValue = Math.round(intensity * 255);
|
||||||
|
|
||||||
|
if (vibeValue > 255) {
|
||||||
|
vibeValue = 255;
|
||||||
|
}
|
||||||
|
if (vibeValue < 1) {
|
||||||
|
stop(device_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
haptic.vib.vibrate(VibrationEffect.createOneShot(length, vibeValue));
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
// Fall back to the generic method, which uses DEFAULT_AMPLITUDE, but works even if
|
||||||
|
// something went horribly wrong with the Android 8.0 APIs.
|
||||||
|
haptic.vib.vibrate(length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SDLHapticHandler {
|
||||||
|
|
||||||
|
static class SDLHaptic {
|
||||||
|
public int device_id;
|
||||||
|
public String name;
|
||||||
|
public Vibrator vib;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ArrayList<SDLHaptic> mHaptics;
|
||||||
|
|
||||||
|
public SDLHapticHandler() {
|
||||||
|
mHaptics = new ArrayList<SDLHaptic>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run(int device_id, float intensity, int length) {
|
||||||
|
SDLHaptic haptic = getHaptic(device_id);
|
||||||
|
if (haptic != null) {
|
||||||
|
haptic.vib.vibrate(length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop(int device_id) {
|
||||||
|
SDLHaptic haptic = getHaptic(device_id);
|
||||||
|
if (haptic != null) {
|
||||||
|
haptic.vib.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pollHapticDevices() {
|
||||||
|
|
||||||
|
final int deviceId_VIBRATOR_SERVICE = 999999;
|
||||||
|
boolean hasVibratorService = false;
|
||||||
|
|
||||||
|
int[] deviceIds = InputDevice.getDeviceIds();
|
||||||
|
// It helps processing the device ids in reverse order
|
||||||
|
// For example, in the case of the XBox 360 wireless dongle,
|
||||||
|
// so the first controller seen by SDL matches what the receiver
|
||||||
|
// considers to be the first controller
|
||||||
|
|
||||||
|
for (int i = deviceIds.length - 1; i > -1; i--) {
|
||||||
|
SDLHaptic haptic = getHaptic(deviceIds[i]);
|
||||||
|
if (haptic == null) {
|
||||||
|
InputDevice device = InputDevice.getDevice(deviceIds[i]);
|
||||||
|
Vibrator vib = device.getVibrator();
|
||||||
|
if (vib.hasVibrator()) {
|
||||||
|
haptic = new SDLHaptic();
|
||||||
|
haptic.device_id = deviceIds[i];
|
||||||
|
haptic.name = device.getName();
|
||||||
|
haptic.vib = vib;
|
||||||
|
mHaptics.add(haptic);
|
||||||
|
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check VIBRATOR_SERVICE */
|
||||||
|
Vibrator vib = (Vibrator) SDL.getContext().getSystemService(Context.VIBRATOR_SERVICE);
|
||||||
|
if (vib != null) {
|
||||||
|
hasVibratorService = vib.hasVibrator();
|
||||||
|
|
||||||
|
if (hasVibratorService) {
|
||||||
|
SDLHaptic haptic = getHaptic(deviceId_VIBRATOR_SERVICE);
|
||||||
|
if (haptic == null) {
|
||||||
|
haptic = new SDLHaptic();
|
||||||
|
haptic.device_id = deviceId_VIBRATOR_SERVICE;
|
||||||
|
haptic.name = "VIBRATOR_SERVICE";
|
||||||
|
haptic.vib = vib;
|
||||||
|
mHaptics.add(haptic);
|
||||||
|
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check removed devices */
|
||||||
|
ArrayList<Integer> removedDevices = null;
|
||||||
|
for (SDLHaptic haptic : mHaptics) {
|
||||||
|
int device_id = haptic.device_id;
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < deviceIds.length; i++) {
|
||||||
|
if (device_id == deviceIds[i]) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device_id != deviceId_VIBRATOR_SERVICE || !hasVibratorService) {
|
||||||
|
if (i == deviceIds.length) {
|
||||||
|
if (removedDevices == null) {
|
||||||
|
removedDevices = new ArrayList<Integer>();
|
||||||
|
}
|
||||||
|
removedDevices.add(device_id);
|
||||||
|
}
|
||||||
|
} // else: don't remove the vibrator if it is still present
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedDevices != null) {
|
||||||
|
for (int device_id : removedDevices) {
|
||||||
|
SDLControllerManager.nativeRemoveHaptic(device_id);
|
||||||
|
for (int i = 0; i < mHaptics.size(); i++) {
|
||||||
|
if (mHaptics.get(i).device_id == device_id) {
|
||||||
|
mHaptics.remove(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected SDLHaptic getHaptic(int device_id) {
|
||||||
|
for (SDLHaptic haptic : mHaptics) {
|
||||||
|
if (haptic.device_id == device_id) {
|
||||||
|
return haptic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener {
|
||||||
|
// Generic Motion (mouse hover, joystick...) events go here
|
||||||
|
@Override
|
||||||
|
public boolean onGenericMotion(View v, MotionEvent event) {
|
||||||
|
float x, y;
|
||||||
|
int action;
|
||||||
|
|
||||||
|
switch ( event.getSource() ) {
|
||||||
|
case InputDevice.SOURCE_JOYSTICK:
|
||||||
|
return SDLControllerManager.handleJoystickMotionEvent(event);
|
||||||
|
|
||||||
|
case InputDevice.SOURCE_MOUSE:
|
||||||
|
action = event.getActionMasked();
|
||||||
|
switch (action) {
|
||||||
|
case MotionEvent.ACTION_SCROLL:
|
||||||
|
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
|
||||||
|
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
|
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_HOVER_MOVE:
|
||||||
|
x = event.getX(0);
|
||||||
|
y = event.getY(0);
|
||||||
|
|
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event was not managed
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean supportsRelativeMouse() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean inRelativeMode() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean setRelativeMouseEnabled(boolean enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reclaimRelativeMouseModeIfNeeded()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getEventX(MotionEvent event) {
|
||||||
|
return event.getX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getEventY(MotionEvent event) {
|
||||||
|
return event.getY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SDLGenericMotionListener_API24 extends SDLGenericMotionListener_API12 {
|
||||||
|
// Generic Motion (mouse hover, joystick...) events go here
|
||||||
|
|
||||||
|
private boolean mRelativeModeEnabled;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onGenericMotion(View v, MotionEvent event) {
|
||||||
|
|
||||||
|
// Handle relative mouse mode
|
||||||
|
if (mRelativeModeEnabled) {
|
||||||
|
if (event.getSource() == InputDevice.SOURCE_MOUSE) {
|
||||||
|
int action = event.getActionMasked();
|
||||||
|
if (action == MotionEvent.ACTION_HOVER_MOVE) {
|
||||||
|
float x = event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
|
||||||
|
float y = event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
|
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event was not managed, call SDLGenericMotionListener_API12 method
|
||||||
|
return super.onGenericMotion(v, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsRelativeMouse() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean inRelativeMode() {
|
||||||
|
return mRelativeModeEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setRelativeMouseEnabled(boolean enabled) {
|
||||||
|
mRelativeModeEnabled = enabled;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getEventX(MotionEvent event) {
|
||||||
|
if (mRelativeModeEnabled) {
|
||||||
|
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
|
||||||
|
} else {
|
||||||
|
return event.getX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getEventY(MotionEvent event) {
|
||||||
|
if (mRelativeModeEnabled) {
|
||||||
|
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
|
||||||
|
} else {
|
||||||
|
return event.getY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 {
|
||||||
|
// Generic Motion (mouse hover, joystick...) events go here
|
||||||
|
private boolean mRelativeModeEnabled;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onGenericMotion(View v, MotionEvent event) {
|
||||||
|
float x, y;
|
||||||
|
int action;
|
||||||
|
|
||||||
|
switch ( event.getSource() ) {
|
||||||
|
case InputDevice.SOURCE_JOYSTICK:
|
||||||
|
return SDLControllerManager.handleJoystickMotionEvent(event);
|
||||||
|
|
||||||
|
case InputDevice.SOURCE_MOUSE:
|
||||||
|
// DeX desktop mouse cursor is a separate non-standard input type.
|
||||||
|
case InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN:
|
||||||
|
action = event.getActionMasked();
|
||||||
|
switch (action) {
|
||||||
|
case MotionEvent.ACTION_SCROLL:
|
||||||
|
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
|
||||||
|
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
|
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_HOVER_MOVE:
|
||||||
|
x = event.getX(0);
|
||||||
|
y = event.getY(0);
|
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InputDevice.SOURCE_MOUSE_RELATIVE:
|
||||||
|
action = event.getActionMasked();
|
||||||
|
switch (action) {
|
||||||
|
case MotionEvent.ACTION_SCROLL:
|
||||||
|
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
|
||||||
|
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
|
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_HOVER_MOVE:
|
||||||
|
x = event.getX(0);
|
||||||
|
y = event.getY(0);
|
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, true);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event was not managed
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsRelativeMouse() {
|
||||||
|
return (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean inRelativeMode() {
|
||||||
|
return mRelativeModeEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setRelativeMouseEnabled(boolean enabled) {
|
||||||
|
if (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */) {
|
||||||
|
if (enabled) {
|
||||||
|
SDLActivity.getContentView().requestPointerCapture();
|
||||||
|
} else {
|
||||||
|
SDLActivity.getContentView().releasePointerCapture();
|
||||||
|
}
|
||||||
|
mRelativeModeEnabled = enabled;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reclaimRelativeMouseModeIfNeeded()
|
||||||
|
{
|
||||||
|
if (mRelativeModeEnabled && !SDLActivity.isDeXMode()) {
|
||||||
|
SDLActivity.getContentView().requestPointerCapture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getEventX(MotionEvent event) {
|
||||||
|
// Relative mouse in capture mode will only have relative for X/Y
|
||||||
|
return event.getX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getEventY(MotionEvent event) {
|
||||||
|
// Relative mouse in capture mode will only have relative for X/Y
|
||||||
|
return event.getY(0);
|
||||||
|
}
|
||||||
|
}
|
405
Android/app/src/main/java/org/libsdl/app/SDLSurface.java
Normal file
@ -0,0 +1,405 @@
|
|||||||
|
package org.libsdl.app;
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.ActivityInfo;
|
||||||
|
import android.hardware.Sensor;
|
||||||
|
import android.hardware.SensorEvent;
|
||||||
|
import android.hardware.SensorEventListener;
|
||||||
|
import android.hardware.SensorManager;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.Display;
|
||||||
|
import android.view.InputDevice;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.Surface;
|
||||||
|
import android.view.SurfaceHolder;
|
||||||
|
import android.view.SurfaceView;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
SDLSurface. This is what we draw on, so we need to know when it's created
|
||||||
|
in order to do anything useful.
|
||||||
|
|
||||||
|
Because of this, that's where we set up the SDL thread
|
||||||
|
*/
|
||||||
|
public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
|
||||||
|
View.OnKeyListener, View.OnTouchListener, SensorEventListener {
|
||||||
|
|
||||||
|
// Sensors
|
||||||
|
protected SensorManager mSensorManager;
|
||||||
|
protected Display mDisplay;
|
||||||
|
|
||||||
|
// Keep track of the surface size to normalize touch events
|
||||||
|
protected float mWidth, mHeight;
|
||||||
|
|
||||||
|
// Is SurfaceView ready for rendering
|
||||||
|
public boolean mIsSurfaceReady;
|
||||||
|
|
||||||
|
// Startup
|
||||||
|
public SDLSurface(Context context) {
|
||||||
|
super(context);
|
||||||
|
getHolder().addCallback(this);
|
||||||
|
|
||||||
|
setFocusable(true);
|
||||||
|
setFocusableInTouchMode(true);
|
||||||
|
requestFocus();
|
||||||
|
setOnKeyListener(this);
|
||||||
|
setOnTouchListener(this);
|
||||||
|
|
||||||
|
mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
|
||||||
|
mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
|
||||||
|
|
||||||
|
setOnGenericMotionListener(SDLActivity.getMotionListener());
|
||||||
|
|
||||||
|
// Some arbitrary defaults to avoid a potential division by zero
|
||||||
|
mWidth = 1.0f;
|
||||||
|
mHeight = 1.0f;
|
||||||
|
|
||||||
|
mIsSurfaceReady = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handlePause() {
|
||||||
|
enableSensor(Sensor.TYPE_ACCELEROMETER, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleResume() {
|
||||||
|
setFocusable(true);
|
||||||
|
setFocusableInTouchMode(true);
|
||||||
|
requestFocus();
|
||||||
|
setOnKeyListener(this);
|
||||||
|
setOnTouchListener(this);
|
||||||
|
enableSensor(Sensor.TYPE_ACCELEROMETER, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Surface getNativeSurface() {
|
||||||
|
return getHolder().getSurface();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when we have a valid drawing surface
|
||||||
|
@Override
|
||||||
|
public void surfaceCreated(SurfaceHolder holder) {
|
||||||
|
Log.v("SDL", "surfaceCreated()");
|
||||||
|
SDLActivity.onNativeSurfaceCreated();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when we lose the surface
|
||||||
|
@Override
|
||||||
|
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||||
|
Log.v("SDL", "surfaceDestroyed()");
|
||||||
|
|
||||||
|
// Transition to pause, if needed
|
||||||
|
SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
|
||||||
|
SDLActivity.handleNativeState();
|
||||||
|
|
||||||
|
mIsSurfaceReady = false;
|
||||||
|
SDLActivity.onNativeSurfaceDestroyed();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when the surface is resized
|
||||||
|
@Override
|
||||||
|
public void surfaceChanged(SurfaceHolder holder,
|
||||||
|
int format, int width, int height) {
|
||||||
|
Log.v("SDL", "surfaceChanged()");
|
||||||
|
|
||||||
|
if (SDLActivity.mSingleton == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mWidth = width;
|
||||||
|
mHeight = height;
|
||||||
|
int nDeviceWidth = width;
|
||||||
|
int nDeviceHeight = height;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SDK_INT >= 17 /* Android 4.2 (JELLY_BEAN_MR1) */) {
|
||||||
|
DisplayMetrics realMetrics = new DisplayMetrics();
|
||||||
|
mDisplay.getRealMetrics( realMetrics );
|
||||||
|
nDeviceWidth = realMetrics.widthPixels;
|
||||||
|
nDeviceHeight = realMetrics.heightPixels;
|
||||||
|
}
|
||||||
|
} catch(Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(SDLActivity.getContext()) {
|
||||||
|
// In case we're waiting on a size change after going fullscreen, send a notification.
|
||||||
|
SDLActivity.getContext().notifyAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.v("SDL", "Window size: " + width + "x" + height);
|
||||||
|
Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
|
||||||
|
SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, mDisplay.getRefreshRate());
|
||||||
|
SDLActivity.onNativeResize();
|
||||||
|
|
||||||
|
// Prevent a screen distortion glitch,
|
||||||
|
// for instance when the device is in Landscape and a Portrait App is resumed.
|
||||||
|
boolean skip = false;
|
||||||
|
int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
|
||||||
|
|
||||||
|
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
|
||||||
|
if (mWidth > mHeight) {
|
||||||
|
skip = true;
|
||||||
|
}
|
||||||
|
} else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
|
||||||
|
if (mWidth < mHeight) {
|
||||||
|
skip = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special Patch for Square Resolution: Black Berry Passport
|
||||||
|
if (skip) {
|
||||||
|
double min = Math.min(mWidth, mHeight);
|
||||||
|
double max = Math.max(mWidth, mHeight);
|
||||||
|
|
||||||
|
if (max / min < 1.20) {
|
||||||
|
Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
|
||||||
|
skip = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't skip in MultiWindow.
|
||||||
|
if (skip) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||||
|
if (SDLActivity.mSingleton.isInMultiWindowMode()) {
|
||||||
|
Log.v("SDL", "Don't skip in Multi-Window");
|
||||||
|
skip = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skip) {
|
||||||
|
Log.v("SDL", "Skip .. Surface is not ready.");
|
||||||
|
mIsSurfaceReady = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
|
||||||
|
SDLActivity.onNativeSurfaceChanged();
|
||||||
|
|
||||||
|
/* Surface is ready */
|
||||||
|
mIsSurfaceReady = true;
|
||||||
|
|
||||||
|
SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
|
||||||
|
SDLActivity.handleNativeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key events
|
||||||
|
@Override
|
||||||
|
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||||
|
return SDLActivity.handleKeyEvent(v, keyCode, event, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch events
|
||||||
|
@Override
|
||||||
|
public boolean onTouch(View v, MotionEvent event) {
|
||||||
|
/* Ref: http://developer.android.com/training/gestures/multi.html */
|
||||||
|
int touchDevId = event.getDeviceId();
|
||||||
|
final int pointerCount = event.getPointerCount();
|
||||||
|
int action = event.getActionMasked();
|
||||||
|
int pointerFingerId;
|
||||||
|
int i = -1;
|
||||||
|
float x,y,p;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Prevent id to be -1, since it's used in SDL internal for synthetic events
|
||||||
|
* Appears when using Android emulator, eg:
|
||||||
|
* adb shell input mouse tap 100 100
|
||||||
|
* adb shell input touchscreen tap 100 100
|
||||||
|
*/
|
||||||
|
if (touchDevId < 0) {
|
||||||
|
touchDevId -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12290 = Samsung DeX mode desktop mouse
|
||||||
|
// 12290 = 0x3002 = 0x2002 | 0x1002 = SOURCE_MOUSE | SOURCE_TOUCHSCREEN
|
||||||
|
// 0x2 = SOURCE_CLASS_POINTER
|
||||||
|
if (event.getSource() == InputDevice.SOURCE_MOUSE || event.getSource() == (InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN)) {
|
||||||
|
int mouseButton = 1;
|
||||||
|
try {
|
||||||
|
Object object = event.getClass().getMethod("getButtonState").invoke(event);
|
||||||
|
if (object != null) {
|
||||||
|
mouseButton = (Integer) object;
|
||||||
|
}
|
||||||
|
} catch(Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
|
||||||
|
// if we are. We'll leverage our existing mouse motion listener
|
||||||
|
SDLGenericMotionListener_API12 motionListener = SDLActivity.getMotionListener();
|
||||||
|
x = motionListener.getEventX(event);
|
||||||
|
y = motionListener.getEventY(event);
|
||||||
|
|
||||||
|
SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode());
|
||||||
|
} else {
|
||||||
|
switch(action) {
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
for (i = 0; i < pointerCount; i++) {
|
||||||
|
pointerFingerId = event.getPointerId(i);
|
||||||
|
x = event.getX(i) / mWidth;
|
||||||
|
y = event.getY(i) / mHeight;
|
||||||
|
p = event.getPressure(i);
|
||||||
|
if (p > 1.0f) {
|
||||||
|
// may be larger than 1.0f on some devices
|
||||||
|
// see the documentation of getPressure(i)
|
||||||
|
p = 1.0f;
|
||||||
|
}
|
||||||
|
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
// Primary pointer up/down, the index is always zero
|
||||||
|
i = 0;
|
||||||
|
/* fallthrough */
|
||||||
|
case MotionEvent.ACTION_POINTER_UP:
|
||||||
|
case MotionEvent.ACTION_POINTER_DOWN:
|
||||||
|
// Non primary pointer up/down
|
||||||
|
if (i == -1) {
|
||||||
|
i = event.getActionIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
pointerFingerId = event.getPointerId(i);
|
||||||
|
x = event.getX(i) / mWidth;
|
||||||
|
y = event.getY(i) / mHeight;
|
||||||
|
p = event.getPressure(i);
|
||||||
|
if (p > 1.0f) {
|
||||||
|
// may be larger than 1.0f on some devices
|
||||||
|
// see the documentation of getPressure(i)
|
||||||
|
p = 1.0f;
|
||||||
|
}
|
||||||
|
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
for (i = 0; i < pointerCount; i++) {
|
||||||
|
pointerFingerId = event.getPointerId(i);
|
||||||
|
x = event.getX(i) / mWidth;
|
||||||
|
y = event.getY(i) / mHeight;
|
||||||
|
p = event.getPressure(i);
|
||||||
|
if (p > 1.0f) {
|
||||||
|
// may be larger than 1.0f on some devices
|
||||||
|
// see the documentation of getPressure(i)
|
||||||
|
p = 1.0f;
|
||||||
|
}
|
||||||
|
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sensor events
|
||||||
|
public void enableSensor(int sensortype, boolean enabled) {
|
||||||
|
// TODO: This uses getDefaultSensor - what if we have >1 accels?
|
||||||
|
if (enabled) {
|
||||||
|
mSensorManager.registerListener(this,
|
||||||
|
mSensorManager.getDefaultSensor(sensortype),
|
||||||
|
SensorManager.SENSOR_DELAY_GAME, null);
|
||||||
|
} else {
|
||||||
|
mSensorManager.unregisterListener(this,
|
||||||
|
mSensorManager.getDefaultSensor(sensortype));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSensorChanged(SensorEvent event) {
|
||||||
|
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
|
||||||
|
|
||||||
|
// Since we may have an orientation set, we won't receive onConfigurationChanged events.
|
||||||
|
// We thus should check here.
|
||||||
|
int newOrientation;
|
||||||
|
|
||||||
|
float x, y;
|
||||||
|
switch (mDisplay.getRotation()) {
|
||||||
|
case Surface.ROTATION_90:
|
||||||
|
x = -event.values[1];
|
||||||
|
y = event.values[0];
|
||||||
|
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE;
|
||||||
|
break;
|
||||||
|
case Surface.ROTATION_270:
|
||||||
|
x = event.values[1];
|
||||||
|
y = -event.values[0];
|
||||||
|
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED;
|
||||||
|
break;
|
||||||
|
case Surface.ROTATION_180:
|
||||||
|
x = -event.values[0];
|
||||||
|
y = -event.values[1];
|
||||||
|
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED;
|
||||||
|
break;
|
||||||
|
case Surface.ROTATION_0:
|
||||||
|
default:
|
||||||
|
x = event.values[0];
|
||||||
|
y = event.values[1];
|
||||||
|
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newOrientation != SDLActivity.mCurrentOrientation) {
|
||||||
|
SDLActivity.mCurrentOrientation = newOrientation;
|
||||||
|
SDLActivity.onNativeOrientationChanged(newOrientation);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
|
||||||
|
y / SensorManager.GRAVITY_EARTH,
|
||||||
|
event.values[2] / SensorManager.GRAVITY_EARTH);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Captured pointer events for API 26.
|
||||||
|
public boolean onCapturedPointerEvent(MotionEvent event)
|
||||||
|
{
|
||||||
|
int action = event.getActionMasked();
|
||||||
|
|
||||||
|
float x, y;
|
||||||
|
switch (action) {
|
||||||
|
case MotionEvent.ACTION_SCROLL:
|
||||||
|
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
|
||||||
|
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
|
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_HOVER_MOVE:
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
x = event.getX(0);
|
||||||
|
y = event.getY(0);
|
||||||
|
SDLActivity.onNativeMouse(0, action, x, y, true);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_BUTTON_PRESS:
|
||||||
|
case MotionEvent.ACTION_BUTTON_RELEASE:
|
||||||
|
|
||||||
|
// Change our action value to what SDL's code expects.
|
||||||
|
if (action == MotionEvent.ACTION_BUTTON_PRESS) {
|
||||||
|
action = MotionEvent.ACTION_DOWN;
|
||||||
|
} else { /* MotionEvent.ACTION_BUTTON_RELEASE */
|
||||||
|
action = MotionEvent.ACTION_UP;
|
||||||
|
}
|
||||||
|
|
||||||
|
x = event.getX(0);
|
||||||
|
y = event.getY(0);
|
||||||
|
int button = event.getButtonState();
|
||||||
|
|
||||||
|
SDLActivity.onNativeMouse(button, action, x, y, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
5
Android/app/src/main/res/anim/slide_in_bottom.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:duration="@android:integer/config_mediumAnimTime"
|
||||||
|
android:fromYDelta="100%p"
|
||||||
|
android:toYDelta="0%p" />
|
5
Android/app/src/main/res/anim/slide_in_top.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:duration="@android:integer/config_mediumAnimTime"
|
||||||
|
android:fromYDelta="-100%p"
|
||||||
|
android:toYDelta="0%p" />
|
5
Android/app/src/main/res/anim/slide_out_bottom.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:duration="@android:integer/config_mediumAnimTime"
|
||||||
|
android:fromYDelta="0%p"
|
||||||
|
android:toYDelta="100%p" />
|
5
Android/app/src/main/res/anim/slide_out_top.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:duration="@android:integer/config_mediumAnimTime"
|
||||||
|
android:fromYDelta="0%p"
|
||||||
|
android:toYDelta="-100%p" />
|
@ -1,6 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:shape="rectangle">
|
android:shape="rectangle">
|
||||||
<solid android:color="?attr/colorBackgroundFloating" />
|
<solid android:color="@color/dark" />
|
||||||
<corners android:radius="12dp" />
|
<corners
|
||||||
|
android:topLeftRadius="5dp"
|
||||||
|
android:topRightRadius="5dp" />
|
||||||
</shape>
|
</shape>
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
Android/app/src/main/res/drawable/bg_common.9.png
Normal file
After Width: | Height: | Size: 473 B |
@ -1,6 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:shape="rectangle">
|
android:shape="rectangle">
|
||||||
<solid android:color="@color/not_white" />
|
<solid android:color="@color/light" />
|
||||||
<corners android:radius="12dp" />
|
<corners
|
||||||
|
android:topLeftRadius="5dp"
|
||||||
|
android:topRightRadius="5dp" />
|
||||||
</shape>
|
</shape>
|
5
Android/app/src/main/res/drawable/btn_green.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/green_pressed" android:state_pressed="true" />
|
||||||
|
<item android:drawable="@drawable/green" />
|
||||||
|
</selector>
|
5
Android/app/src/main/res/drawable/btn_red.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/red_pressed" android:state_pressed="true" />
|
||||||
|
<item android:drawable="@drawable/red" />
|
||||||
|
</selector>
|
5
Android/app/src/main/res/drawable/btn_yellow.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/yellow_pressed" android:state_pressed="true" />
|
||||||
|
<item android:drawable="@drawable/yellow" />
|
||||||
|
</selector>
|
BIN
Android/app/src/main/res/drawable/green.9.png
Normal file
After Width: | Height: | Size: 383 B |
BIN
Android/app/src/main/res/drawable/green_pressed.9.png
Normal file
After Width: | Height: | Size: 383 B |
11
Android/app/src/main/res/drawable/ic_baseline_send.xml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="36dp"
|
||||||
|
android:height="36dp"
|
||||||
|
android:autoMirrored="true"
|
||||||
|
android:tint="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/green_mc"
|
||||||
|
android:pathData="M3.4,20.4l17.45,-7.48c0.81,-0.35 0.81,-1.49 0,-1.84L3.4,3.6c-0.66,-0.29 -1.39,0.2 -1.39,0.91L2,9.12c0,0.5 0.37,0.93 0.87,0.99L17,12 2.87,13.88c-0.5,0.07 -0.87,0.5 -0.87,1l0.01,4.61c0,0.71 0.73,1.2 1.39,0.91z" />
|
||||||
|
</vector>
|
28
Android/app/src/main/res/drawable/loading.xml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:oneshot="false">
|
||||||
|
<item
|
||||||
|
android:drawable="@drawable/loading_anim1"
|
||||||
|
android:duration="125" />
|
||||||
|
<item
|
||||||
|
android:drawable="@drawable/loading_anim2"
|
||||||
|
android:duration="125" />
|
||||||
|
<item
|
||||||
|
android:drawable="@drawable/loading_anim3"
|
||||||
|
android:duration="125" />
|
||||||
|
<item
|
||||||
|
android:drawable="@drawable/loading_anim4"
|
||||||
|
android:duration="125" />
|
||||||
|
<item
|
||||||
|
android:drawable="@drawable/loading_anim5"
|
||||||
|
android:duration="125" />
|
||||||
|
<item
|
||||||
|
android:drawable="@drawable/loading_anim6"
|
||||||
|
android:duration="125" />
|
||||||
|
<item
|
||||||
|
android:drawable="@drawable/loading_anim7"
|
||||||
|
android:duration="125" />
|
||||||
|
<item
|
||||||
|
android:drawable="@drawable/loading_anim8"
|
||||||
|
android:duration="125" />
|
||||||
|
</animation-list>
|
BIN
Android/app/src/main/res/drawable/loading_anim1.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
Android/app/src/main/res/drawable/loading_anim2.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
Android/app/src/main/res/drawable/loading_anim3.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
Android/app/src/main/res/drawable/loading_anim4.png
Normal file
After Width: | Height: | Size: 1021 B |
BIN
Android/app/src/main/res/drawable/loading_anim5.png
Normal file
After Width: | Height: | Size: 1023 B |
BIN
Android/app/src/main/res/drawable/loading_anim6.png
Normal file
After Width: | Height: | Size: 1015 B |
BIN
Android/app/src/main/res/drawable/loading_anim7.png
Normal file
After Width: | Height: | Size: 1016 B |
BIN
Android/app/src/main/res/drawable/loading_anim8.png
Normal file
After Width: | Height: | Size: 1021 B |
BIN
Android/app/src/main/res/drawable/red.9.png
Normal file
After Width: | Height: | Size: 374 B |
BIN
Android/app/src/main/res/drawable/red_pressed.9.png
Normal file
After Width: | Height: | Size: 375 B |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 134 B After Width: | Height: | Size: 134 B |
BIN
Android/app/src/main/res/drawable/yellow.9.png
Normal file
After Width: | Height: | Size: 375 B |
BIN
Android/app/src/main/res/drawable/yellow_pressed.9.png
Normal file
After Width: | Height: | Size: 375 B |
BIN
Android/app/src/main/res/font/multicraftfont.ttf
Normal file
22
Android/app/src/main/res/layout/activity_main.xml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/loading_anim"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:srcCompat="@drawable/loading" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/tv_progress"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/preparing"
|
||||||
|
android:textColor="@color/light"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
</LinearLayout>
|
78
Android/app/src/main/res/layout/connection_dialog.xml
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:baselineAligned="false"
|
||||||
|
android:gravity="center"
|
||||||
|
android:weightSum="1">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/conn_root"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="0.7"
|
||||||
|
android:background="@drawable/bg_common"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
tools:ignore="UselessParent">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
style="?android:attr/textAppearanceLarge"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:drawableStart="@mipmap/ic_dialog"
|
||||||
|
android:drawablePadding="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/conn_title"
|
||||||
|
android:textColor="@color/grey_900" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:text="@string/conn_message"
|
||||||
|
android:textColor="@color/grey_900"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/ignore"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:layout_weight="0.33"
|
||||||
|
android:background="@drawable/btn_yellow"
|
||||||
|
android:text="@string/ignore"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="@android:color/white" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/mobile"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:layout_weight="0.33"
|
||||||
|
android:background="@drawable/btn_green"
|
||||||
|
android:text="@string/conn_mobile"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/wifi"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:layout_weight="0.33"
|
||||||
|
android:background="@drawable/btn_green"
|
||||||
|
android:text="@string/conn_wifi"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="@android:color/white" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
26
Android/app/src/main/res/layout/input_text.xml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/rl"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/input"
|
||||||
|
style="@style/TextInputLayoutStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:background="@drawable/bg_input"
|
||||||
|
android:hint="@string/input_text"
|
||||||
|
android:padding="4dp"
|
||||||
|
app:endIconDrawable="@drawable/ic_baseline_send"
|
||||||
|
app:endIconMode="custom"
|
||||||
|
app:endIconTint="@null"
|
||||||
|
app:hintTextColor="@color/green_mc">
|
||||||
|
|
||||||
|
<com.multicraft.game.CustomEditText
|
||||||
|
android:id="@+id/editText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="60dp" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
</RelativeLayout>
|
28
Android/app/src/main/res/layout/multiline_input.xml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/multiRl"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/multiInput"
|
||||||
|
style="@style/TextInputLayoutStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:background="@drawable/bg_input"
|
||||||
|
android:hint="@string/input_text"
|
||||||
|
android:padding="4dp"
|
||||||
|
app:endIconDrawable="@drawable/ic_baseline_send"
|
||||||
|
app:endIconMode="custom"
|
||||||
|
app:endIconTint="@null"
|
||||||
|
app:hintTextColor="@color/green_mc">
|
||||||
|
|
||||||
|
<com.multicraft.game.CustomEditText
|
||||||
|
android:id="@+id/multiEditText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textMultiLine"
|
||||||
|
android:maxLines="8" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
</RelativeLayout>
|
86
Android/app/src/main/res/layout/restart_dialog.xml
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:baselineAligned="false"
|
||||||
|
android:gravity="center"
|
||||||
|
android:weightSum="1">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/restart_root"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="0.7"
|
||||||
|
android:background="@drawable/bg_common"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
tools:ignore="UselessParent">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
style="?android:attr/textAppearanceLarge"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:drawableStart="@mipmap/ic_dialog"
|
||||||
|
android:drawablePadding="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/sorry"
|
||||||
|
android:textColor="@color/grey_900" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/restart"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingEnd="12dp"
|
||||||
|
android:paddingBottom="4dp"
|
||||||
|
app:srcCompat="@drawable/sad" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/error_desc"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:minHeight="128dp"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:text="@string/restart"
|
||||||
|
android:textColor="@color/grey_900"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/close"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:layout_weight="0.5"
|
||||||
|
android:background="@drawable/btn_red"
|
||||||
|
android:text="@string/close_game"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="@android:color/white" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/restart"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:layout_weight="0.5"
|
||||||
|
android:background="@drawable/btn_green"
|
||||||
|
android:text="@string/restart_game"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="@android:color/white" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
BIN
Android/app/src/main/res/mipmap/ic_dialog.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@ -3,19 +3,15 @@
|
|||||||
<!-- подготовка к запуску -->
|
<!-- подготовка к запуску -->
|
||||||
<string name="preparing">Подготовка к запуску…</string>
|
<string name="preparing">Подготовка к запуску…</string>
|
||||||
<string name="loading">Загрузка…</string>
|
<string name="loading">Загрузка…</string>
|
||||||
<string name="loadingp">Загрузка… %d%%</string>
|
|
||||||
<string name="notification_title">Загрузка MultiCraft</string>
|
<string name="notification_title">Загрузка MultiCraft</string>
|
||||||
<string name="notification_description">Осталось меньше минуты…</string>
|
<string name="notification_description">Осталось меньше минуты…</string>
|
||||||
|
<string name="input_text">Введите текст</string>
|
||||||
<string name="input_text">Введите Текст</string>
|
<string name="input_password">Введите пароль</string>
|
||||||
<string name="input_password">Пароль</string>
|
|
||||||
<string name="done">Готово</string>
|
|
||||||
|
|
||||||
<!-- диалог отсутствия подключения -->
|
<!-- диалог отсутствия подключения -->
|
||||||
<string name="conn_title">Нет Интернет Подключения!</string>
|
<string name="conn_title">Нет Интернет Подключения!</string>
|
||||||
<string name="conn_message">Для полноценной игры, MultiCraft требует подключение к Интернету.\nВ противном случае вам будет недоступно Обновление игры и режим Мультиплеера!</string>
|
<string name="conn_message">Для полноценной игры, MultiCraft требует подключение к Интернету.\nВ противном случае Обновление игры и режим Мультиплеера будут недоступны!</string>
|
||||||
<string name="conn_mobile">3G/4G</string>
|
<string name="ignore">Пропустить</string>
|
||||||
<string name="ignore">Игнорировать</string>
|
|
||||||
|
|
||||||
<!-- Crash -->
|
<!-- Crash -->
|
||||||
<string name="sorry">Нам очень жаль!</string>
|
<string name="sorry">Нам очень жаль!</string>
|
@ -1,14 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
<style name="CustomDialog" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||||
<item name="windowActionBar">false</item>
|
<item name="windowActionBar">false</item>
|
||||||
<item name="android:windowBackground">@drawable/bg</item>
|
<item name="android:windowBackground">@drawable/bg</item>
|
||||||
<item name="android:windowFullscreen">true</item>
|
<item name="android:windowFullscreen">true</item>
|
||||||
|
<item name="android:backgroundDimEnabled">true</item>
|
||||||
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="p">shortEdges</item>
|
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="p">shortEdges</item>
|
||||||
<item name="fontFamily">@font/multicraftfont</item>
|
<item name="fontFamily">@font/multicraftfont</item>
|
||||||
<item name="colorControlActivated">@color/green</item>
|
|
||||||
<item name="colorPrimary">@color/green</item>
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
13
Android/app/src/main/res/values-v27/styles.xml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<style name="CustomDialog" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||||
|
<item name="windowActionBar">false</item>
|
||||||
|
<item name="android:windowIsTranslucent">true</item>
|
||||||
|
<item name="android:windowBackground">@android:color/transparent</item>
|
||||||
|
<item name="android:windowFullscreen">true</item>
|
||||||
|
<item name="android:backgroundDimEnabled">true</item>
|
||||||
|
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="p">shortEdges</item>
|
||||||
|
<item name="fontFamily">@font/multicraftfont</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
6
Android/app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<resources>
|
||||||
|
<color name="light">#FAFAFA</color>
|
||||||
|
<color name="dark">#121212</color>
|
||||||
|
<color name="green_mc">#32783C</color>
|
||||||
|
<color name="grey_900">#212121</color>
|
||||||
|
</resources>
|
@ -1,24 +1,20 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name" translatable="false">MultiCraft</string>
|
<string name="app_name" translatable="false">MultiCraft</string>
|
||||||
|
|
||||||
<!-- preparation for start -->
|
<!-- preparation for start -->
|
||||||
<string name="preparing">Preparing to launch…</string>
|
<string name="preparing">Preparing to launch…</string>
|
||||||
<string name="loading">Loading…</string>
|
<string name="loading">Loading…</string>
|
||||||
<string name="loadingp">Loading… %d%%</string>
|
|
||||||
<string name="notification_title">Loading MultiCraft</string>
|
<string name="notification_title">Loading MultiCraft</string>
|
||||||
<string name="notification_description">Less than 1 minute…</string>
|
<string name="notification_description">Less than 1 minute…</string>
|
||||||
<string name="ok" translatable="false">OK</string>
|
|
||||||
|
|
||||||
<string name="input_text">Text Input</string>
|
<string name="input_text">Enter text</string>
|
||||||
<string name="input_password">Password</string>
|
<string name="input_password">Enter password</string>
|
||||||
<string name="done">Done</string>
|
|
||||||
|
|
||||||
<!-- no connection dialog -->
|
<!-- no connection dialog -->
|
||||||
<string name="conn_title">No Internet Connection!</string>
|
<string name="conn_title">No Internet Connection!</string>
|
||||||
<string name="conn_message">MultiCraft requires an Internet connection to use all game features.\nOtherwise, you will not get Updates and Multiplayer mode will be not available!</string>
|
<string name="conn_message">MultiCraft requires an Internet connection to use all game features.\nOtherwise, you will not get Updates and Multiplayer mode will be not available!</string>
|
||||||
<string name="conn_wifi" translatable="false">Wi-Fi</string>
|
<string name="conn_wifi" translatable="false">Wi-Fi</string>
|
||||||
<string name="conn_mobile">Mobile Data</string>
|
<string name="conn_mobile" translatable="false">LTE</string>
|
||||||
<string name="ignore">Ignore</string>
|
<string name="ignore">Ignore</string>
|
||||||
|
|
||||||
<!-- Crash -->
|
<!-- Crash -->
|
33
Android/app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
<item name="windowActionBar">false</item>
|
||||||
|
<item name="android:windowBackground">@drawable/bg</item>
|
||||||
|
<item name="android:windowFullscreen">true</item>
|
||||||
|
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="p">shortEdges</item>
|
||||||
|
<item name="fontFamily">@font/multicraftfont</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="CustomDialog" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||||
|
<item name="windowActionBar">false</item>
|
||||||
|
<item name="android:windowIsTranslucent">true</item>
|
||||||
|
<item name="android:windowBackground">@android:color/transparent</item>
|
||||||
|
<item name="android:windowFullscreen">true</item>
|
||||||
|
<item name="android:backgroundDimEnabled">true</item>
|
||||||
|
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="p">shortEdges</item>
|
||||||
|
<item name="fontFamily">@font/multicraftfont</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="FullScreenDialogStyle" parent="Theme.MaterialComponents.Light.Dialog">
|
||||||
|
<item name="android:windowFullscreen">true</item>
|
||||||
|
<item name="android:windowIsFloating">false</item>
|
||||||
|
<item name="android:backgroundDimEnabled">false</item>
|
||||||
|
<item name="android:windowBackground">@android:color/transparent</item>
|
||||||
|
<item name="android:windowEnterAnimation">@null</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="TextInputLayoutStyle" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||||
|
<item name="boxStrokeColor">@color/green_mc</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
8
Android/app/src/main/res/xml/data_extraction_rules.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<data-extraction-rules>
|
||||||
|
<cloud-backup>
|
||||||
|
<exclude
|
||||||
|
domain="sharedpref"
|
||||||
|
path="MultiCraftSettings.xml" />
|
||||||
|
</cloud-backup>
|
||||||
|
</data-extraction-rules>
|
@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
project.ext.set("versionMajor", 2) // Version Major
|
project.ext.set("versionMajor", 2) // Version Major
|
||||||
project.ext.set("versionMinor", 0) // Version Minor
|
project.ext.set("versionMinor", 0) // Version Minor
|
||||||
project.ext.set("versionPatch", 1) // Version Patch
|
project.ext.set("versionPatch", 6) // Version Patch
|
||||||
project.ext.set("versionExtra", "") // Version Extra
|
project.ext.set("versionExtra", "") // Version Extra
|
||||||
project.ext.set("versionCode", 100) // Android Version Code
|
project.ext.set("versionCode", 200) // Android Version Code
|
||||||
// NOTE: +2 after each release!
|
project.ext.set("developmentBuild", 0) // Whether it is a development build, or a release
|
||||||
// +1 for ARM and +1 for ARM64 APK's, because
|
// NOTE: +3 after each release!
|
||||||
|
// +1 for ARM, +1 for ARM64 and +1 for x86_64 APK's, because
|
||||||
// each APK must have a larger `versionCode` than the previous
|
// each APK must have a larger `versionCode` than the previous
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
@ -15,9 +16,10 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.1.2'
|
classpath 'com.android.tools.build:gradle:8.2.1'
|
||||||
|
//noinspection GradleDependency
|
||||||
classpath 'de.undercouch:gradle-download-task:4.1.2'
|
classpath 'de.undercouch:gradle-download-task:4.1.2'
|
||||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10'
|
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10'
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
}
|
}
|
||||||
@ -30,7 +32,7 @@ allprojects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task clean(type: Delete) {
|
tasks.register('clean', Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.layout.buildDirectory
|
||||||
delete 'native/deps'
|
delete 'native/deps'
|
||||||
}
|
}
|
14
Android/gradle.properties
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<#if isLowMemory>
|
||||||
|
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
<#else>
|
||||||
|
org.gradle.jvmargs=-Xmx16G -XX:MaxMetaspaceSize=8G -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
</#if>
|
||||||
|
org.gradle.daemon=true
|
||||||
|
org.gradle.parallel=true
|
||||||
|
org.gradle.parallel.threads=8
|
||||||
|
org.gradle.configureondemand=true
|
||||||
|
android.enableJetifier=false
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.defaults.buildfeatures.buildconfig=true
|
||||||
|
android.nonTransitiveRClass=false
|
||||||
|
android.nonFinalResIds=false
|
@ -1,6 +1,6 @@
|
|||||||
#Fri Feb 11 12:29:43 EET 2022
|
#Thu Oct 05 18:46:10 EEST 2023
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
|
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStorePath=wrapper/dists
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
240
Android/gradlew
vendored
Executable file
@ -0,0 +1,240 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
#
|
||||||
|
# https://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.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
37
build/android/gradlew.bat → Android/gradlew.bat
vendored
@ -14,7 +14,7 @@
|
|||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%" == "" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem Gradle startup script for Windows
|
||||||
@ -25,10 +25,13 @@
|
|||||||
if "%OS%"=="Windows_NT" setlocal
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
|||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if "%ERRORLEVEL%" == "0" goto init
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
@ -51,7 +54,7 @@ goto fail
|
|||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto init
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
@ -61,38 +64,26 @@ echo location of your Java installation.
|
|||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:init
|
|
||||||
@rem Get command-line arguments, handling Windows variants
|
|
||||||
|
|
||||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
|
||||||
|
|
||||||
:win9xME_args
|
|
||||||
@rem Slurp the command line arguments.
|
|
||||||
set CMD_LINE_ARGS=
|
|
||||||
set _SKIP=2
|
|
||||||
|
|
||||||
:win9xME_args_slurp
|
|
||||||
if "x%~1" == "x" goto execute
|
|
||||||
|
|
||||||
set CMD_LINE_ARGS=%*
|
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
:fail
|
:fail
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
rem the _cmd.exe /c_ return code!
|
rem the _cmd.exe /c_ return code!
|
||||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
exit /b 1
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
:mainEnd
|
:mainEnd
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
if "%OS%"=="Windows_NT" endlocal
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |