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:
alexytomi
2025-09-25 19:02:32 +08:00
142 changed files with 8361 additions and 173 deletions

View File

@@ -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
View File

@@ -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

View File

@@ -4,7 +4,7 @@
[![Android CI](https://github.com/AngelAuraMC/Amethyst-Android/workflows/Android%20CI/badge.svg)](https://github.com/AngelAuraMC/Amethyst-Android/actions)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/AngelAuraMC/Amethyst-Android)](https://github.com/AngelAuraMC/Amethyst-Android/actions)
[![Crowdin](https://badges.crowdin.net/amethyst/localized.svg)](https://crowdin.com/project/amethyst)
[![Crowdin](https://badges.crowdin.net/pojavlauncher/localized.svg)](https://crowdin.com/project/pojavlauncher)
[![Discord](https://img.shields.io/discord/724163890803638273.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](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

View File

@@ -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")

View File

@@ -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"

View File

@@ -1 +1 @@
53f2f037cef9b7447a6abbdf82150ca8a2f2a587
2016eba00f043842122d7aecb9410cf9371a7693

View File

@@ -1 +1 @@
4903cfc8d3afd63918f59caf0a146efc2d837069
02e09ed8760774bfa1222432ca2ad25b70b191d7

View 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.

View 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

View 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.

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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 ++;
}
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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);

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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+")");

View File

@@ -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();

View File

@@ -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() {

View File

@@ -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();
}

View File

@@ -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()) {

View File

@@ -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

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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)));
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}

View File

@@ -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();

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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 {

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View 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);
}
}
}
}
}
}

View 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;
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View 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;
}
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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();
}

View File

Binary file not shown.

View File

View File

Binary file not shown.

View File

Binary file not shown.

View File

View File

Binary file not shown.

View File

Binary file not shown.

View File

View File

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

0
app_pojavlauncher/src/main/jniLibs/x86/libgl4es_114.so Executable file → Normal file
View File

View File

Binary file not shown.

BIN
app_pojavlauncher/src/main/jniLibs/x86/liblwjgl.so Executable file → Normal file
View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

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