mirror of
https://github.com/AngelAuraMC/Amethyst-Android.git
synced 2026-04-18 08:36:56 -04:00
Merge branch 'v3_openjdk' into l10n_v3_openjdk
Resolving conflict Signed-off-by: alexytomi <60690056+alexytomi@users.noreply.github.com>
This commit is contained in:
13
.github/workflows/android.yml
vendored
13
.github/workflows/android.yml
vendored
@@ -28,12 +28,6 @@ jobs:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
|
||||
- name: Get LTW
|
||||
run: |
|
||||
apt update && apt install wget
|
||||
cd app_pojavlauncher/libs
|
||||
wget https://github.com/AngelAuraMC/LTW/releases/latest/download/ltw-release.aar
|
||||
|
||||
- name: Get JRE 8
|
||||
uses: dawidd6/action-download-artifact@v9
|
||||
with:
|
||||
@@ -66,6 +60,7 @@ jobs:
|
||||
|
||||
- uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
validate-wrappers: false
|
||||
gradle-version: "8.11"
|
||||
|
||||
- name: Build JRE JAR files
|
||||
@@ -76,12 +71,6 @@ jobs:
|
||||
# Build JRE JAR files (security manager, etc...)
|
||||
gradle :jre_lwjgl3glfw:build --no-daemon
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Build Google Play .aab
|
||||
if: github.repository_owner == 'PojavLauncherTeam' && github.ref_name == 'v3_openjdk'
|
||||
run: |
|
||||
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -1,3 +1,9 @@
|
||||
[submodule "MobileGlues"]
|
||||
path = MobileGlues
|
||||
url = https://github.com/MobileGL-Dev/MobileGlues.git
|
||||
[submodule "SDL"]
|
||||
path = app_pojavlauncher/src/main/jni/SDL
|
||||
url = https://github.com/libsdl-org/SDL.git
|
||||
[submodule "sdl2-compat"]
|
||||
path = app_pojavlauncher/src/main/jni/sdl2-compat
|
||||
url = https://github.com/libsdl-org/sdl2-compat.git
|
||||
|
||||
11
README.md
11
README.md
@@ -4,7 +4,7 @@
|
||||
|
||||
[](https://github.com/AngelAuraMC/Amethyst-Android/actions)
|
||||
[](https://github.com/AngelAuraMC/Amethyst-Android/actions)
|
||||
[](https://crowdin.com/project/amethyst)
|
||||
[](https://crowdin.com/project/pojavlauncher)
|
||||
[](https://discord.gg/5ptqkyZxEy)
|
||||
|
||||
*From [Boardwalk](https://github.com/zhuowei/Boardwalk)'s ashes and [PojavLauncher](https://github.com/PojavLauncherTeam/PojavLauncher)'s ruined reputation, here comes Amethyst!*
|
||||
@@ -40,7 +40,7 @@ For more details, check out our [wiki](https://angelauramc.dev/wiki)!
|
||||
|
||||
You can get Amethyst via two methods:
|
||||
|
||||
1. **Releases:** Download the prebuilt app from our [stable releases](https://github.com/AngelAuraMC/Amethyst-Android/releases) or [automatic builds](https://github.com/AngelAuraMC/Amethyst-Android/actions).
|
||||
1. **Releases:** Download the latest prebuilt app from [nightly.link](https://nightly.link/AngelAuraMC/Amethyst-Android/workflows/android/v3_openjdk/app-debug.zip) or select an older version from our [automatic builds](https://github.com/AngelAuraMC/Amethyst-Android/actions).
|
||||
2. **Build from Source:** Follow the [building instructions](#building) below.
|
||||
|
||||
## Building
|
||||
@@ -123,7 +123,7 @@ PojavLauncher is licensed under [GNU LGPLv3](https://github.com/AngelAuraMC/Amet
|
||||
* Android Support Libraries: [Apache License 2.0](https://android.googlesource.com/platform/prebuilts/maven_repo/android/+/master/NOTICE.txt).
|
||||
* [GL4ES](https://github.com/AngelAuraMC/gl4es): [MIT License](https://github.com/ptitSeb/gl4es/blob/master/LICENSE).
|
||||
* [MobileGlues](https://github.com/MobileGL-Dev/MobileGlues): [LGPL-2.1 License](https://github.com/MobileGL-Dev/MobileGlues/blob/dev-es/LICENSE).
|
||||
* [ANGLE](https://chromium.googlesource.com/angle/angle): [All Rights Reserved](app_pojavlauncher/src/main/jniLibs/ANGLE_LICENSE).
|
||||
* [ANGLE](https://chromium.googlesource.com/angle/angle): [All Rights Reserved](app_pojavlauncher/src/main/assets/licenses/ANGLE_LICENSE).
|
||||
* [OpenJDK](https://github.com/AngelAuraMC/openjdk-multiarch-jdk8u): [GNU GPLv2 License](https://openjdk.java.net/legal/gplv2+ce.html).
|
||||
* [LWJGL3](https://github.com/AngelAuraMC/lwjgl3): [BSD-3 License](https://github.com/LWJGL/lwjgl3/blob/master/LICENSE.md).
|
||||
* [LWJGLX](https://github.com/AngelAuraMC/lwjglx) (LWJGL2 API compatibility layer for LWJGL3): unknown license.
|
||||
@@ -132,6 +132,11 @@ PojavLauncher is licensed under [GNU LGPLv3](https://github.com/AngelAuraMC/Amet
|
||||
* [bhook](https://github.com/bytedance/bhook) (Used for exit code trapping): [MIT license](https://github.com/bytedance/bhook/blob/main/LICENSE).
|
||||
* [libepoxy](https://github.com/anholt/libepoxy): [MIT License](https://github.com/anholt/libepoxy/blob/master/COPYING).
|
||||
* [virglrenderer](https://github.com/AngelAuraMC/virglrenderer): [MIT License](https://gitlab.freedesktop.org/virgl/virglrenderer/-/blob/master/COPYING).
|
||||
* [OpenAL-Soft](https://github.com/kcat/openal-soft): [GNU GPLv2](app_pojavlauncher/src/main/assets/licenses/OPENAL-SOFT_GPL2)
|
||||
* [oboe](https://github.com/google/oboe): [Apache License 2.0](app_pojavlauncher/src/main/assets/licenses/OBOE_APACHE2).
|
||||
* [pfffft](https://bitbucket.org/jpommier/pffft/src/master/): [ARR](app_pojavlauncher/src/main/assets/licenses/PFFFT_LICENSE)
|
||||
* [SDL3](https://github.com/libsdl-org/SDL): [zlib License](https://github.com/libsdl-org/SDL/blob/main/LICENSE.txt)
|
||||
* [sdl2-compat](https://github.com/libsdl-org/sdl2-compat): [zlib License](https://github.com/libsdl-org/sdl2-compat/blob/main/LICENSE.txt)
|
||||
* Thanks to [MCHeads](https://mc-heads.net) for providing Minecraft avatars.
|
||||
|
||||
## Roadmap
|
||||
|
||||
@@ -228,6 +228,8 @@ dependencies {
|
||||
|
||||
// implementation 'net.sourceforge.streamsupport:streamsupport-cfuture:1.7.0'
|
||||
|
||||
implementation 'top.fifthlight.touchcontroller:proxy-client-android:0.0.4'
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
|
||||
implementation project(":MobileGlues")
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<application
|
||||
android:name=".PojavApplication"
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
53f2f037cef9b7447a6abbdf82150ca8a2f2a587
|
||||
2016eba00f043842122d7aecb9410cf9371a7693
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
4903cfc8d3afd63918f59caf0a146efc2d837069
|
||||
02e09ed8760774bfa1222432ca2ad25b70b191d7
|
||||
202
app_pojavlauncher/src/main/assets/licenses/OBOE_APACHE2
Normal file
202
app_pojavlauncher/src/main/assets/licenses/OBOE_APACHE2
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
437
app_pojavlauncher/src/main/assets/licenses/OPENAL-SOFT_GPL2
Normal file
437
app_pojavlauncher/src/main/assets/licenses/OPENAL-SOFT_GPL2
Normal file
@@ -0,0 +1,437 @@
|
||||
GNU LIBRARY GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1991 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the library GPL. It is
|
||||
numbered 2 because it goes with version 2 of the ordinary GPL.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Library General Public License, applies to some
|
||||
specially designated Free Software Foundation software, and to any
|
||||
other libraries whose authors decide to use it. You can use it for
|
||||
your libraries, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if
|
||||
you distribute copies of the library, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link a program with the library, you must provide
|
||||
complete object files to the recipients so that they can relink them
|
||||
with the library, after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
Our method of protecting your rights has two steps: (1) copyright
|
||||
the library, and (2) offer you this license which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
Also, for each distributor's protection, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
library. If the library is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original
|
||||
version, so that any problems introduced by others will not reflect on
|
||||
the original authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that companies distributing free
|
||||
software will individually obtain patent licenses, thus in effect
|
||||
transforming the program into proprietary software. To prevent this,
|
||||
we have made it clear that any patent must be licensed for everyone's
|
||||
free use or not licensed at all.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the ordinary
|
||||
GNU General Public License, which was designed for utility programs. This
|
||||
license, the GNU Library General Public License, applies to certain
|
||||
designated libraries. This license is quite different from the ordinary
|
||||
one; be sure to read it in full, and don't assume that anything in it is
|
||||
the same as in the ordinary license.
|
||||
|
||||
The reason we have a separate public license for some libraries is that
|
||||
they blur the distinction we usually make between modifying or adding to a
|
||||
program and simply using it. Linking a program with a library, without
|
||||
changing the library, is in some sense simply using the library, and is
|
||||
analogous to running a utility program or application program. However, in
|
||||
a textual and legal sense, the linked executable is a combined work, a
|
||||
derivative of the original library, and the ordinary General Public License
|
||||
treats it as such.
|
||||
|
||||
Because of this blurred distinction, using the ordinary General
|
||||
Public License for libraries did not effectively promote software
|
||||
sharing, because most developers did not use the libraries. We
|
||||
concluded that weaker conditions might promote sharing better.
|
||||
|
||||
However, unrestricted linking of non-free programs would deprive the
|
||||
users of those programs of all benefit from the free status of the
|
||||
libraries themselves. This Library General Public License is intended to
|
||||
permit developers of non-free programs to use free libraries, while
|
||||
preserving your freedom as a user of such programs to change the free
|
||||
libraries that are incorporated in them. (We have not seen how to achieve
|
||||
this as regards changes in header files, but we have achieved it as regards
|
||||
changes in the actual functions of the Library.) The hope is that this
|
||||
will lead to faster development of free libraries.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, while the latter only
|
||||
works together with the library.
|
||||
|
||||
Note that it is possible for a library to be covered by the ordinary
|
||||
General Public License rather than by this special one.
|
||||
|
||||
GNU LIBRARY GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library which
|
||||
contains a notice placed by the copyright holder or other authorized
|
||||
party saying it may be distributed under the terms of this Library
|
||||
General Public License (also called "this License"). Each licensee is
|
||||
addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a
|
||||
table of data to be supplied by an application program that uses
|
||||
the facility, other than as an argument passed when the facility
|
||||
is invoked, then you must make a good faith effort to ensure that,
|
||||
in the event an application does not supply such function or
|
||||
table, the facility still operates, and performs whatever part of
|
||||
its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||
License instead of this License to a given copy of the Library. To do
|
||||
this, you must alter all the notices that refer to this License, so
|
||||
that they refer to the ordinary GNU General Public License, version 2,
|
||||
instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of
|
||||
the Library into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the
|
||||
source code from the same place satisfies the requirement to
|
||||
distribute the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a
|
||||
work, in isolation, is not a derivative work of the Library, and
|
||||
therefore falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License.
|
||||
Section 6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data
|
||||
structure layouts and accessors, and small macros and small inline
|
||||
functions (ten lines or less in length), then the use of the object
|
||||
file is unrestricted, regardless of whether it is legally a derivative
|
||||
work. (Executables containing this object code plus portions of the
|
||||
Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also compile or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
modification of the work for the customer's own use and reverse
|
||||
engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood
|
||||
that the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
c) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
d) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the source code distributed need not include anything that is normally
|
||||
distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities. This must be distributed under the terms of the
|
||||
Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the
|
||||
Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Library at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Library by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License may add
|
||||
an explicit geographical distribution limitation excluding those countries,
|
||||
so that distribution is permitted only in or among countries not thus
|
||||
excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Library General Public License from time to time.
|
||||
Such new versions will be similar in spirit to the present version,
|
||||
but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
38
app_pojavlauncher/src/main/assets/licenses/PFFFT_LICENSE
Normal file
38
app_pojavlauncher/src/main/assets/licenses/PFFFT_LICENSE
Normal file
@@ -0,0 +1,38 @@
|
||||
A modified PFFFT is included, with the following license.
|
||||
|
||||
Copyright (c) 2023 Christopher Robinson
|
||||
|
||||
Copyright (c) 2013 Julien Pommier ( pommier@modartt.com )
|
||||
|
||||
Copyright (c) 2004 the University Corporation for Atmospheric
|
||||
Research ("UCAR"). All rights reserved. Developed by NCAR's
|
||||
Computational and Information Systems Laboratory, UCAR,
|
||||
www.cisl.ucar.edu.
|
||||
|
||||
Redistribution and use of the Software in source and binary forms,
|
||||
with or without modification, is permitted provided that the
|
||||
following conditions are met:
|
||||
|
||||
- Neither the names of NCAR's Computational and Information Systems
|
||||
Laboratory, the University Corporation for Atmospheric Research,
|
||||
nor the names of its sponsors or contributors may be used to
|
||||
endorse or promote products derived from this Software without
|
||||
specific prior written permission.
|
||||
|
||||
- Redistributions of source code must retain the above copyright
|
||||
notices, this list of conditions, and the disclaimer below.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions, and the disclaimer below in the
|
||||
documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
|
||||
SOFTWARE.
|
||||
@@ -60,11 +60,10 @@ public class LoggerView extends ConstraintLayout {
|
||||
(compoundButton, isChecked) -> {
|
||||
mLogTextView.setVisibility(isChecked ? VISIBLE : GONE);
|
||||
if(isChecked) {
|
||||
Logger.setLogListener(mLogListener);
|
||||
Logger.addLogListener(mLogListener);
|
||||
}else{
|
||||
mLogTextView.setText("");
|
||||
Logger.setLogListener(null); // Makes the JNI code be able to skip expensive logger callbacks
|
||||
// NOTE: was tested by rapidly smashing the log on/off button, no sync issues found :)
|
||||
Logger.removeLogListener(mLogListener);
|
||||
}
|
||||
});
|
||||
mLogToggle.setChecked(false);
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.Uri;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
@@ -47,6 +48,7 @@ import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import fr.spse.extended_view.ExtendedTextView;
|
||||
|
||||
@@ -276,6 +278,10 @@ public class mcAccountSpinner extends AppCompatSpinner implements AdapterView.On
|
||||
}
|
||||
|
||||
private void performLogin(MinecraftAccount minecraftAccount){
|
||||
// Logging in when there's no internet is useless. This should really be turned into a network callback though.
|
||||
if(!Tools.isOnline(getContext())){
|
||||
return;
|
||||
}
|
||||
if(minecraftAccount.isLocal()) return;
|
||||
|
||||
mLoginBarPaint.setColor(getResources().getColor(R.color.minebutton_color));
|
||||
@@ -296,14 +302,23 @@ public class mcAccountSpinner extends AppCompatSpinner implements AdapterView.On
|
||||
PojavProfile.setCurrentProfile(getContext(), mAccountList.get(position));
|
||||
selectedAccount = PojavProfile.getCurrentProfileContent(getContext(), mAccountList.get(position));
|
||||
|
||||
|
||||
// WORKAROUND
|
||||
// Account file corrupted due to previous versions having improper encoding
|
||||
if (selectedAccount == null){
|
||||
removeCurrentAccount();
|
||||
pickAccount(-1);
|
||||
setSelection(0);
|
||||
return;
|
||||
Context ctx = Objects.requireNonNull(getContext());
|
||||
|
||||
new AlertDialog.Builder(ctx)
|
||||
.setCancelable(false)
|
||||
.setTitle(R.string.account_corrupted)
|
||||
.setMessage(R.string.login_again)
|
||||
.setPositiveButton(R.string.delete_account_and_login, (dialog, which) -> {
|
||||
removeCurrentAccount();
|
||||
pickAccount(-1);
|
||||
setSelection(0);
|
||||
})
|
||||
.show();
|
||||
|
||||
|
||||
}
|
||||
setSelection(position);
|
||||
}else {
|
||||
|
||||
@@ -15,6 +15,7 @@ public class EfficientAndroidLWJGLKeycode {
|
||||
//The value its LWJGL equivalent.
|
||||
private static final int KEYCODE_COUNT = 106;
|
||||
private static final int[] sAndroidKeycodes = new int[KEYCODE_COUNT];
|
||||
private static final int[] sLwjglKeycodesReversed = new int[LwjglGlfwKeycode.GLFW_KEY_LAST];
|
||||
private static final short[] sLwjglKeycodes = new short[KEYCODE_COUNT];
|
||||
private static String[] androidKeyNameArray; /* = new String[androidKeycodes.length]; */
|
||||
private static int mTmpCount = 0;
|
||||
@@ -198,6 +199,28 @@ public class EfficientAndroidLWJGLKeycode {
|
||||
sendKeyPress(getValueByIndex(index));
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a GLFW keycode and returns its char primitive. Works with Shift/Caps Lock.
|
||||
* <p>
|
||||
* Non-letter characters return U+0000.
|
||||
*
|
||||
* @param lwjglGlfwKeycode A GLFW key code macro (e.g., {@link LwjglGlfwKeycode#GLFW_KEY_W}).
|
||||
*/
|
||||
public static char getLwjglChar(int lwjglGlfwKeycode){
|
||||
int androidKeycode = sAndroidKeycodes[sLwjglKeycodesReversed[lwjglGlfwKeycode]];
|
||||
KeyEvent key = new KeyEvent(KeyEvent.ACTION_UP, androidKeycode);
|
||||
char charToSend;
|
||||
charToSend = ((char) key.getUnicodeChar());
|
||||
int currentMods = CallbackBridge.getCurrentMods();
|
||||
if (Character.isLetter(charToSend) && (
|
||||
((currentMods & LwjglGlfwKeycode.GLFW_MOD_SHIFT) != 0) ^
|
||||
((currentMods & LwjglGlfwKeycode.GLFW_MOD_CAPS_LOCK) != 0))
|
||||
){
|
||||
charToSend = Character.toUpperCase(charToSend);
|
||||
}
|
||||
return charToSend;
|
||||
}
|
||||
|
||||
public static short getValueByIndex(int index) {
|
||||
return sLwjglKeycodes[index];
|
||||
}
|
||||
@@ -218,6 +241,7 @@ public class EfficientAndroidLWJGLKeycode {
|
||||
private static void add(int androidKeycode, short LWJGLKeycode){
|
||||
sAndroidKeycodes[mTmpCount] = androidKeycode;
|
||||
sLwjglKeycodes[mTmpCount] = LWJGLKeycode;
|
||||
sLwjglKeycodesReversed[LWJGLKeycode] = mTmpCount;
|
||||
mTmpCount ++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package net.kdt.pojavlaunch;
|
||||
|
||||
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
|
||||
import static net.kdt.pojavlaunch.Tools.hasNoOnlineProfileDialog;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
@@ -139,7 +141,7 @@ public class LauncherActivity extends BaseActivity {
|
||||
}
|
||||
|
||||
if (isOlderThan13) {
|
||||
Toast.makeText(this, R.string.toast_not_available_demo, Toast.LENGTH_LONG).show();
|
||||
hasNoOnlineProfileDialog(this, getString(R.string.global_error), getString(R.string.demo_versions_supported));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -164,7 +166,9 @@ public class LauncherActivity extends BaseActivity {
|
||||
};
|
||||
|
||||
private ActivityResultLauncher<String> mRequestNotificationPermissionLauncher;
|
||||
private ActivityResultLauncher<String> mRequestMicrophonePermissionLauncher;
|
||||
private WeakReference<Runnable> mRequestNotificationPermissionRunnable;
|
||||
private WeakReference<Runnable> mRequestMicrophonePermissionRunnable;
|
||||
|
||||
@Override
|
||||
protected boolean shouldIgnoreNotch() {
|
||||
@@ -204,6 +208,16 @@ public class LauncherActivity extends BaseActivity {
|
||||
}
|
||||
}
|
||||
);
|
||||
mRequestMicrophonePermissionLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.RequestPermission(),
|
||||
isAllowed -> {
|
||||
if(!isAllowed) handleNoNotificationPermission();
|
||||
else {
|
||||
Runnable runnable = Tools.getWeakReference(mRequestMicrophonePermissionRunnable);
|
||||
if(runnable != null) runnable.run();
|
||||
}
|
||||
}
|
||||
);
|
||||
getWindow().setBackgroundDrawable(null);
|
||||
bindViews();
|
||||
checkNotificationPermission();
|
||||
@@ -341,6 +355,11 @@ public class LauncherActivity extends BaseActivity {
|
||||
this,
|
||||
Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_DENIED;
|
||||
}
|
||||
public boolean checkForMicrophonePermission() {
|
||||
return ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_DENIED;
|
||||
}
|
||||
|
||||
public void askForNotificationPermission(Runnable onSuccessRunnable) {
|
||||
if(Build.VERSION.SDK_INT < 33) return;
|
||||
@@ -350,6 +369,13 @@ public class LauncherActivity extends BaseActivity {
|
||||
mRequestNotificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
|
||||
}
|
||||
|
||||
public void askForMicrophonePermission(Runnable onSuccessRunnable) {
|
||||
if(onSuccessRunnable != null) {
|
||||
mRequestMicrophonePermissionRunnable = new WeakReference<>(onSuccessRunnable);
|
||||
}
|
||||
mRequestMicrophonePermissionLauncher.launch(Manifest.permission.RECORD_AUDIO);
|
||||
}
|
||||
|
||||
/** Stuff all the view boilerplate here */
|
||||
private void bindViews(){
|
||||
mFragmentView = findViewById(R.id.container_fragment);
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
package net.kdt.pojavlaunch;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/** Singleton class made to log on one file
|
||||
* The singleton part can be removed but will require more implementation from the end-dev
|
||||
*/
|
||||
@Keep
|
||||
public class Logger {
|
||||
private static ArrayList<eventLogListener> logListeners;
|
||||
private static boolean nativeLogListenerSet = false;
|
||||
|
||||
/** Print the text to the log file if not censored */
|
||||
public static native void appendToLog(String text);
|
||||
|
||||
@@ -14,12 +21,39 @@ public class Logger {
|
||||
/** Reset the log file, effectively erasing any previous logs */
|
||||
public static native void begin(String logFilePath);
|
||||
|
||||
/** Small listener for anything listening to the log */
|
||||
/** Add a listener for the logfile, ask the native side for a listener if needed */
|
||||
public static void addLogListener(eventLogListener logListeners) {
|
||||
if (Logger.logListeners == null) Logger.logListeners = new ArrayList<>();
|
||||
Logger.logListeners.add(logListeners);
|
||||
if (Logger.nativeLogListenerSet) return;
|
||||
|
||||
setLogListener(text -> {
|
||||
for (Logger.eventLogListener logListener: Logger.logListeners) {
|
||||
logListener.onEventLogged(text);
|
||||
}
|
||||
});
|
||||
Logger.nativeLogListenerSet = true;
|
||||
}
|
||||
/** Remove a listener for the logfile, unset the native listener if no listeners left */
|
||||
public static void removeLogListener(eventLogListener logListener) {
|
||||
if (Logger.logListeners == null) return;
|
||||
Logger.logListeners.remove(logListener);
|
||||
if (Logger.logListeners.isEmpty()){
|
||||
// Makes the JNI code be able to skip expensive logger callbacks
|
||||
// NOTE: was tested by rapidly smashing the log on/off button, no sync issues found :)
|
||||
setLogListener(null);
|
||||
Logger.nativeLogListenerSet = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Small listener for anything listening to the log
|
||||
* Performs double duty as being the interface for java listeners and the native callback
|
||||
*/
|
||||
@Keep
|
||||
public interface eventLogListener {
|
||||
void onEventLogged(String text);
|
||||
}
|
||||
|
||||
/** Link a log listener to the logger */
|
||||
public static native void setLogListener(eventLogListener logListener);
|
||||
private static native void setLogListener(eventLogListener logListener);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package net.kdt.pojavlaunch;
|
||||
|
||||
import static net.kdt.pojavlaunch.Tools.currentDisplayMetrics;
|
||||
import static net.kdt.pojavlaunch.Tools.dialogForceClose;
|
||||
import static net.kdt.pojavlaunch.Tools.runMethodbyReflection;
|
||||
import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_ENABLE_GYRO;
|
||||
import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_SUSTAINED_PERFORMANCE;
|
||||
import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_USE_ALTERNATE_SURFACE;
|
||||
@@ -25,13 +26,11 @@ import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.util.Log;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
@@ -63,20 +62,26 @@ import net.kdt.pojavlaunch.prefs.QuickSettingSideDialog;
|
||||
import net.kdt.pojavlaunch.services.GameService;
|
||||
import net.kdt.pojavlaunch.utils.JREUtils;
|
||||
import net.kdt.pojavlaunch.utils.MCOptionUtils;
|
||||
import net.kdt.pojavlaunch.utils.TouchControllerUtils;
|
||||
import net.kdt.pojavlaunch.value.MinecraftAccount;
|
||||
import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles;
|
||||
import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile;
|
||||
|
||||
import org.libsdl.app.SDL;
|
||||
import org.libsdl.app.SDLSurface;
|
||||
import org.lwjgl.glfw.CallbackBridge;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
public class MainActivity extends BaseActivity implements ControlButtonMenuListener, EditorExitable, ServiceConnection {
|
||||
public static volatile ClipboardManager GLOBAL_CLIPBOARD;
|
||||
public static final String TAG = "MainActivity";
|
||||
public static final String INTENT_MINECRAFT_VERSION = "intent_version";
|
||||
|
||||
volatile public static boolean isInputStackCall;
|
||||
protected static View.OnGenericMotionListener motionListener = (v, event) -> false;
|
||||
|
||||
public static TouchCharInput touchCharInput;
|
||||
private MinecraftGLSurface minecraftGLView;
|
||||
@@ -102,8 +107,43 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (LauncherPreferences.PREF_GAMEPAD_SDL_PASSTHRU) {
|
||||
// TODO: Use lower level HID capture that needs a dialogue box from the user for the
|
||||
// app to fully take focus of the input devices. Might cause issues with older android
|
||||
// versions so we don't use that right now. Needs testing.
|
||||
// Currently tried but only identification works OOTB, inputs aren't being sent.
|
||||
|
||||
// TODO: Use a hook to load SDL logic depending on whether libSDL3.so is loaded.
|
||||
try {
|
||||
// Note: This doesn't dlopen it for the mod, they still have to do it themselves
|
||||
// Why? https://github.com/android/ndk/issues/201#issuecomment-248060092
|
||||
// Just in case that gets deleted off the internet:
|
||||
// "On Android only the main executable and LD_PRELOADs are considered to be
|
||||
// RTLD_GLOBAL, all the dependencies of the main executable remain RTLD_LOCAL." - dimitry
|
||||
SDL.loadLibrary("SDL3", this);
|
||||
SDL.loadLibrary("SDL2", this);
|
||||
SDL.initialize();
|
||||
SDL.setupJNI();
|
||||
SDL.setContext(this);
|
||||
new SDLSurface(this);
|
||||
motionListener = (View.OnGenericMotionListener)
|
||||
runMethodbyReflection("org.libsdl.app.SDLActivity",
|
||||
"getMotionListener");
|
||||
if (LauncherPreferences.PREF_GAMEPAD_FORCEDSDL_PASSTHRU) Tools.SDL.initializeControllerSubsystems();
|
||||
} catch (UnsatisfiedLinkError ignored) {
|
||||
// Ignore because if SDL.setupJNI(); fails, SDL wasn't loaded.
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Tools.showErrorRemote("SDL did not load properly.", e);
|
||||
}
|
||||
}
|
||||
|
||||
minecraftProfile = LauncherProfiles.getCurrentProfile();
|
||||
MCOptionUtils.load(Tools.getGameDirPath(minecraftProfile).getAbsolutePath());
|
||||
|
||||
String gameDirPath = Tools.getGameDirPath(minecraftProfile).getAbsolutePath();
|
||||
MCOptionUtils.load(gameDirPath);
|
||||
if (Tools.hasTouchController(new File(gameDirPath)) || LauncherPreferences.PREF_FORCE_ENABLE_TOUCHCONTROLLER) {
|
||||
TouchControllerUtils.initialize(this);
|
||||
}
|
||||
|
||||
Intent gameServiceIntent = new Intent(this, GameService.class);
|
||||
// Start the service a bit early
|
||||
@@ -283,6 +323,7 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe
|
||||
mQuickSettingSideDialog.cancel();
|
||||
}
|
||||
CallbackBridge.nativeSetWindowAttrib(LwjglGlfwKeycode.GLFW_HOVERED, 0);
|
||||
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@@ -309,7 +350,6 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
if(mGyroControl != null) mGyroControl.updateOrientation();
|
||||
// Layout resize is practically guaranteed on a configuration change, and `onConfigurationChanged`
|
||||
// does not implicitly start a layout. So, request a layout and expect the screen dimensions to be valid after the]
|
||||
@@ -349,9 +389,50 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe
|
||||
}
|
||||
|
||||
private void runCraft(String versionId, JMinecraftVersionList.Version version) throws Throwable {
|
||||
LauncherPreferences.writeMGRendererSettings(); // No MG detection for you
|
||||
if(Tools.LOCAL_RENDERER == null) {
|
||||
Tools.LOCAL_RENDERER = LauncherPreferences.PREF_RENDERER;
|
||||
String assetVersion;
|
||||
if (version.inheritsFrom != null) { // We are almost definitely modded if this runs
|
||||
File vanillaJsonFile = new File(Tools.DIR_HOME_VERSION + "/" + version.inheritsFrom + "/" + version.inheritsFrom + ".json");
|
||||
JMinecraftVersionList.Version vanillaJson;
|
||||
try { // Get the vanilla json from modded instance
|
||||
vanillaJson = Tools.GLOBAL_GSON.fromJson(Tools.read(vanillaJsonFile.getAbsolutePath()), JMinecraftVersionList.Version.class);
|
||||
} catch (IOException ignored) { // Should never happen, we check for this in MinecraftDownloader().start()
|
||||
throw new RuntimeException(getString(R.string.error_vanilla_json_corrupt));
|
||||
}
|
||||
// Something went wrong if this is somehow not the case anymore
|
||||
if (!Objects.equals(vanillaJson.assets, vanillaJson.assetIndex.id))
|
||||
Tools.showErrorRemote(new RuntimeException(getString(R.string.error_vanilla_json_corrupt)));
|
||||
assetVersion = vanillaJson.assets;
|
||||
} else {
|
||||
// Else assume we are vanilla
|
||||
if (!Objects.equals(version.assets, version.assetIndex.id))
|
||||
Tools.showErrorRemote(new RuntimeException(getString(R.string.error_vanilla_json_corrupt)));
|
||||
assetVersion = version.assets;
|
||||
}
|
||||
// Autoselect renderer
|
||||
if (Tools.LOCAL_RENDERER == null) {
|
||||
// 25w09a is when HolyGL4ES starts showing a black screen upon world load.
|
||||
// There is no way to consistently check for that without breaking mod loaders
|
||||
// for old versions like legacy fabric so we start from 25w07a instead
|
||||
|
||||
// 25w07a assets and assetIndex.id is set to 23, 25w08a and 25w09a is 24.
|
||||
|
||||
// 1.19.3 snapshots and all future versions restarted assets and assetsIndex.id
|
||||
// to 1 and started counting up from there
|
||||
|
||||
// Previous versions had "1.19" and "1.18" and such, with April Fools versions
|
||||
// being even more inconsistent like "3D Shareware v1.34" for the 2019 April Fools
|
||||
// or 1.RV-Pre1 for 2016, thankfully now they don't seem to do that anymore and just
|
||||
// use the incrementing system they now have
|
||||
|
||||
// I could probably read the manifest itself then check which position the `id` field is
|
||||
// and count from there since its ordered latest to oldest but that uses way more code
|
||||
// for basically 3 peoples benefit
|
||||
try {
|
||||
int assetID = Integer.parseInt(assetVersion);
|
||||
// Check if below 25w08a
|
||||
Tools.LOCAL_RENDERER = (assetID <= 23) ? "opengles2" : "opengles_mobileglues";
|
||||
// Then assume 1.19.2 and below
|
||||
} catch (NumberFormatException e) { Tools.LOCAL_RENDERER = "opengles2"; }
|
||||
}
|
||||
if(!Tools.checkRendererCompatible(this, Tools.LOCAL_RENDERER)) {
|
||||
Tools.RenderersList renderersList = Tools.getCompatibleRenderers(this);
|
||||
@@ -360,9 +441,22 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe
|
||||
Tools.LOCAL_RENDERER = firstCompatibleRenderer;
|
||||
Tools.releaseRenderersCache();
|
||||
}
|
||||
|
||||
// MCL-3732 Mitigation
|
||||
// I don't trust the bug tracker. 'server-resource-pack" was removed in 1.20.3-pre3
|
||||
// so we use 12 to detect that. We still generate till 1.20.5 else we don't cover
|
||||
// 1.20.3-pre2 and such. Better to over than to under.
|
||||
File folder = new File(Tools.getGameDirPath(minecraftProfile), "server-resource-pack");
|
||||
try {
|
||||
if (Integer.parseInt(assetVersion) <= 12) folder.mkdir();
|
||||
} catch (NumberFormatException e) { folder.mkdir(); }
|
||||
|
||||
MinecraftAccount minecraftAccount = PojavProfile.getCurrentProfileContent(this, null);
|
||||
Logger.appendToLog("--------- Starting game with Launcher Debug!");
|
||||
Tools.printLauncherInfo(versionId, Tools.isValidString(minecraftProfile.javaArgs) ? minecraftProfile.javaArgs : LauncherPreferences.PREF_CUSTOM_JAVA_ARGS);
|
||||
if(Tools.LOCAL_RENDERER.equals("opengles_mobileglues")) {
|
||||
LauncherPreferences.writeMGRendererSettings();
|
||||
}
|
||||
JREUtils.redirectAndPrintJRELog();
|
||||
LauncherProfiles.load();
|
||||
int requiredJavaVersion = 8;
|
||||
@@ -576,4 +670,22 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe
|
||||
return minecraftGLView.dispatchCapturedPointerEvent(ev);
|
||||
else return super.dispatchTrackballEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowFocusChanged(boolean hasFocus) {
|
||||
if (hasFocus) {
|
||||
Tools.setFullscreen(this, setFullscreen());
|
||||
}
|
||||
super.onWindowFocusChanged(hasFocus);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrimMemory(int level) {
|
||||
super.onTrimMemory(level);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.kdt.pojavlaunch;
|
||||
|
||||
import static net.kdt.pojavlaunch.MainActivity.touchCharInput;
|
||||
import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_MOUSE_GRAB_FORCE;
|
||||
import static net.kdt.pojavlaunch.utils.MCOptionUtils.getMcScale;
|
||||
import static org.lwjgl.glfw.CallbackBridge.sendMouseButton;
|
||||
import static org.lwjgl.glfw.CallbackBridge.windowHeight;
|
||||
@@ -39,9 +40,13 @@ import net.kdt.pojavlaunch.customcontrols.mouse.TouchEventProcessor;
|
||||
import net.kdt.pojavlaunch.prefs.LauncherPreferences;
|
||||
import net.kdt.pojavlaunch.utils.JREUtils;
|
||||
import net.kdt.pojavlaunch.utils.MCOptionUtils;
|
||||
import net.kdt.pojavlaunch.utils.TouchControllerUtils;
|
||||
|
||||
import org.libsdl.app.SDLActivity;
|
||||
import org.libsdl.app.SDLControllerManager;
|
||||
import org.lwjgl.glfw.CallbackBridge;
|
||||
|
||||
|
||||
import fr.spse.gamepad_remapper.GamepadHandler;
|
||||
import fr.spse.gamepad_remapper.RemapperManager;
|
||||
import fr.spse.gamepad_remapper.RemapperView;
|
||||
@@ -77,12 +82,14 @@ public class MinecraftGLSurface extends View implements GrabListener, DirectGame
|
||||
final Object mSurfaceReadyListenerLock = new Object();
|
||||
/* View holding the surface, either a SurfaceView or a TextureView */
|
||||
View mSurface;
|
||||
String TAG = "MinecraftGLSurface";
|
||||
|
||||
private final InGameEventProcessor mIngameProcessor = new InGameEventProcessor(mSensitivityFactor);
|
||||
private final InGUIEventProcessor mInGUIProcessor = new InGUIEventProcessor();
|
||||
private TouchEventProcessor mCurrentTouchProcessor = mInGUIProcessor;
|
||||
private AndroidPointerCapture mPointerCapture;
|
||||
private boolean mLastGrabState = false;
|
||||
public static boolean sdlEnabled = false;
|
||||
|
||||
public MinecraftGLSurface(Context context) {
|
||||
this(context, null);
|
||||
@@ -92,6 +99,7 @@ public class MinecraftGLSurface extends View implements GrabListener, DirectGame
|
||||
super(context, attributeSet);
|
||||
setFocusable(true);
|
||||
CallbackBridge.setDirectGamepadEnableHandler(this);
|
||||
SDLControllerManager.setDirectGamepadEnableHandler(this);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@@ -192,6 +200,13 @@ public class MinecraftGLSurface extends View implements GrabListener, DirectGame
|
||||
if(toolType == MotionEvent.TOOL_TYPE_MOUSE) {
|
||||
if(Tools.isAndroid8OrHigher() &&
|
||||
mPointerCapture != null) {
|
||||
// Can't handleAutomaticCapture if mouse isn't captured
|
||||
if (!CallbackBridge.isGrabbing() // Only capture if not in menu and user said so
|
||||
&& !PREF_MOUSE_GRAB_FORCE) {
|
||||
// This returns true but we really can't consume this.
|
||||
// Else we don't receive ACTION_MOVE
|
||||
return !dispatchGenericMotionEvent(e);
|
||||
}
|
||||
mPointerCapture.handleAutomaticCapture();
|
||||
return true;
|
||||
}
|
||||
@@ -202,16 +217,17 @@ public class MinecraftGLSurface extends View implements GrabListener, DirectGame
|
||||
CallbackBridge.sendCursorPos( e.getX(i) * LauncherPreferences.PREF_SCALE_FACTOR, e.getY(i) * LauncherPreferences.PREF_SCALE_FACTOR);
|
||||
return true; //mouse event handled successfully
|
||||
}
|
||||
TouchControllerUtils.processTouchEvent(e, this);
|
||||
if (mIngameProcessor == null || mInGUIProcessor == null) return true;
|
||||
return mCurrentTouchProcessor.processTouchEvent(e);
|
||||
}
|
||||
|
||||
private void createGamepad(View contextView, InputDevice inputDevice) {
|
||||
if(CallbackBridge.sGamepadDirectInput) {
|
||||
if(CallbackBridge.sGamepadDirectInput && !sdlEnabled) {
|
||||
mGamepadHandler = new DirectGamepad();
|
||||
}else {
|
||||
}else if(!sdlEnabled) {
|
||||
mGamepadHandler = new Gamepad(contextView, inputDevice, DefaultDataProvider.INSTANCE, true);
|
||||
}
|
||||
}else mGamepadHandler = (code, value) -> {}; // Ensure it isn't null while also not processing the events.
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,9 +236,18 @@ public class MinecraftGLSurface extends View implements GrabListener, DirectGame
|
||||
@SuppressLint("NewApi")
|
||||
@Override
|
||||
public boolean dispatchGenericMotionEvent(MotionEvent event) {
|
||||
if(sdlEnabled && Gamepad.isGamepadEvent(event)) {
|
||||
try {
|
||||
MainActivity.motionListener.onGenericMotion(this, event);
|
||||
return true;
|
||||
} catch (Throwable ignored){
|
||||
Log.e(TAG, "SDL failed to send motionevent!");
|
||||
}
|
||||
}
|
||||
super.dispatchGenericMotionEvent(event);
|
||||
int mouseCursorIndex = -1;
|
||||
|
||||
if(Gamepad.isGamepadEvent(event)){
|
||||
if(!sdlEnabled && Gamepad.isGamepadEvent(event)){
|
||||
if(mGamepadHandler == null) createGamepad(this, event.getDevice());
|
||||
|
||||
mInputManager.handleMotionEventInput(getContext(), event, mGamepadHandler);
|
||||
@@ -239,9 +264,9 @@ public class MinecraftGLSurface extends View implements GrabListener, DirectGame
|
||||
|
||||
// Make sure we grabbed the mouse if necessary
|
||||
updateGrabState(CallbackBridge.isGrabbing());
|
||||
|
||||
switch(event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
CallbackBridge.mouseX = (event.getX(mouseCursorIndex) * LauncherPreferences.PREF_SCALE_FACTOR);
|
||||
CallbackBridge.mouseY = (event.getY(mouseCursorIndex) * LauncherPreferences.PREF_SCALE_FACTOR);
|
||||
CallbackBridge.sendCursorPos(CallbackBridge.mouseX, CallbackBridge.mouseY);
|
||||
@@ -293,8 +318,18 @@ public class MinecraftGLSurface extends View implements GrabListener, DirectGame
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if(Gamepad.isGamepadEvent(event)){
|
||||
// Android bundles in garbage KeyEvents for compatibility with old apps
|
||||
// that don't have controller code so we are, checking for em.
|
||||
boolean isGamepadEvent = Gamepad.isGamepadEvent(event);
|
||||
if (sdlEnabled && isGamepadEvent) {
|
||||
try {
|
||||
SDLActivity.handleKeyEvent(this, eventKeycode, event, null);
|
||||
return true;
|
||||
} catch (Throwable ignored){
|
||||
Log.e(TAG, "SDL failed to send keyevent!");
|
||||
}
|
||||
}
|
||||
if(!sdlEnabled && isGamepadEvent){
|
||||
if(mGamepadHandler == null) createGamepad(this, event.getDevice());
|
||||
|
||||
mInputManager.handleKeyEventInput(getContext(), event, mGamepadHandler);
|
||||
|
||||
@@ -8,6 +8,13 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import net.kdt.pojavlaunch.value.MinecraftAccount;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class PojavProfile {
|
||||
private static final String PROFILE_PREF = "pojav_profile";
|
||||
private static final String PROFILE_PREF_FILE = "file";
|
||||
@@ -29,6 +36,27 @@ public class PojavProfile {
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
public static List<MinecraftAccount> getAllProfiles(){
|
||||
List<MinecraftAccount> mcAccountList = new ArrayList<>();;
|
||||
for (String accountName : getAllProfilesList()){
|
||||
if (MinecraftAccount.load(accountName) != null) {
|
||||
mcAccountList.add(MinecraftAccount.load(accountName));
|
||||
}
|
||||
}
|
||||
return mcAccountList;
|
||||
}
|
||||
|
||||
public static List<String> getAllProfilesList(){
|
||||
List<String> accountList = new ArrayList<>();
|
||||
File accountFolder = new File(Tools.DIR_ACCOUNT_NEW);
|
||||
if(accountFolder.exists() && accountFolder.list() != null){
|
||||
for (String fileName : Objects.requireNonNull(accountFolder.list())) {
|
||||
accountList.add(fileName.substring(0, fileName.length() - 5));
|
||||
}
|
||||
}
|
||||
return accountList;
|
||||
}
|
||||
|
||||
public static void setCurrentProfile(@NonNull Context ctx, @Nullable Object obj) {
|
||||
SharedPreferences.Editor pref = getPrefs(ctx).edit();
|
||||
|
||||
@@ -3,6 +3,7 @@ package net.kdt.pojavlaunch;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.os.Build.VERSION_CODES.P;
|
||||
import static net.kdt.pojavlaunch.PojavApplication.sExecutorService;
|
||||
import static net.kdt.pojavlaunch.PojavProfile.getAllProfiles;
|
||||
import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_IGNORE_NOTCH;
|
||||
import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_NOTCH_SIZE;
|
||||
|
||||
@@ -21,10 +22,13 @@ import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.FileObserver;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.DocumentsContract;
|
||||
@@ -32,6 +36,7 @@ import android.provider.OpenableColumns;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.InputDevice;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.EditText;
|
||||
@@ -74,6 +79,7 @@ import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile;
|
||||
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.libsdl.app.SDLControllerManager;
|
||||
import org.lwjgl.glfw.CallbackBridge;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
@@ -87,6 +93,7 @@ import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.ParseException;
|
||||
@@ -224,6 +231,27 @@ public final class Tools {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for TouchController mod to automatically enable TouchController mod support.
|
||||
*
|
||||
* @param gameDir current game directory
|
||||
* @return whether TouchController is found
|
||||
*/
|
||||
public static boolean hasTouchController(File gameDir) {
|
||||
File modsDir = new File(gameDir, "mods");
|
||||
File[] mods = modsDir.listFiles(file -> file.isFile() && file.getName().endsWith(".jar"));
|
||||
if (mods == null) {
|
||||
return false;
|
||||
}
|
||||
for (File file : mods) {
|
||||
String name = file.getName().toLowerCase(Locale.ROOT);
|
||||
if (name.contains("touchcontroller")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize OpenGL and do checks to see if the GPU of the device is affected by the render
|
||||
* distance issue.
|
||||
@@ -286,6 +314,8 @@ public final class Tools {
|
||||
}
|
||||
LauncherProfiles.load();
|
||||
File gamedir = Tools.getGameDirPath(minecraftProfile);
|
||||
startControllableMitigation(activity, gamedir);
|
||||
startOldLegacy4JMitigation(activity, gamedir);
|
||||
if(checkRenderDistance(gamedir)) {
|
||||
LifecycleAwareAlertDialog.DialogCreator dialogCreator = ((alertDialog, dialogBuilder) ->
|
||||
dialogBuilder.setMessage(activity.getString(R.string.ltw_render_distance_warning_msg))
|
||||
@@ -338,7 +368,11 @@ public final class Tools {
|
||||
|
||||
javaArgList.addAll(Arrays.asList(getMinecraftJVMArgs(versionId, gamedir)));
|
||||
javaArgList.add("-cp");
|
||||
javaArgList.add(launchClassPath + ":" + getLWJGL3ClassPath());
|
||||
if (launchClassPath.contains("bta-client-")){ // BTADownloadTask.BASE_JSON sets this. Jank.
|
||||
// BTA for some reason needs this to be last or else it uses the wrong lwjgl
|
||||
javaArgList.add(launchClassPath + ":" + getLWJGL3ClassPath());
|
||||
// Legacy Fabric needs this to be first or else it uses the wrong lwjgl
|
||||
} else javaArgList.add(getLWJGL3ClassPath() + ":" + launchClassPath);
|
||||
|
||||
javaArgList.add(versionInfo.mainClass);
|
||||
javaArgList.addAll(Arrays.asList(launchArgs));
|
||||
@@ -350,6 +384,109 @@ public final class Tools {
|
||||
// If we returned, this means that the JVM exit dialog has been shown and we don't need to be active anymore.
|
||||
// We never return otherwise. The process will be killed anyway, and thus we will become inactive
|
||||
}
|
||||
private static Logger.eventLogListener controllableMitigationLogListener;
|
||||
/*
|
||||
* This is does not work when debugging. This is not reliable.
|
||||
* This is a monstrosity that races the mod, trying to ensure that when the folder is checked
|
||||
* after extraction but before dlopen, it is empty, so it loads the bundled SDL2 we have instead
|
||||
*/
|
||||
private static void startControllableMitigation(Activity activity ,File gamedir) {
|
||||
String TAG = "ControllableMitigation";
|
||||
File deleted = new File(gamedir + "/controllable_natives/SDL");
|
||||
boolean hasControllable = false;
|
||||
File modsDir = new File(gamedir, "mods");
|
||||
File[] mods = modsDir.listFiles(file -> file.isFile() && file.getName().endsWith(".jar"));
|
||||
if (mods != null) {
|
||||
for (File file : mods) {
|
||||
String name = file.getName();
|
||||
if (name.contains("controllable")) {
|
||||
hasControllable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasControllable) {
|
||||
Tools.runOnUiThread(() -> {
|
||||
Tools.dialog(activity, activity.getString(R.string.global_warning), activity.getString(R.string.controllableFound));
|
||||
});
|
||||
Thread mitigationThread = new Thread(() -> {
|
||||
// This is total garbage but it seems to be the best jank for the job
|
||||
Log.i(TAG, "Controllable detected! Starting mitigation thread");
|
||||
try {org.apache.commons.io.FileUtils.deleteDirectory(deleted);} catch (IOException ignored) {}
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
// Looks for controllable_natives/SDL/<sdl_version_number>/libSDL2.so and
|
||||
// deletes it. We can assume array index 0 because this dir gets fully deleted
|
||||
// before the loop is started.
|
||||
if (deleted.isDirectory()) {
|
||||
if (deleted.listFiles().length > 0) {
|
||||
if (deleted.listFiles()[0].listFiles().length > 0) {
|
||||
if (deleted.listFiles()[0].listFiles()[0].exists()) {
|
||||
deleted.listFiles()[0].listFiles()[0].delete();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// We can end here because SdlNativeLibraryLoader only extracts libSDL2.so once
|
||||
// If NativeLibrary can't find it in the folder to load() it uses java.library.path
|
||||
Log.i(TAG, "Success! Ending Controllable crash mitigation..");
|
||||
});
|
||||
mitigationThread.start();
|
||||
controllableMitigationLogListener = loggedLine -> {
|
||||
// Hard off switch if it somehow didn't delete anything, just in case.
|
||||
if (loggedLine.contains("Sound engine started") && mitigationThread.isAlive()) {
|
||||
Log.i(TAG, "Nothing happened. Ending Controllable crash mitigation..");
|
||||
Logger.removeLogListener(controllableMitigationLogListener);
|
||||
mitigationThread.interrupt();
|
||||
}
|
||||
};
|
||||
Logger.addLogListener(controllableMitigationLogListener);
|
||||
}
|
||||
}
|
||||
|
||||
private static Logger.eventLogListener oldL4JMitigationLogListener;
|
||||
/// TODO: Remove when the time is right
|
||||
/**
|
||||
* Legacy4J for a long time had broken SDL detection for android, we need to check and
|
||||
* accommodate this for now. At least until the broken logic are on versions considered
|
||||
* obsolete.
|
||||
* <p>
|
||||
* This is of course, very jank, it does not work for anything below 1.7.5 but why is anyone
|
||||
* on that version anyway? Legacy4J has LTS for like all the versions.
|
||||
*/
|
||||
private static void startOldLegacy4JMitigation(Activity activity, File gamedir) {
|
||||
boolean hasLegacy4J = false;
|
||||
File modsDir = new File(gamedir, "mods");
|
||||
File[] mods = modsDir.listFiles(file -> file.isFile() && file.getName().endsWith(".jar"));
|
||||
if(mods != null) {
|
||||
for (File file : mods) {
|
||||
String name = file.getName();
|
||||
if (name.contains("Legacy4J")) {
|
||||
hasLegacy4J = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasLegacy4J) {
|
||||
String TAG = "OldLegacy4JMitigation";
|
||||
Log.i(TAG, "Legacy4J detected!");
|
||||
oldL4JMitigationLogListener = loggedLine -> {
|
||||
if (LauncherPreferences.PREF_GAMEPAD_SDL_PASSTHRU && loggedLine.contains("literal{SDL3 (isXander's libsdl4j)} isn't supported in this system. GLFW will be used instead.")) {
|
||||
Log.i(TAG, "Old version of Legacy4J detected! Force enabling SDL");
|
||||
Tools.SDL.initializeControllerSubsystems();
|
||||
Tools.runOnUiThread(() -> {
|
||||
Tools.dialog(activity, activity.getString(R.string.global_warning), activity.getString(R.string.oldL4JFound));
|
||||
});
|
||||
Logger.removeLogListener(oldL4JMitigationLogListener);
|
||||
} else if (LauncherPreferences.PREF_GAMEPAD_SDL_PASSTHRU && loggedLine.contains("Added SDL Controller Mappings")) {
|
||||
Log.i(TAG, "Fixed version of Legacy4J detected! Have fun!");
|
||||
Logger.removeLogListener(oldL4JMitigationLogListener);
|
||||
}
|
||||
};
|
||||
Logger.addLogListener(oldL4JMitigationLogListener);
|
||||
}
|
||||
}
|
||||
|
||||
public static File getGameDirPath(@NonNull MinecraftProfile minecraftProfile){
|
||||
if(minecraftProfile.gameDir != null){
|
||||
@@ -798,6 +935,9 @@ public final class Tools {
|
||||
public static void dialogOnUiThread(final Activity activity, final CharSequence title, final CharSequence message) {
|
||||
activity.runOnUiThread(()->dialog(activity, title, message));
|
||||
}
|
||||
public static void dialogOnUiThread(final Activity activity, final int title, final int message) {
|
||||
dialogOnUiThread(activity, activity.getString(title), activity.getString(message));
|
||||
}
|
||||
|
||||
public static void dialog(final Context context, final CharSequence title, final CharSequence message) {
|
||||
new AlertDialog.Builder(context)
|
||||
@@ -899,7 +1039,7 @@ public final class Tools {
|
||||
insertSafety(inheritsVer, customVer,
|
||||
"assetIndex", "assets", "id",
|
||||
"mainClass", "minecraftArguments",
|
||||
"releaseTime", "time", "type"
|
||||
"releaseTime", "time", "type", "inheritsFrom"
|
||||
);
|
||||
|
||||
// Go through the libraries, remove the ones overridden by the custom version
|
||||
@@ -1429,6 +1569,18 @@ public final class Tools {
|
||||
OBSOLETE_RESOURCES_PATH = DIR_GAME_NEW + "/resources";
|
||||
}
|
||||
|
||||
private static NetworkInfo getActiveNetworkInfo(Context ctx) {
|
||||
ConnectivityManager connMgr = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
|
||||
return networkInfo; // This can return null when there is no wifi or data connected
|
||||
}
|
||||
|
||||
public static boolean isOnline(Context ctx) {
|
||||
NetworkInfo info = getActiveNetworkInfo(ctx);
|
||||
if(info == null) return false;
|
||||
return (info.isConnected());
|
||||
}
|
||||
|
||||
public static boolean isDemoProfile(Context ctx){
|
||||
MinecraftAccount currentProfile = PojavProfile.getCurrentProfileContent(ctx, null);
|
||||
return currentProfile != null && currentProfile.isDemo();
|
||||
@@ -1438,4 +1590,117 @@ public final class Tools {
|
||||
MinecraftAccount currentProfile = PojavProfile.getCurrentProfileContent(ctx, null);
|
||||
return currentProfile == null || currentProfile.isLocal();
|
||||
}
|
||||
public static boolean hasOnlineProfile(){
|
||||
for (MinecraftAccount accountToCheck : getAllProfiles()) {
|
||||
if (!accountToCheck.isLocal() && !accountToCheck.isDemo()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void hasNoOnlineProfileDialog(Activity activity, @Nullable Runnable run, @Nullable String customTitle, @Nullable String customMessage){
|
||||
if (hasOnlineProfile() && !Tools.isDemoProfile(activity)){
|
||||
if (run != null) { // Demo profile handling should be using customTitle and customMessage
|
||||
run.run();
|
||||
}
|
||||
} else { // If there is no online profile, show a dialog
|
||||
customTitle = customTitle == null ? activity.getString(R.string.no_minecraft_account_found) : customTitle;
|
||||
customMessage = customMessage == null ? activity.getString(R.string.feature_requires_java_account) : customMessage;
|
||||
dialogOnUiThread(activity, customTitle, customMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Some boilerplate to reduce boilerplate elsewhere
|
||||
public static void hasNoOnlineProfileDialog(Activity activity){
|
||||
hasNoOnlineProfileDialog(activity, null, null, null);
|
||||
}
|
||||
public static void hasNoOnlineProfileDialog(Activity activity, Runnable run){
|
||||
hasNoOnlineProfileDialog(activity, run, null, null);
|
||||
}
|
||||
public static void hasNoOnlineProfileDialog(Activity activity, String customTitle, String customMessage){
|
||||
hasNoOnlineProfileDialog(activity, null, customTitle, customMessage);
|
||||
}
|
||||
|
||||
public static String getSelectedVanillaMcVer(){
|
||||
String selectedProfile = LauncherPreferences.DEFAULT_PREF.getString(LauncherPreferences.PREF_KEY_CURRENT_PROFILE, "");
|
||||
MinecraftProfile selected = LauncherProfiles.mainProfileJson.profiles.get(selectedProfile);
|
||||
if (selected == null) { // This should NEVER happen.
|
||||
throw new RuntimeException("No profile selected, how did you reach this? Go ask in the discord or github");
|
||||
}
|
||||
String currentMCVersion = selected.lastVersionId;
|
||||
String vanillaVersion = currentMCVersion;
|
||||
File providedJsonFile = new File(Tools.DIR_HOME_VERSION + "/" + currentMCVersion + "/" + currentMCVersion + ".json");
|
||||
JMinecraftVersionList.Version providedJsonVersion = null;
|
||||
try {
|
||||
providedJsonVersion = Tools.GLOBAL_GSON.fromJson(Tools.read(providedJsonFile.getAbsolutePath()), JMinecraftVersionList.Version.class);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
try {
|
||||
vanillaVersion = providedJsonVersion.inheritsFrom != null ? providedJsonVersion.inheritsFrom : vanillaVersion;
|
||||
} catch (NullPointerException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return vanillaVersion;
|
||||
}
|
||||
|
||||
public static Integer mcVersiontoInt(String mcVersion){
|
||||
String[] sVersionArray = mcVersion.split("\\.");
|
||||
String[] iVersionArray = new String[3];
|
||||
// Make sure this is actually a version string
|
||||
for (int i = 0; i < iVersionArray.length; i++) {
|
||||
try {
|
||||
// Ensure there's padding
|
||||
sVersionArray[i] = String.format("%3s", sVersionArray[i]).replace(' ', '0');
|
||||
// Grab only the last 3, MCJE 999.999.999 isnt coming soon anyway
|
||||
sVersionArray[i] = sVersionArray[i].substring(sVersionArray[i].length() - 3);
|
||||
} catch (ArrayIndexOutOfBoundsException ignored){
|
||||
// If we don't get 3 a third array, pad with 0s because it's probably 1.21 or something
|
||||
iVersionArray[i] = "000";
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
// Verify its a real deal, legit number
|
||||
Integer.parseInt(sVersionArray[i]);
|
||||
iVersionArray[i] = sVersionArray[i];
|
||||
} catch (NumberFormatException e) {
|
||||
throw new RuntimeException("Tools(mcVersiontoInt): Invalid version string");
|
||||
}
|
||||
}
|
||||
return Integer.parseInt(iVersionArray[0] + iVersionArray[1] + iVersionArray[2]);
|
||||
}
|
||||
|
||||
public static boolean isPointerDeviceConnected() {
|
||||
int[] deviceIds = InputDevice.getDeviceIds();
|
||||
for (int id : deviceIds) {
|
||||
InputDevice device = InputDevice.getDevice(id);
|
||||
if (device == null) continue;
|
||||
int sources = device.getSources();
|
||||
if ((sources & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE
|
||||
|| (sources & InputDevice.SOURCE_TOUCHPAD) == InputDevice.SOURCE_TOUCHPAD
|
||||
|| (sources & InputDevice.SOURCE_TRACKBALL) == InputDevice.SOURCE_TRACKBALL) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Object runMethodbyReflection(String className, String methodName) throws ReflectiveOperationException{
|
||||
Class<?> clazz = Class.forName(className);
|
||||
Method method = clazz.getDeclaredMethod(methodName);
|
||||
method.setAccessible(true);
|
||||
Object motionListener = method.invoke(null);
|
||||
assert motionListener != null;
|
||||
return motionListener;
|
||||
}
|
||||
|
||||
static class SDL {
|
||||
/**
|
||||
* Initializes gamepad, joystick, and event subsystems.
|
||||
* This triggers {@link SDLControllerManager#pollInputDevices()} and subsequently disables
|
||||
* the emulated gamepad implementation.
|
||||
*/
|
||||
public static native void initializeControllerSubsystems();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import net.kdt.pojavlaunch.EfficientAndroidLWJGLKeycode;
|
||||
import net.kdt.pojavlaunch.LwjglGlfwKeycode;
|
||||
import net.kdt.pojavlaunch.MainActivity;
|
||||
import net.kdt.pojavlaunch.R;
|
||||
@@ -191,7 +192,7 @@ public class ControlButton extends TextView implements ControlInterface {
|
||||
setActivated(isDown);
|
||||
for(int keycode : mProperties.keycodes){
|
||||
if(keycode >= GLFW_KEY_UNKNOWN){
|
||||
sendKeyPress(keycode, CallbackBridge.getCurrentMods(), isDown);
|
||||
sendKeyPress(keycode, EfficientAndroidLWJGLKeycode.getLwjglChar(keycode), CallbackBridge.getCurrentMods(), isDown);
|
||||
CallbackBridge.setModifiers(keycode, isDown);
|
||||
}else{
|
||||
Log.i("punjabilauncher", "sendSpecialKey("+keycode+","+isDown+")");
|
||||
|
||||
@@ -90,6 +90,7 @@ public class Gamepad implements GrabListener, GamepadHandler {
|
||||
private boolean mRemoved = false;
|
||||
|
||||
public Gamepad(View contextView, InputDevice inputDevice, GamepadDataProvider mapProvider, boolean showCursor){
|
||||
|
||||
Settings.setDeadzoneScale(PREF_DEADZONE_SCALE);
|
||||
|
||||
mScreenChoreographer = Choreographer.getInstance();
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
package net.kdt.pojavlaunch.customcontrols.mouse;
|
||||
|
||||
import static net.kdt.pojavlaunch.prefs.LauncherPreferences.DEFAULT_PREF;
|
||||
import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_MOUSE_GRAB_FORCE;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.view.InputDevice;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import net.kdt.pojavlaunch.GrabListener;
|
||||
import net.kdt.pojavlaunch.MinecraftGLSurface;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.prefs.LauncherPreferences;
|
||||
@@ -15,7 +22,7 @@ import net.kdt.pojavlaunch.prefs.LauncherPreferences;
|
||||
import org.lwjgl.glfw.CallbackBridge;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public class AndroidPointerCapture implements ViewTreeObserver.OnWindowFocusChangeListener, View.OnCapturedPointerListener {
|
||||
public class AndroidPointerCapture implements ViewTreeObserver.OnWindowFocusChangeListener, View.OnCapturedPointerListener, GrabListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final float TOUCHPAD_SCROLL_THRESHOLD = 1;
|
||||
private final AbstractTouchpad mTouchpad;
|
||||
private final View mHostView;
|
||||
@@ -32,14 +39,43 @@ public class AndroidPointerCapture implements ViewTreeObserver.OnWindowFocusChan
|
||||
this.mHostView = hostView;
|
||||
hostView.setOnCapturedPointerListener(this);
|
||||
hostView.getViewTreeObserver().addOnWindowFocusChangeListener(this);
|
||||
DEFAULT_PREF.registerOnSharedPreferenceChangeListener(this);
|
||||
CallbackBridge.addGrabListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not the touchpad is already enabled and if user prefers virtual cursor
|
||||
* if they don't, the touchpad is not enabled
|
||||
*/
|
||||
private void enableTouchpadIfNecessary() {
|
||||
if(!mTouchpad.getDisplayState()) mTouchpad.enable(true);
|
||||
if(!mTouchpad.getDisplayState() && PREF_MOUSE_GRAB_FORCE) mTouchpad.enable(true);
|
||||
}
|
||||
|
||||
// Needed so it releases the cursor when inside game menu
|
||||
@Override
|
||||
public void onGrabState(boolean isGrabbing) {
|
||||
handleAutomaticCapture();
|
||||
}
|
||||
// It's only here so the side-dialog changes it live
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, @Nullable String key) {
|
||||
if (sharedPreferences.getBoolean("always_grab_mouse", true)){
|
||||
enableTouchpadIfNecessary();
|
||||
} else mTouchpad.disable();
|
||||
handleAutomaticCapture();
|
||||
}
|
||||
|
||||
public void handleAutomaticCapture() {
|
||||
if(!mHostView.hasWindowFocus()) {
|
||||
// isGrabbing checks for whether we are in menu
|
||||
if (!CallbackBridge.isGrabbing()
|
||||
&& !PREF_MOUSE_GRAB_FORCE) {
|
||||
mHostView.releasePointerCapture();
|
||||
return;
|
||||
}
|
||||
if (mHostView.hasPointerCapture()) {
|
||||
enableTouchpadIfNecessary();
|
||||
}
|
||||
if (!mHostView.hasWindowFocus()) {
|
||||
mHostView.requestFocus();
|
||||
} else {
|
||||
mHostView.requestPointerCapture();
|
||||
@@ -128,7 +164,11 @@ public class AndroidPointerCapture implements ViewTreeObserver.OnWindowFocusChan
|
||||
|
||||
@Override
|
||||
public void onWindowFocusChanged(boolean hasFocus) {
|
||||
if(hasFocus && Tools.isAndroid8OrHigher()) mHostView.requestPointerCapture();
|
||||
if (!CallbackBridge.isGrabbing() // Only capture if not in menu and user said so
|
||||
&& !PREF_MOUSE_GRAB_FORCE) {
|
||||
return;
|
||||
}
|
||||
if (hasFocus && Tools.isAndroid8OrHigher()) mHostView.requestPointerCapture();
|
||||
}
|
||||
|
||||
public void detach() {
|
||||
|
||||
@@ -132,7 +132,7 @@ public class Touchpad extends View implements GrabListener, AbstractTouchpad {
|
||||
public void enable(boolean supposed) {
|
||||
if(mDisplayState) return;
|
||||
mDisplayState = true;
|
||||
if(supposed && CallbackBridge.isGrabbing()) return;
|
||||
if(supposed && CallbackBridge.isGrabbing() && LauncherPreferences.PREF_MOUSE_GRAB_FORCE) return;
|
||||
_enable();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package net.kdt.pojavlaunch.fragments;
|
||||
|
||||
import static net.kdt.pojavlaunch.Tools.hasOnlineProfile;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
@@ -31,6 +33,10 @@ public class LocalLoginFragment extends Fragment {
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
// This is overkill but meh
|
||||
if (!hasOnlineProfile()){
|
||||
Tools.swapFragment(requireActivity(), MainMenuFragment.class, MainMenuFragment.TAG, null);
|
||||
}
|
||||
mUsernameEditText = view.findViewById(R.id.login_edit_email);
|
||||
view.findViewById(R.id.login_button).setOnClickListener(v -> {
|
||||
if(!checkEditText()) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package net.kdt.pojavlaunch.fragments;
|
||||
|
||||
import static net.kdt.pojavlaunch.Tools.hasNoOnlineProfileDialog;
|
||||
import static net.kdt.pojavlaunch.Tools.hasOnlineProfile;
|
||||
import static net.kdt.pojavlaunch.Tools.openPath;
|
||||
import static net.kdt.pojavlaunch.Tools.shareLog;
|
||||
|
||||
@@ -53,11 +55,13 @@ public class MainMenuFragment extends Fragment {
|
||||
mNewsButton.setOnClickListener(v -> Tools.openURL(requireActivity(), Tools.URL_HOME));
|
||||
mDiscordButton.setOnClickListener(v -> Tools.openURL(requireActivity(), getString(R.string.discord_invite)));
|
||||
mCustomControlButton.setOnClickListener(v -> startActivity(new Intent(requireContext(), CustomControlsActivity.class)));
|
||||
mInstallJarButton.setOnClickListener(v -> runInstallerWithConfirmation(false));
|
||||
mInstallJarButton.setOnLongClickListener(v->{
|
||||
runInstallerWithConfirmation(true);
|
||||
return true;
|
||||
});
|
||||
if (hasOnlineProfile()) {
|
||||
mInstallJarButton.setOnClickListener(v -> runInstallerWithConfirmation(false));
|
||||
mInstallJarButton.setOnLongClickListener(v -> {
|
||||
runInstallerWithConfirmation(true);
|
||||
return true;
|
||||
});
|
||||
} else mInstallJarButton.setOnClickListener(v -> hasNoOnlineProfileDialog(requireActivity()));
|
||||
mEditProfileButton.setOnClickListener(v -> mVersionSpinner.openProfileEditor(requireActivity()));
|
||||
|
||||
mPlayButton.setOnClickListener(v -> ExtraCore.setValue(ExtraConstants.LAUNCH_GAME, true));
|
||||
@@ -65,13 +69,12 @@ public class MainMenuFragment extends Fragment {
|
||||
mShareLogsButton.setOnClickListener((v) -> shareLog(requireContext()));
|
||||
|
||||
mOpenDirectoryButton.setOnClickListener((v)-> {
|
||||
Tools.switchDemo(Tools.isDemoProfile(v.getContext())); // avoid switching accounts being able to access
|
||||
if(Tools.isDemoProfile(v.getContext())){
|
||||
Toast.makeText(v.getContext(), R.string.toast_not_available_demo, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
if (Tools.isDemoProfile(v.getContext())){ // Say a different message when on demo profile since they might see the hidden demo folder
|
||||
hasNoOnlineProfileDialog(getActivity(), getString(R.string.demo_unsupported), getString(R.string.change_account));
|
||||
} else if (!hasOnlineProfile()) { // Otherwise display the generic pop-up to log in
|
||||
hasNoOnlineProfileDialog(requireActivity());
|
||||
} else openPath(v.getContext(), getCurrentProfileDirectory(), false);
|
||||
|
||||
openPath(v.getContext(), getCurrentProfileDirectory(), false);
|
||||
});
|
||||
|
||||
|
||||
@@ -97,12 +100,6 @@ public class MainMenuFragment extends Fragment {
|
||||
}
|
||||
|
||||
private void runInstallerWithConfirmation(boolean isCustomArgs) {
|
||||
// avoid using custom installers to install a version
|
||||
if(Tools.isLocalProfile(requireContext()) || Tools.isDemoProfile(requireContext())){
|
||||
Toast.makeText(requireContext(), R.string.toast_not_available_demo, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ProgressKeeper.getTaskCount() == 0)
|
||||
Tools.installMod(requireActivity(), isCustomArgs);
|
||||
else
|
||||
|
||||
@@ -17,7 +17,6 @@ import androidx.fragment.app.Fragment;
|
||||
import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.extra.ExtraCore;
|
||||
import net.kdt.pojavlaunch.mirrors.DownloadMirror;
|
||||
import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener;
|
||||
import net.kdt.pojavlaunch.modloaders.ModloaderListenerProxy;
|
||||
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
|
||||
@@ -106,7 +105,7 @@ public abstract class ModVersionListFragment<T> extends Fragment implements Runn
|
||||
Object forgeVersion = expandableListView.getExpandableListAdapter().getChild(i, i1);
|
||||
ModloaderListenerProxy taskProxy = new ModloaderListenerProxy();
|
||||
Runnable downloadTask = createDownloadTask(forgeVersion, taskProxy);
|
||||
setTaskProxy(taskProxy);
|
||||
setTaskProxyValue(taskProxy);
|
||||
taskProxy.attachListener(this);
|
||||
mExpandableListView.setEnabled(false);
|
||||
new Thread(downloadTask).start();
|
||||
@@ -118,7 +117,7 @@ public abstract class ModVersionListFragment<T> extends Fragment implements Runn
|
||||
Tools.runOnUiThread(()->{
|
||||
Context context = requireContext();
|
||||
getTaskProxy().detachListener();
|
||||
setTaskProxy(null);
|
||||
deleteTaskProxy();
|
||||
mExpandableListView.setEnabled(true);
|
||||
// Read the comment in FabricInstallFragment.onDownloadFinished() to see how this works
|
||||
getParentFragmentManager().popBackStackImmediate();
|
||||
@@ -131,7 +130,7 @@ public abstract class ModVersionListFragment<T> extends Fragment implements Runn
|
||||
Tools.runOnUiThread(()->{
|
||||
Context context = requireContext();
|
||||
getTaskProxy().detachListener();
|
||||
setTaskProxy(null);
|
||||
deleteTaskProxy();
|
||||
mExpandableListView.setEnabled(true);
|
||||
Tools.dialog(context,
|
||||
context.getString(R.string.global_error),
|
||||
@@ -144,15 +143,18 @@ public abstract class ModVersionListFragment<T> extends Fragment implements Runn
|
||||
Tools.runOnUiThread(()->{
|
||||
Context context = requireContext();
|
||||
getTaskProxy().detachListener();
|
||||
setTaskProxy(null);
|
||||
deleteTaskProxy();
|
||||
mExpandableListView.setEnabled(true);
|
||||
Tools.showError(context, e);
|
||||
});
|
||||
}
|
||||
|
||||
private void setTaskProxy(ModloaderListenerProxy proxy) {
|
||||
private void setTaskProxyValue(ModloaderListenerProxy proxy) {
|
||||
ExtraCore.setValue(mExtraTag, proxy);
|
||||
}
|
||||
private void deleteTaskProxy(){
|
||||
ExtraCore.removeValue(mExtraTag);
|
||||
}
|
||||
|
||||
private ModloaderListenerProxy getTaskProxy() {
|
||||
return (ModloaderListenerProxy) ExtraCore.getValue(mExtraTag);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package net.kdt.pojavlaunch.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.ExpandableListAdapter;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.kdt.pojavlaunch.JavaGUILauncherActivity;
|
||||
import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.modloaders.ForgeDownloadTask;
|
||||
import net.kdt.pojavlaunch.modloaders.ForgeUtils;
|
||||
import net.kdt.pojavlaunch.modloaders.ModloaderListenerProxy;
|
||||
import net.kdt.pojavlaunch.modloaders.NeoForgeDownloadTask;
|
||||
import net.kdt.pojavlaunch.modloaders.NeoForgeVersionListAdapter;
|
||||
import net.kdt.pojavlaunch.utils.DownloadUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class NeoForgeInstallFragment extends ModVersionListFragment<List<String>> {
|
||||
public static final String TAG = "NeoForgeInstallFragment";
|
||||
public NeoForgeInstallFragment() {
|
||||
super(TAG);
|
||||
}
|
||||
|
||||
private static final String NEOFORGE_METADATA_URL = "https://meta.prismlauncher.org/v1/net.neoforged/index.json";
|
||||
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTitleText() {
|
||||
return R.string.neoforge_dl_select_version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNoDataMsg() {
|
||||
return R.string.neoforge_dl_no_installer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> loadVersionList() {
|
||||
String test = null;
|
||||
try {
|
||||
test = DownloadUtils.downloadStringCached(NEOFORGE_METADATA_URL, "neoforge_versions", input -> input);
|
||||
} catch (Exception e) {
|
||||
Tools.showErrorRemote(e);
|
||||
}
|
||||
return Collections.singletonList(test);
|
||||
// Moved the parsing logic to the adapter because there is no way to get this info easily, we use prism's index
|
||||
// since neoforge doesn't actually give this information easily anywhere.
|
||||
// To clarify, neoforge does not provide maven APIs to get supported Minecraft versions for each loader version
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpandableListAdapter createAdapter(List<String> versionList, LayoutInflater layoutInflater) {
|
||||
return new NeoForgeVersionListAdapter(versionList, layoutInflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Runnable createDownloadTask(Object selectedVersion, ModloaderListenerProxy listenerProxy) {
|
||||
return new NeoForgeDownloadTask(listenerProxy, (String) selectedVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadFinished(Context context, File downloadedFile) {
|
||||
Intent modInstallerStartIntent = new Intent(context, JavaGUILauncherActivity.class);
|
||||
modInstallerStartIntent.putExtra("javaArgs", "-jar "+downloadedFile.getAbsolutePath()+" --install-client");
|
||||
context.startActivity(modInstallerStartIntent);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package net.kdt.pojavlaunch.fragments;
|
||||
|
||||
import static net.kdt.pojavlaunch.Tools.hasNoOnlineProfileDialog;
|
||||
import static net.kdt.pojavlaunch.Tools.hasOnlineProfile;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
@@ -33,6 +36,8 @@ public class ProfileTypeSelectFragment extends Fragment {
|
||||
tryInstall(FabricInstallFragment.class, FabricInstallFragment.TAG));
|
||||
view.findViewById(R.id.modded_profile_forge).setOnClickListener((v)->
|
||||
tryInstall(ForgeInstallFragment.class, ForgeInstallFragment.TAG));
|
||||
view.findViewById(R.id.modded_profile_neoforge).setOnClickListener((v)->
|
||||
tryInstall(NeoForgeInstallFragment.class, NeoForgeInstallFragment.TAG));
|
||||
view.findViewById(R.id.modded_profile_modpack).setOnClickListener((v)->
|
||||
tryInstall(SearchModFragment.class, SearchModFragment.TAG));
|
||||
view.findViewById(R.id.modded_profile_quilt).setOnClickListener((v)->
|
||||
@@ -42,8 +47,8 @@ public class ProfileTypeSelectFragment extends Fragment {
|
||||
}
|
||||
|
||||
private void tryInstall(Class<? extends Fragment> fragmentClass, String tag){
|
||||
if(Tools.isLocalProfile(requireContext()) || Tools.isDemoProfile(requireContext())){
|
||||
Toast.makeText(requireContext(), R.string.toast_not_available_demo, Toast.LENGTH_LONG).show();
|
||||
if(!hasOnlineProfile()){
|
||||
hasNoOnlineProfileDialog(requireActivity());
|
||||
} else {
|
||||
Tools.swapFragment(requireActivity(), fragmentClass, tag, null);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package net.kdt.pojavlaunch.fragments;
|
||||
|
||||
import static net.kdt.pojavlaunch.Tools.hasNoOnlineProfileDialog;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
@@ -24,6 +26,6 @@ public class SelectAuthFragment extends Fragment {
|
||||
Button mLocalButton = view.findViewById(R.id.button_local_authentication);
|
||||
|
||||
mMicrosoftButton.setOnClickListener(v -> Tools.swapFragment(requireActivity(), MicrosoftLoginFragment.class, MicrosoftLoginFragment.TAG, null));
|
||||
mLocalButton.setOnClickListener(v -> Tools.swapFragment(requireActivity(), LocalLoginFragment.class, LocalLoginFragment.TAG, null));
|
||||
mLocalButton.setOnClickListener(v -> hasNoOnlineProfileDialog(requireActivity(), () -> Tools.swapFragment(requireActivity(), LocalLoginFragment.class, LocalLoginFragment.TAG, null)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,14 +76,18 @@ public class DownloadMirror {
|
||||
* @param urlInput The original (Mojang) URL for the download
|
||||
* @return the length of the file denoted by the URL in bytes, or -1 if not available
|
||||
*/
|
||||
public static long getContentLengthMirrored(int downloadClass, String urlInput) throws IOException {
|
||||
long length = DownloadUtils.getContentLength(getMirrorMapping(downloadClass, urlInput));
|
||||
if(length < 1) {
|
||||
Log.w("DownloadMirror", "Unable to get content length from mirror");
|
||||
Log.i("DownloadMirror", "Falling back to default source");
|
||||
return DownloadUtils.getContentLength(urlInput);
|
||||
}else {
|
||||
return length;
|
||||
public static long getContentLengthMirrored(int downloadClass, String urlInput){
|
||||
try {
|
||||
long length = DownloadUtils.getContentLength(getMirrorMapping(downloadClass, urlInput));
|
||||
if (length < 1) {
|
||||
Log.w("DownloadMirror", "Unable to get content length from mirror");
|
||||
Log.i("DownloadMirror", "Falling back to default source");
|
||||
return DownloadUtils.getContentLength(urlInput);
|
||||
} else {
|
||||
return length;
|
||||
}
|
||||
} catch (IOException ignored) { // If error happens, fallback to old file counter instead of size. This shouldn't really happen unless offline though.
|
||||
return -1L;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package net.kdt.pojavlaunch.modloaders;
|
||||
|
||||
import com.kdt.mcgui.ProgressLayout;
|
||||
|
||||
import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
|
||||
import net.kdt.pojavlaunch.utils.DownloadUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class NeoForgeDownloadTask implements Runnable, Tools.DownloaderFeedback {
|
||||
private String mDownloadUrl;
|
||||
private String mFullVersion;
|
||||
private String mLoaderVersion;
|
||||
private String mGameVersion;
|
||||
private final ModloaderDownloadListener mListener;
|
||||
public NeoForgeDownloadTask(ModloaderDownloadListener listener, String forgeVersion) {
|
||||
this.mListener = listener;
|
||||
this.mDownloadUrl = "https://maven.neoforged.net/releases/net/neoforged/neoforge/"+ forgeVersion +"/neoforge-"+forgeVersion+"-installer.jar";
|
||||
this.mFullVersion = forgeVersion;
|
||||
}
|
||||
|
||||
public NeoForgeDownloadTask(ModloaderDownloadListener listener, String gameVersion, String loaderVersion) {
|
||||
this.mListener = listener;
|
||||
this.mLoaderVersion = loaderVersion;
|
||||
this.mGameVersion = gameVersion;
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
if(determineDownloadUrl()) {
|
||||
downloadForge();
|
||||
}
|
||||
ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateProgress(int curr, int max) {
|
||||
int progress100 = (int)(((float)curr / (float)max)*100f);
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, progress100, R.string.forge_dl_progress, mFullVersion);
|
||||
}
|
||||
|
||||
private void downloadForge() {
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.forge_dl_progress, mFullVersion);
|
||||
try {
|
||||
File destinationFile = new File(Tools.DIR_CACHE, "neoforge-installer.jar");
|
||||
byte[] buffer = new byte[8192];
|
||||
DownloadUtils.downloadFileMonitored(mDownloadUrl, destinationFile, buffer, this);
|
||||
mListener.onDownloadFinished(destinationFile);
|
||||
}catch (FileNotFoundException e) {
|
||||
mListener.onDataNotAvailable();
|
||||
} catch (IOException e) {
|
||||
mListener.onDownloadError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean determineDownloadUrl() {
|
||||
if(mDownloadUrl != null && mFullVersion != null) return true;
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.forge_dl_searching);
|
||||
try {
|
||||
if(!findVersion()) {
|
||||
mListener.onDataNotAvailable();
|
||||
return false;
|
||||
}
|
||||
}catch (IOException e) {
|
||||
mListener.onDownloadError(e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean findVersion() throws IOException {
|
||||
List<String> forgeVersions = ForgeUtils.downloadForgeVersions();
|
||||
if(forgeVersions == null) return false;
|
||||
String versionStart = mGameVersion+"-"+mLoaderVersion;
|
||||
for(String versionName : forgeVersions) {
|
||||
if(!versionName.startsWith(versionStart)) continue;
|
||||
mFullVersion = versionName;
|
||||
mDownloadUrl = ForgeUtils.getInstallerUrl(mFullVersion);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package net.kdt.pojavlaunch.modloaders;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseExpandableListAdapter;
|
||||
import android.widget.ExpandableListAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
public class NeoForgeVersionListAdapter extends BaseExpandableListAdapter implements ExpandableListAdapter {
|
||||
private final LayoutInflater mLayoutInflater;
|
||||
private final LinkedHashMap<String, LinkedHashSet<String>> minecraftToLoaderVersionsHashmap;
|
||||
private LinkedHashSet<String> generatedHashSet = null;
|
||||
|
||||
|
||||
public NeoForgeVersionListAdapter(List<String> forgeVersions, LayoutInflater layoutInflater) {
|
||||
this.mLayoutInflater = layoutInflater;
|
||||
minecraftToLoaderVersionsHashmap = new LinkedHashMap<>();
|
||||
JsonArray versionsJsonArray = JsonParser.parseString(forgeVersions.get(0)).getAsJsonObject().getAsJsonArray("versions");
|
||||
|
||||
ArrayList<JsonElement> sortedVersionsList = new ArrayList<>();
|
||||
for (JsonElement elem : versionsJsonArray) {
|
||||
sortedVersionsList.add(elem);
|
||||
}
|
||||
Collections.sort(sortedVersionsList, (o1, o2) -> {
|
||||
String versionString1 = ((JsonObject) o1).get("requires").getAsJsonArray().get(0).getAsJsonObject().get("equals").getAsString();
|
||||
String versionString2 = ((JsonObject) o2).get("requires").getAsJsonArray().get(0).getAsJsonObject().get("equals").getAsString();
|
||||
return versionString2.compareTo(versionString1); // Sorts by Minecraft version
|
||||
});
|
||||
|
||||
for (JsonElement sortedVersionPick : sortedVersionsList) {
|
||||
String loaderVersion = ((JsonObject) sortedVersionPick).get("version").getAsString();
|
||||
String minecraftVersion = ((JsonObject) sortedVersionPick).get("requires").getAsJsonArray().get(0).getAsJsonObject().get("equals").getAsString();
|
||||
if (minecraftToLoaderVersionsHashmap.containsKey(minecraftVersion)) {
|
||||
minecraftToLoaderVersionsHashmap.get(minecraftVersion).add(loaderVersion);
|
||||
} else {
|
||||
generatedHashSet = new LinkedHashSet<>();
|
||||
generatedHashSet.add(loaderVersion);
|
||||
minecraftToLoaderVersionsHashmap.put(minecraftVersion, generatedHashSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public int getGroupCount() {
|
||||
return minecraftToLoaderVersionsHashmap.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildrenCount(int i) {
|
||||
return new ArrayList<>(minecraftToLoaderVersionsHashmap.values()).get(i).size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getGroup(int i) {
|
||||
return getGameVersion(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChild(int i, int i1) {
|
||||
return getForgeVersion(i, i1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getGroupId(int i) {
|
||||
return i;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getChildId(int i, int i1) {
|
||||
return i1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStableIds() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getGroupView(int i, boolean b, View convertView, ViewGroup viewGroup) {
|
||||
if(convertView == null)
|
||||
convertView = mLayoutInflater.inflate(android.R.layout.simple_expandable_list_item_1, viewGroup, false);
|
||||
|
||||
((TextView) convertView).setText(getGameVersion(i));
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getChildView(int i, int i1, boolean b, View convertView, ViewGroup viewGroup) {
|
||||
if(convertView == null)
|
||||
convertView = mLayoutInflater.inflate(android.R.layout.simple_expandable_list_item_1, viewGroup, false);
|
||||
((TextView) convertView).setText(getForgeVersion(i, i1));
|
||||
return convertView;
|
||||
}
|
||||
|
||||
private String getGameVersion(int i) {
|
||||
return minecraftToLoaderVersionsHashmap.keySet().toArray()[i].toString();
|
||||
}
|
||||
|
||||
private String getForgeVersion(int i, int i1){
|
||||
return new ArrayList<>(minecraftToLoaderVersionsHashmap.values()).get(i).toArray()[i1].toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChildSelectable(int i, int i1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ public class LauncherPreferences {
|
||||
public static final String PREF_KEY_SKIP_NOTIFICATION_CHECK = "skipNotificationPermissionCheck";
|
||||
|
||||
public static SharedPreferences DEFAULT_PREF;
|
||||
public static String PREF_RENDERER = "opengles2";
|
||||
|
||||
public static boolean PREF_IGNORE_NOTCH = false;
|
||||
public static int PREF_NOTCH_SIZE = 0;
|
||||
@@ -40,6 +39,8 @@ public class LauncherPreferences {
|
||||
public static final String PREF_VERSION_REPOS = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
|
||||
public static boolean PREF_CHECK_LIBRARY_SHA = true;
|
||||
public static boolean PREF_DISABLE_GESTURES = false;
|
||||
public static boolean PREF_GAMEPAD_SDL_PASSTHRU = false;
|
||||
public static boolean PREF_GAMEPAD_FORCEDSDL_PASSTHRU = false;
|
||||
public static boolean PREF_DISABLE_SWAP_HAND = false;
|
||||
public static float PREF_MOUSESPEED = 1f;
|
||||
public static int PREF_RAM_ALLOCATION;
|
||||
@@ -70,6 +71,10 @@ public class LauncherPreferences {
|
||||
public static String PREF_DOWNLOAD_SOURCE = "default";
|
||||
public static boolean PREF_SKIP_NOTIFICATION_PERMISSION_CHECK = false;
|
||||
public static boolean PREF_VSYNC_IN_ZINK = true;
|
||||
public static boolean PREF_FORCE_ENABLE_TOUCHCONTROLLER = false;
|
||||
public static int PREF_TOUCHCONTROLLER_VIBRATE_LENGTH = 100;
|
||||
|
||||
public static boolean PREF_MOUSE_GRAB_FORCE = false;
|
||||
|
||||
|
||||
public static void loadPreferences(Context ctx) {
|
||||
@@ -77,7 +82,6 @@ public class LauncherPreferences {
|
||||
Tools.initStorageConstants(ctx);
|
||||
boolean isDevicePowerful = isDevicePowerful(ctx);
|
||||
|
||||
PREF_RENDERER = DEFAULT_PREF.getString("renderer", "opengles2");
|
||||
PREF_BUTTONSIZE = DEFAULT_PREF.getInt("buttonscale", 100);
|
||||
PREF_MOUSESCALE = DEFAULT_PREF.getInt("mousescale", 100)/100f;
|
||||
PREF_MOUSESPEED = ((float)DEFAULT_PREF.getInt("mousespeed",100))/100f;
|
||||
@@ -87,6 +91,8 @@ public class LauncherPreferences {
|
||||
PREF_FORCE_ENGLISH = DEFAULT_PREF.getBoolean("force_english", false);
|
||||
PREF_CHECK_LIBRARY_SHA = DEFAULT_PREF.getBoolean("checkLibraries",true);
|
||||
PREF_DISABLE_GESTURES = DEFAULT_PREF.getBoolean("disableGestures",false);
|
||||
PREF_GAMEPAD_SDL_PASSTHRU = DEFAULT_PREF.getBoolean("gamepadPassthru",false);
|
||||
PREF_GAMEPAD_FORCEDSDL_PASSTHRU = DEFAULT_PREF.getBoolean("gamepadPassthruForced",false);
|
||||
PREF_DISABLE_SWAP_HAND = DEFAULT_PREF.getBoolean("disableDoubleTap", false);
|
||||
PREF_RAM_ALLOCATION = DEFAULT_PREF.getInt("allocation", findBestRAMAllocation(ctx));
|
||||
PREF_CUSTOM_JAVA_ARGS = DEFAULT_PREF.getString("javaArgs", "");
|
||||
@@ -112,6 +118,9 @@ public class LauncherPreferences {
|
||||
PREF_VERIFY_MANIFEST = DEFAULT_PREF.getBoolean("verifyManifest", true);
|
||||
PREF_SKIP_NOTIFICATION_PERMISSION_CHECK = DEFAULT_PREF.getBoolean(PREF_KEY_SKIP_NOTIFICATION_CHECK, false);
|
||||
PREF_VSYNC_IN_ZINK = DEFAULT_PREF.getBoolean("vsync_in_zink", true);
|
||||
PREF_FORCE_ENABLE_TOUCHCONTROLLER = DEFAULT_PREF.getBoolean("forceEnableTouchController", false);
|
||||
PREF_TOUCHCONTROLLER_VIBRATE_LENGTH = DEFAULT_PREF.getInt("touchControllerVibrateLength", 100);
|
||||
PREF_MOUSE_GRAB_FORCE = DEFAULT_PREF.getBoolean("always_grab_mouse", false);
|
||||
|
||||
String argLwjglLibname = "-Dorg.lwjgl.opengl.libname=";
|
||||
for (String arg : JREUtils.parseJavaArguments(PREF_CUSTOM_JAVA_ARGS)) {
|
||||
@@ -231,12 +240,16 @@ public class LauncherPreferences {
|
||||
// These guys are SwitchPreferences so they get special treatment, they need to be converted to ints
|
||||
int gl43exts = DEFAULT_PREF.getBoolean("mg_renderer_setting_gl43ext", false) ? 1 : 0;
|
||||
int computeShaderext = DEFAULT_PREF.getBoolean("mg_renderer_computeShaderext", false) ? 1 : 0;
|
||||
int angleDepthClearFixMode = DEFAULT_PREF.getBoolean("mg_renderer_setting_angleDepthClearFixMode", false) ? 1 : 0;
|
||||
int timerQueryExt = DEFAULT_PREF.getBoolean("mg_renderer_setting_timerQueryExt", false) ? 1 : 0;
|
||||
MGConfigJson.put("enableExtGL43", gl43exts);
|
||||
MGConfigJson.put("enableExtComputeShader", computeShaderext);
|
||||
|
||||
MGConfigJson.put("enableCompatibleMode", Integer.parseInt(DEFAULT_PREF.getString("", "0"))); // Placeholder, doesn't do anything on current MG
|
||||
MGConfigJson.put("multidrawMode", Integer.parseInt(DEFAULT_PREF.getString("mg_renderer_setting_multidraw", "0")));
|
||||
MGConfigJson.put("maxGlslCacheSize", Integer.parseInt(DEFAULT_PREF.getString("mg_renderer_setting_glsl_cache_size", "2048")));
|
||||
MGConfigJson.put("angleDepthClearFixMode", angleDepthClearFixMode);
|
||||
MGConfigJson.put("enableExtTimerQuery", timerQueryExt);
|
||||
if (DEFAULT_PREF.getBoolean("mg_renderer_multidrawCompute", false)) {
|
||||
MGConfigJson.put("multidrawMode", 5); // Special handling for the (special mayhaps) compute emulation
|
||||
} else MGConfigJson.put("multidrawMode", Integer.parseInt(DEFAULT_PREF.getString("mg_renderer_setting_multidraw", "0")));
|
||||
MGConfigJson.put("maxGlslCacheSize", Integer.parseInt(DEFAULT_PREF.getString("mg_renderer_setting_glsl_cache_size", "128")));
|
||||
File configFile = new File(Tools.DIR_DATA + "/MobileGlues", "config.json");
|
||||
FileUtils.ensureParentDirectory(configFile);
|
||||
try {
|
||||
|
||||
@@ -7,6 +7,7 @@ import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_GYRO_INVERT_Y;
|
||||
import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_GYRO_SENSITIVITY;
|
||||
import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_LONGPRESS_TRIGGER;
|
||||
import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_MOUSESPEED;
|
||||
import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_MOUSE_GRAB_FORCE;
|
||||
import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_SCALE_FACTOR;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
@@ -31,11 +32,11 @@ public abstract class QuickSettingSideDialog extends com.kdt.SideDialogView {
|
||||
|
||||
private SharedPreferences.Editor mEditor;
|
||||
@SuppressLint("UseSwitchCompatOrMaterialCode")
|
||||
private Switch mGyroSwitch, mGyroXSwitch, mGyroYSwitch, mGestureSwitch;
|
||||
private Switch mGyroSwitch, mGyroXSwitch, mGyroYSwitch, mGestureSwitch, mMouseGrabSwitch;
|
||||
private CustomSeekbar mGyroSensitivityBar, mMouseSpeedBar, mGestureDelayBar, mResolutionBar;
|
||||
private TextView mGyroSensitivityText, mGyroSensitivityDisplayText, mMouseSpeedText, mGestureDelayText, mGestureDelayDisplayText, mResolutionText;
|
||||
|
||||
private boolean mOriginalGyroEnabled, mOriginalGyroXEnabled, mOriginalGyroYEnabled, mOriginalGestureDisabled;
|
||||
private boolean mOriginalGyroEnabled, mOriginalGyroXEnabled, mOriginalGyroYEnabled, mOriginalGestureDisabled, mOriginalMouseGrab;
|
||||
private float mOriginalGyroSensitivity, mOriginalMouseSpeed, mOriginalResolution;
|
||||
private int mOriginalGestureDelay;
|
||||
|
||||
@@ -65,6 +66,7 @@ public abstract class QuickSettingSideDialog extends com.kdt.SideDialogView {
|
||||
mGyroXSwitch = mDialogContent.findViewById(R.id.checkboxGyroX);
|
||||
mGyroYSwitch = mDialogContent.findViewById(R.id.checkboxGyroY);
|
||||
mGestureSwitch = mDialogContent.findViewById(R.id.checkboxGesture);
|
||||
mMouseGrabSwitch = mDialogContent.findViewById(R.id.always_grab_mouse_side_dialog);
|
||||
|
||||
mGyroSensitivityBar = mDialogContent.findViewById(R.id.editGyro_seekbar);
|
||||
mMouseSpeedBar = mDialogContent.findViewById(R.id.editMouseSpeed_seekbar);
|
||||
@@ -86,6 +88,7 @@ public abstract class QuickSettingSideDialog extends com.kdt.SideDialogView {
|
||||
mOriginalGyroXEnabled = PREF_GYRO_INVERT_X;
|
||||
mOriginalGyroYEnabled = PREF_GYRO_INVERT_Y;
|
||||
mOriginalGestureDisabled = PREF_DISABLE_GESTURES;
|
||||
mOriginalMouseGrab = PREF_MOUSE_GRAB_FORCE;
|
||||
|
||||
mOriginalGyroSensitivity = PREF_GYRO_SENSITIVITY;
|
||||
mOriginalMouseSpeed = PREF_MOUSESPEED;
|
||||
@@ -96,6 +99,7 @@ public abstract class QuickSettingSideDialog extends com.kdt.SideDialogView {
|
||||
mGyroXSwitch.setChecked(mOriginalGyroXEnabled);
|
||||
mGyroYSwitch.setChecked(mOriginalGyroYEnabled);
|
||||
mGestureSwitch.setChecked(mOriginalGestureDisabled);
|
||||
mMouseGrabSwitch.setChecked(mOriginalMouseGrab);
|
||||
|
||||
mGyroSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
PREF_ENABLE_GYRO = isChecked;
|
||||
@@ -122,6 +126,11 @@ public abstract class QuickSettingSideDialog extends com.kdt.SideDialogView {
|
||||
mEditor.putBoolean("disableGestures", isChecked);
|
||||
});
|
||||
|
||||
mMouseGrabSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
PREF_MOUSE_GRAB_FORCE = isChecked;
|
||||
mEditor.putBoolean("always_grab_mouse", isChecked);
|
||||
});
|
||||
|
||||
mGyroSensitivityBar.setOnSeekBarChangeListener((SimpleSeekBarListener) (seekBar, progress, fromUser) -> {
|
||||
PREF_GYRO_SENSITIVITY = progress / 100f;
|
||||
mEditor.putInt("gyroSensitivity", progress);
|
||||
@@ -156,6 +165,7 @@ public abstract class QuickSettingSideDialog extends com.kdt.SideDialogView {
|
||||
setSeekTextPercent(mResolutionText, mResolutionBar.getProgress());
|
||||
|
||||
|
||||
updateMouseGrabVisibility();
|
||||
updateGyroVisibility(mOriginalGyroEnabled);
|
||||
updateGestureVisibility(mOriginalGestureDisabled);
|
||||
}
|
||||
@@ -172,6 +182,10 @@ public abstract class QuickSettingSideDialog extends com.kdt.SideDialogView {
|
||||
target.setText(target.getContext().getString(format, value));
|
||||
}
|
||||
|
||||
private void updateMouseGrabVisibility(){
|
||||
mMouseGrabSwitch.setVisibility(Tools.isPointerDeviceConnected()? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void updateGyroVisibility(boolean isEnabled) {
|
||||
int visibility = isEnabled ? View.VISIBLE : View.GONE;
|
||||
mGyroXSwitch.setVisibility(visibility);
|
||||
@@ -202,6 +216,7 @@ public abstract class QuickSettingSideDialog extends com.kdt.SideDialogView {
|
||||
mGyroXSwitch.setOnCheckedChangeListener(null);
|
||||
mGyroYSwitch.setOnCheckedChangeListener(null);
|
||||
mGestureSwitch.setOnCheckedChangeListener(null);
|
||||
mMouseGrabSwitch.setOnCheckedChangeListener(null);
|
||||
|
||||
mGyroSensitivityBar.setOnSeekBarChangeListener(null);
|
||||
mMouseSpeedBar.setOnSeekBarChangeListener(null);
|
||||
@@ -225,6 +240,7 @@ public abstract class QuickSettingSideDialog extends com.kdt.SideDialogView {
|
||||
PREF_GYRO_INVERT_X = mOriginalGyroXEnabled;
|
||||
PREF_GYRO_INVERT_Y = mOriginalGyroYEnabled;
|
||||
PREF_DISABLE_GESTURES = mOriginalGestureDisabled;
|
||||
PREF_MOUSE_GRAB_FORCE = mOriginalMouseGrab;
|
||||
|
||||
PREF_GYRO_SENSITIVITY = mOriginalGyroSensitivity;
|
||||
PREF_MOUSESPEED = mOriginalMouseSpeed;
|
||||
|
||||
@@ -13,6 +13,7 @@ import net.kdt.pojavlaunch.prefs.LauncherPreferences;
|
||||
|
||||
public class LauncherPreferenceControlFragment extends LauncherPreferenceFragment {
|
||||
private boolean mGyroAvailable = false;
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle b, String str) {
|
||||
// Get values
|
||||
@@ -20,6 +21,7 @@ public class LauncherPreferenceControlFragment extends LauncherPreferenceFragmen
|
||||
int prefButtonSize = (int) LauncherPreferences.PREF_BUTTONSIZE;
|
||||
int mouseScale = (int) (LauncherPreferences.PREF_MOUSESCALE * 100);
|
||||
int gyroSampleRate = LauncherPreferences.PREF_GYRO_SAMPLE_RATE;
|
||||
int touchControllerVibrateLength = LauncherPreferences.PREF_TOUCHCONTROLLER_VIBRATE_LENGTH;
|
||||
float mouseSpeed = LauncherPreferences.PREF_MOUSESPEED;
|
||||
float gyroSpeed = LauncherPreferences.PREF_GYRO_SENSITIVITY;
|
||||
float joystickDeadzone = LauncherPreferences.PREF_DEADZONE_SCALE;
|
||||
@@ -45,7 +47,7 @@ public class LauncherPreferenceControlFragment extends LauncherPreferenceFragmen
|
||||
|
||||
CustomSeekBarPreference seek6 = requirePreference("mousespeed",
|
||||
CustomSeekBarPreference.class);
|
||||
seek6.setValue((int)(mouseSpeed *100f));
|
||||
seek6.setValue((int) (mouseSpeed * 100f));
|
||||
seek6.setSuffix(" %");
|
||||
|
||||
CustomSeekBarPreference deadzoneSeek = requirePreference("gamepad_deadzone_scale",
|
||||
@@ -55,22 +57,29 @@ public class LauncherPreferenceControlFragment extends LauncherPreferenceFragmen
|
||||
|
||||
|
||||
Context context = getContext();
|
||||
if(context != null) {
|
||||
if (context != null) {
|
||||
mGyroAvailable = Tools.deviceSupportsGyro(context);
|
||||
}
|
||||
PreferenceCategory gyroCategory = requirePreference("gyroCategory",
|
||||
PreferenceCategory gyroCategory = requirePreference("gyroCategory",
|
||||
PreferenceCategory.class);
|
||||
gyroCategory.setVisible(mGyroAvailable);
|
||||
|
||||
CustomSeekBarPreference gyroSensitivitySeek = requirePreference("gyroSensitivity",
|
||||
CustomSeekBarPreference.class);
|
||||
gyroSensitivitySeek.setValue((int) (gyroSpeed*100f));
|
||||
gyroSensitivitySeek.setValue((int) (gyroSpeed * 100f));
|
||||
gyroSensitivitySeek.setSuffix(" %");
|
||||
|
||||
CustomSeekBarPreference gyroSampleRateSeek = requirePreference("gyroSampleRate",
|
||||
CustomSeekBarPreference.class);
|
||||
gyroSampleRateSeek.setValue(gyroSampleRate);
|
||||
gyroSampleRateSeek.setSuffix(" ms");
|
||||
|
||||
CustomSeekBarPreference touchControllerVibrateLengthSeek = requirePreference(
|
||||
"touchControllerVibrateLength",
|
||||
CustomSeekBarPreference.class);
|
||||
touchControllerVibrateLengthSeek.setValue(touchControllerVibrateLength);
|
||||
touchControllerVibrateLengthSeek.setSuffix(" ms");
|
||||
|
||||
computeVisibility();
|
||||
}
|
||||
|
||||
@@ -80,7 +89,7 @@ public class LauncherPreferenceControlFragment extends LauncherPreferenceFragmen
|
||||
computeVisibility();
|
||||
}
|
||||
|
||||
private void computeVisibility(){
|
||||
private void computeVisibility() {
|
||||
requirePreference("timeLongPressTrigger").setVisible(!LauncherPreferences.PREF_DISABLE_GESTURES);
|
||||
requirePreference("gyroSensitivity").setVisible(LauncherPreferences.PREF_ENABLE_GYRO);
|
||||
requirePreference("gyroSampleRate").setVisible(LauncherPreferences.PREF_ENABLE_GYRO);
|
||||
|
||||
@@ -35,6 +35,7 @@ public class LauncherPreferenceFragment extends PreferenceFragmentCompat impleme
|
||||
|
||||
private void setupNotificationRequestPreference() {
|
||||
Preference mRequestNotificationPermissionPreference = requirePreference("notification_permission_request");
|
||||
Preference mMicrophonePermissionPreference = requirePreference("microphone_permission_request");
|
||||
Activity activity = getActivity();
|
||||
if(activity instanceof LauncherActivity) {
|
||||
LauncherActivity launcherActivity = (LauncherActivity)activity;
|
||||
@@ -43,6 +44,11 @@ public class LauncherPreferenceFragment extends PreferenceFragmentCompat impleme
|
||||
launcherActivity.askForNotificationPermission(()->mRequestNotificationPermissionPreference.setVisible(false));
|
||||
return true;
|
||||
});
|
||||
mMicrophonePermissionPreference.setVisible(!launcherActivity.checkForMicrophonePermission());
|
||||
mMicrophonePermissionPreference.setOnPreferenceClickListener(preference -> {
|
||||
launcherActivity.askForMicrophonePermission(()->mMicrophonePermissionPreference.setVisible(false));
|
||||
return true;
|
||||
});
|
||||
}else{
|
||||
mRequestNotificationPermissionPreference.setVisible(false);
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.EditTextPreference;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.SwitchPreference;
|
||||
|
||||
import net.kdt.pojavlaunch.R;
|
||||
|
||||
@@ -18,10 +18,16 @@ import java.util.Objects;
|
||||
|
||||
public class LauncherPreferenceRendererSettingsFragment extends LauncherPreferenceFragment {
|
||||
EditTextPreference GLSLCachePreference;
|
||||
ListPreference MultiDrawEmulationPreference;
|
||||
SwitchPreference ComputeMultiDrawPreference;
|
||||
Preference.SummaryProvider MultiDrawSummaryProvider;
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle b, String str) {
|
||||
addPreferencesFromResource(R.xml.pref_renderer);
|
||||
GLSLCachePreference = findPreference("mg_renderer_setting_glsl_cache_size");
|
||||
ComputeMultiDrawPreference = findPreference("mg_renderer_multidrawCompute");
|
||||
MultiDrawEmulationPreference = findPreference("mg_renderer_setting_multidraw");
|
||||
GLSLCachePreference.setOnBindEditTextListener((editText) -> {
|
||||
editText.setInputType(TYPE_CLASS_NUMBER);
|
||||
editText.addTextChangedListener(new TextWatcher() {
|
||||
@@ -29,18 +35,20 @@ public class LauncherPreferenceRendererSettingsFragment extends LauncherPreferen
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
// Nothing, its boilerplate
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
// Nothing, its boilerplate
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
// This is just to handle the summary not updating when its above max int value
|
||||
// Horrible I know.
|
||||
if (editText.getText().toString().isEmpty()){
|
||||
if (editText.getText().toString().isEmpty()) {
|
||||
editText.setText("0");
|
||||
}
|
||||
if (Long.parseLong(editText.getText().toString()) > Integer.MAX_VALUE){
|
||||
if (Long.parseLong(editText.getText().toString()) > Integer.MAX_VALUE) {
|
||||
editText.setError("Too big! Setting to maximum value");
|
||||
editText.setText(String.valueOf(Integer.MAX_VALUE));
|
||||
}
|
||||
@@ -49,12 +57,30 @@ public class LauncherPreferenceRendererSettingsFragment extends LauncherPreferen
|
||||
});
|
||||
});
|
||||
updateGLSLCacheSummary(); // Just updates the summary with the value when user opens the menu. Yes it's out of place.
|
||||
updateMultiDrawSummary(); // Same as above
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences p, String s) {
|
||||
GLSLCachePreference = findPreference("mg_renderer_setting_glsl_cache_size");
|
||||
updateGLSLCacheSummary();
|
||||
updateMultiDrawSummary();
|
||||
}
|
||||
|
||||
private void updateMultiDrawSummary() {
|
||||
if (MultiDrawEmulationPreference != null) {
|
||||
if (MultiDrawEmulationPreference.getSummaryProvider() != null) {
|
||||
MultiDrawSummaryProvider = MultiDrawEmulationPreference.getSummaryProvider();
|
||||
}
|
||||
if (ComputeMultiDrawPreference.isChecked()) {
|
||||
MultiDrawEmulationPreference.setEnabled(false);
|
||||
MultiDrawEmulationPreference.setSummaryProvider(null);
|
||||
MultiDrawEmulationPreference.setSummary("(Experimental) Compute");
|
||||
} else if (MultiDrawEmulationPreference != null) {
|
||||
MultiDrawEmulationPreference.setEnabled(true);
|
||||
MultiDrawEmulationPreference.setSummaryProvider(MultiDrawSummaryProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateGLSLCacheSummary() {
|
||||
@@ -62,6 +88,8 @@ public class LauncherPreferenceRendererSettingsFragment extends LauncherPreferen
|
||||
if (Objects.equals(Objects.requireNonNull(this.GLSLCachePreference).getText(), "") || Integer.parseInt(Objects.requireNonNull(this.GLSLCachePreference.getText())) == 0) {
|
||||
this.GLSLCachePreference.setSummary(getString(R.string.global_off));
|
||||
} else this.GLSLCachePreference.setSummary(this.GLSLCachePreference.getText() + " MB");
|
||||
} catch (Exception e){ e.printStackTrace(); }
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,12 +45,6 @@ public class LauncherPreferenceVideoFragment extends LauncherPreferenceFragment
|
||||
requirePreference("alternate_surface", SwitchPreferenceCompat.class).setChecked(LauncherPreferences.PREF_USE_ALTERNATE_SURFACE);
|
||||
requirePreference("force_vsync", SwitchPreferenceCompat.class).setChecked(LauncherPreferences.PREF_FORCE_VSYNC);
|
||||
|
||||
ListPreference rendererListPreference = requirePreference("renderer",
|
||||
ListPreference.class);
|
||||
Tools.RenderersList renderersList = Tools.getCompatibleRenderers(getContext());
|
||||
rendererListPreference.setEntries(renderersList.rendererDisplayNames);
|
||||
rendererListPreference.setEntryValues(renderersList.rendererIds.toArray(new String[0]));
|
||||
|
||||
computeVisibility();
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ public class AsyncVersionList {
|
||||
|
||||
public void getVersionList(@Nullable VersionDoneListener listener, boolean secondPass){
|
||||
sExecutorService.execute(() -> {
|
||||
File versionFile = new File(Tools.DIR_DATA + "/version_list.json");
|
||||
File versionFile = new File(Tools.DIR_CACHE + "/version_list.json");
|
||||
JMinecraftVersionList versionList = null;
|
||||
try{
|
||||
if(!versionFile.exists() || (System.currentTimeMillis() > versionFile.lastModified() + 86400000 )){
|
||||
@@ -68,7 +68,7 @@ public class AsyncVersionList {
|
||||
|
||||
// Then save the version list
|
||||
//TODO make it not save at times ?
|
||||
FileOutputStream fos = new FileOutputStream(Tools.DIR_DATA + "/version_list.json");
|
||||
FileOutputStream fos = new FileOutputStream(Tools.DIR_CACHE + "/version_list.json");
|
||||
fos.write(jsonString.getBytes());
|
||||
fos.close();
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ package net.kdt.pojavlaunch.tasks;
|
||||
import static net.kdt.pojavlaunch.PojavApplication.sExecutorService;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -25,9 +28,12 @@ import net.kdt.pojavlaunch.value.DependentLibrary;
|
||||
import net.kdt.pojavlaunch.value.MinecraftClientInfo;
|
||||
import net.kdt.pojavlaunch.value.MinecraftLibraryArtifact;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -56,6 +62,7 @@ public class MinecraftDownloader {
|
||||
private static final ThreadLocal<byte[]> sThreadLocalDownloadBuffer = new ThreadLocal<>();
|
||||
|
||||
private boolean isLocalProfile = false;
|
||||
private boolean isOnline;
|
||||
|
||||
/**
|
||||
* Start the game version download process on the global executor service.
|
||||
@@ -65,11 +72,13 @@ public class MinecraftDownloader {
|
||||
* @param listener The download status listener
|
||||
*/
|
||||
public void start(@Nullable Activity activity, @Nullable JMinecraftVersionList.Version version,
|
||||
@NonNull String realVersion, // this was there for a reason
|
||||
@NonNull String realVersion,
|
||||
@NonNull AsyncMinecraftDownloader.DoneListener listener) {
|
||||
if(activity != null){
|
||||
isLocalProfile = Tools.isLocalProfile(activity);
|
||||
isOnline = Tools.isOnline(activity);
|
||||
Tools.switchDemo(Tools.isDemoProfile(activity));
|
||||
|
||||
} else {
|
||||
isLocalProfile = true;
|
||||
Tools.switchDemo(true);
|
||||
@@ -77,14 +86,32 @@ public class MinecraftDownloader {
|
||||
|
||||
sExecutorService.execute(() -> {
|
||||
try {
|
||||
if(isLocalProfile){
|
||||
throw new RuntimeException("Download failed. Please make sure you are logged in with a Microsoft Account.");
|
||||
}
|
||||
if(isLocalProfile || !isOnline) {
|
||||
String versionMessage = realVersion; // Use provided version unless we find its a modded instance
|
||||
|
||||
// See if provided version is a modded version and if that version depends on another jar, check for presence of both jar's .json.
|
||||
try {
|
||||
// This reads the .json associated with the provided version. If it fails, we can assume it's not installed.
|
||||
File providedJsonFile = new File(Tools.DIR_HOME_VERSION + "/" + realVersion + "/" + realVersion + ".json");
|
||||
JMinecraftVersionList.Version providedJson = Tools.GLOBAL_GSON.fromJson(Tools.read(providedJsonFile.getAbsolutePath()), JMinecraftVersionList.Version.class);
|
||||
|
||||
// This checks if running modded version that depends on other jars, so we use that for the error message.
|
||||
File vanillaJsonFile = new File(Tools.DIR_HOME_VERSION + "/" + providedJson.inheritsFrom + "/" + providedJson.inheritsFrom + ".json");
|
||||
versionMessage = providedJson.inheritsFrom != null ? providedJson.inheritsFrom : versionMessage;
|
||||
|
||||
// Ensure they're both not some 0 byte corrupted json
|
||||
if (providedJsonFile.length() == 0 || vanillaJsonFile.exists() && vanillaJsonFile.length() == 0){
|
||||
throw new RuntimeException("Minecraft "+versionMessage+ " is needed by " +realVersion); }
|
||||
|
||||
listener.onDownloadDone();
|
||||
} catch (Exception e) {
|
||||
String tryagain = !isOnline ? "Please ensure you have an internet connection" : "Please try again on your Microsoft Account";
|
||||
Tools.showErrorRemote(versionMessage + " is not currently installed. "+ tryagain, e);
|
||||
}
|
||||
}else {
|
||||
downloadGame(activity, version, realVersion);
|
||||
listener.onDownloadDone();
|
||||
}catch (UnknownHostException e){
|
||||
Log.i("DownloadMirror", e.toString());
|
||||
Tools.showErrorRemote("Can't download Minecraft, no internet connection found", e);
|
||||
}
|
||||
}catch (Exception e) {
|
||||
listener.onDownloadFailed(e);
|
||||
}
|
||||
@@ -473,18 +500,49 @@ public class MinecraftDownloader {
|
||||
* Since Minecraft libraries are stored in maven repositories, try to use
|
||||
* this when downloading libraries without hashes in the json.
|
||||
*/
|
||||
private void tryGetLibrarySha1() {
|
||||
private void tryGetLibrarySha1() throws IOException {
|
||||
File sha1CacheDir = new File(Tools.DIR_CACHE + "/sha1hashes");
|
||||
File cacheFile = new File(sha1CacheDir.getAbsolutePath() + FileUtils.getFileName(mTargetUrl) + ".sha");
|
||||
|
||||
// Only use cache when its offline. No point in having cache invalidation now!
|
||||
if (!isOnline || !LauncherPreferences.PREF_CHECK_LIBRARY_SHA) { // Well not only offlines..this setting speeds up launch times at least!
|
||||
try (BufferedReader cacheFileReader = new BufferedReader(new FileReader(cacheFile))) {
|
||||
mTargetSha1 = cacheFileReader.readLine();
|
||||
if (mTargetSha1 != null) {
|
||||
Log.i("MinecraftDownloader", "Reading Hash from cache: " + mTargetSha1 + " from " + cacheFile);
|
||||
} else if (cacheFile.exists()) {
|
||||
Log.i("MinecraftDownloader", "Deleting invalid hash from cache: " + cacheFile);
|
||||
cacheFile.delete();
|
||||
}
|
||||
} catch (FileNotFoundException ignored) {
|
||||
mTargetSha1 = null;
|
||||
Log.w("MinecraftDownloader", "Failed to read hash for " + cacheFile);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
String resultHash = null;
|
||||
try {
|
||||
resultHash = downloadSha1();
|
||||
// The hash is a 40-byte download.
|
||||
mInternetUsageCounter.getAndAdd(40);
|
||||
}catch (IOException e) {
|
||||
} catch (IOException e) {
|
||||
Log.i("MinecraftDownloader", "Failed to download hash", e);
|
||||
if (cacheFile.exists() && new BufferedReader(new FileReader(cacheFile)).readLine() == null) {
|
||||
Log.i("MinecraftDownloader", "Deleting failed hash download from cache: " + cacheFile);
|
||||
cacheFile.delete();
|
||||
}
|
||||
}
|
||||
if(resultHash != null) {
|
||||
Log.i("MinecraftDownloader", "Got hash: "+resultHash+ " for "+FileUtils.getFileName(mTargetUrl));
|
||||
if (resultHash != null) {
|
||||
Log.i("MinecraftDownloader", "Got hash: " + resultHash + " for " + FileUtils.getFileName(mTargetUrl));
|
||||
mTargetSha1 = resultHash;
|
||||
if (!sha1CacheDir.exists()) {
|
||||
sha1CacheDir.mkdir(); // If mkdir() fails, something went wrong with initializing /data/data/. mkdirs() isn't used on purpose
|
||||
}
|
||||
try (FileWriter writeHash = new FileWriter(cacheFile)) {
|
||||
Log.i("MinecraftDownloader", "Saving hash: " + resultHash + " for " + FileUtils.getFileName(mTargetUrl) + " to " + cacheFile);
|
||||
writeHash.write(resultHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,12 @@ public class DownloadUtils {
|
||||
FileUtils.ensureParentDirectory(out);
|
||||
try (FileOutputStream fileOutputStream = new FileOutputStream(out)) {
|
||||
download(url, fileOutputStream);
|
||||
} catch (IOException e) {
|
||||
if (out.length() < 1) { // Only delete it if file is 0 bytes cause this file might already be downloaded and something else went wrong.
|
||||
Log.i("DownloadUtils", "Cleaning up failed download: " + out.getAbsolutePath());
|
||||
out.delete();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import net.kdt.pojavlaunch.multirt.MultiRTUtils;
|
||||
import net.kdt.pojavlaunch.multirt.Runtime;
|
||||
import net.kdt.pojavlaunch.plugins.FFmpegPlugin;
|
||||
import net.kdt.pojavlaunch.prefs.*;
|
||||
|
||||
import org.lwjgl.glfw.*;
|
||||
|
||||
public class JREUtils {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package net.kdt.pojavlaunch.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Vibrator;
|
||||
|
||||
import top.fifthlight.touchcontroller.proxy.client.LauncherProxyClient;
|
||||
import top.fifthlight.touchcontroller.proxy.client.MessageTransport;
|
||||
import top.fifthlight.touchcontroller.proxy.client.android.transport.UnixSocketTransportKt;
|
||||
import top.fifthlight.touchcontroller.proxy.message.VibrateMessage;
|
||||
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.util.Log;
|
||||
import android.util.SparseIntArray;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import net.kdt.pojavlaunch.prefs.LauncherPreferences;
|
||||
|
||||
public class TouchControllerUtils {
|
||||
private TouchControllerUtils() {
|
||||
}
|
||||
|
||||
public static LauncherProxyClient proxyClient;
|
||||
private static final String socketName = "Amethyst";
|
||||
|
||||
private static class VibrationHandler implements LauncherProxyClient.VibrationHandler {
|
||||
private final Vibrator vibrator;
|
||||
|
||||
public VibrationHandler(Vibrator vibrator) {
|
||||
this.vibrator = vibrator;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("DEPRECATION")
|
||||
public void vibrate(@NonNull VibrateMessage.Kind kind) {
|
||||
vibrator.vibrate(LauncherPreferences.PREF_TOUCHCONTROLLER_VIBRATE_LENGTH);
|
||||
}
|
||||
}
|
||||
|
||||
private static final SparseIntArray pointerIdMap = new SparseIntArray();
|
||||
private static int nextPointerId = 1;
|
||||
|
||||
public static void processTouchEvent(MotionEvent motionEvent, View view) {
|
||||
if (proxyClient == null) {
|
||||
return;
|
||||
}
|
||||
int pointerId;
|
||||
switch (motionEvent.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
pointerId = nextPointerId++;
|
||||
pointerIdMap.put(motionEvent.getPointerId(0), pointerId);
|
||||
proxyClient.addPointer(pointerId, motionEvent.getX(0) / view.getWidth(), motionEvent.getY(0) / view.getHeight());
|
||||
break;
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
pointerId = nextPointerId++;
|
||||
int actionIndex = motionEvent.getActionIndex();
|
||||
pointerIdMap.put(motionEvent.getPointerId(actionIndex), pointerId);
|
||||
proxyClient.addPointer(pointerId, motionEvent.getX(actionIndex) / view.getWidth(), motionEvent.getY(actionIndex) / view.getHeight());
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
for (int i = 0; i < motionEvent.getPointerCount(); i++) {
|
||||
pointerId = pointerIdMap.get(motionEvent.getPointerId(i));
|
||||
if (pointerId == 0) {
|
||||
Log.d("TouchController", "Move pointerId is 0");
|
||||
continue;
|
||||
}
|
||||
proxyClient.addPointer(pointerId, motionEvent.getX(i) / view.getWidth(), motionEvent.getY(i) / view.getHeight());
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
if (proxyClient != null) {
|
||||
proxyClient.clearPointer();
|
||||
pointerIdMap.clear();
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
if (proxyClient != null) {
|
||||
int i = motionEvent.getActionIndex();
|
||||
pointerId = pointerIdMap.get(motionEvent.getPointerId(i));
|
||||
if (pointerId == 0) {
|
||||
Log.d("TouchController", "Pointer up pointerId is 0");
|
||||
break;
|
||||
}
|
||||
pointerIdMap.delete(pointerId);
|
||||
proxyClient.removePointer(pointerId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void initialize(Context context) {
|
||||
if (proxyClient != null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Os.setenv("TOUCH_CONTROLLER_PROXY_SOCKET", socketName, true);
|
||||
} catch (ErrnoException e) {
|
||||
Log.w("TouchController", "Failed to set TouchController environment variable", e);
|
||||
}
|
||||
MessageTransport transport = UnixSocketTransportKt.UnixSocketTransport(socketName);
|
||||
proxyClient = new LauncherProxyClient(transport);
|
||||
proxyClient.run();
|
||||
Vibrator vibrator = ContextCompat.getSystemService(context, Vibrator.class);
|
||||
if (vibrator != null) {
|
||||
LauncherProxyClient.VibrationHandler vibrationHandler = new VibrationHandler(vibrator);
|
||||
proxyClient.setVibrationHandler(vibrationHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import android.graphics.Bitmap;
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
@@ -68,7 +69,7 @@ public class MinecraftAccount {
|
||||
public static MinecraftAccount parse(String content) throws JsonSyntaxException {
|
||||
return Tools.GLOBAL_GSON.fromJson(content, MinecraftAccount.class);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static MinecraftAccount load(String name) {
|
||||
if(!accountExists(name)) return null;
|
||||
try {
|
||||
@@ -92,7 +93,7 @@ public class MinecraftAccount {
|
||||
acc.msaRefreshToken = "0";
|
||||
}
|
||||
return acc;
|
||||
} catch(IOException | JsonSyntaxException e) {
|
||||
} catch(NullPointerException | IOException | JsonSyntaxException e) {
|
||||
Log.e(MinecraftAccount.class.getName(), "Caught an exception while loading the profile",e);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* This file is part of SDL3 android-project java code.
|
||||
* Licensed under the zlib license: https://www.libsdl.org/license.php
|
||||
*/
|
||||
|
||||
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 writeReport(byte[] report, boolean feature);
|
||||
public boolean readReport(byte[] report, boolean feature);
|
||||
public void setFrozen(boolean frozen);
|
||||
public void close();
|
||||
public void shutdown();
|
||||
}
|
||||
@@ -0,0 +1,650 @@
|
||||
/*
|
||||
* This file is part of SDL3 android-project java code.
|
||||
* Licensed under the zlib license: https://www.libsdl.org/license.php
|
||||
*/
|
||||
|
||||
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.HIDDeviceReportResponse(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, true);
|
||||
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 writeReport(byte[] report, boolean feature) {
|
||||
if (!isRegistered()) {
|
||||
Log.e(TAG, "Attempted writeReport before Steam Controller is registered!");
|
||||
if (mIsConnected) {
|
||||
probeService(this);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (feature) {
|
||||
// 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, "writeFeatureReport " + HexDump.dumpHexString(actual_report));
|
||||
writeCharacteristic(reportCharacteristic, actual_report);
|
||||
return report.length;
|
||||
} else {
|
||||
//Log.v(TAG, "writeOutputReport " + HexDump.dumpHexString(report));
|
||||
writeCharacteristic(reportCharacteristic, report);
|
||||
return report.length;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readReport(byte[] report, boolean feature) {
|
||||
if (!isRegistered()) {
|
||||
Log.e(TAG, "Attempted readReport before Steam Controller is registered!");
|
||||
if (mIsConnected) {
|
||||
probeService(this);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (feature) {
|
||||
readCharacteristic(reportCharacteristic);
|
||||
return true;
|
||||
} else {
|
||||
// Not implemented
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,690 @@
|
||||
/*
|
||||
* This file is part of SDL3 android-project java code.
|
||||
* Licensed under the zlib license: https://www.libsdl.org/license.php
|
||||
*/
|
||||
|
||||
package org.libsdl.app;
|
||||
|
||||
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.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.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);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
mContext.registerReceiver(mUsbBroadcast, filter, Context.RECEIVER_EXPORTED);
|
||||
} else {
|
||||
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 = {
|
||||
0x03f0, // HP
|
||||
0x044f, // Thrustmaster
|
||||
0x045e, // Microsoft
|
||||
0x0738, // Mad Catz
|
||||
0x0b05, // ASUS
|
||||
0x0e6f, // PDP
|
||||
0x0f0d, // Hori
|
||||
0x10f5, // Turtle Beach
|
||||
0x1532, // Razer Wildcat
|
||||
0x20d6, // PowerA
|
||||
0x24c6, // PowerA
|
||||
0x2dc8, // 8BitDo
|
||||
0x2e24, // Hyperkin
|
||||
0x3537, // GameSir
|
||||
};
|
||||
|
||||
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(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeBluetooth() {
|
||||
Log.d(TAG, "Initializing Bluetooth");
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 31 /* Android 12 */ &&
|
||||
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH_CONNECT, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH_CONNECT");
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
mContext.registerReceiver(mBluetoothBroadcast, filter, Context.RECEIVER_EXPORTED);
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 33 /* Android 14.0 (U) */) {
|
||||
Intent intent = new Intent(HIDDeviceManager.ACTION_USB_PERMISSION);
|
||||
intent.setPackage(mContext.getPackageName());
|
||||
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, intent, flags));
|
||||
} else {
|
||||
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 writeReport(int deviceID, byte[] report, boolean feature) {
|
||||
try {
|
||||
//Log.v(TAG, "writeReport deviceID=" + deviceID + " length=" + report.length);
|
||||
HIDDevice device;
|
||||
device = getDevice(deviceID);
|
||||
if (device == null) {
|
||||
HIDDeviceDisconnected(deviceID);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return device.writeReport(report, feature);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public boolean readReport(int deviceID, byte[] report, boolean feature) {
|
||||
try {
|
||||
//Log.v(TAG, "readReport deviceID=" + deviceID);
|
||||
HIDDevice device;
|
||||
device = getDevice(deviceID);
|
||||
if (device == null) {
|
||||
HIDDeviceDisconnected(deviceID);
|
||||
return false;
|
||||
}
|
||||
|
||||
return device.readReport(report, feature);
|
||||
} 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, boolean bBluetooth);
|
||||
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 HIDDeviceReportResponse(int deviceID, byte[] report);
|
||||
}
|
||||
323
app_pojavlauncher/src/main/java/org/libsdl/app/HIDDeviceUSB.java
Normal file
323
app_pojavlauncher/src/main/java/org/libsdl/app/HIDDeviceUSB.java
Normal file
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
* This file is part of SDL3 android-project java code.
|
||||
* Licensed under the zlib license: https://www.libsdl.org/license.php
|
||||
*/
|
||||
|
||||
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 writeReport(byte[] report, boolean feature) {
|
||||
if (mConnection == null) {
|
||||
Log.w(TAG, "writeReport() called with no device connection");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (feature) {
|
||||
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, "writeFeatureReport() returned " + res + " on device " + getDeviceName());
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (skipped_report_id) {
|
||||
++length;
|
||||
}
|
||||
return length;
|
||||
} else {
|
||||
int res = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000);
|
||||
if (res != report.length) {
|
||||
Log.w(TAG, "writeOutputReport() returned " + res + " on device " + getDeviceName());
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readReport(byte[] report, boolean feature) {
|
||||
int res = -1;
|
||||
int offset = 0;
|
||||
int length = report.length;
|
||||
boolean skipped_report_id = false;
|
||||
byte report_number = report[0];
|
||||
|
||||
if (mConnection == null) {
|
||||
Log.w(TAG, "readReport() called with no device connection");
|
||||
return false;
|
||||
}
|
||||
|
||||
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*/,
|
||||
((feature ? 3/*HID feature*/ : 1/*HID Input*/) << 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.HIDDeviceReportResponse(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
95
app_pojavlauncher/src/main/java/org/libsdl/app/SDL.java
Normal file
95
app_pojavlauncher/src/main/java/org/libsdl/app/SDL.java
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* This file is part of SDL3 android-project java code.
|
||||
* Licensed under the zlib license: https://www.libsdl.org/license.php
|
||||
*/
|
||||
|
||||
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 {
|
||||
loadLibrary(libraryName, mContext);
|
||||
}
|
||||
|
||||
public static void loadLibrary(String libraryName, Context context) 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 = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker");
|
||||
Class<?> relinkListenerClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener");
|
||||
Class<?> contextClass = context.getClassLoader().loadClass("android.content.Context");
|
||||
Class<?> stringClass = context.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, context, 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;
|
||||
}
|
||||
2233
app_pojavlauncher/src/main/java/org/libsdl/app/SDLActivity.java
Normal file
2233
app_pojavlauncher/src/main/java/org/libsdl/app/SDLActivity.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* This file is part of SDL3 android-project java code.
|
||||
* Licensed under the zlib license: https://www.libsdl.org/license.php
|
||||
*/
|
||||
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioDeviceCallback;
|
||||
import android.media.AudioDeviceInfo;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
public class SDLAudioManager {
|
||||
protected static final String TAG = "SDLAudio";
|
||||
|
||||
protected static Context mContext;
|
||||
|
||||
private static AudioDeviceCallback mAudioDeviceCallback;
|
||||
|
||||
public static void initialize() {
|
||||
mAudioDeviceCallback = null;
|
||||
|
||||
if(Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */)
|
||||
{
|
||||
mAudioDeviceCallback = new AudioDeviceCallback() {
|
||||
@Override
|
||||
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
|
||||
for (AudioDeviceInfo deviceInfo : addedDevices) {
|
||||
addAudioDevice(deviceInfo.isSink(), deviceInfo.getProductName().toString(), deviceInfo.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
|
||||
for (AudioDeviceInfo deviceInfo : removedDevices) {
|
||||
removeAudioDevice(deviceInfo.isSink(), deviceInfo.getId());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static void setContext(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
public static void release(Context context) {
|
||||
// no-op atm
|
||||
}
|
||||
|
||||
// Audio
|
||||
|
||||
private static AudioDeviceInfo getInputAudioDeviceInfo(int deviceId) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
for (AudioDeviceInfo deviceInfo : audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) {
|
||||
if (deviceInfo.getId() == deviceId) {
|
||||
return deviceInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static AudioDeviceInfo getPlaybackAudioDeviceInfo(int deviceId) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
for (AudioDeviceInfo deviceInfo : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) {
|
||||
if (deviceInfo.getId() == deviceId) {
|
||||
return deviceInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void registerAudioDeviceCallback() {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
// get an initial list now, before hotplug callbacks fire.
|
||||
for (AudioDeviceInfo dev : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) {
|
||||
if (dev.getType() == AudioDeviceInfo.TYPE_TELEPHONY) {
|
||||
continue; // Device cannot be opened
|
||||
}
|
||||
addAudioDevice(dev.isSink(), dev.getProductName().toString(), dev.getId());
|
||||
}
|
||||
for (AudioDeviceInfo dev : audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) {
|
||||
addAudioDevice(dev.isSink(), dev.getProductName().toString(), dev.getId());
|
||||
}
|
||||
audioManager.registerAudioDeviceCallback(mAudioDeviceCallback, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void unregisterAudioDeviceCallback() {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
audioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback);
|
||||
}
|
||||
}
|
||||
|
||||
/** This method is called by SDL using JNI. */
|
||||
public static void audioSetThreadPriority(boolean recording, int device_id) {
|
||||
try {
|
||||
|
||||
/* Set thread name */
|
||||
if (recording) {
|
||||
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 recording, int deviceId);
|
||||
|
||||
public static native void addAudioDevice(boolean recording, String name, int deviceId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,876 @@
|
||||
/*
|
||||
* This file is part of SDL3 android-project java code.
|
||||
* This file has been modified for this project's needs.
|
||||
* Licensed under the zlib license: https://www.libsdl.org/license.php
|
||||
*/
|
||||
|
||||
package org.libsdl.app;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
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.os.VibratorManager;
|
||||
import android.system.Os;
|
||||
import android.util.Log;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import net.kdt.pojavlaunch.MinecraftGLSurface;
|
||||
import net.kdt.pojavlaunch.customcontrols.gamepad.direct.DirectGamepadEnableHandler;
|
||||
import net.kdt.pojavlaunch.prefs.LauncherPreferences;
|
||||
|
||||
|
||||
public class SDLControllerManager
|
||||
{
|
||||
|
||||
public static native int nativeSetupJNI();
|
||||
|
||||
public static native void nativeAddJoystick(int device_id, String name, String desc,
|
||||
int vendor_id, int product_id,
|
||||
int button_mask,
|
||||
int naxes, int axis_mask, int nhats, boolean can_rumble);
|
||||
public static native void nativeRemoveJoystick(int device_id);
|
||||
public static native void nativeAddHaptic(int device_id, String name);
|
||||
public static native void nativeRemoveHaptic(int device_id);
|
||||
public static native boolean onNativePadDown(int device_id, int keycode);
|
||||
public static native boolean 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 >= 31 /* Android 12.0 (S) */) {
|
||||
mHapticHandler = new SDLHapticHandler_API31();
|
||||
} else 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 hapticRumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) {
|
||||
mHapticHandler.rumble(device_id, low_frequency_intensity, high_frequency_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)
|
||||
);
|
||||
}
|
||||
|
||||
public static void setDirectGamepadEnableHandler(DirectGamepadEnableHandler h) {
|
||||
SDLJoystickHandler_API16.sDirectGamepadEnableHandler = h;
|
||||
}
|
||||
}
|
||||
|
||||
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>();
|
||||
}
|
||||
|
||||
protected static DirectGamepadEnableHandler sDirectGamepadEnableHandler;
|
||||
private static boolean firstPollDone = false;
|
||||
@Override
|
||||
public void pollInputDevices() {
|
||||
if (!firstPollDone) {
|
||||
MinecraftGLSurface.sdlEnabled = true;
|
||||
if (sDirectGamepadEnableHandler != null){
|
||||
sDirectGamepadEnableHandler.onDirectGamepadEnabled();
|
||||
}
|
||||
Log.i("SDL", "SDL detected! Enabling..");
|
||||
sDirectGamepadEnableHandler = null;
|
||||
firstPollDone = true;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean can_rumble = false;
|
||||
if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) {
|
||||
VibratorManager manager = joystickDevice.getVibratorManager();
|
||||
int[] vibrators = manager.getVibratorIds();
|
||||
if (vibrators.length > 0) {
|
||||
can_rumble = true;
|
||||
}
|
||||
}
|
||||
|
||||
mJoysticks.add(joystick);
|
||||
SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc,
|
||||
getVendorId(joystickDevice), getProductId(joystickDevice),
|
||||
getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, can_rumble);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 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_API31 extends SDLHapticHandler {
|
||||
@Override
|
||||
public void run(int device_id, float intensity, int length) {
|
||||
SDLHaptic haptic = getHaptic(device_id);
|
||||
if (haptic != null) {
|
||||
vibrate(haptic.vib, intensity, length);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) {
|
||||
InputDevice device = InputDevice.getDevice(device_id);
|
||||
if (device == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
VibratorManager manager = device.getVibratorManager();
|
||||
int[] vibrators = manager.getVibratorIds();
|
||||
if (vibrators.length >= 2) {
|
||||
vibrate(manager.getVibrator(vibrators[0]), low_frequency_intensity, length);
|
||||
vibrate(manager.getVibrator(vibrators[1]), high_frequency_intensity, length);
|
||||
} else if (vibrators.length == 1) {
|
||||
float intensity = (low_frequency_intensity * 0.6f) + (high_frequency_intensity * 0.4f);
|
||||
vibrate(manager.getVibrator(vibrators[0]), intensity, length);
|
||||
}
|
||||
}
|
||||
|
||||
private void vibrate(Vibrator vibrator, float intensity, int length) {
|
||||
if (intensity == 0.0f) {
|
||||
vibrator.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
int value = Math.round(intensity * 255);
|
||||
if (value > 255) {
|
||||
value = 255;
|
||||
}
|
||||
if (value < 1) {
|
||||
vibrator.cancel();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(length, value));
|
||||
}
|
||||
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.
|
||||
vibrator.vibrate(length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SDLHapticHandler_API26 extends SDLHapticHandler {
|
||||
@Override
|
||||
public void run(int device_id, float intensity, int length) {
|
||||
SDLHaptic haptic = getHaptic(device_id);
|
||||
if (haptic != null) {
|
||||
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 rumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) {
|
||||
// Not supported in older APIs
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
/* 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;
|
||||
if (device_id != deviceId_VIBRATOR_SERVICE || !hasVibratorService) {
|
||||
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_API14 implements View.OnGenericMotionListener {
|
||||
// Generic Motion (mouse hover, joystick...) events go here
|
||||
@Override
|
||||
public boolean onGenericMotion(View v, MotionEvent event) {
|
||||
if (event.getSource() == InputDevice.SOURCE_JOYSTICK)
|
||||
return SDLControllerManager.handleJoystickMotionEvent(event);
|
||||
|
||||
float x, y;
|
||||
int action = event.getActionMasked();
|
||||
int pointerCount = event.getPointerCount();
|
||||
boolean consumed = false;
|
||||
|
||||
for (int i = 0; i < pointerCount; i++) {
|
||||
int toolType = event.getToolType(i);
|
||||
|
||||
if (toolType == MotionEvent.TOOL_TYPE_MOUSE) {
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_SCROLL:
|
||||
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, i);
|
||||
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, i);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||
consumed = true;
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
x = getEventX(event, i);
|
||||
y = getEventY(event, i);
|
||||
|
||||
SDLActivity.onNativeMouse(0, action, x, y, checkRelativeEvent(event));
|
||||
consumed = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (toolType == MotionEvent.TOOL_TYPE_STYLUS || toolType == MotionEvent.TOOL_TYPE_ERASER) {
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_HOVER_ENTER:
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
case MotionEvent.ACTION_HOVER_EXIT:
|
||||
x = event.getX(i);
|
||||
y = event.getY(i);
|
||||
float 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;
|
||||
}
|
||||
|
||||
// BUTTON_STYLUS_PRIMARY is 2^5, so shift by 4, and apply SDL_PEN_INPUT_DOWN/SDL_PEN_INPUT_ERASER_TIP
|
||||
int buttons = (event.getButtonState() >> 4) | (1 << (toolType == MotionEvent.TOOL_TYPE_STYLUS ? 0 : 30));
|
||||
|
||||
SDLActivity.onNativePen(event.getPointerId(i), buttons, action, x, y, p);
|
||||
consumed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
public boolean supportsRelativeMouse() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean inRelativeMode() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean setRelativeMouseEnabled(boolean enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void reclaimRelativeMouseModeIfNeeded() {
|
||||
|
||||
}
|
||||
|
||||
public boolean checkRelativeEvent(MotionEvent event) {
|
||||
return inRelativeMode();
|
||||
}
|
||||
|
||||
public float getEventX(MotionEvent event, int pointerIndex) {
|
||||
return event.getX(pointerIndex);
|
||||
}
|
||||
|
||||
public float getEventY(MotionEvent event, int pointerIndex) {
|
||||
return event.getY(pointerIndex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SDLGenericMotionListener_API24 extends SDLGenericMotionListener_API14 {
|
||||
// Generic Motion (mouse hover, joystick...) events go here
|
||||
|
||||
private boolean mRelativeModeEnabled;
|
||||
|
||||
@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, int pointerIndex) {
|
||||
if (mRelativeModeEnabled && event.getToolType(pointerIndex) == MotionEvent.TOOL_TYPE_MOUSE) {
|
||||
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X, pointerIndex);
|
||||
} else {
|
||||
return event.getX(pointerIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getEventY(MotionEvent event, int pointerIndex) {
|
||||
if (mRelativeModeEnabled && event.getToolType(pointerIndex) == MotionEvent.TOOL_TYPE_MOUSE) {
|
||||
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y, pointerIndex);
|
||||
} else {
|
||||
return event.getY(pointerIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 {
|
||||
// Generic Motion (mouse hover, joystick...) events go here
|
||||
private boolean mRelativeModeEnabled;
|
||||
|
||||
@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 boolean checkRelativeEvent(MotionEvent event) {
|
||||
return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getEventX(MotionEvent event, int pointerIndex) {
|
||||
// Relative mouse in capture mode will only have relative for X/Y
|
||||
return event.getX(pointerIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getEventY(MotionEvent event, int pointerIndex) {
|
||||
// Relative mouse in capture mode will only have relative for X/Y
|
||||
return event.getY(pointerIndex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* This file is part of SDL3 android-project java code.
|
||||
* Licensed under the zlib license: https://www.libsdl.org/license.php
|
||||
*/
|
||||
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.content.*;
|
||||
import android.view.*;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
/* This is a fake invisible editor view that receives the input and defines the
|
||||
* pan&scan region
|
||||
*/
|
||||
public class SDLDummyEdit extends View implements View.OnKeyListener
|
||||
{
|
||||
InputConnection ic;
|
||||
int input_type;
|
||||
|
||||
public SDLDummyEdit(Context context) {
|
||||
super(context);
|
||||
setFocusableInTouchMode(true);
|
||||
setFocusable(true);
|
||||
setOnKeyListener(this);
|
||||
}
|
||||
|
||||
public void setInputType(int input_type) {
|
||||
this.input_type = input_type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCheckIsTextEditor() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
return SDLActivity.handleKeyEvent(v, keyCode, event, ic);
|
||||
}
|
||||
|
||||
//
|
||||
@Override
|
||||
public boolean onKeyPreIme (int keyCode, KeyEvent event) {
|
||||
// As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event
|
||||
// FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639
|
||||
// FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not
|
||||
// FIXME: A more effective solution would be to assume our Layout to be RelativeLayout or LinearLayout
|
||||
// FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android
|
||||
// FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :)
|
||||
if (event.getAction()==KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
if (SDLActivity.mTextEdit != null && SDLActivity.mTextEdit.getVisibility() == View.VISIBLE) {
|
||||
SDLActivity.onNativeKeyboardFocusLost();
|
||||
}
|
||||
}
|
||||
return super.onKeyPreIme(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
||||
ic = new SDLInputConnection(this, true);
|
||||
|
||||
outAttrs.inputType = input_type;
|
||||
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI |
|
||||
EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */;
|
||||
|
||||
return ic;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* This file is part of SDL3 android-project java code.
|
||||
* Licensed under the zlib license: https://www.libsdl.org/license.php
|
||||
*/
|
||||
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.os.Build;
|
||||
import android.text.Editable;
|
||||
import android.view.*;
|
||||
import android.view.inputmethod.BaseInputConnection;
|
||||
import android.widget.EditText;
|
||||
|
||||
public class SDLInputConnection extends BaseInputConnection
|
||||
{
|
||||
protected EditText mEditText;
|
||||
protected String mCommittedText = "";
|
||||
|
||||
public SDLInputConnection(View targetView, boolean fullEditor) {
|
||||
super(targetView, fullEditor);
|
||||
mEditText = new EditText(SDL.getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editable getEditable() {
|
||||
return mEditText.getEditableText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sendKeyEvent(KeyEvent event) {
|
||||
/*
|
||||
* This used to handle the keycodes from soft keyboard (and IME-translated input from hardkeyboard)
|
||||
* However, as of Ice Cream Sandwich and later, almost all soft keyboard doesn't generate key presses
|
||||
* and so we need to generate them ourselves in commitText. To avoid duplicates on the handful of keys
|
||||
* that still do, we empty this out.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Return DOES still generate a key event, however. So rather than using it as the 'click a button' key
|
||||
* as we do with physical keyboards, let's just use it to hide the keyboard.
|
||||
*/
|
||||
|
||||
if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
|
||||
if (SDLActivity.onNativeSoftReturnKey()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return super.sendKeyEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean commitText(CharSequence text, int newCursorPosition) {
|
||||
if (!super.commitText(text, newCursorPosition)) {
|
||||
return false;
|
||||
}
|
||||
updateText();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setComposingText(CharSequence text, int newCursorPosition) {
|
||||
if (!super.setComposingText(text, newCursorPosition)) {
|
||||
return false;
|
||||
}
|
||||
updateText();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
|
||||
if (Build.VERSION.SDK_INT <= 29 /* Android 10.0 (Q) */) {
|
||||
// Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection
|
||||
// and https://bugzilla.libsdl.org/show_bug.cgi?id=2265
|
||||
if (beforeLength > 0 && afterLength == 0) {
|
||||
// backspace(s)
|
||||
while (beforeLength-- > 0) {
|
||||
nativeGenerateScancodeForUnichar('\b');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!super.deleteSurroundingText(beforeLength, afterLength)) {
|
||||
return false;
|
||||
}
|
||||
updateText();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void updateText() {
|
||||
final Editable content = getEditable();
|
||||
if (content == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String text = content.toString();
|
||||
int compareLength = Math.min(text.length(), mCommittedText.length());
|
||||
int matchLength, offset;
|
||||
|
||||
/* Backspace over characters that are no longer in the string */
|
||||
for (matchLength = 0; matchLength < compareLength; ) {
|
||||
int codePoint = mCommittedText.codePointAt(matchLength);
|
||||
if (codePoint != text.codePointAt(matchLength)) {
|
||||
break;
|
||||
}
|
||||
matchLength += Character.charCount(codePoint);
|
||||
}
|
||||
/* FIXME: This doesn't handle graphemes, like '🌬️' */
|
||||
for (offset = matchLength; offset < mCommittedText.length(); ) {
|
||||
int codePoint = mCommittedText.codePointAt(offset);
|
||||
nativeGenerateScancodeForUnichar('\b');
|
||||
offset += Character.charCount(codePoint);
|
||||
}
|
||||
|
||||
if (matchLength < text.length()) {
|
||||
String pendingText = text.subSequence(matchLength, text.length()).toString();
|
||||
if (!SDLActivity.dispatchingKeyEvent()) {
|
||||
for (offset = 0; offset < pendingText.length(); ) {
|
||||
int codePoint = pendingText.codePointAt(offset);
|
||||
if (codePoint == '\n') {
|
||||
if (SDLActivity.onNativeSoftReturnKey()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
/* Higher code points don't generate simulated scancodes */
|
||||
if (codePoint > 0 && codePoint < 128) {
|
||||
nativeGenerateScancodeForUnichar((char)codePoint);
|
||||
}
|
||||
offset += Character.charCount(codePoint);
|
||||
}
|
||||
}
|
||||
SDLInputConnection.nativeCommitText(pendingText, 0);
|
||||
}
|
||||
mCommittedText = text;
|
||||
}
|
||||
|
||||
public static native void nativeCommitText(String text, int newCursorPosition);
|
||||
|
||||
public static native void nativeGenerateScancodeForUnichar(char c);
|
||||
}
|
||||
|
||||
412
app_pojavlauncher/src/main/java/org/libsdl/app/SDLSurface.java
Normal file
412
app_pojavlauncher/src/main/java/org/libsdl/app/SDLSurface.java
Normal file
@@ -0,0 +1,412 @@
|
||||
/*
|
||||
* This file is part of SDL3 android-project java code.
|
||||
* Licensed under the zlib license: https://www.libsdl.org/license.php
|
||||
*/
|
||||
|
||||
package org.libsdl.app;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.graphics.Insets;
|
||||
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.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
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.OnApplyWindowInsetsListener, 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();
|
||||
setOnApplyWindowInsetsListener(this);
|
||||
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();
|
||||
setOnApplyWindowInsetsListener(this);
|
||||
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;
|
||||
float density = 1.0f;
|
||||
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;
|
||||
// Use densityDpi instead of density to more closely match what the UI scale is
|
||||
density = (float)realMetrics.densityDpi / 160.0f;
|
||||
}
|
||||
} 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, density, 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 if we might be multi-window or have popup dialogs
|
||||
if (skip) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
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();
|
||||
}
|
||||
|
||||
// Window inset
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||
if (Build.VERSION.SDK_INT >= 30 /* Android 11 (R) */) {
|
||||
Insets combined = insets.getInsets(WindowInsets.Type.systemBars() |
|
||||
WindowInsets.Type.systemGestures() |
|
||||
WindowInsets.Type.mandatorySystemGestures() |
|
||||
WindowInsets.Type.tappableElement() |
|
||||
WindowInsets.Type.displayCutout());
|
||||
|
||||
SDLActivity.onNativeInsetsChanged(combined.left, combined.right, combined.top, combined.bottom);
|
||||
}
|
||||
|
||||
// Pass these to any child views in case they need them
|
||||
return insets;
|
||||
}
|
||||
|
||||
// Key events
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
return SDLActivity.handleKeyEvent(v, keyCode, event, null);
|
||||
}
|
||||
|
||||
private float getNormalizedX(float x)
|
||||
{
|
||||
if (mWidth <= 1) {
|
||||
return 0.5f;
|
||||
} else {
|
||||
return (x / (mWidth - 1));
|
||||
}
|
||||
}
|
||||
|
||||
private float getNormalizedY(float y)
|
||||
{
|
||||
if (mHeight <= 1) {
|
||||
return 0.5f;
|
||||
} else {
|
||||
return (y / (mHeight - 1));
|
||||
}
|
||||
}
|
||||
|
||||
// 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 pointerId;
|
||||
int i = 0;
|
||||
float x,y,p;
|
||||
|
||||
if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN)
|
||||
i = event.getActionIndex();
|
||||
|
||||
do {
|
||||
int toolType = event.getToolType(i);
|
||||
|
||||
if (toolType == MotionEvent.TOOL_TYPE_MOUSE) {
|
||||
int buttonState = event.getButtonState();
|
||||
boolean relative = false;
|
||||
|
||||
// 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_API14 motionListener = SDLActivity.getMotionListener();
|
||||
x = motionListener.getEventX(event, i);
|
||||
y = motionListener.getEventY(event, i);
|
||||
relative = motionListener.inRelativeMode();
|
||||
|
||||
SDLActivity.onNativeMouse(buttonState, action, x, y, relative);
|
||||
} else if (toolType == MotionEvent.TOOL_TYPE_STYLUS || toolType == MotionEvent.TOOL_TYPE_ERASER) {
|
||||
pointerId = event.getPointerId(i);
|
||||
x = event.getX(i);
|
||||
y = event.getY(i);
|
||||
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;
|
||||
}
|
||||
|
||||
// BUTTON_STYLUS_PRIMARY is 2^5, so shift by 4, and apply SDL_PEN_INPUT_DOWN/SDL_PEN_INPUT_ERASER_TIP
|
||||
int buttonState = (event.getButtonState() >> 4) | (1 << (toolType == MotionEvent.TOOL_TYPE_STYLUS ? 0 : 30));
|
||||
|
||||
SDLActivity.onNativePen(pointerId, buttonState, action, x, y, p);
|
||||
} else { // MotionEvent.TOOL_TYPE_FINGER or MotionEvent.TOOL_TYPE_UNKNOWN
|
||||
pointerId = event.getPointerId(i);
|
||||
x = getNormalizedX(event.getX(i));
|
||||
y = getNormalizedY(event.getY(i));
|
||||
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, pointerId, action, x, y, p);
|
||||
}
|
||||
|
||||
// Non-primary up/down
|
||||
if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN)
|
||||
break;
|
||||
} while (++i < pointerCount);
|
||||
|
||||
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 newRotation;
|
||||
|
||||
float x, y;
|
||||
switch (mDisplay.getRotation()) {
|
||||
case Surface.ROTATION_0:
|
||||
default:
|
||||
x = event.values[0];
|
||||
y = event.values[1];
|
||||
newRotation = 0;
|
||||
break;
|
||||
case Surface.ROTATION_90:
|
||||
x = -event.values[1];
|
||||
y = event.values[0];
|
||||
newRotation = 90;
|
||||
break;
|
||||
case Surface.ROTATION_180:
|
||||
x = -event.values[0];
|
||||
y = -event.values[1];
|
||||
newRotation = 180;
|
||||
break;
|
||||
case Surface.ROTATION_270:
|
||||
x = event.values[1];
|
||||
y = -event.values[0];
|
||||
newRotation = 270;
|
||||
break;
|
||||
}
|
||||
|
||||
if (newRotation != SDLActivity.mCurrentRotation) {
|
||||
SDLActivity.mCurrentRotation = newRotation;
|
||||
SDLActivity.onNativeRotationChanged(newRotation);
|
||||
}
|
||||
|
||||
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();
|
||||
int pointerCount = event.getPointerCount();
|
||||
|
||||
for (int i = 0; i < pointerCount; i++) {
|
||||
float x, y;
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_SCROLL:
|
||||
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, i);
|
||||
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, i);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
x = event.getX(i);
|
||||
y = event.getY(i);
|
||||
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(i);
|
||||
y = event.getY(i);
|
||||
int button = event.getButtonState();
|
||||
|
||||
SDLActivity.onNativeMouse(button, action, x, y, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -56,16 +56,33 @@ public class CallbackBridge {
|
||||
nativeSendCursorPos(mouseX, mouseY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends keycodes if keycode is populated. Used for in-game controls.
|
||||
* Sends character if keychar is populated. Used for chat and text input.
|
||||
* You can refer to glfwSetKeyCallback for the arguments.
|
||||
* @param keycode LwjglGlfwKeycode
|
||||
* @param keychar Literal char. Modifier keys does not affect this.
|
||||
* @param scancode
|
||||
* @param modifiers The action is one of The action is one of GLFW_PRESS, or GLFW_RELEASE.
|
||||
* We don't have GLFW_REPEAT working.
|
||||
* @param isDown If its being pressed down or not. 1 is true.
|
||||
*/
|
||||
public static void sendKeycode(int keycode, char keychar, int scancode, int modifiers, boolean isDown) {
|
||||
// TODO CHECK: This may cause input issue, not receive input!
|
||||
if(keycode != 0) nativeSendKey(keycode,scancode,isDown ? 1 : 0, modifiers);
|
||||
if(isDown && keychar != '\u0000') {
|
||||
// Only controlmaps goes through here, that means we need to block ISOControl or else
|
||||
// Minecraft tries to type :TAB: as a character in chat, fails, and then ignores the key,
|
||||
// breaking the tab autofill function in old versions. (like 1.12.2, 1.8.9).
|
||||
if(isDown && !Character.isISOControl(keychar)) {
|
||||
nativeSendCharMods(keychar,modifiers);
|
||||
nativeSendChar(keychar);
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendChar(char keychar, int modifiers){
|
||||
// Only an EditText goes through here, that means emojis are allowed, so no isISOControl
|
||||
// cause we might break emoji mods then.
|
||||
// See net/kdt/pojavlaunch/customcontrols/keyboard/TouchCharInput.java#L147 (onTextChanged)
|
||||
nativeSendCharMods(keychar,modifiers);
|
||||
nativeSendChar(keychar);
|
||||
}
|
||||
@@ -82,6 +99,10 @@ public class CallbackBridge {
|
||||
CallbackBridge.sendKeycode(keyCode, keyChar, scancode, modifiers, status);
|
||||
}
|
||||
|
||||
public static void sendKeyPress(int keyCode, char keyChar, int modifiers, boolean status) {
|
||||
sendKeyPress(keyCode, keyChar, 0, modifiers, status);
|
||||
}
|
||||
|
||||
public static void sendKeyPress(int keyCode) {
|
||||
sendKeyPress(keyCode, CallbackBridge.getCurrentMods(), true);
|
||||
sendKeyPress(keyCode, CallbackBridge.getCurrentMods(), false);
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
LOCAL_PATH := $(call my-dir)
|
||||
HERE_PATH := $(LOCAL_PATH)
|
||||
|
||||
include $(LOCAL_PATH)/SDL/Android.mk
|
||||
# include $(HERE_PATH)/crash_dump/libbase/Android.mk
|
||||
# include $(HERE_PATH)/crash_dump/libbacktrace/Android.mk
|
||||
# include $(HERE_PATH)/crash_dump/debuggerd/Android.mk
|
||||
|
||||
LOCAL_PATH := $(HERE_PATH)
|
||||
# This is a modified snippet taken from the sdl2-compat repository
|
||||
# Licensed under the zlib license: https://www.libsdl.org/license.php
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := SDL2
|
||||
LOCAL_SHARED_LIBRARIES := SDL3
|
||||
LOCAL_C_INCLUDES := \
|
||||
$(LOCAL_PATH)/overrides \
|
||||
$(LOCAL_PATH)/sdl2-compat/include.SDL2 \
|
||||
$(LOCAL_PATH)/SDL/include
|
||||
LOCAL_SRC_FILES := sdl2-compat/src/dynapi/SDL_dynapi.c sdl2-compat/src/sdl2_compat.c
|
||||
LOCAL_CFLAGS += -DGL_GLEXT_PROTOTYPES -DHAVE_ALLOCA -DHAVE_ALLOCA_H -DSDL_INCLUDE_STDBOOL_H
|
||||
LOCAL_CFLAGS += -Wall -Wextra -Wno-unused-parameter -Wno-unused-local-typedefs
|
||||
LOCAL_LDLIBS := -ldl -llog
|
||||
LOCAL_LDFLAGS := -Wl,--no-undefined
|
||||
ifeq ($(NDK_DEBUG),1)
|
||||
cmd-strip :=
|
||||
endif
|
||||
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
# End snippet
|
||||
|
||||
LOCAL_PATH := $(HERE_PATH)
|
||||
|
||||
@@ -15,6 +36,7 @@ include $(CLEAR_VARS)
|
||||
# Link GLESv2 for test
|
||||
LOCAL_LDLIBS := -ldl -llog -landroid
|
||||
# -lGLESv2
|
||||
LOCAL_SHARED_LIBRARIES := SDL3
|
||||
LOCAL_MODULE := pojavexec
|
||||
# LOCAL_CFLAGS += -DDEBUG
|
||||
# -DGLES_TEST
|
||||
|
||||
1
app_pojavlauncher/src/main/jni/SDL
Submodule
1
app_pojavlauncher/src/main/jni/SDL
Submodule
Submodule app_pojavlauncher/src/main/jni/SDL added at 96292a5b46
@@ -553,4 +553,17 @@ Java_org_lwjgl_glfw_CallbackBridge_nativeCreateGamepadButtonBuffer(JNIEnv *env,
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_org_lwjgl_glfw_CallbackBridge_nativeCreateGamepadAxisBuffer(JNIEnv *env, jclass clazz) {
|
||||
return (*env)->NewDirectByteBuffer(env, &pojav_environ->gamepadState.axes, sizeof(pojav_environ->gamepadState.axes));
|
||||
}
|
||||
}
|
||||
|
||||
// HACK: Legacy4J has faulty detection that hardwires us to GLFW unless we init SDL ourselves.
|
||||
// This is a horribly made function that should really have more checks around it but meh.
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
static inline void initSubsystem(void) {
|
||||
SDL_Init(SDL_INIT_GAMEPAD | SDL_INIT_JOYSTICK | SDL_INIT_EVENTS);
|
||||
}
|
||||
JNIEXPORT void JNICALL
|
||||
Java_net_kdt_pojavlaunch_Tools_00024SDL_initializeControllerSubsystems(JNIEnv *env, jclass clazz){
|
||||
// Please ensure that you have already dlopen'ed SDL3 before calling this.
|
||||
initSubsystem();
|
||||
}
|
||||
|
||||
1
app_pojavlauncher/src/main/jni/sdl2-compat
Submodule
1
app_pojavlauncher/src/main/jni/sdl2-compat
Submodule
Submodule app_pojavlauncher/src/main/jni/sdl2-compat added at 3a6d7d244a
Binary file not shown.
BIN
app_pojavlauncher/src/main/jniLibs/arm64-v8a/libGLESv1_CM_angle.so
Executable file
BIN
app_pojavlauncher/src/main/jniLibs/arm64-v8a/libGLESv1_CM_angle.so
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
0
app_pojavlauncher/src/main/jniLibs/arm64-v8a/libgl4es_114.so
Executable file → Normal file
0
app_pojavlauncher/src/main/jniLibs/arm64-v8a/libgl4es_114.so
Executable file → Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
0
app_pojavlauncher/src/main/jniLibs/arm64-v8a/libunpack200.so
Executable file → Normal file
0
app_pojavlauncher/src/main/jniLibs/arm64-v8a/libunpack200.so
Executable file → Normal file
BIN
app_pojavlauncher/src/main/jniLibs/arm64-v8a/libvulkan_freedreno.so
Executable file → Normal file
BIN
app_pojavlauncher/src/main/jniLibs/arm64-v8a/libvulkan_freedreno.so
Executable file → Normal file
Binary file not shown.
Binary file not shown.
BIN
app_pojavlauncher/src/main/jniLibs/armeabi-v7a/libGLESv1_CM_angle.so
Executable file
BIN
app_pojavlauncher/src/main/jniLibs/armeabi-v7a/libGLESv1_CM_angle.so
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
0
app_pojavlauncher/src/main/jniLibs/armeabi-v7a/libgl4es_114.so
Executable file → Normal file
0
app_pojavlauncher/src/main/jniLibs/armeabi-v7a/libgl4es_114.so
Executable file → Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
0
app_pojavlauncher/src/main/jniLibs/armeabi-v7a/libunpack200.so
Executable file → Normal file
0
app_pojavlauncher/src/main/jniLibs/armeabi-v7a/libunpack200.so
Executable file → Normal file
Binary file not shown.
BIN
app_pojavlauncher/src/main/jniLibs/x86/libGLESv1_CM_angle.so
Executable file
BIN
app_pojavlauncher/src/main/jniLibs/x86/libGLESv1_CM_angle.so
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
0
app_pojavlauncher/src/main/jniLibs/x86/libgl4es_114.so
Executable file → Normal file
0
app_pojavlauncher/src/main/jniLibs/x86/libgl4es_114.so
Executable file → Normal file
Binary file not shown.
BIN
app_pojavlauncher/src/main/jniLibs/x86/liblwjgl.so
Executable file → Normal file
BIN
app_pojavlauncher/src/main/jniLibs/x86/liblwjgl.so
Executable file → Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user