Compare commits
18 commits
master
...
VERSION_01
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02338e9b29 | ||
|
|
c62e6c14d7 | ||
|
|
029c2a822f | ||
|
|
e9dfb22080 | ||
|
|
c05f142aa5 | ||
|
|
8fe6030820 | ||
|
|
24d190223d | ||
|
|
151dcdd08f | ||
|
|
a2c47ef406 | ||
|
|
1f3a7051b9 | ||
|
|
9ea4754597 | ||
|
|
2682d5506a | ||
|
|
c78c0d5d2c | ||
|
|
313c190712 | ||
|
|
a5a2ae65ea | ||
|
|
c4266ad8d3 | ||
|
|
4f52a369da | ||
|
|
368a1166ce |
|
|
@ -16,6 +16,7 @@
|
|||
<classpathentry kind="lib" path="libs/libtiled.jar"/>
|
||||
<classpathentry kind="lib" path="libs/log4j.jar" sourcepath="/log4j"/>
|
||||
<classpathentry kind="lib" path="libs/mysql-connector-java-8.0.13.jar"/>
|
||||
<classpathentry kind="lib" path="libs/simple.jar"/>
|
||||
<classpathentry kind="lib" path="libs/swing-layout.jar"/>
|
||||
<classpathentry kind="lib" path="libs/json-simple-1.1.1.jar"/>
|
||||
<classpathentry kind="lib" path="libs/junit-4.12.jar"/>
|
||||
|
|
@ -23,7 +24,7 @@
|
|||
<classpathentry kind="lib" path="libs/marauroa.jar" sourcepath="/marauroa"/>
|
||||
<classpathentry kind="lib" path="libs/luaj-jse-3.0.1.jar" sourcepath="/luaj"/>
|
||||
<classpathentry kind="lib" path="libs/jakarta.annotation-api-2.1.1.jar"/>
|
||||
<classpathentry exported="true" kind="lib" path="libs/tomcat-embed-core-10.1.13.jar"/>
|
||||
<classpathentry kind="lib" path="libs/tomcat-embed-websocket-10.1.13.jar"/>
|
||||
<classpathentry kind="output" path="build/eclipse"/>
|
||||
<classpathentry kind="lib" path="libs/tomcat-embed-core-10.1.7.jar"/>
|
||||
<classpathentry kind="lib" path="libs/tomcat-embed-websocket-10.1.7.jar"/>
|
||||
<classpathentry kind="output" path="bin"/>
|
||||
</classpath>
|
||||
|
|
|
|||
62
.github/workflows/android-build.yml
vendored
|
|
@ -1,62 +0,0 @@
|
|||
name: Android Build Test
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'app/android/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'app/android/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- id: job_info
|
||||
uses: daleyjem/gh-job-id@v1
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
job_name: 'build'
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set Up JDK
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
|
||||
- name: Validate Gradle Wrapper .jar
|
||||
run: |
|
||||
cd app/android/gradle/wrapper
|
||||
curl --location --output gradle-wrapper.jar.sha256 \
|
||||
https://services.gradle.org/distributions/gradle-7.5.1-wrapper.jar.sha256
|
||||
echo " gradle-wrapper.jar" >> gradle-wrapper.jar.sha256
|
||||
sha256sum --check gradle-wrapper.jar.sha256
|
||||
|
||||
- name: Build APKs with Ant
|
||||
uses: cedx/setup-ant@v6
|
||||
with:
|
||||
version: '1.10'
|
||||
- run: ant -noinput -buildfile build.xml android -Dgradle.params="assembleDebug assembleRelease"
|
||||
|
||||
- name: Failure Report
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
if: failure()
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_DEVELOPMENT }}
|
||||
with:
|
||||
title: 'Android Build Failed'
|
||||
description: '${{ steps.job_info.outputs.html_url }}'
|
||||
nodetail: true
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
if: failure()
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_DEVLOG }}
|
||||
with:
|
||||
title: 'Android Build Failed'
|
||||
description: '${{ steps.job_info.outputs.html_url }}'
|
||||
nodetail: true
|
||||
14
.github/workflows/codeql-analysis.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
|
@ -29,15 +29,15 @@ jobs:
|
|||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v1
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
# with:
|
||||
# languages: go, javascript, csharp, python, cpp, java
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
#- name: Autobuild
|
||||
# uses: github/codeql-action/autobuild@v3
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
|
@ -46,7 +46,9 @@ jobs:
|
|||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
- run: ant -noinput -f build.xml clean compile
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v1
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: "16"
|
||||
distribution: 'adopt'
|
||||
|
|
@ -39,7 +39,7 @@ jobs:
|
|||
- name: Build APKs
|
||||
run: >
|
||||
ant -noinput -buildfile build.xml dist_android_client
|
||||
-Dgradle.params="-Pandroid.ver=${{ steps.ver.outputs.value }} assembleDebug assembleRelease"
|
||||
-Dgradle.params="--no-daemon -Pandroid.ver=${{ steps.ver.outputs.value }} assembleDebug assemblePrerelease"
|
||||
|
||||
- name: Publish
|
||||
uses: softprops/action-gh-release@v1
|
||||
|
|
|
|||
4
.github/workflows/unittest.yml
vendored
|
|
@ -7,9 +7,9 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 8
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: '8'
|
||||
distribution: 'adopt'
|
||||
|
|
|
|||
3
.gitignore
vendored
|
|
@ -40,6 +40,7 @@ javadocs
|
|||
statserver_stats.xml
|
||||
/*.ks
|
||||
build_client_data
|
||||
/bin
|
||||
.byecycle
|
||||
build_server_xmlconf
|
||||
*.umlproject
|
||||
|
|
@ -48,5 +49,3 @@ build_server_xmlconf
|
|||
/map.map
|
||||
/hs_err_*.log
|
||||
/node_modules/
|
||||
/log/
|
||||
/build-archive/
|
||||
|
|
|
|||
11
.project
|
|
@ -20,15 +20,4 @@
|
|||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
|
||||
</natures>
|
||||
<filteredResources>
|
||||
<filter>
|
||||
<id>1771748271774</id>
|
||||
<name></name>
|
||||
<type>30</type>
|
||||
<matcher>
|
||||
<id>org.eclipse.core.resources.regexFilterMatcher</id>
|
||||
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
|
||||
</matcher>
|
||||
</filter>
|
||||
</filteredResources>
|
||||
</projectDescription>
|
||||
|
|
|
|||
22
.travis.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
language: java
|
||||
before_install:
|
||||
- pip install --user codecov
|
||||
dist: bionic
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- ant-optional
|
||||
service:
|
||||
- xvfb
|
||||
script: xvfb-run -e /dev/stdout -a ant test
|
||||
notifications:
|
||||
irc:
|
||||
channels:
|
||||
- "irc.libera.chat#arianne"
|
||||
- "irc.libera.chat#arianne-devel"
|
||||
on_success: change
|
||||
on_failure: change
|
||||
use_notice: false
|
||||
skip_join: false
|
||||
after_success:
|
||||
- codecov
|
||||
50
README.md
|
|
@ -11,7 +11,7 @@ Stendhal is a fully fledged multiplayer online adventures game (MORPG). It is co
|
|||
Stendhal features a new, rich and expanding world in which you can explore towns, buildings, plains, caves and dungeons.
|
||||
You will meet NPCs and acquire tasks and quests for valuable experience.
|
||||
|
||||
Your character will develop and grow and with each new level up become stronger and better. With the money you acquire you can buy new items and improve your armor and weapons. And for the blood thirsty of you; satisfy your killing desires by roaming the world in search of evil monsters!
|
||||
Your character will develop and grow and with each new level up become stronger and better. With the money you acquire you can buy new items and improve your armour and weapons. And for the blood thirsty of you; satisfy your killing desires by roaming the world in search of evil monsters!
|
||||
|
||||
Stendhal is platform independent, and completely open source.
|
||||
|
||||
|
|
@ -20,38 +20,19 @@ So what are you waiting for?! A whole new world awaits...
|
|||
|
||||
## How to play
|
||||
|
||||
### Web Browser
|
||||
|
||||
[](https://stendhalgame.org/account/mycharacters.html)
|
||||
|
||||
Play in any web browser for desktop or mobile with JavaScript support. Click on the "play now"
|
||||
button to create or log in to your account.
|
||||
|
||||
### Mobile
|
||||
|
||||
Play using the [progressive web app](https://en.wikipedia.org/wiki/Progressive_web_app). Just visit
|
||||
[stendhalgame.org](https://stendhalgame.org/) in your mobile browser and click "install" when prompted.
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="75">](https://f-droid.org/packages/org.stendhalgame.client/)
|
||||
|
||||
Play on your Android device using the dedicated app. You can get it from the
|
||||
[F-Droid App Store](https://f-droid.org/) or download it from [stendhalgame.org](https://stendhalgame.org/download.html).
|
||||
|
||||
### Classic Desktop
|
||||
|
||||
[](https://stendhalgame.org/account/mycharacters.html)
|
||||
[](https://arianne-project.org/download/stendhal.zip)
|
||||
|
||||
Play using the classic desktop client. You will need Java which you can download from
|
||||
[java.com](https://www.java.com/en/download) on Microsoft Windows. Java is included in
|
||||
most Linux distributions. Both Oracle Java and OpenJDK are supported.
|
||||
You need Java which you can download from [https://www.java.com](https://www.java.com/en/download) on Microsoft Windows. Java is included in most Linux distributions. Both Oracle Java and OpenJDK are supported.
|
||||
|
||||
Please download Stendhal from [stendhalgame.org](https://stendhalgame.org/download.html).<br>
|
||||
You can double click on `stendhal-starter.jar` and the client will run.<br>
|
||||
If you prefer to run from command line, then just execute this command in the stendhal folder:
|
||||
Please download Stendhal from [https://stendhalgame.org](https://stendhalgame.org)<br>
|
||||
You can double click on stendhal-starter.jar and the client will run.<br>
|
||||
If you prefer to run from command line, then just execute this command
|
||||
in the stendhal folder:
|
||||
|
||||
`java -jar stendhal-starter.jar`
|
||||
|
||||
There is a [manual](https://stendhalgame.org/wiki/Stendhal_Manual), and there is Help from the client menu icons in the upper right hand side of the game screen.
|
||||
There is a [manual](https://stendhalgame.org/wiki/Stendhal_Manual), and there is Help from the client menu icons in the upper right hand side of the game screen.
|
||||
|
||||
|
||||
## Development environment
|
||||
|
|
@ -63,18 +44,13 @@ There is a [manual](https://stendhalgame.org/wiki/Stendhal_Manual), and there is
|
|||
|
||||
|
||||
## Legal
|
||||
The server and java client is free software; you can redistribute it and/or modify it under the terms of the **GNU General Public License** 2 or later as published by the Free Software Foundation.
|
||||
|
||||
Stendhal is free software. You can redistribute it and/or modify under the following terms:
|
||||
The web client is free software; you can redistribute it and/or modify it under the terms of the **GNU Affero General Public License** 3 or later as published by the Free Software Foundation.
|
||||
|
||||
- The **server**, **java client**, and **Android client**: *GNU General Public License* 2 or later as published by the Free Software Foundation
|
||||
- The **web client** and **native client**: *GNU Affero General Public License* 3 or later as published by the Free Software Foundation with the definition of "System libraries" being extended to include authentication providers and software distribution platforms.
|
||||
- The **nativehelper** extensions: *GNU Lesser General Public License* 3 or later as published by the Free Software Foundation
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
||||
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
[LICENSE.txt](LICENSE.txt) and [src/js/LICENSE.html](src/js/LICENSE.html) file for more details.
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the [LICENSE.txt](LICENSE.txt) file for more details.
|
||||
|
||||
Stendhal(c) is copyright of Miguel Angel Blanch Lardin, 2005-2008, arianne_rpg at users dot sourceforge dot net and others<br>
|
||||
Stendhal(c) is copyright of the Arianne Project, 2006-2026, arianne-general at lists dot sourceforge dot net
|
||||
Stendhal(c) is copyright of the Arianne Project, 2006-2021, arianne-general at lists dot sourceforge dot net
|
||||
|
||||
Please have a look at the list of [contributors](doc/contributors.md#contributors).
|
||||
Please have a look at the list of [contributors](https://github.com/arianne/stendhal/blob/master/doc/contributors.md#contributors).
|
||||
|
|
|
|||
1
app/android/.gitignore → android/.gitignore
vendored
|
|
@ -8,4 +8,3 @@
|
|||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
/deps/
|
||||
13
android/TODO.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
TODO:
|
||||
- show input preview when typing
|
||||
- scale page to fit inside view
|
||||
- create activity for about information
|
||||
- add credits to about information
|
||||
- switch to Log4j for logging (maybe)
|
||||
- use center of d-pad for setting position
|
||||
- d-pads need to be different sizes for different resolutions
|
||||
- either use unique PNGs or convert to scalable SVGs
|
||||
|
||||
BUILD TODO:
|
||||
- script to download Android SDK
|
||||
79
android/app/build.gradle
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 32
|
||||
|
||||
def buildDate = new Date().format("yyyyMMdd")
|
||||
|
||||
def appVer
|
||||
if (project.hasProperty("android.ver") && project["android.ver"]) {
|
||||
appVer = project["android.ver"]
|
||||
} else {
|
||||
appVer = "${buildDate}"
|
||||
}
|
||||
|
||||
if (project.hasProperty("android.rev") && project["android.rev"]) {
|
||||
appVer += "-" + project["android.rev"]
|
||||
}
|
||||
|
||||
def outputName = "stendhal-webview-${appVer}"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.arianne.stendhal.client"
|
||||
minSdk 21
|
||||
targetSdk 32
|
||||
versionCode 1
|
||||
versionName "${appVer}"
|
||||
setProperty("archivesBaseName", "${outputName}")
|
||||
setProperty("buildDir", "${projectDir}/../../build/build_android_client")
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
targetSdkVersion 32
|
||||
minSdkVersion 21
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
|
||||
prerelease {
|
||||
versionNameSuffix "-pre"
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
versionNameSuffix "-debug"
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes.all { type ->
|
||||
if (!type.name.equals("debug")) {
|
||||
sourceSets[type.name].java { srcDirs "src/shared/java/" }
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
compileSdkVersion 32
|
||||
buildToolsVersion '32.0.0'
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
//testImplementation 'junit:junit:4.+'
|
||||
//androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
//androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
implementation files('../../libs/marauroa-client-only.jar')
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2022 - Arianne *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.arianne.stendhal.client;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.util.Log;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
/**
|
||||
* A class to output debugging information to logcat file.
|
||||
*/
|
||||
public class DebugLog {
|
||||
|
||||
private static File logsDir;
|
||||
|
||||
private static AppCompatActivity mainActivity;
|
||||
|
||||
public static enum DebugLevel {
|
||||
INFO("INFO"),
|
||||
WARN("WARN"),
|
||||
ERROR("ERROR"),
|
||||
DEBUG("DEBUG");
|
||||
|
||||
private final String label;
|
||||
|
||||
private DebugLevel(final String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
|
||||
private static DebugLog instance;
|
||||
|
||||
|
||||
public static DebugLog get() {
|
||||
if (instance == null) {
|
||||
instance = new DebugLog();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static void init(final File dir, final AppCompatActivity activity) {
|
||||
logsDir = new File(dir.getPath() + "/logs");
|
||||
mainActivity = activity;
|
||||
|
||||
writeLine("\n // -- debugging initialized -- //", null);
|
||||
writeLine("logs directory: " + logsDir.getPath());
|
||||
}
|
||||
|
||||
public static void writeLine(final String text) {
|
||||
writeLine(text, DebugLevel.DEBUG);
|
||||
}
|
||||
|
||||
public static void writeLine(String text, final DebugLevel level) {
|
||||
if (logsDir == null) {
|
||||
System.err.println("ERROR: DebugLog not initialized. Call DebugLog.init.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!logsDir.exists()) {
|
||||
logsDir.mkdirs();
|
||||
}
|
||||
|
||||
if (level != null) {
|
||||
text = new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss").format(new Date())
|
||||
+ ": " + level.label + ": " + text;
|
||||
}
|
||||
text = text + "\n";
|
||||
|
||||
final String logFile = logsDir.getPath() + "/debug-"
|
||||
+ new SimpleDateFormat("yyyy.MM.dd").format(new Date()) + ".txt";
|
||||
|
||||
BufferedWriter buffer;
|
||||
try {
|
||||
buffer = new BufferedWriter(new FileWriter(logFile, true));
|
||||
buffer.write(text);
|
||||
if (buffer != null) {
|
||||
buffer.close();
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
System.err.println("ERROR: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void info(final String text) {
|
||||
Log.i("DebugLog", text);
|
||||
writeLine(text, DebugLevel.INFO);
|
||||
}
|
||||
|
||||
public static void warn(final String text) {
|
||||
Log.w("DebugLog", text);
|
||||
writeLine(text, DebugLevel.WARN);
|
||||
}
|
||||
|
||||
public static void error(final String text) {
|
||||
Log.e("DebugLog", text);
|
||||
writeLine(text, DebugLevel.ERROR);
|
||||
}
|
||||
|
||||
public static void debug(final String text) {
|
||||
Log.d("DebugLog", text);
|
||||
writeLine(text, DebugLevel.DEBUG);
|
||||
}
|
||||
|
||||
public static void notify(final String message) {
|
||||
notify(message, DebugLevel.DEBUG);
|
||||
}
|
||||
|
||||
public static void notify(final String message, DebugLevel level) {
|
||||
if (mainActivity == null) {
|
||||
System.err.println("ERROR: DebugLog not initialized. Call DebugLog.init.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (level == null) {
|
||||
level = DebugLevel.DEBUG;
|
||||
}
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(mainActivity);
|
||||
builder.setTitle(level.label);
|
||||
builder.setMessage(message);
|
||||
|
||||
builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
dialog.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
public static String getLogsDir() {
|
||||
return logsDir.getPath();
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,5 @@
|
|||
<resources>
|
||||
|
||||
<string name="app_name">Stendhal (debug)</string>
|
||||
<string name="intent_url_scheme">stendhaldebug8gps5y99pu</string>
|
||||
<string name="build_type">debug</string>
|
||||
|
||||
</resources>
|
||||
40
android/app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.arianne.stendhal.client">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Stendhal.FullScreen">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:exported="true"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".PreferencesActivity"
|
||||
android:label="Preferences"
|
||||
android:exported="true"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="preferences" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
Before Width: | Height: | Size: 505 KiB After Width: | Height: | Size: 505 KiB |
|
|
@ -0,0 +1,503 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2022 - Arianne *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.arianne.stendhal.client;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.inputmethod.BaseInputConnection;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.webkit.DownloadListener;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import marauroa.common.Pair;
|
||||
|
||||
import org.arianne.stendhal.client.input.DPad;
|
||||
import org.arianne.stendhal.client.js.JSInterface;
|
||||
import org.arianne.stendhal.client.sound.MusicPlayer;
|
||||
|
||||
|
||||
public class ClientView extends WebView {
|
||||
|
||||
private static ClientView instance;
|
||||
|
||||
private ImageView splash;
|
||||
|
||||
private final String defaultServer = "https://stendhalgame.org/";
|
||||
private String clientUrlSuffix = "client";
|
||||
|
||||
private boolean testing = false;
|
||||
private Boolean debugging;
|
||||
private static PageId currentPage;
|
||||
// denotes previous touch was remapped to mouse event
|
||||
private boolean touchOverridden = false;
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the static instance.
|
||||
*/
|
||||
public static ClientView get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public ClientView(final Context ctx) {
|
||||
super(ctx);
|
||||
onInit();
|
||||
}
|
||||
|
||||
public ClientView(final Context ctx, final AttributeSet attrs) {
|
||||
super(ctx, attrs);
|
||||
onInit();
|
||||
}
|
||||
|
||||
public ClientView(final Context ctx, final AttributeSet attrs, final int style) {
|
||||
super(ctx, attrs, style);
|
||||
onInit();
|
||||
}
|
||||
|
||||
public ClientView(final Context ctx, final AttributeSet attrs, final int style,
|
||||
final boolean prvtBrowse) {
|
||||
super(ctx, attrs, style, prvtBrowse);
|
||||
onInit();
|
||||
}
|
||||
|
||||
private void onInit() {
|
||||
instance = this;
|
||||
|
||||
setBackgroundColor(android.graphics.Color.TRANSPARENT);
|
||||
|
||||
final WebSettings viewSettings = getSettings();
|
||||
viewSettings.setJavaScriptEnabled(true);
|
||||
viewSettings.setDomStorageEnabled(true);
|
||||
|
||||
// keep elements in position in portrait mode
|
||||
viewSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);// <-- SINGLE_COLUMN deprecated
|
||||
viewSettings.setLoadWithOverviewMode(true);
|
||||
viewSettings.setUseWideViewPort(true);
|
||||
|
||||
// disable zoom
|
||||
viewSettings.setSupportZoom(false);
|
||||
viewSettings.setBuiltInZoomControls(false);
|
||||
viewSettings.setDisplayZoomControls(false);
|
||||
|
||||
if (debugEnabled()) {
|
||||
// make WebView debuggable for debug builds
|
||||
setWebContentsDebuggingEnabled(true);
|
||||
}
|
||||
|
||||
initWebViewClient();
|
||||
initTouchHandler();
|
||||
initJSInterface();
|
||||
initDownloadHandler();
|
||||
}
|
||||
|
||||
private void initWebViewClient() {
|
||||
// XXX: not sure setting WebChromClient is doing anything, was recommended to
|
||||
// fix touchmove events not registering
|
||||
setWebChromeClient(new WebChromeClient());
|
||||
|
||||
setWebViewClient(new WebViewClient() {
|
||||
/* handle changing URLs */
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(final WebView view, final String url) {
|
||||
if (!isInternalUrl(url)) {
|
||||
// FIXME: should we ask for confirmation?
|
||||
MainActivity.get().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
|
||||
return true;
|
||||
}
|
||||
|
||||
view.loadUrl(checkClientUrl(url));
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageStarted(final WebView view, final String url, final Bitmap favicon) {
|
||||
DebugLog.debug("loading URL: " + url);
|
||||
|
||||
MusicPlayer.stopMusic();
|
||||
super.onPageStarted(view, url, favicon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(final WebView view, final String url) {
|
||||
super.onPageFinished(view, url);
|
||||
|
||||
if (isClientUrl(url)) {
|
||||
currentPage = PageId.WEBCLIENT;
|
||||
} else if (url.equals("") || url.equals("about:blank")) {
|
||||
currentPage = PageId.TITLE;
|
||||
if (PreferencesActivity.getBoolean("title_music", true)) {
|
||||
playTitleMusic();
|
||||
}
|
||||
} else {
|
||||
currentPage = PageId.OTHER;
|
||||
}
|
||||
Menu.get().updateButtons();
|
||||
|
||||
// only show d-pad on client screen
|
||||
final DPad pad = DPad.getCurrentPad();
|
||||
if (pad != null && PreferencesActivity.getBoolean("show_dpad", false)) {
|
||||
if (isGameActive()) {
|
||||
pad.show();
|
||||
} else {
|
||||
pad.hide();
|
||||
}
|
||||
}
|
||||
|
||||
DebugLog.debug("page id: " + currentPage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initTouchHandler() {
|
||||
setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(final View view, final MotionEvent event) {
|
||||
if (touchOverridden) {
|
||||
// reset touch event state
|
||||
touchOverridden = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
final MotionEvent.PointerProperties[] props = {new MotionEvent.PointerProperties()};
|
||||
event.getPointerProperties(0, props[0]);
|
||||
|
||||
if (isGameActive() && PreferencesActivity.getBoolean("map_touches", false)
|
||||
&& (props[0].toolType == MotionEvent.TOOL_TYPE_FINGER
|
||||
|| props[0].toolType == MotionEvent.TOOL_TYPE_STYLUS)) {
|
||||
Integer action = event.getAction();
|
||||
|
||||
/*
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
action = MotionEvent.ACTION_BUTTON_PRESS;
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
action = MotionEvent.ACTION_BUTTON_RELEASE;
|
||||
break;
|
||||
}
|
||||
*/
|
||||
|
||||
DebugLog.debug("mapping touch to mouse event");
|
||||
|
||||
props[0].toolType = MotionEvent.TOOL_TYPE_MOUSE;
|
||||
|
||||
final MotionEvent.PointerCoords[] coords = {new MotionEvent.PointerCoords()};
|
||||
event.getPointerCoords(0, coords[0]);
|
||||
|
||||
touchOverridden = true;
|
||||
|
||||
view.dispatchTouchEvent(MotionEvent.obtain(event.getDownTime(),
|
||||
event.getEventTime(), event.getAction(), event.getPointerCount(), props,
|
||||
coords, event.getMetaState(), MotionEvent.BUTTON_PRIMARY,
|
||||
event.getXPrecision(), event.getYPrecision(), event.getDeviceId(),
|
||||
event.getEdgeFlags(), event.getSource(), event.getFlags()));
|
||||
|
||||
return true; // consume old event
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(final EditorInfo outAttrs) {
|
||||
// allows capturing soft keyboard events
|
||||
return new BaseInputConnection(this, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(final KeyEvent event) {
|
||||
final boolean ret = super.dispatchKeyEvent(event);
|
||||
|
||||
// hide keyboard when "enter" pressed
|
||||
if (isGameActive() && event.getAction() == KeyEvent.ACTION_UP
|
||||
&& event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
|
||||
((InputMethodManager) MainActivity.get()
|
||||
.getSystemService(Context.INPUT_METHOD_SERVICE))
|
||||
.hideSoftInputFromWindow(this.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private void initJSInterface() {
|
||||
addJavascriptInterface(JSInterface.get(), "jsi");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles downloading screenshot created by web client.
|
||||
*/
|
||||
private void initDownloadHandler() {
|
||||
setDownloadListener(new DownloadListener() {
|
||||
@Override
|
||||
public void onDownloadStart(final String url, final String userAgent,
|
||||
final String contentDisposition, final String mimetype,
|
||||
final long contentLength) {
|
||||
|
||||
final Pair<Boolean, String> res = new DownloadHandler().download(url, mimetype);
|
||||
if (res.first()) {
|
||||
final String msg = "Downloaded file: " + res.second();
|
||||
DebugLog.debug(msg);
|
||||
Notifier.toast(msg);
|
||||
} else {
|
||||
final String msg = res.second();
|
||||
DebugLog.error(msg);
|
||||
Notifier.toast("ERROR: " + msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows initial title screen.
|
||||
*/
|
||||
public void loadTitleScreen() {
|
||||
setSplashResource(R.drawable.splash);
|
||||
loadUrl("about:blank");
|
||||
Menu.get().show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to connect to client host.
|
||||
*/
|
||||
public void loadLogin() {
|
||||
if (debugEnabled() && PreferencesActivity.getString("client_url").trim().equals("")) {
|
||||
// debug builds support choosing between main & test server
|
||||
selectServer();
|
||||
} else {
|
||||
onSelectServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the background image.
|
||||
*
|
||||
* @param resId
|
||||
* Resource ID.
|
||||
*/
|
||||
private void setSplashResource(final int resId) {
|
||||
if (splash == null) {
|
||||
splash = (ImageView) MainActivity.get().findViewById(R.id.splash);
|
||||
splash.setBackgroundColor(android.graphics.Color.TRANSPARENT);
|
||||
}
|
||||
|
||||
splash.setImageResource(resId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a message dialog for user to choose between main & test servers.
|
||||
*/
|
||||
private void selectServer() {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.get());
|
||||
builder.setMessage("Select a server");
|
||||
|
||||
builder.setPositiveButton("Main", new DialogInterface.OnClickListener() {
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
testing = false;
|
||||
dialog.cancel();
|
||||
onSelectServer();
|
||||
}
|
||||
});
|
||||
|
||||
builder.setNegativeButton("Testing", new DialogInterface.OnClickListener() {
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
testing = true;
|
||||
clientUrlSuffix = "testclient";
|
||||
dialog.cancel();
|
||||
onSelectServer();
|
||||
}
|
||||
});
|
||||
|
||||
final AlertDialog selectServer = builder.create();
|
||||
selectServer.show();
|
||||
}
|
||||
|
||||
private void onSelectServer() {
|
||||
// remove splash image
|
||||
setSplashResource(android.R.color.transparent);
|
||||
|
||||
final String custom_page = checkCustomServer();
|
||||
if (custom_page != null) {
|
||||
DebugLog.debug("Connecing to custom page: " + custom_page);
|
||||
|
||||
loadUrl(custom_page);
|
||||
} else {
|
||||
// initial page
|
||||
loadUrl(defaultServer + "account/mycharacters.html");
|
||||
|
||||
if (testing) {
|
||||
DebugLog.debug("Connecting to test server");
|
||||
} else {
|
||||
DebugLog.debug("Connecting to main server");
|
||||
|
||||
Notifier.get().showMessage("CAUTION: This software is in early development and not recommended"
|
||||
+ " for use on the main server. Proceed with caution.", false);
|
||||
}
|
||||
}
|
||||
|
||||
currentPage = PageId.OTHER;
|
||||
// hide menu after exiting title screen
|
||||
Menu.get().hide();
|
||||
}
|
||||
|
||||
private String checkCustomServer() {
|
||||
final String cs = PreferencesActivity.getString("client_url", "").trim();
|
||||
|
||||
if (cs.equals("")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return cs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a URL is a link to one of the web clients.
|
||||
*
|
||||
* @param url
|
||||
* URL to be checked.
|
||||
* @return
|
||||
* <code>true</code> if url links to "client" or "testclient".
|
||||
*/
|
||||
private boolean isClientUrl(final String url) {
|
||||
final String custom_client = PreferencesActivity.getString("client_url").trim();
|
||||
if (!custom_client.equals("")) {
|
||||
return url.contains(custom_client);
|
||||
}
|
||||
|
||||
return url.contains("stendhalgame.org/client/")
|
||||
|| url.contains("stendhalgame.org/testclient/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats client URL for currently selected server.
|
||||
*
|
||||
* @param url
|
||||
* URL to be checked.
|
||||
* @return
|
||||
* URL to be loaded.
|
||||
*/
|
||||
private String checkClientUrl(String url) {
|
||||
if (isClientUrl(url)) {
|
||||
String replaceSuffix = "/testclient/";
|
||||
if (testing) {
|
||||
replaceSuffix = "/client/";
|
||||
}
|
||||
|
||||
url = url.replace(replaceSuffix, "/" + clientUrlSuffix + "/");
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if requested URL is whitelisted to be opened within WebView client.
|
||||
*
|
||||
* @param url
|
||||
* URL to be checked.
|
||||
* @return
|
||||
* <code>true</code> if URL is under domain stendhalgame.org or localhost.
|
||||
*/
|
||||
private boolean isInternalUrl(final String url) {
|
||||
final String domain = getDomain(url);
|
||||
final String cs = checkCustomServer();
|
||||
|
||||
if (cs != null) {
|
||||
return domain.startsWith(getDomain(cs));
|
||||
} else {
|
||||
return domain.startsWith("stendhalgame.org") || domain.startsWith("localhost");
|
||||
}
|
||||
}
|
||||
|
||||
private String getDomain(final String url) {
|
||||
return url.replaceAll("^https://", "").replaceAll("^http://", "")
|
||||
.replaceAll("^www\\.", "");
|
||||
}
|
||||
|
||||
public static boolean onTitleScreen() {
|
||||
return currentPage == PageId.TITLE;
|
||||
}
|
||||
|
||||
public static boolean isGameActive() {
|
||||
return currentPage == PageId.WEBCLIENT;
|
||||
}
|
||||
|
||||
public static PageId getCurrentPageId() {
|
||||
return currentPage;
|
||||
}
|
||||
|
||||
public static void playTitleMusic(String musicId) {
|
||||
if (musicId == null) {
|
||||
musicId = PreferencesActivity.getString("song_list");
|
||||
}
|
||||
|
||||
int id = R.raw.title_01;
|
||||
switch (musicId) {
|
||||
case "title_02":
|
||||
id = R.raw.title_02;
|
||||
break;
|
||||
case "title_03":
|
||||
id = R.raw.title_03;
|
||||
break;
|
||||
case "title_04":
|
||||
id = R.raw.title_04;
|
||||
break;
|
||||
case "title_05":
|
||||
id = R.raw.title_05;
|
||||
break;
|
||||
}
|
||||
|
||||
DebugLog.debug("playing music: " + musicId);
|
||||
|
||||
MusicPlayer.playMusic(id, true);
|
||||
}
|
||||
|
||||
public static void playTitleMusic() {
|
||||
playTitleMusic(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this is a debug build.
|
||||
*
|
||||
* @return
|
||||
* <code>true</code> if debug flag set.
|
||||
*/
|
||||
private boolean debugEnabled() {
|
||||
if (debugging != null) {
|
||||
return debugging;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
debugging = (MainActivity.get().getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
|
||||
} else {
|
||||
debugging = false;
|
||||
}
|
||||
|
||||
return debugging;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2022 - Arianne *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.arianne.stendhal.client;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.util.Base64;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
|
||||
import marauroa.common.Pair;
|
||||
|
||||
|
||||
/**
|
||||
* The main purpose of this class is to handle downloading screenshots
|
||||
* created by the web client page.
|
||||
*/
|
||||
public class DownloadHandler {
|
||||
|
||||
public Pair<Boolean, String> download(final String url, final String mimetype) {
|
||||
final Pair<Boolean, String> res = new Pair<Boolean, String>(false, null);
|
||||
|
||||
final Uri uri = Uri.parse(url);
|
||||
final String scheme = uri.getScheme();
|
||||
final String storageState = Environment.getExternalStorageState();
|
||||
|
||||
if (!ClientView.get().isGameActive()) {
|
||||
res.setSecond("downloading from this page not supported");
|
||||
} else if (!"data".equals(scheme)) {
|
||||
res.setSecond("download type \"" + scheme + "\" not supported");
|
||||
} else if (!"image/png".equals(mimetype) || !url.startsWith("data:image/png;base64,")) {
|
||||
res.setSecond("mimetype not supported: " + mimetype);
|
||||
} else if (!Environment.MEDIA_MOUNTED.equals(storageState)) {
|
||||
res.setSecond("storage not available for writing (state: " + storageState + ")");
|
||||
} else {
|
||||
final File targetDir = new File(Environment.getExternalStorageDirectory()
|
||||
+ "/Pictures/Screenshots");
|
||||
// TODO: format output name using YYYYMMDD_HH.MM.SS.MS
|
||||
final String targetName = "stendhal_" + System.currentTimeMillis() + ".png";
|
||||
|
||||
DebugLog.debug("Saving screenshot: " + targetDir.getPath()
|
||||
+ "/" + targetName + " (" + mimetype + ")");
|
||||
|
||||
String msg;
|
||||
String stacktrace = null;
|
||||
try {
|
||||
if (!targetDir.exists()) {
|
||||
targetDir.mkdirs();
|
||||
}
|
||||
|
||||
final byte[] data = Base64.decode(url.split("base64,")[1], Base64.DEFAULT);
|
||||
final FileOutputStream fos = new FileOutputStream(new File(targetDir, targetName));
|
||||
fos.write(data);
|
||||
fos.close();
|
||||
|
||||
res.setFirst(true);
|
||||
msg = "saved screenshot to " + targetDir.getPath() + "/" + targetName;
|
||||
} catch (final java.lang.NoClassDefFoundError e) {
|
||||
msg = "an error occurred while decoding data (see debug log for more info)";
|
||||
stacktrace = stackTraceToString(e);
|
||||
} catch (java.io.IOException e) {
|
||||
msg = "an error occurred while attempting to write file (see debug log for more info)";
|
||||
stacktrace = stackTraceToString(e);
|
||||
}
|
||||
|
||||
res.setSecond(msg);
|
||||
if (stacktrace != null) {
|
||||
DebugLog.error(stacktrace.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an exception stacktrace to string.
|
||||
*
|
||||
* @param e
|
||||
* The error with stacktrace information.
|
||||
* @return
|
||||
* String formatted stacktrace.
|
||||
*/
|
||||
private String stackTraceToString(final Throwable e) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append(e.toString());
|
||||
for (final StackTraceElement ste: e.getStackTrace()) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append("\n");
|
||||
}
|
||||
sb.append(ste.toString());
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2022 - Arianne *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.arianne.stendhal.client;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
|
||||
import org.arianne.stendhal.client.input.DPad;
|
||||
import org.arianne.stendhal.client.input.DPadArrows;
|
||||
import org.arianne.stendhal.client.input.DPadJoy;
|
||||
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private static MainActivity instance;
|
||||
|
||||
private ConstraintLayout layout;
|
||||
private ClientView client;
|
||||
private Menu menu;
|
||||
|
||||
|
||||
public static MainActivity get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
try {
|
||||
super.onCreate(savedInstanceState);
|
||||
instance = this;
|
||||
|
||||
// initialize debug logging mechanism
|
||||
DebugLog.init(getExternalFilesDir(null), this);
|
||||
|
||||
setContentView(R.layout.activity_main);
|
||||
layout = (ConstraintLayout) findViewById(R.id.content);
|
||||
menu = new Menu(this);
|
||||
client = (ClientView) findViewById(R.id.clientWebView);
|
||||
|
||||
// initialize d-pads
|
||||
final DPad arrowPad = DPadArrows.get();
|
||||
final DPad joyPad = DPadJoy.get();
|
||||
|
||||
if (PreferencesActivity.getBoolean("dpad_joy", true)) {
|
||||
DPad.setCurrentPad(joyPad);
|
||||
} else {
|
||||
DPad.setCurrentPad(arrowPad);
|
||||
}
|
||||
|
||||
layout.addView(arrowPad.getLayout());
|
||||
layout.addView(joyPad.getLayout());
|
||||
|
||||
client.loadTitleScreen();
|
||||
} catch (final Exception e) {
|
||||
e.printStackTrace();
|
||||
DebugLog.error(e.toString());
|
||||
DebugLog.error("// -- //");
|
||||
for (final StackTraceElement ste: e.getStackTrace()) {
|
||||
DebugLog.error(ste.toString());
|
||||
}
|
||||
DebugLog.error("// -- //");
|
||||
Notifier.get().showPrompt(
|
||||
"An unhandled exception has occurred: " + e.getMessage()
|
||||
+ "\n\nYou can report this error at: https://stendhalgame.org/development/bug.html",
|
||||
new Notifier.Action() {
|
||||
protected void onCall() {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
menu.toggleVisibility();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to connect to client host.
|
||||
*/
|
||||
public void loadLogin() {
|
||||
client.loadLogin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a dialog confirm exiting activity.
|
||||
*/
|
||||
public void onRequestQuit() {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setMessage("Quit Stendhal?");
|
||||
|
||||
builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
builder.setNegativeButton("No", new DialogInterface.OnClickListener() {
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
dialog.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
final AlertDialog confirmQuit = builder.create();
|
||||
confirmQuit.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
DPadArrows.get().onRefreshView();
|
||||
DPadJoy.get().onRefreshView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates direction pad position when screen orientation changes.
|
||||
*/
|
||||
@Override
|
||||
public void onConfigurationChanged(final Configuration config) {
|
||||
super.onConfigurationChanged(config);
|
||||
|
||||
DPadArrows.get().onRefreshView();
|
||||
DPadJoy.get().onRefreshView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
DebugLog.debug(MainActivity.class.getName() + ".finish() called");
|
||||
|
||||
super.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
DebugLog.debug(MainActivity.class.getName() + ".onDestroy() called");
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
public void showSettings() {
|
||||
startActivity(new Intent(MainActivity.this, PreferencesActivity.class));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2022-2024 - Faiumoni e. V. *
|
||||
* Copyright © 2022 - Arianne *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
|
|
@ -9,45 +9,40 @@
|
|||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.stendhalgame.client;
|
||||
package org.arianne.stendhal.client;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.Button;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
|
||||
/**
|
||||
* Main menu for navigation & configuration.
|
||||
*/
|
||||
public class Menu {
|
||||
|
||||
/** Toolbar representing menu. */
|
||||
private static Menu instance;
|
||||
|
||||
private final Context ctx;
|
||||
private final Toolbar nav;
|
||||
|
||||
private Button btn_connect;
|
||||
private Button btn_title;
|
||||
private Button btn_reload;
|
||||
|
||||
/** Singleton instance. */
|
||||
private static Menu instance;
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves menu singleton instance.
|
||||
*/
|
||||
public static Menu get() {
|
||||
if (Menu.instance == null) {
|
||||
Menu.instance = new Menu();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hidden singleton constructor.
|
||||
*/
|
||||
private Menu() {
|
||||
nav = (Toolbar) MainActivity.get().findViewById(R.id.menu_main);
|
||||
public Menu(final Context ctx) {
|
||||
instance = this;
|
||||
this.ctx = ctx;
|
||||
|
||||
nav = (Toolbar) ((Activity) ctx).findViewById(R.id.menu_main);
|
||||
|
||||
nav.setTag(nav.getVisibility());
|
||||
nav.getViewTreeObserver().addOnGlobalLayoutListener(
|
||||
new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
|
|
@ -58,30 +53,20 @@ public class Menu {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
initButtonHandlers();
|
||||
Logger.debug("menu initialized");
|
||||
|
||||
DebugLog.debug("menu initialized");
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the menu toolbar.
|
||||
*/
|
||||
public Toolbar getInternal() {
|
||||
return nav;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows or hides menu.
|
||||
*
|
||||
* @param vType
|
||||
* Visibility type. One of `View.VISIBLE`, `View.INVISIBLE`, or `View.GONE`.
|
||||
*/
|
||||
public void setVisibility(final int vType) {
|
||||
nav.setVisibility(vType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles visible state of menu.
|
||||
*/
|
||||
public void toggleVisibility() {
|
||||
if (nav.getVisibility() == View.GONE) {
|
||||
nav.setVisibility(View.VISIBLE);
|
||||
|
|
@ -90,25 +75,16 @@ public class Menu {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets menu visibility to visible.
|
||||
*/
|
||||
public void show() {
|
||||
nav.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets menu visibility to hidden.
|
||||
*/
|
||||
public void hide() {
|
||||
nav.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes displayed menu buttons.
|
||||
*/
|
||||
public void updateButtons() {
|
||||
final PageId page = MainActivity.get().getActiveClientView().getCurrentPageId();
|
||||
final PageId page = ClientView.getCurrentPageId();
|
||||
|
||||
btn_connect.setVisibility(View.GONE);
|
||||
btn_title.setVisibility(View.GONE);
|
||||
|
|
@ -126,11 +102,10 @@ public class Menu {
|
|||
* Sets actions when buttons are pressed.
|
||||
*/
|
||||
private void initButtonHandlers() {
|
||||
final MainActivity activity = MainActivity.get();
|
||||
final MainActivity activity = (MainActivity) ctx;
|
||||
|
||||
btn_connect = (Button) activity.findViewById(R.id.btn_connect);
|
||||
btn_connect.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
activity.loadLogin();
|
||||
}
|
||||
|
|
@ -138,20 +113,17 @@ public class Menu {
|
|||
|
||||
btn_title = (Button) activity.findViewById(R.id.btn_title);
|
||||
btn_title.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
nav.setVisibility(View.GONE);
|
||||
|
||||
Notifier.showPrompt("Return to title screen?",
|
||||
Notifier.get().showPrompt("Return to title screen?",
|
||||
new Notifier.Action() {
|
||||
@Override
|
||||
protected void onCall() {
|
||||
MainActivity.get().getActiveClientView().loadTitleScreen();
|
||||
ClientView.get().loadTitleScreen();
|
||||
updateButtons();
|
||||
}
|
||||
},
|
||||
new Notifier.Action() {
|
||||
@Override
|
||||
protected void onCall() {/* do nothing */}
|
||||
}
|
||||
);
|
||||
|
|
@ -160,15 +132,14 @@ public class Menu {
|
|||
|
||||
btn_reload = (Button) activity.findViewById(R.id.btn_reload);
|
||||
btn_reload.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
nav.setVisibility(View.GONE);
|
||||
|
||||
Notifier.showPrompt("Reload page?",
|
||||
Notifier.get().showPrompt("Reload page?",
|
||||
new Notifier.Action() {
|
||||
@Override
|
||||
protected void onCall() {
|
||||
MainActivity.get().getActiveClientView().reload();
|
||||
ClientView.get().reload();
|
||||
}
|
||||
},
|
||||
new Notifier.Action() {
|
||||
|
|
@ -182,57 +153,38 @@ public class Menu {
|
|||
|
||||
final Button btn_settings = (Button) activity.findViewById(R.id.btn_settings);
|
||||
btn_settings.setOnClickListener(new ClickListener() {
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
super.onClick(v);
|
||||
|
||||
activity.showSettings();
|
||||
((MainActivity) ctx).showSettings();
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
final Button btn_about = (Button) activity.findViewById(R.id.btn_about);
|
||||
btn_about.setOnClickListener(new ClickListener() {
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
super.onClick(v);
|
||||
|
||||
// TODO: create proper about activity
|
||||
|
||||
final ClientView client = MainActivity.get().getCurrentView();
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.get());
|
||||
final String buildType = AppInfo.getBuildType();
|
||||
String msg = "Android client version: " + AppInfo.getBuildVersion();
|
||||
if (!"prod".equals(buildType)) {
|
||||
msg += "\nAndroid client build: " + AppInfo.getBuildType();
|
||||
String server_ver = "unavailable";
|
||||
if (!(ClientView.getCurrentPageId() == PageId.WEBCLIENT)) {
|
||||
server_ver = "not connected";
|
||||
}
|
||||
if (client.debugEnabled()) {
|
||||
msg += "\nClient type: " + client.getSelectedClient()
|
||||
+ "\nServer type: " + client.getSelectedServer()
|
||||
+ "\nLogs directory: " + Logger.getLogsDir();
|
||||
}
|
||||
msg += "\n\nTitle Music:"
|
||||
+ "\n\n- Treasure Hunter"
|
||||
+ "\n by Tad Miller (TAD)"
|
||||
+ "\n - opengameart.org/node/127124"
|
||||
+ "\n\n- Woodland Fantasy"
|
||||
+ "\n by Matthew Pablo"
|
||||
+ "\n - matthewpablo.com"
|
||||
+ "\n - opengameart.org/node/31929"
|
||||
+ "\n\n- Land of Fearless"
|
||||
+ "\n by Alexandr Zhelanov"
|
||||
+ "\n - soundcloud.com/alexandr-zhelanov"
|
||||
+ "\n - opengameart.org/node/79931"
|
||||
+ "\n\n- Rejoicing"
|
||||
+ "\n by RandomMind"
|
||||
+ "\n - opengameart.org/node/100213"
|
||||
+ "\n\n- The Old Tower Inn"
|
||||
+ "\n by RandomMind"
|
||||
+ "\n - opengameart.org/node/87510";
|
||||
builder.setMessage(msg);
|
||||
|
||||
// FIXME: how to find server version if connected?
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder((Activity) ctx);
|
||||
builder.setMessage("WebView client version: "
|
||||
+ BuildConfig.VERSION_NAME + "\nServer version: " + server_ver
|
||||
+ "\n\nTitle Music:"
|
||||
+ "\n- \"Treasure Hunter\": Tad Miller (TAD)"
|
||||
+ "\n- \"Woodland Fantasy\": Matthew Pablo http://www.matthewpablo.com/"
|
||||
+ "\n- \"Land of Fearless\": Alexandr Zhelanov https://soundcloud.com/alexandr-zhelanov"
|
||||
+ "\n- \"Medieval: Rejoicing\": RandomMind"
|
||||
+ "\n- \"Medieval: The Old Tower Inn\": RandomMind");
|
||||
|
||||
builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
dialog.cancel();
|
||||
}
|
||||
|
|
@ -241,11 +193,9 @@ public class Menu {
|
|||
builder.create().show();
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
final Button btn_quit = (Button) activity.findViewById(R.id.btn_quit);
|
||||
btn_quit.setOnClickListener(new ClickListener() {
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
super.onClick(v);
|
||||
activity.onRequestQuit();
|
||||
|
|
@ -255,13 +205,9 @@ public class Menu {
|
|||
updateButtons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener to handle menu button presses.
|
||||
*/
|
||||
private class ClickListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
if (!MainActivity.get().getActiveClientView().onTitleScreen()) {
|
||||
if (!ClientView.onTitleScreen()) {
|
||||
nav.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2022-2024 - Faiumoni e. V. *
|
||||
* Copyright © 2022 - Arianne *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.stendhalgame.client;
|
||||
package org.arianne.stendhal.client;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
|
|
@ -19,32 +19,38 @@ import android.widget.Toast;
|
|||
|
||||
public class Notifier {
|
||||
|
||||
private static AlertDialog.Builder builder;
|
||||
private AlertDialog.Builder builder;
|
||||
|
||||
private static Notifier instance;
|
||||
|
||||
|
||||
/**
|
||||
* Static methods only.
|
||||
*/
|
||||
private Notifier() {
|
||||
// do nothing
|
||||
public static Notifier get() {
|
||||
if (instance == null) {
|
||||
instance = new Notifier();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static void showDialog() {
|
||||
private Notifier() {}
|
||||
|
||||
private void showDialog() {
|
||||
if (builder != null) {
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
builder = null;
|
||||
}
|
||||
|
||||
private static void createDialog(final Context ctx) {
|
||||
private void createDialog(final Context ctx) {
|
||||
builder = new AlertDialog.Builder(ctx);
|
||||
}
|
||||
|
||||
private static void createDialog() {
|
||||
private void createDialog() {
|
||||
createDialog(MainActivity.get());
|
||||
}
|
||||
|
||||
public static void showMessage(final String msg, final boolean cancelable, final String title) {
|
||||
public void showMessage(final String msg, final boolean cancelable, final String title) {
|
||||
createDialog();
|
||||
|
||||
builder.setCancelable(cancelable);
|
||||
|
|
@ -53,7 +59,6 @@ public class Notifier {
|
|||
builder.setTitle(title);
|
||||
}
|
||||
builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
dialog.cancel();
|
||||
}
|
||||
|
|
@ -62,19 +67,19 @@ public class Notifier {
|
|||
showDialog();
|
||||
}
|
||||
|
||||
public static void showMessage(final String msg, final boolean cancelable) {
|
||||
public void showMessage(final String msg, final boolean cancelable) {
|
||||
showMessage(msg, cancelable, null);
|
||||
}
|
||||
|
||||
public static void showMessage(final String msg) {
|
||||
public void showMessage(final String msg) {
|
||||
showMessage(msg, true, null);
|
||||
}
|
||||
|
||||
public static void showError(final String msg) {
|
||||
public void showError(final String msg) {
|
||||
showMessage(msg, false, "Error");
|
||||
}
|
||||
|
||||
public static void showPrompt(final Context ctx, final String msg, final Action... actions) {
|
||||
public void showPrompt(final Context ctx, final String msg, final Action... actions) {
|
||||
createDialog(ctx);
|
||||
|
||||
builder.setCancelable(false);
|
||||
|
|
@ -93,7 +98,6 @@ public class Notifier {
|
|||
}
|
||||
|
||||
builder.setPositiveButton(labelY, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
dialog.cancel();
|
||||
|
||||
|
|
@ -113,7 +117,6 @@ public class Notifier {
|
|||
}
|
||||
|
||||
builder.setNegativeButton(labelN, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
dialog.cancel();
|
||||
|
||||
|
|
@ -134,7 +137,6 @@ public class Notifier {
|
|||
}
|
||||
|
||||
builder.setNeutralButton(labelO, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
dialog.cancel();
|
||||
|
||||
|
|
@ -148,7 +150,7 @@ public class Notifier {
|
|||
showDialog();
|
||||
}
|
||||
|
||||
public static void showPrompt(final String msg, final Action... actions) {
|
||||
public void showPrompt(final String msg, final Action... actions) {
|
||||
showPrompt(MainActivity.get(), msg, actions);
|
||||
}
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2022-2024 - Faiumoni e. V. *
|
||||
* Copyright © 2022 - Arianne *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.stendhalgame.client;
|
||||
package org.arianne.stendhal.client;
|
||||
|
||||
|
||||
public enum PageId {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2022-2024 - Faiumoni e. V. *
|
||||
* Copyright © 2022 - Arianne *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
|
|
@ -9,47 +9,38 @@
|
|||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.stendhalgame.client;
|
||||
package org.arianne.stendhal.client;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.os.Bundle;
|
||||
import android.preference.CheckBoxPreference;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.CheckBoxPreference;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.arianne.stendhal.client.input.DPad;
|
||||
import org.arianne.stendhal.client.input.DPadArrows;
|
||||
import org.arianne.stendhal.client.input.DPadJoy;
|
||||
import org.arianne.stendhal.client.sound.MusicPlayer;
|
||||
|
||||
|
||||
/**
|
||||
* Interface for configuring app preferences.
|
||||
*/
|
||||
public class PreferencesActivity extends AppCompatActivity {
|
||||
|
||||
/** Preferences activity instance. */
|
||||
private static PreferencesActivity instance;
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves preferences activity instance.
|
||||
*/
|
||||
public static PreferencesActivity get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the activity is created.
|
||||
*/
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// FIXME: may be considered unsafe as this is not technically a singleton
|
||||
instance = this;
|
||||
|
||||
// use same orientation as main activity
|
||||
setRequestedOrientation(MainActivity.get().getRequestedOrientation());
|
||||
|
||||
setContentView(R.layout.activity_preferences);
|
||||
|
||||
if (findViewById(R.id.preferencesFrame) != null) {
|
||||
|
|
@ -57,8 +48,8 @@ public class PreferencesActivity extends AppCompatActivity {
|
|||
return;
|
||||
}
|
||||
|
||||
getSupportFragmentManager().beginTransaction().add(R.id.preferencesFrame,
|
||||
new PFragment()).commit();
|
||||
getFragmentManager().beginTransaction().add(R.id.preferencesFrame,
|
||||
new PFragment()).commit();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -128,62 +119,63 @@ public class PreferencesActivity extends AppCompatActivity {
|
|||
// SharedPreferences.getInt is returning string
|
||||
final Object obj = getSharedPreferences().getString(key, String.valueOf(defVal));
|
||||
if (obj instanceof String) {
|
||||
Logger.debug("PreferencesActivity.getInt: casting string return value to integer");
|
||||
DebugLog.debug("PreferencesActivity.getInt: casting string return value to integer");
|
||||
return Integer.parseInt((String) obj);
|
||||
} else {
|
||||
Logger.debug("PreferencesActivity.getInt return value is integer");
|
||||
DebugLog.debug("PreferencesActivity.getInt return value is integer");
|
||||
return (Integer) obj;
|
||||
}
|
||||
} catch (final NumberFormatException e) {
|
||||
final String errMsg = "An error occurred: " + e.getMessage();
|
||||
Logger.error(errMsg + "\n" + e.getStackTrace());
|
||||
Notifier.showError(errMsg + "\n\nSee " + Logger.getLogsDir()
|
||||
DebugLog.error(errMsg + "\n" + e.getStackTrace());
|
||||
Notifier.get().showError(errMsg + "\n\nSee " + DebugLog.getLogsDir()
|
||||
+ " for more information.");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
Logger.debug(PreferencesActivity.class.getName() + ".finish() called");
|
||||
DebugLog.debug(PreferencesActivity.class.getName() + ".finish() called");
|
||||
|
||||
super.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
Logger.debug(PreferencesActivity.class.getName() + ".onDestroy() called");
|
||||
DebugLog.debug(PreferencesActivity.class.getName() + ".onDestroy() called");
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
|
||||
public static class PFragment extends PreferenceFragmentCompat
|
||||
public static class PFragment extends PreferenceFragment
|
||||
implements OnSharedPreferenceChangeListener {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
initListeners();
|
||||
}
|
||||
|
||||
private void initListeners() {
|
||||
final PreferenceManager pm = getPreferenceManager();
|
||||
|
||||
// FIXME: should update playing music
|
||||
final Preference reset = pm.findPreference("reset");
|
||||
if (reset != null) {
|
||||
reset.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(final Preference pref) {
|
||||
Notifier.showPrompt(getActivity(), "Restore prefrences defaults?",
|
||||
Notifier.get().showPrompt(getActivity(), "Restore prefrences defaults?",
|
||||
new Notifier.Action() {
|
||||
@Override
|
||||
protected void onCall() {
|
||||
Logger.debug("restoring preferences default values");
|
||||
DebugLog.debug("restoring preferences default values");
|
||||
restoreDefaults();
|
||||
}
|
||||
},
|
||||
new Notifier.Action() {
|
||||
@Override
|
||||
protected void onCall() {
|
||||
// do nothing
|
||||
}
|
||||
|
|
@ -193,20 +185,19 @@ public class PreferencesActivity extends AppCompatActivity {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
Logger.warn("preference \"reset\" not found");
|
||||
DebugLog.warn("preference \"reset\" not found");
|
||||
}
|
||||
|
||||
final CheckBoxPreference title_music = (CheckBoxPreference) pm.findPreference("title_music");
|
||||
if (title_music != null) {
|
||||
title_music.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(final Preference pref, final Object obj) {
|
||||
// set music state as soon as preference is changed
|
||||
if (MainActivity.get().getActiveClientView().onTitleScreen()) {
|
||||
if (ClientView.onTitleScreen()) {
|
||||
if (!((Boolean) obj)) {
|
||||
MusicPlayer.stopMusic();
|
||||
} else if (!MusicPlayer.isPlaying()) {
|
||||
MusicPlayer.playTitleMusic();
|
||||
ClientView.playTitleMusic();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -226,14 +217,13 @@ public class PreferencesActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
song_list.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(final Preference pref, final Object obj) {
|
||||
// set music state as soon as preference is changed
|
||||
if (MainActivity.get().getActiveClientView().onTitleScreen()) {
|
||||
Logger.debug("changing title music preference: " + (String) obj);
|
||||
if (ClientView.onTitleScreen()) {
|
||||
DebugLog.debug("changing title music preference: " + (String) obj);
|
||||
|
||||
if (title_music.isChecked()) {
|
||||
MusicPlayer.playTitleMusic((String) obj);
|
||||
ClientView.playTitleMusic((String) obj);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -241,35 +231,6 @@ public class PreferencesActivity extends AppCompatActivity {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
final Preference clear_cache = pm.findPreference("clear_cache");
|
||||
if (clear_cache != null) {
|
||||
clear_cache.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(final Preference pref) {
|
||||
Notifier.showPrompt(getActivity(), "Delete cached data?",
|
||||
new Notifier.Action() {
|
||||
@Override
|
||||
protected void onCall() {
|
||||
Logger.debug("clearing cache");
|
||||
for (final ClientView clientView: MainActivity.get().getClientViewList()) {
|
||||
clientView.clearCache(true);
|
||||
}
|
||||
}
|
||||
},
|
||||
new Notifier.Action() {
|
||||
@Override
|
||||
protected void onCall() {
|
||||
// do nothing
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Logger.warn("preference \"clear_cache\" not found");
|
||||
}
|
||||
}
|
||||
|
||||
private void restoreDefaults() {
|
||||
|
|
@ -285,9 +246,25 @@ public class PreferencesActivity extends AppCompatActivity {
|
|||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences sp, final String key) {
|
||||
if ("orientation".equals(key)) {
|
||||
Logger.debug("orientation set to \"" + PreferencesActivity.getString(key) + "\"");
|
||||
MainActivity.get().updateOrientation();
|
||||
if (key.equals("show_dpad") || key.equals("dpad_joy")) {
|
||||
// reset all d-pads states to hidden
|
||||
DPadArrows.get().hide();
|
||||
DPadJoy.get().hide();
|
||||
|
||||
if (getBoolean("dpad_joy", true)) {
|
||||
DPad.setCurrentPad(DPadJoy.get());
|
||||
} else {
|
||||
DPad.setCurrentPad(DPadArrows.get());
|
||||
}
|
||||
|
||||
// restore visible state
|
||||
final DPad currentPad = DPad.getCurrentPad();
|
||||
if (currentPad != null && ClientView.isGameActive() && getBoolean("show_dpad", false)) {
|
||||
currentPad.show();
|
||||
currentPad.onRefreshView();
|
||||
}
|
||||
} else if (key.equals("joypad_sensitivity")) {
|
||||
DPadJoy.get().setSensitivity(getInt("joypad_sensitivity", 25));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2022 - Arianne *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.arianne.stendhal.client.input;
|
||||
|
||||
import android.view.View;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
|
||||
import marauroa.common.Pair;
|
||||
|
||||
|
||||
public abstract class DPad {
|
||||
|
||||
public static enum Dir {
|
||||
NONE,
|
||||
LEFT,
|
||||
RIGHT,
|
||||
UP,
|
||||
DOWN;
|
||||
}
|
||||
|
||||
private static DPad currentPad = null;
|
||||
|
||||
|
||||
public abstract ConstraintLayout getLayout();
|
||||
|
||||
public abstract void setVisibility(final int vis);
|
||||
|
||||
public abstract boolean isVisible();
|
||||
|
||||
public void show() {
|
||||
setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public abstract void setPosition(final int x, final int y);
|
||||
|
||||
public abstract Pair<Integer, Integer> getPosition();
|
||||
|
||||
public abstract Pair<Integer, Integer> getSize();
|
||||
|
||||
public abstract void onRefreshView();
|
||||
|
||||
public static void setCurrentPad(final DPad pad) {
|
||||
currentPad = pad;
|
||||
}
|
||||
|
||||
public static DPad getCurrentPad() {
|
||||
return currentPad;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2022 - Arianne *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.arianne.stendhal.client.input;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Point;
|
||||
import android.view.Display;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import marauroa.common.Pair;
|
||||
import org.arianne.stendhal.client.DebugLog;
|
||||
import org.arianne.stendhal.client.MainActivity;
|
||||
import org.arianne.stendhal.client.PreferencesActivity;
|
||||
import org.arianne.stendhal.client.R;
|
||||
|
||||
|
||||
/**
|
||||
* A directional pad on the screen.
|
||||
*
|
||||
* TODO: Option to use joystick style d-pad. Currently setting "dpad_joy"
|
||||
* is disabled in preferences manager.
|
||||
*/
|
||||
public class DPadArrows extends DPad {
|
||||
|
||||
private static DPadArrows instance;
|
||||
|
||||
private static Context ctx;
|
||||
|
||||
private final List<ArrowView> arrows;
|
||||
private final ConstraintLayout layout;
|
||||
|
||||
|
||||
public static DPadArrows get() {
|
||||
if (instance == null) {
|
||||
instance = new DPadArrows();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private DPadArrows() {
|
||||
ctx = (Context) MainActivity.get();
|
||||
|
||||
arrows = new LinkedList<>();
|
||||
layout = new ConstraintLayout(ctx);
|
||||
layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
|
||||
|
||||
final ArrowView arrow_l = new ArrowView(ctx, Dir.LEFT);
|
||||
final ArrowView arrow_r = new ArrowView(ctx, Dir.RIGHT);
|
||||
final ArrowView arrow_u = new ArrowView(ctx, Dir.UP);
|
||||
final ArrowView arrow_d = new ArrowView(ctx, Dir.DOWN);
|
||||
|
||||
arrows.add(arrow_l);
|
||||
arrows.add(arrow_r);
|
||||
arrows.add(arrow_u);
|
||||
arrows.add(arrow_d);
|
||||
|
||||
for (final ArrowView av: arrows) {
|
||||
layout.addView(av);
|
||||
}
|
||||
|
||||
// hidden by default
|
||||
hide();
|
||||
}
|
||||
|
||||
public ConstraintLayout getLayout() {
|
||||
return layout;
|
||||
}
|
||||
|
||||
public void setVisibility(final int vis) {
|
||||
for (final ArrowView av: arrows) {
|
||||
av.setVisibility(vis);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
for (final ArrowView av: arrows) {
|
||||
if (av.getVisibility() != View.VISIBLE) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setPosition(final int x, final int y) {
|
||||
ArrowView av = arrows.get(0);
|
||||
av.setX(x);
|
||||
av.setY(y + 60);
|
||||
|
||||
av = arrows.get(1);
|
||||
av.setX(x + 150);
|
||||
av.setY(y + 60);
|
||||
|
||||
av = arrows.get(2);
|
||||
av.setX(x + 60);
|
||||
av.setY(y);
|
||||
|
||||
av = arrows.get(3);
|
||||
av.setX(x + 60);
|
||||
av.setY(y + 150);
|
||||
}
|
||||
|
||||
private int getX() {
|
||||
return (int) arrows.get(0).getX();
|
||||
}
|
||||
|
||||
private int getY() {
|
||||
return (int) arrows.get(2).getY();
|
||||
}
|
||||
|
||||
public Pair<Integer, Integer> getPosition() {
|
||||
return new Pair<Integer, Integer>(getX(), getY());
|
||||
}
|
||||
|
||||
private int getWidth() {
|
||||
final ArrowView aRight = arrows.get(1);
|
||||
final int absX = ((int) aRight.getX()) + aRight.getWidth();
|
||||
|
||||
return absX - getX();
|
||||
}
|
||||
|
||||
private int getHeight() {
|
||||
final ArrowView aDown = arrows.get(3);
|
||||
final int absY = ((int) aDown.getY()) + aDown.getHeight();
|
||||
|
||||
return absY - getY();
|
||||
}
|
||||
|
||||
public Pair<Integer, Integer> getSize() {
|
||||
return new Pair<Integer, Integer>(getWidth(), getHeight());
|
||||
}
|
||||
|
||||
public void onRefreshView() {
|
||||
final Point size = new Point();
|
||||
final Display disp = MainActivity.get().getWindowManager().getDefaultDisplay();
|
||||
disp.getSize(size);
|
||||
|
||||
final PreferencesActivity prefs = PreferencesActivity.get();
|
||||
final int x = prefs.getInt("dpad_offset_x", 100);
|
||||
final int y = size.y - prefs.getInt("dpad_offset_y", 300);
|
||||
|
||||
setPosition(x, y);
|
||||
|
||||
if (isVisible()) {
|
||||
DebugLog.debug("DPadArrows: pos (" + getX() + "," + getY() + "), size ("
|
||||
+ getWidth() + "," + getHeight() + ")");
|
||||
DebugLog.debug("DPadArrows: left arrow: x (" + arrows.get(0).getX() + "), width ("
|
||||
+ arrows.get(0).getWidth() + ")");
|
||||
DebugLog.debug("DPadArrows: down arrow: y (" + arrows.get(3).getY() + "), height ("
|
||||
+ arrows.get(3).getHeight() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class ArrowView extends ImageView {
|
||||
|
||||
private final Dir dir;
|
||||
private final int id;
|
||||
private final int id_active;
|
||||
|
||||
private boolean keyDown = false;
|
||||
|
||||
|
||||
public ArrowView(final Context ctx, final Dir dir) {
|
||||
super(ctx);
|
||||
|
||||
this.dir = dir;
|
||||
|
||||
switch (dir) {
|
||||
case LEFT:
|
||||
id = R.drawable.dpad_arrow_left;
|
||||
id_active = R.drawable.dpad_arrow_left_active;
|
||||
break;
|
||||
case RIGHT:
|
||||
id = R.drawable.dpad_arrow_right;
|
||||
id_active = R.drawable.dpad_arrow_right_active;
|
||||
break;
|
||||
case UP:
|
||||
id = R.drawable.dpad_arrow_up;
|
||||
id_active = R.drawable.dpad_arrow_up_active;
|
||||
break;
|
||||
default:
|
||||
id = R.drawable.dpad_arrow_down;
|
||||
id_active = R.drawable.dpad_arrow_down_active;
|
||||
break;
|
||||
}
|
||||
|
||||
setBackgroundColor(android.graphics.Color.TRANSPARENT);
|
||||
setImageResource(id);
|
||||
|
||||
setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(final View view, final MotionEvent event) {
|
||||
final int action = event.getAction();
|
||||
|
||||
Integer newAction = null;
|
||||
Integer keyCode = null;
|
||||
|
||||
if (action == MotionEvent.ACTION_DOWN && !keyDown) {
|
||||
setImageResource(id_active);
|
||||
newAction = KeyEvent.ACTION_DOWN;
|
||||
keyDown = true;
|
||||
} else if (action == MotionEvent.ACTION_UP && keyDown) {
|
||||
setImageResource(id);
|
||||
newAction = KeyEvent.ACTION_UP;
|
||||
keyDown = false;
|
||||
}
|
||||
|
||||
if (newAction != null) {
|
||||
|
||||
switch (dir) {
|
||||
case LEFT:
|
||||
keyCode = KeyEvent.KEYCODE_DPAD_LEFT;
|
||||
break;
|
||||
case RIGHT:
|
||||
keyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||
break;
|
||||
case UP:
|
||||
keyCode = KeyEvent.KEYCODE_DPAD_UP;
|
||||
break;
|
||||
case DOWN:
|
||||
keyCode = KeyEvent.KEYCODE_DPAD_DOWN;
|
||||
break;
|
||||
}
|
||||
|
||||
MainActivity.get().dispatchKeyEvent(new KeyEvent(newAction, keyCode));
|
||||
return true; // consume event
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,332 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2022 - Arianne *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.arianne.stendhal.client.input;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Point;
|
||||
import android.view.Display;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import marauroa.common.Pair;
|
||||
import org.arianne.stendhal.client.DebugLog;
|
||||
import org.arianne.stendhal.client.MainActivity;
|
||||
import org.arianne.stendhal.client.PreferencesActivity;
|
||||
import org.arianne.stendhal.client.R;
|
||||
|
||||
|
||||
public class DPadJoy extends DPad {
|
||||
|
||||
private static DPadJoy instance;
|
||||
|
||||
private static Context ctx;
|
||||
|
||||
private static ConstraintLayout layout;
|
||||
|
||||
private static OuterButton outerButton;
|
||||
private static int sensitivity;
|
||||
|
||||
|
||||
public static DPadJoy get() {
|
||||
if (instance == null) {
|
||||
instance = new DPadJoy();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private DPadJoy() {
|
||||
ctx = (Context) MainActivity.get();
|
||||
layout = new ConstraintLayout(ctx);
|
||||
layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
|
||||
|
||||
outerButton = new OuterButton(ctx);
|
||||
outerButton.addToLayout();
|
||||
|
||||
setSensitivity(PreferencesActivity.getInt("joypad_sensitivity", 25));
|
||||
|
||||
// hidden by default
|
||||
hide();
|
||||
}
|
||||
|
||||
public ConstraintLayout getLayout() {
|
||||
return layout;
|
||||
}
|
||||
|
||||
public void setVisibility(final int vis) {
|
||||
outerButton.setVisibility(vis);
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
return outerButton.isVisible();
|
||||
}
|
||||
|
||||
public void setPosition(final int x, final int y) {
|
||||
outerButton.setPosition(x, y);
|
||||
}
|
||||
|
||||
private int getX() {
|
||||
return (int) outerButton.getX();
|
||||
}
|
||||
|
||||
private int getY() {
|
||||
return (int) outerButton.getY();
|
||||
}
|
||||
|
||||
public Pair<Integer, Integer> getPosition() {
|
||||
return new Pair<Integer, Integer>(getX(), getY());
|
||||
}
|
||||
|
||||
private int getWidth() {
|
||||
return outerButton.getWidth();
|
||||
}
|
||||
|
||||
private int getHeight() {
|
||||
return outerButton.getHeight();
|
||||
}
|
||||
|
||||
public Pair<Integer, Integer> getSize() {
|
||||
return new Pair<Integer, Integer>(getWidth(), getHeight());
|
||||
}
|
||||
|
||||
public void setSensitivity(final int value) {
|
||||
sensitivity = value;
|
||||
}
|
||||
|
||||
public void onRefreshView() {
|
||||
final Point size = new Point();
|
||||
final Display disp = MainActivity.get().getWindowManager().getDefaultDisplay();
|
||||
disp.getSize(size);
|
||||
|
||||
final PreferencesActivity prefs = PreferencesActivity.get();
|
||||
final int x = prefs.getInt("dpad_offset_x", 100);
|
||||
final int y = size.y - prefs.getInt("dpad_offset_y", 300);
|
||||
|
||||
setPosition(x, y);
|
||||
|
||||
if (isVisible()) {
|
||||
DebugLog.debug("DPadJoy: pos (" + getX() + "," + getY() + "), size ("
|
||||
+ getWidth() + "," + getHeight() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class OuterButton extends JoyPadButton {
|
||||
|
||||
private final InnerButton innerButton;
|
||||
|
||||
private DPad.Dir currentDir = DPad.Dir.NONE;
|
||||
private boolean keyDown = false;
|
||||
private final List<Integer> pressedKeys;
|
||||
|
||||
|
||||
public OuterButton(final Context ctx) {
|
||||
super(ctx);
|
||||
|
||||
pressedKeys = new ArrayList<>();
|
||||
|
||||
setBackgroundColor(android.graphics.Color.TRANSPARENT);
|
||||
setImageResource(R.drawable.dpad_circle_outer);
|
||||
|
||||
innerButton = new InnerButton(ctx);
|
||||
innerButton.setBackgroundColor(android.graphics.Color.TRANSPARENT);
|
||||
innerButton.setImageResource(R.drawable.dpad_circle_inner);
|
||||
|
||||
setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(final View view, final MotionEvent event) {
|
||||
final int action = event.getAction();
|
||||
|
||||
DebugLog.debug("joypad action: " + action);
|
||||
|
||||
if (action == MotionEvent.ACTION_DOWN && !keyDown) {
|
||||
keyDown = true;
|
||||
innerButton.setImageResource(R.drawable.dpad_circle_inner_active);
|
||||
innerButton.setRelativeX(event.getX());
|
||||
innerButton.setRelativeY(event.getY());
|
||||
|
||||
final DPad.Dir newDir = getTouchDirection();
|
||||
if (newDir != currentDir) {
|
||||
currentDir = newDir;
|
||||
onDirectionChange(KeyEvent.ACTION_DOWN);
|
||||
}
|
||||
} else if (action == MotionEvent.ACTION_UP && keyDown) {
|
||||
keyDown = false;
|
||||
innerButton.setImageResource(R.drawable.dpad_circle_inner);
|
||||
innerButton.centerOn((JoyPadButton) view);
|
||||
|
||||
clearPresses();
|
||||
currentDir = DPad.Dir.NONE;
|
||||
} else if (action == MotionEvent.ACTION_MOVE && keyDown) {
|
||||
innerButton.setRelativeX(event.getX());
|
||||
innerButton.setRelativeY(event.getY());
|
||||
|
||||
final DPad.Dir newDir = getTouchDirection();
|
||||
if (newDir != currentDir) {
|
||||
currentDir = newDir;
|
||||
onDirectionChange(KeyEvent.ACTION_DOWN);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void addToLayout() {
|
||||
layout.addView(this);
|
||||
layout.addView(innerButton);
|
||||
|
||||
// FIXME: inner button not centering at startup
|
||||
innerButton.centerOn(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisibility(final int vis) {
|
||||
innerButton.setVisibility(vis);
|
||||
super.setVisibility(vis);
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
return getVisibility() == View.VISIBLE
|
||||
&& innerButton.getVisibility() == View.VISIBLE;
|
||||
}
|
||||
|
||||
public void setPosition(final int x, final int y) {
|
||||
setX(x);
|
||||
setY(y);
|
||||
|
||||
innerButton.centerOn(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines pressed direction based on inner button position
|
||||
* relative to outer button position.
|
||||
*/
|
||||
private DPad.Dir getTouchDirection() {
|
||||
final float outerX = getCenterX();
|
||||
final float outerY = getCenterY();
|
||||
final float innerX = innerButton.getCenterX();
|
||||
final float innerY = innerButton.getCenterY();
|
||||
final float offsetX = outerX - innerX;
|
||||
final float offsetY = outerY - innerY;
|
||||
|
||||
final int absX = (int) Math.abs(offsetX);
|
||||
final int absY = (int) Math.abs(offsetY);
|
||||
|
||||
if (absX < sensitivity && absY < sensitivity) {
|
||||
return DPad.Dir.NONE;
|
||||
}
|
||||
|
||||
// horizontal directions take precedence if they are the same as vertical
|
||||
if (absX >= absY) {
|
||||
if (innerX < outerX) {
|
||||
return DPad.Dir.LEFT;
|
||||
} else if (innerX > outerX) {
|
||||
return DPad.Dir.RIGHT;
|
||||
}
|
||||
} else {
|
||||
if (innerY < outerY) {
|
||||
return DPad.Dir.UP;
|
||||
} else if (innerY > outerY) {
|
||||
return DPad.Dir.DOWN;
|
||||
}
|
||||
}
|
||||
|
||||
return DPad.Dir.NONE;
|
||||
}
|
||||
|
||||
private void clearPresses() {
|
||||
for (final int key: pressedKeys) {
|
||||
MainActivity.get().dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, key));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a direction key press event to client.
|
||||
*/
|
||||
private void onDirectionChange(final int action) {
|
||||
Integer keyCode = null;
|
||||
|
||||
switch (currentDir) {
|
||||
case LEFT:
|
||||
keyCode = KeyEvent.KEYCODE_DPAD_LEFT;
|
||||
break;
|
||||
case RIGHT:
|
||||
keyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||
break;
|
||||
case UP:
|
||||
keyCode = KeyEvent.KEYCODE_DPAD_UP;
|
||||
break;
|
||||
case DOWN:
|
||||
keyCode = KeyEvent.KEYCODE_DPAD_DOWN;
|
||||
break;
|
||||
}
|
||||
|
||||
if (keyCode != null) {
|
||||
// clear pressed keys before dispatching new key press
|
||||
clearPresses();
|
||||
|
||||
if (action == KeyEvent.ACTION_DOWN) {
|
||||
MainActivity.get().dispatchKeyEvent(new KeyEvent(action, keyCode));
|
||||
pressedKeys.add(keyCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class InnerButton extends JoyPadButton {
|
||||
|
||||
public InnerButton(final Context ctx) {
|
||||
super(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets X coordinate relative to OuterButton.
|
||||
*/
|
||||
public void setRelativeX(float x) {
|
||||
x = x + outerButton.getX();
|
||||
|
||||
final float maxL = outerButton.getX();
|
||||
final float maxR = maxL + outerButton.getWidth();
|
||||
|
||||
// don't move outside bounds of outer button
|
||||
if (x >= maxL && x <= maxR) {
|
||||
// use circle center
|
||||
setX(x - (getWidth() / 2));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets Y coordinate relative to OuterButton.
|
||||
*/
|
||||
public void setRelativeY(float y) {
|
||||
y = y + outerButton.getY();
|
||||
|
||||
final float maxT = outerButton.getY();
|
||||
final float maxB = maxT + outerButton.getHeight();
|
||||
|
||||
// don't move outside bounds of outer button
|
||||
if (y >= maxT && y <= maxB) {
|
||||
// use circle center
|
||||
setY(y - (getHeight() / 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2024 - Faiumoni e. V. *
|
||||
* Copyright © 2022 - Arianne *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
|
|
@ -9,33 +9,29 @@
|
|||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package games.stendhal.server.entity.npc.behaviour.impl.idle;
|
||||
package org.arianne.stendhal.client.input;
|
||||
|
||||
import games.stendhal.server.entity.npc.NPC;
|
||||
import android.content.Context;
|
||||
//import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
|
||||
public abstract class AbstractIdleBehaviour implements IdleBehaviour {
|
||||
abstract class JoyPadButton extends ImageView {
|
||||
|
||||
@Override
|
||||
public abstract void perform(NPC npc);
|
||||
|
||||
@Override
|
||||
public void onMoved(NPC npc) {
|
||||
// does nothing in this implementation
|
||||
public JoyPadButton(final Context ctx) {
|
||||
super(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
// does nothing in this implementation
|
||||
protected float getCenterX() {
|
||||
return getX() + (getWidth() / 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleSimpleCollision(NPC npc, int nx, int ny) {
|
||||
return false;
|
||||
protected float getCenterY() {
|
||||
return getY() + (getHeight() / 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleObjectCollision(NPC npc) {
|
||||
return false;
|
||||
protected void centerOn(final JoyPadButton parent) {
|
||||
super.setX(parent.getCenterX() - (getWidth() / 2));
|
||||
super.setY(parent.getCenterY() - (getHeight() / 2));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2023 - Stendhal *
|
||||
* Copyright © 2022 - Arianne *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
|
|
@ -9,20 +9,29 @@
|
|||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package games.stendhal.server.entity.npc;
|
||||
package org.arianne.stendhal.client.js;
|
||||
|
||||
public class CrystalNPCBase extends SpeakerNPC {
|
||||
import android.webkit.JavascriptInterface;
|
||||
|
||||
public CrystalNPCBase(final String name) {
|
||||
super(name);
|
||||
|
||||
// hide location from website
|
||||
hideLocation();
|
||||
|
||||
public class JSInterface {
|
||||
|
||||
/** Singleton instance. */
|
||||
private static JSInterface instance;
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the singleton instance.
|
||||
*/
|
||||
public static JSInterface get() {
|
||||
if (instance == null) {
|
||||
instance = new JSInterface();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createPath() {
|
||||
// doesn't move
|
||||
setPath(null);
|
||||
private JSInterface() {
|
||||
// singleton
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2022-2024 - Faiumoni e. V. *
|
||||
* Copyright © 2022 - Arianne *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
|
|
@ -9,14 +9,17 @@
|
|||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.stendhalgame.client;
|
||||
|
||||
import java.io.IOException;
|
||||
package org.arianne.stendhal.client.sound;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.arianne.stendhal.client.DebugLog;
|
||||
import org.arianne.stendhal.client.MainActivity;
|
||||
|
||||
|
||||
public class MusicPlayer {
|
||||
|
||||
|
|
@ -25,24 +28,22 @@ public class MusicPlayer {
|
|||
|
||||
public static void playMusic(final Context ctx, final int id, final boolean loop) {
|
||||
if (isPlaying()) {
|
||||
Logger.debug("freeing up MusicPlayer instance to play new song");
|
||||
DebugLog.debug("freeing up MusicPlayer instance to play new song");
|
||||
stopMusic();
|
||||
}
|
||||
|
||||
Logger.debug("starting music (loop: " + loop + ")");
|
||||
DebugLog.debug("starting music (loop: " + loop + ")");
|
||||
|
||||
mplayer = new MediaPlayer();
|
||||
mplayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
|
||||
@Override
|
||||
public void onPrepared(final MediaPlayer mp) {
|
||||
Logger.debug("starting music");
|
||||
DebugLog.debug("starting music");
|
||||
// FIXME: not working
|
||||
//mplayer.setLooping(loop);
|
||||
mplayer.start();
|
||||
}
|
||||
});
|
||||
mplayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
|
||||
@Override
|
||||
public void onCompletion(final MediaPlayer mp) {
|
||||
// free up resources & reset player to null
|
||||
stopMusic();
|
||||
|
|
@ -58,7 +59,7 @@ public class MusicPlayer {
|
|||
mplayer.setDataSource(ctx, parseResourceUri(id));
|
||||
mplayer.prepareAsync();
|
||||
} catch (final IOException e) {
|
||||
Logger.error("failed to load resource " + id + ":\n" + e.getStackTrace());
|
||||
DebugLog.error("failed to load resource " + id + ":\n" + e.getStackTrace());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,46 +67,10 @@ public class MusicPlayer {
|
|||
playMusic(MainActivity.get(), id, loop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays selected music.
|
||||
*
|
||||
* @param musicId
|
||||
* String identifier of music to play or `null` to play music configured in preferences.
|
||||
*/
|
||||
public static void playTitleMusic(String musicId) {
|
||||
if (musicId == null) {
|
||||
musicId = PreferencesActivity.getString("song_list");
|
||||
}
|
||||
int id = R.raw.title_01;
|
||||
switch (musicId) {
|
||||
case "title_02":
|
||||
id = R.raw.title_02;
|
||||
break;
|
||||
case "title_03":
|
||||
id = R.raw.title_03;
|
||||
break;
|
||||
case "title_04":
|
||||
id = R.raw.title_04;
|
||||
break;
|
||||
case "title_05":
|
||||
id = R.raw.title_05;
|
||||
break;
|
||||
}
|
||||
Logger.debug("playing music: " + musicId);
|
||||
MusicPlayer.playMusic(id, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays music configured in preferences.
|
||||
*/
|
||||
public static void playTitleMusic() {
|
||||
playTitleMusic(null);
|
||||
}
|
||||
|
||||
public static void stopMusic() {
|
||||
if (mplayer != null) {
|
||||
if (mplayer.isPlaying()) {
|
||||
Logger.debug("stopping music");
|
||||
DebugLog.debug("stopping music");
|
||||
mplayer.stop();
|
||||
}
|
||||
mplayer.release();
|
||||
BIN
android/app/src/main/res/drawable/dpad_arrow_down.png
Normal file
|
After Width: | Height: | Size: 342 B |
BIN
android/app/src/main/res/drawable/dpad_arrow_down_active.png
Normal file
|
After Width: | Height: | Size: 354 B |
BIN
android/app/src/main/res/drawable/dpad_arrow_left.png
Normal file
|
After Width: | Height: | Size: 389 B |
BIN
android/app/src/main/res/drawable/dpad_arrow_left_active.png
Normal file
|
After Width: | Height: | Size: 400 B |
BIN
android/app/src/main/res/drawable/dpad_arrow_right.png
Normal file
|
After Width: | Height: | Size: 375 B |
BIN
android/app/src/main/res/drawable/dpad_arrow_right_active.png
Normal file
|
After Width: | Height: | Size: 386 B |
BIN
android/app/src/main/res/drawable/dpad_arrow_up.png
Normal file
|
After Width: | Height: | Size: 292 B |
BIN
android/app/src/main/res/drawable/dpad_arrow_up_active.png
Normal file
|
After Width: | Height: | Size: 303 B |
BIN
android/app/src/main/res/drawable/dpad_circle_inner.png
Normal file
|
After Width: | Height: | Size: 441 B |
BIN
android/app/src/main/res/drawable/dpad_circle_inner_active.png
Normal file
|
After Width: | Height: | Size: 443 B |
BIN
android/app/src/main/res/drawable/dpad_circle_outer.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
98
android/app/src/main/res/layout/activity_main.xml
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="org.arianne.stendhal.client.StendhalWebView">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/splash"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
/>
|
||||
|
||||
<org.arianne.stendhal.client.ClientView
|
||||
android:id="@+id/clientWebView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
/>
|
||||
|
||||
<Toolbar
|
||||
android:id="@+id/menu_main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#80ffffff"
|
||||
android:logo="@mipmap/ic_launcher_round"
|
||||
android:title="Menu"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="right">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_connect"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Connect"
|
||||
android:background="#00000000"
|
||||
/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Title"
|
||||
android:background="#00000000"
|
||||
/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_reload"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Reload"
|
||||
android:background="#00000000"
|
||||
/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_settings"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Settings"
|
||||
android:background="#00000000"
|
||||
/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_about"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="About"
|
||||
android:background="#00000000"
|
||||
/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_quit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Quit"
|
||||
android:background="#00000000"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</Toolbar>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
18
android/app/src/main/res/layout/activity_preferences.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/preferences"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="org.arianne.stendhal.client.PreferencesActivity">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/preferencesFrame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
21
android/app/src/main/res/values/array.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<resources>
|
||||
|
||||
<string-array name="titleMusic">
|
||||
<item>Treasure Hunter</item>
|
||||
<item>Woodland Fantasy</item>
|
||||
<item>Land of Fearless</item>
|
||||
<item>Medieval: Rejoicing</item>
|
||||
<item>Medieval: The Old Tower Inn</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="titleMusicId">
|
||||
<item>title_01</item>
|
||||
<item>title_02</item>
|
||||
<item>title_03</item>
|
||||
<item>title_04</item>
|
||||
<item>title_05</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
7
android/app/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="app_name">Stendhal</string>
|
||||
|
||||
</resources>
|
||||
107
android/app/src/main/res/xml/preferences.xml
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<PreferenceCategory android:title="General">
|
||||
|
||||
<EditTextPreference
|
||||
android:key="client_url"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:title="Client URL"
|
||||
android:summary="Custom URL pointing to web client."
|
||||
android:selectAllOnFocus="true"
|
||||
/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="Input">
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="show_dpad"
|
||||
android:defaultValue="false"
|
||||
android:title="Show D-Pad"
|
||||
android:summary="Show on-screen directional pad."
|
||||
/>
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="dpad_joy"
|
||||
android:defaultValue="true"
|
||||
android:title="Joystick Style D-Pad (experimental)"
|
||||
android:summary="Use a joystick style directional pad."
|
||||
android:dependency="show_dpad"
|
||||
/>
|
||||
|
||||
<EditTextPreference
|
||||
android:key="joypad_sensitivity"
|
||||
android:inputType="numberDecimal"
|
||||
android:digits="0123456789"
|
||||
android:defaultValue="25"
|
||||
android:title="Joypad Sensitivity"
|
||||
android:summary="Threshold, in pixels, at which joypad will dispatch move event (lower = more sensitive)."
|
||||
android:dependency="dpad_joy"
|
||||
android:selectAllOnFocus="true"
|
||||
/>
|
||||
|
||||
<EditTextPreference
|
||||
android:key="dpad_offset_x"
|
||||
android:inputType="numberDecimal"
|
||||
android:digits="0123456789"
|
||||
android:defaultValue="100"
|
||||
android:title="D-Pad Offset X"
|
||||
android:summary="Horizontal offset of directional pad (from left)."
|
||||
android:dependency="show_dpad"
|
||||
android:selectAllOnFocus="true"
|
||||
/>
|
||||
|
||||
<EditTextPreference
|
||||
android:key="dpad_offset_y"
|
||||
android:inputType="numberDecimal"
|
||||
android:digits="0123456789"
|
||||
android:defaultValue="300"
|
||||
android:title="D-Pad Offset Y"
|
||||
android:summary="Vertical offset of directional pad (from bottom)."
|
||||
android:dependency="show_dpad"
|
||||
android:selectAllOnFocus="true"
|
||||
/>
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="map_touches"
|
||||
android:defaultValue="false"
|
||||
android:title="Remap Touches (experimental)"
|
||||
android:summary="Map touches to mouse click events."
|
||||
/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="Audio">
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="title_music"
|
||||
android:defaultValue="true"
|
||||
android:title="Title Music"
|
||||
android:summary="Play title screen music."
|
||||
/>
|
||||
|
||||
<ListPreference
|
||||
android:key="song_list"
|
||||
android:dependency="title_music"
|
||||
android:title="Songs"
|
||||
android:entries="@array/titleMusic"
|
||||
android:entryValues="@array/titleMusicId"
|
||||
/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="Meta">
|
||||
|
||||
<Preference
|
||||
android:key="reset"
|
||||
android:title="Restore Defaults"
|
||||
android:summary="Restore all preferences default values."
|
||||
/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2022 - Arianne *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.arianne.stendhal.client;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
|
||||
/**
|
||||
* Dummy class for release builds.
|
||||
*/
|
||||
public class DebugLog {
|
||||
|
||||
private static File logsDir;
|
||||
|
||||
public static enum DebugLevel {
|
||||
INFO,
|
||||
WARN,
|
||||
ERROR,
|
||||
DEBUG
|
||||
}
|
||||
|
||||
private static DebugLog instance;
|
||||
|
||||
|
||||
public static DebugLog get() {
|
||||
if (instance == null) {
|
||||
instance = new DebugLog();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static void init(final File dir, final AppCompatActivity activity) {
|
||||
logsDir = new File(dir.getPath() + "/logs");
|
||||
}
|
||||
|
||||
public static void writeLine(final String text) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public static void writeLine(final String text, final DebugLevel level) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public static void info(final String text) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public static void warn(final String text) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public static void error(final String text) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public static void debug(final String text) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public static void notify(final String message) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public static void notify(final String message, final DebugLevel level) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public static String getLogsDir() {
|
||||
return logsDir.getPath();
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ buildscript {
|
|||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.4.2'
|
||||
classpath "com.android.tools.build:gradle:7.1.1"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx512m -Dfile.encoding=UTF-8
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
|
|
@ -16,6 +16,4 @@ org.gradle.jvmargs=-Xmx512m -Dfile.encoding=UTF-8
|
|||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
# Stop daemon after build
|
||||
org.gradle.daemon=false
|
||||
android.enableJetifier=true
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
185
android/gradlew
vendored
Executable file
|
|
@ -0,0 +1,185 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
14
app/android/gradlew.bat → android/gradlew.bat
vendored
|
|
@ -14,7 +14,7 @@
|
|||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
|||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
|
@ -75,15 +75,13 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
|
@ -6,4 +6,4 @@ dependencyResolutionManagement {
|
|||
}
|
||||
}
|
||||
rootProject.name = "Stendhal"
|
||||
include ':client'
|
||||
include ':app'
|
||||
1
app/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
/godot/
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
|
||||
TODO:
|
||||
- create activity for about information
|
||||
- add credits to about information
|
||||
- switch to Log4j for logging (maybe)
|
||||
- write debugging logs to sub-directory in user's home instead of ~/Android/data/org.stendhalgame.client
|
||||
- replace title_03 (Land of Fearless), title_04 (Rejoicing), & title_05 (The Old Tower Inn) music with non-looping versions or fix loops
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "org.stendhalgame.client"
|
||||
compileSdk 32
|
||||
|
||||
defaultConfig {
|
||||
applicationId namespace
|
||||
minSdk 21
|
||||
targetSdk 32
|
||||
versionCode 1049005
|
||||
versionName "1.49.5"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
targetSdkVersion 32
|
||||
minSdkVersion 21
|
||||
}
|
||||
|
||||
// F-Droid doesn't allow encrypted DependencyInfoBlock in APK
|
||||
dependenciesInfo {
|
||||
// disables dependency metadata when building APKs
|
||||
includeInApk = false
|
||||
// disables dependency metadata when building Android App Bundles
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
// build output
|
||||
project.setProperty("archivesBaseName", "${namespace}-${defaultConfig.versionName}")
|
||||
project.setProperty("buildDir", "${projectDir}/../../../build/build_android_client")
|
||||
|
||||
/* A keystore.properties file can be placed in the root Android project directory and must
|
||||
* include the following values:
|
||||
* storeFile=<path-to-keystore-file>
|
||||
* storePassword=<store-password>
|
||||
* keyPassword=<key-password>
|
||||
* keyAlias=<key-alias>
|
||||
* The `storeFile` path value must use double backslashes (\\) on Windows.
|
||||
*/
|
||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
def keystoreProperties = new Properties()
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
// denotes whether a signing key is found/configured
|
||||
def unsigned = true;
|
||||
// property to force use of ~/.android/debug.keystore signing key & generate if not found
|
||||
def generateDebugKey = project.properties["generate-debug-key"] != null;
|
||||
|
||||
signingConfigs {
|
||||
main {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
// use key configured in keystore.properties file
|
||||
storeFile file(keystoreProperties["storeFile"])
|
||||
storePassword keystoreProperties["storePassword"]
|
||||
keyPassword keystoreProperties["keyPassword"]
|
||||
keyAlias keystoreProperties["keyAlias"]
|
||||
unsigned = false;
|
||||
} else if (signingConfigs.debug.storeFile.exists()) {
|
||||
// use debug key
|
||||
initWith signingConfigs.debug
|
||||
unsigned = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (generateDebugKey && signingConfigs.main != signingConfigs.debug) {
|
||||
// force using debug key
|
||||
main {
|
||||
initWith signingConfigs.debug
|
||||
}
|
||||
unsigned = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!unsigned && !generateDebugKey && !signingConfigs.main.storeFile.exists()) {
|
||||
throw new Error("Signing key not found: " + signingConfigs.main.storeFile);
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
versionNameSuffix "-debug"
|
||||
// prevent automatic generation of ~/.android/debug.keystore
|
||||
signingConfig null
|
||||
}
|
||||
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
|
||||
if (generateDebugKey) {
|
||||
// force using debug key & generate if not found
|
||||
debug.signingConfig signingConfigs.debug
|
||||
release.signingConfig signingConfigs.debug
|
||||
} else if (!unsigned) {
|
||||
// apply configured signing key to builds
|
||||
debug.signingConfig signingConfigs.main
|
||||
release.signingConfig signingConfigs.main
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes.all { type ->
|
||||
if (!type.name.equals("debug")) {
|
||||
sourceSets[type.name].java { srcDirs "src/shared/java/" }
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.all {
|
||||
def buildRoot = "../../../../"
|
||||
if (variant.name.equals("release")) {
|
||||
// exclude "-release" suffix
|
||||
outputFileName = outputFileName.replaceAll("(-release)(?!.*-release)", "")
|
||||
}
|
||||
// output to <source_root>/build
|
||||
outputFileName = new File("${buildRoot}", "${outputFileName}")
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
compileSdkVersion 32
|
||||
buildToolsVersion '32.0.0'
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
// print some build info for debugging
|
||||
println("Package: " + namespace)
|
||||
println("Application ID: " + defaultConfig.applicationId)
|
||||
println("Version: " + defaultConfig.versionCode)
|
||||
println("Output basename: " + archivesBaseName)
|
||||
println("Signing key: " + (unsigned ? "unsigned" : signingConfigs.main.storeFile))
|
||||
println("Build directory: " + project.buildDir)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.appcompat:appcompat:1.4.1"
|
||||
implementation "com.google.android.material:material:1.4.0"
|
||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
||||
//testImplementation 'junit:junit:4.+'
|
||||
//androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
//androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- allow client url http://localhost:xxxx via Chrome forwarding to a local server -->
|
||||
<application android:usesCleartextTraffic="true">
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
@ -1,277 +0,0 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2022-2024 - Faiumoni e. V. *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.stendhalgame.client;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.util.Log;
|
||||
|
||||
|
||||
/**
|
||||
* A class to output debugging information to logcat file.
|
||||
*/
|
||||
public class Logger {
|
||||
|
||||
/** Attribute denoting state of initialization. */
|
||||
private static boolean initialized = false;
|
||||
/** Logs directory. */
|
||||
private static File logsDir;
|
||||
|
||||
/** Singleton instance. */
|
||||
private static Logger instance;
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves singleton instance.
|
||||
*/
|
||||
public static Logger get() {
|
||||
if (Logger.instance == null) {
|
||||
Logger.instance = new Logger();
|
||||
}
|
||||
return Logger.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hidden singleton constructor.
|
||||
*/
|
||||
private Logger() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes logs directory.
|
||||
*/
|
||||
public static void init(final File dir) {
|
||||
if (Logger.initialized) {
|
||||
Logger.warn(true, "tried to re-initialize logger");
|
||||
return;
|
||||
}
|
||||
Logger.initialized = true;
|
||||
logsDir = new File(dir.getPath() + "/logs");
|
||||
writeLine("\n // -- debugging initialized -- //", null);
|
||||
writeLine("logs directory: " + logsDir.getPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a line of text to log file.
|
||||
*
|
||||
* @param text
|
||||
* Text to be written.
|
||||
*/
|
||||
public static void writeLine(final String text) {
|
||||
writeLine(text, LogLevel.DEBUG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a line of text to log file.
|
||||
*
|
||||
* @param text
|
||||
* Text to be written.
|
||||
* @param level
|
||||
* Logging verbosity level.
|
||||
*/
|
||||
public static void writeLine(String text, final LogLevel level) {
|
||||
if (logsDir == null) {
|
||||
System.err.println("ERROR: Logger not initialized. Call Logger.init.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!logsDir.exists()) {
|
||||
logsDir.mkdirs();
|
||||
}
|
||||
|
||||
if (level != null) {
|
||||
text = new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss").format(new Date())
|
||||
+ ": " + level.label + ": " + text;
|
||||
}
|
||||
text = text + "\n";
|
||||
|
||||
final String logFile = logsDir.getPath() + "/debug-"
|
||||
+ new SimpleDateFormat("yyyy.MM.dd").format(new Date()) + ".txt";
|
||||
|
||||
BufferedWriter buffer;
|
||||
try {
|
||||
buffer = new BufferedWriter(new FileWriter(logFile, true));
|
||||
buffer.write(text);
|
||||
if (buffer != null) {
|
||||
buffer.close();
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
System.err.println("ERROR: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a line of text to log file at `LogLevel.INFO` verbosity.
|
||||
*
|
||||
* @param text
|
||||
* Text to be written.
|
||||
*/
|
||||
public static void info(final String text) {
|
||||
Log.i("Logger", text);
|
||||
writeLine(text, LogLevel.INFO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a line of text to log file at `LogLevel.INFO` verbosity.
|
||||
*
|
||||
* @param notify
|
||||
* If `true` a toast notification is displayed to user.
|
||||
* @param text
|
||||
* Text to be written.
|
||||
*/
|
||||
public static void info(final boolean notify, final String text) {
|
||||
Logger.info(text);
|
||||
if (notify) {
|
||||
Logger.notify(text, LogLevel.INFO);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a line of text to log file at `LogLevel.WARN` verbosity.
|
||||
*
|
||||
* @param text
|
||||
* Text to be written.
|
||||
*/
|
||||
public static void warn(final String text) {
|
||||
Log.w("Logger", text);
|
||||
writeLine(text, LogLevel.WARN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a line of text to log file at `LogLevel.WARN` verbosity.
|
||||
*
|
||||
* @param notify
|
||||
* If `true` a toast notification is displayed to user.
|
||||
* @param text
|
||||
* Text to be written.
|
||||
*/
|
||||
public static void warn(final boolean notify, final String text) {
|
||||
Logger.warn(text);
|
||||
if (notify) {
|
||||
Logger.notify(text, LogLevel.WARN);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a line of text to log file at `LogLevel.ERROR` verbosity.
|
||||
*
|
||||
* @param text
|
||||
* Text to be written.
|
||||
*/
|
||||
public static void error(final String text) {
|
||||
Log.e("Logger", text);
|
||||
writeLine(text, LogLevel.ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a line of text to log file at `LogLevel.ERROR` verbosity.
|
||||
*
|
||||
* @param notify
|
||||
* If `true` a toast notification is displayed to user.
|
||||
* @param text
|
||||
* Text to be written.
|
||||
*/
|
||||
public static void error(final boolean notify, final String text) {
|
||||
Logger.error(text);
|
||||
if (notify) {
|
||||
Logger.notify(text, LogLevel.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a line of text to log file at `LogLevel.DEBUG` verbosity.
|
||||
*
|
||||
* @param text
|
||||
* Text to be written.
|
||||
*/
|
||||
public static void debug(final String text) {
|
||||
Log.d("Logger", text);
|
||||
writeLine(text, LogLevel.DEBUG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a line of text to log file at `LogLevel.DEBUG` verbosity.
|
||||
*
|
||||
* @param notify
|
||||
* If `true` a toast notification is displayed to user.
|
||||
* @param text
|
||||
* Text to be written.
|
||||
*/
|
||||
public static void debug(final boolean notify, final String text) {
|
||||
Logger.debug(text);
|
||||
if (notify) {
|
||||
Logger.notify(text, LogLevel.DEBUG);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a toast notification to user.
|
||||
*
|
||||
* @param text
|
||||
* Text to display in notification.
|
||||
*/
|
||||
public static void notify(final String text) {
|
||||
notify(text, LogLevel.INFO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a toast notification to user.
|
||||
*
|
||||
* Does not output to log file.
|
||||
*
|
||||
* @param text
|
||||
* Text to display in notification.
|
||||
* @param level
|
||||
* Logging verbosity level.
|
||||
*/
|
||||
public static void notify(final String text, LogLevel level) {
|
||||
if (!Logger.initialized) {
|
||||
System.err.println("ERROR: Logger not initialized. Call Logger.init.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (level == null) {
|
||||
level = LogLevel.INFO;
|
||||
}
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.get());
|
||||
builder.setTitle(level.label);
|
||||
builder.setMessage(text);
|
||||
|
||||
builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
dialog.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves logs directory.
|
||||
*
|
||||
* @return
|
||||
* Directory path to outpu logs.
|
||||
*/
|
||||
public static String getLogsDir() {
|
||||
return logsDir.getPath();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Stendhal.FullScreen">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".PreferencesActivity"
|
||||
android:label="Preferences"
|
||||
android:exported="true"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="preferences" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".OpenAppByUrl"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="@string/intent_url_scheme"
|
||||
android:host="callback" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2024 - Faiumoni e. V. *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
package org.stendhalgame.client;
|
||||
|
||||
|
||||
/**
|
||||
* App configuration information.
|
||||
*/
|
||||
class AppInfo {
|
||||
|
||||
/**
|
||||
* Retrieves configured build type string.
|
||||
*/
|
||||
public static String getBuildType() {
|
||||
return MainActivity.get().getResources().getString(R.string.build_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves configured build version string.
|
||||
*/
|
||||
public static String getBuildVersion() {
|
||||
return MainActivity.get().getResources().getString(R.string.build_version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves configured intent URL scheme string.
|
||||
*/
|
||||
public static String getIntentUrlScheme() {
|
||||
return MainActivity.get().getResources().getString(R.string.intent_url_scheme);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,603 +0,0 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2022-2024 - Faiumoni e. V. *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.stendhalgame.client;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.webkit.DownloadListener;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
|
||||
/**
|
||||
* Interface to handle web content.
|
||||
*/
|
||||
public class ClientView extends WebView {
|
||||
|
||||
/** Client URL path. */
|
||||
private String clientUrlSuffix = "client";
|
||||
|
||||
/** Determines type of client to connect to. */
|
||||
private boolean testClient = false;
|
||||
/** Determines type of server to connect to. */
|
||||
private boolean testServer = false;
|
||||
private Boolean debugging;
|
||||
/** ID of page currently loaded. */
|
||||
private PageId currentPage;
|
||||
/** ID of page previously loaded. */
|
||||
private PageId previousPage;
|
||||
|
||||
private String stateId = "";
|
||||
private String seed = "";
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new view.
|
||||
*
|
||||
* @param ctx
|
||||
* Activity Context to access application assets.
|
||||
*/
|
||||
public ClientView(final Context ctx) {
|
||||
super(ctx);
|
||||
onInit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new view.
|
||||
*
|
||||
* @param ctx
|
||||
* Activity Context to access application assets.
|
||||
* @param attrs
|
||||
* AttributeSet passed to parent (may be `null`).
|
||||
*/
|
||||
public ClientView(final Context ctx, final AttributeSet attrs) {
|
||||
super(ctx, attrs);
|
||||
onInit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new view.
|
||||
*
|
||||
* @param ctx
|
||||
* Activity Context to access application assets.
|
||||
* @param attrs
|
||||
* AttributeSet passed to parent (may be `null`).
|
||||
* @param style
|
||||
* Attribute in the current theme that contains a reference to a style resource that supplies
|
||||
* default values for the view (can be 0 to not look for defaults).
|
||||
*/
|
||||
public ClientView(final Context ctx, final AttributeSet attrs, final int style) {
|
||||
super(ctx, attrs, style);
|
||||
onInit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes client WebView interface.
|
||||
*/
|
||||
private void onInit() {
|
||||
setActive(false); // main activity manages initial visibility
|
||||
setBackgroundColor(Color.TRANSPARENT);
|
||||
|
||||
// expand to size of parent constraints
|
||||
setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
|
||||
final WebSettings viewSettings = getSettings();
|
||||
viewSettings.setJavaScriptEnabled(true);
|
||||
viewSettings.setDomStorageEnabled(true);
|
||||
|
||||
// keep elements in position in portrait mode
|
||||
viewSettings.setLoadWithOverviewMode(true);
|
||||
viewSettings.setUseWideViewPort(true);
|
||||
|
||||
// zoom controls
|
||||
viewSettings.setSupportZoom(true);
|
||||
viewSettings.setBuiltInZoomControls(true);
|
||||
viewSettings.setDisplayZoomControls(false);
|
||||
|
||||
// allow autoplay of music
|
||||
viewSettings.setMediaPlaybackRequiresUserGesture(false);
|
||||
|
||||
if (debugEnabled()) {
|
||||
// make WebView debuggable for debug builds
|
||||
setWebContentsDebuggingEnabled(true);
|
||||
}
|
||||
|
||||
initWebViewClient();
|
||||
initDownloadHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets active state of this client view.
|
||||
*
|
||||
* @param active
|
||||
* If `true` shows this client view, otherwise hides it.
|
||||
*/
|
||||
public void setActive(final boolean active) {
|
||||
if (active) {
|
||||
setVisibility(ClientView.VISIBLE);
|
||||
return;
|
||||
}
|
||||
setVisibility(ClientView.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks active state of client view.
|
||||
*
|
||||
* @return
|
||||
* `true` is this view is visible.
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return getVisibility() == ClientView.VISIBLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes method overrides to handle page loading.
|
||||
*/
|
||||
private void initWebViewClient() {
|
||||
// XXX: not sure setting WebChromClient is doing anything, was recommended to
|
||||
// fix touchmove events not registering
|
||||
setWebChromeClient(new WebChromeClient());
|
||||
|
||||
setWebViewClient(new WebViewClient() {
|
||||
/**
|
||||
* Handles pages loaded indirectly.
|
||||
*
|
||||
* @param view
|
||||
* The default WebView instance handling the request.
|
||||
* @param request
|
||||
* Web request including the URL to be loaded.
|
||||
* @return
|
||||
* `true` to abort default loading.
|
||||
*/
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(final WebView view, final WebResourceRequest request) {
|
||||
Uri uri = request.getUrl();
|
||||
final Uri.Builder builder = uri.buildUpon();
|
||||
if (UrlHelper.isLoginUri(uri)) {
|
||||
builder.appendQueryParameter("build", AppInfo.getBuildType());
|
||||
builder.appendQueryParameter("version", AppInfo.getBuildVersion());
|
||||
builder.appendQueryParameter("state", stateId);
|
||||
builder.appendQueryParameter("seed", seed);
|
||||
}
|
||||
uri = builder.build();
|
||||
if (!UrlHelper.isInternalUri(uri)) {
|
||||
// open external links in default browser/app
|
||||
// FIXME: should we ask for confirmation?
|
||||
try {
|
||||
MainActivity.get().startActivity(new Intent(Intent.ACTION_VIEW, uri));
|
||||
} catch (final ActivityNotFoundException e) {
|
||||
Notifier.toast("No app found to open this link");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
view.loadUrl(UrlHelper.checkClientUrl(uri.toString()));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before a URL is loaded.
|
||||
*
|
||||
* @param view
|
||||
* WebView instance handling page contents.
|
||||
* @param url
|
||||
* HTTP string of target page.
|
||||
* @param favicon
|
||||
* Favicon bitmap image.
|
||||
*/
|
||||
@Override
|
||||
public void onPageStarted(final WebView view, final String url, final Bitmap favicon) {
|
||||
MusicPlayer.stopMusic();
|
||||
super.onPageStarted(view, url, favicon);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a URL is loaded.
|
||||
*
|
||||
* @param view
|
||||
* WebView instance handling page contents.
|
||||
* @param url
|
||||
* HTTP string of target page.
|
||||
*/
|
||||
@Override
|
||||
public void onPageFinished(final WebView view, final String url) {
|
||||
super.onPageFinished(view, url);
|
||||
if (UrlHelper.isClientUrl(url)) {
|
||||
setPage(PageId.WEBCLIENT);
|
||||
} else if (url.equals("") || url.equals("about:blank")) {
|
||||
setPage(PageId.TITLE);
|
||||
if (PreferencesActivity.getBoolean("title_music", true)) {
|
||||
MusicPlayer.playTitleMusic();
|
||||
}
|
||||
} else {
|
||||
setPage(PageId.OTHER);
|
||||
}
|
||||
Menu.get().updateButtons();
|
||||
Logger.debug("page id: " + currentPage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* deprecated?
|
||||
*/
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(final KeyEvent event) {
|
||||
final boolean ret = super.dispatchKeyEvent(event);
|
||||
// hide keyboard when "enter" pressed
|
||||
if (isGameActive() && event.getAction() == KeyEvent.ACTION_UP
|
||||
&& event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
|
||||
((InputMethodManager) MainActivity.get()
|
||||
.getSystemService(Context.INPUT_METHOD_SERVICE))
|
||||
.hideSoftInputFromWindow(this.getWindowToken(), 0);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes interface for handling screenshots.
|
||||
*/
|
||||
private void initDownloadHandler() {
|
||||
setDownloadListener(new DownloadListener() {
|
||||
/**
|
||||
* Handles downloading screenshot created by web client.
|
||||
*
|
||||
* TODO: get filename to pass to `DownloadHandler.download` from "download" property of
|
||||
* anchor
|
||||
*
|
||||
* @param url
|
||||
* Content URL to the screenshot image.
|
||||
* @param userAgent
|
||||
* User agent used for download.
|
||||
* @param contentDisposition
|
||||
* Content-disposition HTTP header, if present.
|
||||
* @param mimetype
|
||||
* Content type reported by server (web client creates PNG image).
|
||||
* @param contentLength
|
||||
* File size reported by server.
|
||||
*/
|
||||
@Override
|
||||
public void onDownloadStart(final String url, final String userAgent,
|
||||
final String contentDisposition, final String mimetype, final long contentLength) {
|
||||
final DownloadHandler handler = new DownloadHandler();
|
||||
handler.download(url, mimetype);
|
||||
if (handler.getResult()) {
|
||||
final String msg = "Downloaded file: " + handler.getMessage();
|
||||
Logger.debug(msg);
|
||||
Notifier.toast(msg);
|
||||
} else {
|
||||
final String msg = handler.getMessage();
|
||||
Logger.error(msg);
|
||||
Notifier.toast("ERROR: " + msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies intent state & parses & loads target URL.
|
||||
*
|
||||
* @param intent
|
||||
* Called login intent with state verification.
|
||||
*/
|
||||
public void checkLoginIntent(final Intent intent) {
|
||||
final Uri intentUri = intent.getData();
|
||||
final String url = intentUri.getQueryParameter("url");
|
||||
final String loginseed = intentUri.getQueryParameter("loginseed");
|
||||
final String intentStateId = intentUri.getQueryParameter("state");
|
||||
if (stateId == null || intentStateId == null || url == null || "".equals(stateId) || !stateId.equals(intentStateId)) {
|
||||
final String err = "There was an error verifying login";
|
||||
Logger.error(err + " (\"" + stateId + "\" == \"" + intentStateId + "\")");
|
||||
Notifier.showMessage(err);
|
||||
if (currentPage == null || PageId.TITLE.equals(previousPage)) {
|
||||
// reload title
|
||||
loadTitleScreen();
|
||||
}
|
||||
return;
|
||||
}
|
||||
String completeUrl = url + "&loginseed=" + loginseed + seed;
|
||||
loadUrl(UrlHelper.checkClientUrl(completeUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if URL to load is test web client.
|
||||
*/
|
||||
public boolean isTestClient() {
|
||||
return testClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if connection is to test server.
|
||||
*/
|
||||
public boolean isTestServer() {
|
||||
return testServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a string representing the selected client.
|
||||
*
|
||||
* @return
|
||||
* One of "main", "test", or "none".
|
||||
*/
|
||||
public String getSelectedClient() {
|
||||
if (onTitleScreen()) {
|
||||
return "none";
|
||||
}
|
||||
return testClient ? "test" : "main";
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a string representing the selected server.
|
||||
*
|
||||
* @return
|
||||
* One of "main", "test", or "none".
|
||||
*/
|
||||
public String getSelectedServer() {
|
||||
if (onTitleScreen()) {
|
||||
return "none";
|
||||
}
|
||||
return testServer ? "test" : "main";
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets selected client & server values to default.
|
||||
*/
|
||||
private void reset() {
|
||||
testClient = false;
|
||||
testServer = false;
|
||||
clientUrlSuffix = "client";
|
||||
stateId = "";
|
||||
seed = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves client type string.
|
||||
*
|
||||
* @return
|
||||
* Either "client" or "testclient".
|
||||
*/
|
||||
public String getClientUrlSuffix() {
|
||||
return clientUrlSuffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random string for identifying state.
|
||||
*/
|
||||
private String generateRandomString() {
|
||||
// ensure state is reset before generating
|
||||
String result = "";
|
||||
|
||||
// useable characters
|
||||
String charList = "0123456789";
|
||||
for (char c = 'A'; c <= 'Z'; c++) {
|
||||
charList += c;
|
||||
}
|
||||
for (char c = 'a'; c <= 'z'; c++) {
|
||||
charList += c;
|
||||
}
|
||||
|
||||
// generate random state
|
||||
final Random rand = new Random();
|
||||
final int ccount = charList.length();
|
||||
for (int idx = 0; idx <= 20 ; idx++) {
|
||||
result += charList.charAt(rand.nextInt(ccount));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens location in client WebView.
|
||||
*
|
||||
* @param url
|
||||
* URL to load.
|
||||
*/
|
||||
@Override
|
||||
public void loadUrl(final String url) {
|
||||
Logger.debug("Loading URL: " + url);
|
||||
super.loadUrl(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows initial title screen.
|
||||
*/
|
||||
public void loadTitleScreen() {
|
||||
reset();
|
||||
setPage(PageId.TITLE);
|
||||
SplashUtil.get().setVisible(true);
|
||||
loadUrl("about:blank");
|
||||
Menu.get().show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to connect to client host.
|
||||
*
|
||||
* FIXME: rename as it may cause confusion in regards to loading page "account/login.html"
|
||||
*/
|
||||
public void loadLogin() {
|
||||
if (debugEnabled() && PreferencesActivity.getString("client_url").trim().equals("")) {
|
||||
// debug builds support choosing between main & test client/server
|
||||
selectClient();
|
||||
} else {
|
||||
onSelectServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a message dialog for user to choose between main & test clients.
|
||||
*/
|
||||
private void selectClient() {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
|
||||
builder.setMessage("Select client");
|
||||
|
||||
builder.setPositiveButton("Main", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
testClient = false;
|
||||
clientUrlSuffix = "client";
|
||||
// skip server confirmation as only test client has this option
|
||||
onSelectServer();
|
||||
dialog.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
builder.setNegativeButton("Test", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
testClient = true;
|
||||
clientUrlSuffix = "testclient";
|
||||
selectServer();
|
||||
dialog.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a message dialog for user to choose between main & test servers.
|
||||
*/
|
||||
private void selectServer() {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
|
||||
builder.setMessage("Select server");
|
||||
|
||||
builder.setPositiveButton("Main", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
testServer = false;
|
||||
dialog.cancel();
|
||||
onSelectServer();
|
||||
}
|
||||
});
|
||||
|
||||
builder.setNegativeButton("Test", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
testServer = true;
|
||||
dialog.cancel();
|
||||
onSelectServer();
|
||||
}
|
||||
});
|
||||
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to server & loads initial page.
|
||||
*/
|
||||
private void onSelectServer() {
|
||||
// create a unique state
|
||||
stateId = generateRandomString();
|
||||
seed = generateRandomString();
|
||||
// hide splash image
|
||||
SplashUtil.get().setVisible(false);
|
||||
|
||||
final String initialPage = UrlHelper.getInitialPageUrl();
|
||||
Logger.debug("Loading initial page: " + initialPage);
|
||||
loadUrl(initialPage);
|
||||
setPage(PageId.OTHER);
|
||||
// hide menu after exiting title screen
|
||||
Menu.get().hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if user has specified a URL pointing to a custom server/client.
|
||||
*
|
||||
* @return
|
||||
* Server address specified in preferences or `null`.
|
||||
*/
|
||||
public String checkCustomServer() {
|
||||
final String cs = PreferencesActivity.getString("client_url", "").trim();
|
||||
if (cs.equals("")) {
|
||||
return null;
|
||||
}
|
||||
return cs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if title page is currently loaded.
|
||||
*
|
||||
* @return
|
||||
* `true` if current page matches `PageId.TITLE`.
|
||||
*/
|
||||
public boolean onTitleScreen() {
|
||||
return currentPage == PageId.TITLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if web client page is currently loaded.
|
||||
*
|
||||
* @return
|
||||
* `true` if current page matches `PageId.WEBCLIENT`.
|
||||
*/
|
||||
public boolean isGameActive() {
|
||||
return currentPage == PageId.WEBCLIENT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves ID of current page.
|
||||
*
|
||||
* @return
|
||||
* Current `PageId`.
|
||||
*/
|
||||
public PageId getCurrentPageId() {
|
||||
return currentPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets current page ID.
|
||||
*
|
||||
* @param newPage
|
||||
* Page ID to be used.
|
||||
*/
|
||||
private void setPage(final PageId newPage) {
|
||||
previousPage = currentPage;
|
||||
currentPage = newPage;
|
||||
if (previousPage == null) {
|
||||
previousPage = currentPage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this is a debug build.
|
||||
*
|
||||
* @return
|
||||
* `true` if debug flag set.
|
||||
*/
|
||||
public boolean debugEnabled() {
|
||||
if (debugging != null) {
|
||||
return debugging;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
debugging = (MainActivity.get().getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
|
||||
} else {
|
||||
debugging = false;
|
||||
}
|
||||
return debugging;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2022-2024 - Faiumoni e. V. *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.stendhalgame.client;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.util.Base64;
|
||||
|
||||
|
||||
/**
|
||||
* The main purpose of this class is to handle downloading screenshots
|
||||
* created by the web client page.
|
||||
*/
|
||||
public class DownloadHandler {
|
||||
|
||||
private static enum DownloadType {
|
||||
IMAGE_PNG("image/png"),
|
||||
TEXT_PLAIN("text/plain"),
|
||||
UNSUPPORTED(null);
|
||||
|
||||
private String value;
|
||||
|
||||
private DownloadType(final String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static DownloadType fromString(final String value) {
|
||||
for (final DownloadType v: DownloadType.values()) {
|
||||
if (v.value != null && v.value.equals(value)) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return DownloadType.UNSUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean result = false;
|
||||
private String message = null;
|
||||
|
||||
|
||||
/**
|
||||
* Checks for a supported MIME type.
|
||||
*
|
||||
* @param url
|
||||
* File or data URL.
|
||||
* @param mimetype
|
||||
* Detected MIME type.
|
||||
* @return
|
||||
* Download type or unsupported.
|
||||
*/
|
||||
private DownloadType checkMimeType(final String url, final String mimetype) {
|
||||
final DownloadType dtype = DownloadType.fromString(mimetype);
|
||||
if (DownloadType.UNSUPPORTED.equals(dtype) && url.startsWith("data:image/png;base64,")) {
|
||||
return DownloadType.IMAGE_PNG;
|
||||
}
|
||||
return dtype;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes file data to device storage.
|
||||
*
|
||||
* @param dir
|
||||
* Directory where new file is to be created.
|
||||
* @param basename
|
||||
* Filename of new file.
|
||||
* @param data
|
||||
* File contents to be written to storage.
|
||||
*/
|
||||
private void downloadInternal(final File dir, final String basename, final byte[] data) {
|
||||
String stacktrace = null;
|
||||
try {
|
||||
if (!dir.exists()) {
|
||||
dir.mkdirs();
|
||||
}
|
||||
final FileOutputStream fos = new FileOutputStream(new File(dir, basename));
|
||||
fos.write(data);
|
||||
fos.close();
|
||||
this.result = true;
|
||||
} catch (final java.lang.NoClassDefFoundError e) {
|
||||
this.message = "an error occurred while decoding data (see debug log for more info)";
|
||||
stacktrace = stackTraceToString(e);
|
||||
} catch (java.io.IOException e) {
|
||||
this.message = "an error occurred while attempting to write file (see debug log for more info)";
|
||||
stacktrace = stackTraceToString(e);
|
||||
}
|
||||
|
||||
if (stacktrace != null) {
|
||||
Logger.error(stacktrace.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes file data to device storage.
|
||||
*
|
||||
* @param url
|
||||
* A data URL.
|
||||
* @param mimetype
|
||||
* Detected MIME type.
|
||||
* @param targetName
|
||||
* Filename for new file.
|
||||
*/
|
||||
public void download(final String url, final String mimetype, String targetName) {
|
||||
|
||||
final Uri uri = Uri.parse(url);
|
||||
final String scheme = uri.getScheme();
|
||||
final String storageState = Environment.getExternalStorageState();
|
||||
final DownloadType dtype = checkMimeType(url, mimetype);
|
||||
|
||||
if (!MainActivity.get().getActiveClientView().isGameActive()) {
|
||||
this.message = "downloading from this page not supported";
|
||||
} else if (!"data".equals(scheme)) {
|
||||
// only data URL supported
|
||||
this.message = "download type \"" + scheme + "\" not supported";
|
||||
} else if (DownloadType.UNSUPPORTED.equals(dtype)) {
|
||||
this.message = "mimetype not supported: " + mimetype;
|
||||
} else if (!Environment.MEDIA_MOUNTED.equals(storageState)) {
|
||||
this.message = "storage not available for writing (state: " + storageState + ")";
|
||||
} else if (DownloadType.IMAGE_PNG.equals(dtype)) {
|
||||
final File targetDir = new File(Environment.getExternalStorageDirectory()
|
||||
+ "/Pictures/Screenshots");
|
||||
if (targetName == null) {
|
||||
// default filename
|
||||
targetName = "stendhal_" + new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss").format(new Date())
|
||||
+ ".png";
|
||||
}
|
||||
|
||||
Logger.debug("Saving screenshot: " + targetDir.getPath()
|
||||
+ "/" + targetName + " (" + mimetype + ")");
|
||||
|
||||
final byte[] data = Base64.decode(url.split("base64,")[1], Base64.DEFAULT);
|
||||
downloadInternal(targetDir, targetName, data);
|
||||
if (this.result) {
|
||||
this.message = "saved screenshot to " + targetDir.getPath() + "/" + targetName;
|
||||
}
|
||||
} else if (DownloadType.TEXT_PLAIN.equals(dtype)) {
|
||||
// `Environment.DIRECTORY_DOWNLOADS` fails on newer Android versions
|
||||
//final File targetDir = new File(Environment.DIRECTORY_DOWNLOADS);
|
||||
final File targetDir = new File(Environment.getExternalStorageDirectory() + "/Download");
|
||||
if (targetName == null) {
|
||||
// default filename
|
||||
targetName = "stendhal_" + new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss").format(new Date())
|
||||
+ ".txt";
|
||||
}
|
||||
|
||||
Logger.debug("Saving file: " + targetDir.getPath()
|
||||
+ "/" + targetName + " (" + mimetype + ")");
|
||||
|
||||
final String[] parts = url.split(",");
|
||||
byte[] data;
|
||||
if (parts[0].endsWith(";base64")) {
|
||||
data = Base64.decode(parts[1], Base64.DEFAULT);
|
||||
} else {
|
||||
try {
|
||||
data = URLDecoder.decode(parts[1], StandardCharsets.UTF_8.name())
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
} catch (final java.io.UnsupportedEncodingException e) {
|
||||
this.message = "an error occurred while attempting to decode data URL (see debug log for more info)";
|
||||
Logger.error(stackTraceToString(e));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
downloadInternal(targetDir, targetName, data);
|
||||
if (this.result) {
|
||||
this.message = "file saved to " + targetDir.getPath() + "/" + targetName;
|
||||
}
|
||||
} else {
|
||||
this.message = "an unknown error occurred";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes file data to device storage.
|
||||
*
|
||||
* @param url
|
||||
* A data URL.
|
||||
* @param mimetype
|
||||
* Detected MIME type.
|
||||
*/
|
||||
public void download(final String url, final String mimetype) {
|
||||
download(url, mimetype, null);
|
||||
}
|
||||
|
||||
public boolean getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an exception stacktrace to string.
|
||||
*
|
||||
* @param e
|
||||
* The error with stacktrace information.
|
||||
* @return
|
||||
* String formatted stacktrace.
|
||||
*/
|
||||
private String stackTraceToString(final Throwable e) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append(e.toString());
|
||||
for (final StackTraceElement ste: e.getStackTrace()) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append("\n");
|
||||
}
|
||||
sb.append(ste.toString());
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2024 - Faiumoni e. V. *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.stendhalgame.client;
|
||||
|
||||
|
||||
/**
|
||||
* Logger verbosity levels.
|
||||
*/
|
||||
public enum LogLevel {
|
||||
|
||||
// available verbosity levels
|
||||
INFO("INFO"),
|
||||
WARN("WARN"),
|
||||
ERROR("ERROR"),
|
||||
DEBUG("DEBUG");
|
||||
|
||||
/** String identifier. */
|
||||
public final String label;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new logging verbosity level.
|
||||
*
|
||||
* @param label
|
||||
* String identifier.
|
||||
*/
|
||||
private LogLevel(final String label) {
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,282 +0,0 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2022-2024 - Faiumoni e. V. *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.stendhalgame.client;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.view.ViewGroup;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
|
||||
/**
|
||||
* Main app activity.
|
||||
*/
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
/** Menu instance. */
|
||||
private Menu menu;
|
||||
/** Active clients. **/
|
||||
private ViewGroup clientList;
|
||||
|
||||
/** Static activity instance. */
|
||||
private static MainActivity instance;
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves activity instance.
|
||||
*
|
||||
* NOTE: This may not be necessary if Android has built-in methods to retrieve current activity.
|
||||
*/
|
||||
public static MainActivity get() {
|
||||
return MainActivity.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when main activity is created.
|
||||
*/
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
try {
|
||||
super.onCreate(savedInstanceState);
|
||||
// FIXME: may be considered unsafe as this is not technically a singleton
|
||||
MainActivity.instance = this;
|
||||
|
||||
// initialize debug logging mechanism
|
||||
Logger.init(getExternalFilesDir(null));
|
||||
|
||||
setContentView(R.layout.activity_main);
|
||||
clientList = findViewById(R.id.clientList);
|
||||
createClientView();
|
||||
// NOTE: client view instance must be created before initializing menu
|
||||
menu = Menu.get();
|
||||
updateOrientation();
|
||||
} catch (final Exception e) {
|
||||
// TODO: add option to save to file or copy to clipboard the error
|
||||
e.printStackTrace();
|
||||
Logger.error(e.toString());
|
||||
Logger.error("// -- //");
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
for (final StackTraceElement ste: e.getStackTrace()) {
|
||||
final String traceLine = ste.toString();
|
||||
if (sb.length() > 0) {
|
||||
sb.append("\n");
|
||||
}
|
||||
sb.append(traceLine);
|
||||
Logger.error(traceLine);
|
||||
}
|
||||
Logger.error("// -- //");
|
||||
Notifier.showPrompt(
|
||||
"An unhandled exception has occurred: \"" + e.getMessage() + "\""
|
||||
+ "\n\nYou can report this error at: https://stendhalgame.org/development/bug.html"
|
||||
+ "\n\nStack trace:\n" + sb.toString(),
|
||||
new Notifier.Action() {
|
||||
@Override
|
||||
protected void onCall() {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles toggling menu when the system "back" button is pressed.
|
||||
*/
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
menu.toggleVisibility();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates view to host a client instance.
|
||||
*/
|
||||
private void createClientView() {
|
||||
final ClientView clientView = new ClientView(this);
|
||||
setActiveClientView(clientView);
|
||||
clientList.addView(clientView);
|
||||
clientView.loadTitleScreen();
|
||||
// show splash when a new view is created
|
||||
SplashUtil.get().setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets client view to be shown and hides all others.
|
||||
*
|
||||
* @param clientView
|
||||
* The `ClientView` instance to be visible.
|
||||
*/
|
||||
private void setActiveClientView(final ClientView clientView) {
|
||||
for (int idx = 0; idx < clientList.getChildCount(); idx++) {
|
||||
((ClientView) clientList.getChildAt(idx)).setActive(false);
|
||||
}
|
||||
clientView.setActive(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets client view to be shown and hides all others.
|
||||
*
|
||||
* @param clientIndex
|
||||
* The index of `ClientView` instance to be visible.
|
||||
*/
|
||||
private void setActiveClientView(final int clientIndex) {
|
||||
if (clientIndex < 0 || clientIndex >= clientList.getChildCount()) {
|
||||
Logger.error(true, "Tried to access invalid client index: " + clientIndex);
|
||||
return;
|
||||
}
|
||||
setActiveClientView((ClientView) clientList.getChildAt(clientIndex));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves active client view.
|
||||
*
|
||||
* @return
|
||||
* `ClientView` instance that is visible.
|
||||
*/
|
||||
public ClientView getActiveClientView() {
|
||||
for (int idx = 0; idx < clientList.getChildCount(); idx++) {
|
||||
final ClientView clientView = (ClientView) clientList.getChildAt(idx);
|
||||
if (clientView.isActive()) {
|
||||
return clientView;
|
||||
}
|
||||
}
|
||||
// default to first client view
|
||||
return (ClientView) clientList.getChildAt(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves list of available client views.
|
||||
*
|
||||
* @return
|
||||
* List containing all created `ClientView` instances.
|
||||
*/
|
||||
public List<ClientView> getClientViewList() {
|
||||
final List<ClientView> available = new LinkedList<>();
|
||||
for (int idx = 0; idx < clientList.getChildCount(); idx++) {
|
||||
available.add((ClientView) clientList.getChildAt(idx));
|
||||
}
|
||||
return available;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to connect to client host.
|
||||
*/
|
||||
public void loadLogin() {
|
||||
getActiveClientView().loadLogin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves app orientation.
|
||||
*
|
||||
* @return
|
||||
* One of `Configuration.ORIENTATION_PORTRAIT` (1), `Configuration.ORIENTATION_LANDSCAPE` (2),
|
||||
* or `Configuration.ORIENTATION_UNDEFINED` (0).
|
||||
*/
|
||||
public int getOrientation() {
|
||||
return getResources().getConfiguration().orientation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets screen orientation to user setting or locks in landscape or portrait.
|
||||
*/
|
||||
public void updateOrientation() {
|
||||
final String value = PreferencesActivity.getString("orientation");
|
||||
int orient = ActivityInfo.SCREEN_ORIENTATION_USER;
|
||||
switch (value) {
|
||||
case "landscape":
|
||||
orient = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
|
||||
break;
|
||||
case "portrait":
|
||||
orient = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
|
||||
break;
|
||||
}
|
||||
// set orientation for main & sub activities
|
||||
setRequestedOrientation(orient);
|
||||
final PreferencesActivity preferencesActivity = PreferencesActivity.get();
|
||||
if (preferencesActivity != null) {
|
||||
preferencesActivity.setRequestedOrientation(orient);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a dialog to confirm exiting activity.
|
||||
*/
|
||||
public void onRequestQuit() {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setMessage("Quit Stendhal?");
|
||||
|
||||
builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
finish();
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
}
|
||||
});
|
||||
|
||||
builder.setNegativeButton("No", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
dialog.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
final AlertDialog confirmQuit = builder.create();
|
||||
confirmQuit.show();
|
||||
}
|
||||
|
||||
//~ @Override
|
||||
//~ protected void onResume() {
|
||||
//~ super.onResume();
|
||||
//~ }
|
||||
|
||||
/**
|
||||
* Listens for changes to orientation.
|
||||
*/
|
||||
@Override
|
||||
public void onConfigurationChanged(final Configuration config) {
|
||||
super.onConfigurationChanged(config);
|
||||
final ClientView clientView = getActiveClientView();
|
||||
if (clientView != null && PageId.TITLE.equals(clientView.getCurrentPageId())) {
|
||||
SplashUtil.get().update();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when main activity ends.
|
||||
*/
|
||||
@Override
|
||||
public void finish() {
|
||||
Logger.debug(MainActivity.class.getName() + ".finish() called");
|
||||
super.finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when main activity is destroyed.
|
||||
*/
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
Logger.debug(MainActivity.class.getName() + ".onDestroy() called");
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates preferences activity.
|
||||
*/
|
||||
public void showSettings() {
|
||||
startActivity(new Intent(this, PreferencesActivity.class));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2024 - Faiumoni e. V. *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.stendhalgame.client;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
public class OpenAppByUrl extends Activity {
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Intent intent = getIntent();
|
||||
// String action = intent.getAction();
|
||||
Uri data = intent.getData();
|
||||
Logger.debug("URL: " + data);
|
||||
MainActivity.get().getActiveClientView().checkLoginIntent(intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2024 - Faiumoni e. V. *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.stendhalgame.client;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.widget.ImageView;
|
||||
|
||||
|
||||
public class SplashUtil {
|
||||
|
||||
/** Image used as title page background. */
|
||||
private final ImageView splash;
|
||||
|
||||
/** Singleton instance. */
|
||||
private static SplashUtil instance;
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves singleton instance.
|
||||
*/
|
||||
public static SplashUtil get() {
|
||||
if (SplashUtil.instance == null) {
|
||||
SplashUtil.instance = new SplashUtil();
|
||||
}
|
||||
return SplashUtil.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hidden singleton constructor.
|
||||
*/
|
||||
private SplashUtil() {
|
||||
splash = (ImageView) MainActivity.get().findViewById(R.id.splash);
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the background splash image.
|
||||
*
|
||||
* @param resId
|
||||
* Resource ID.
|
||||
* @param bgColor
|
||||
* Coloring to fill behind splash image.
|
||||
*/
|
||||
private void setImage(final int resId, final int bgColor) {
|
||||
splash.setBackgroundColor(bgColor);
|
||||
splash.setImageResource(resId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets splash image dependent on device orientation.
|
||||
*/
|
||||
public void update() {
|
||||
if (!isVisible()) {
|
||||
return;
|
||||
}
|
||||
int resId = R.drawable.splash;
|
||||
if (MainActivity.get().getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
|
||||
resId = R.drawable.splash_portrait;
|
||||
}
|
||||
// light blue background color
|
||||
setImage(resId, 0xff6c9ed1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows or hides background splash image.
|
||||
*
|
||||
* @param visible
|
||||
* `true` if image should visible, `false` if not.
|
||||
*/
|
||||
public void setVisible(final boolean visible) {
|
||||
splash.setVisibility(visible ? ImageView.VISIBLE : ImageView.GONE);
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if splash is visible.
|
||||
*/
|
||||
public boolean isVisible() {
|
||||
return ImageView.VISIBLE == splash.getVisibility();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,280 +0,0 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2024 - Faiumoni e. V. *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.stendhalgame.client;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
|
||||
class UrlHelper {
|
||||
|
||||
private static final String defaultServer = "https://stendhalgame.org/";
|
||||
|
||||
|
||||
private UrlHelper() {
|
||||
// static methods only
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a URI from a string HTTP URL.
|
||||
*
|
||||
* @return
|
||||
* `android.net.Uri`.
|
||||
*/
|
||||
public static Uri toUri(String url) {
|
||||
if (!url.startsWith("https://") && !url.startsWith("http://") && !url.startsWith("about:")) {
|
||||
// Uri.getHost returns `null` if localhost not prefixed with protocol
|
||||
url = "http://" + url;
|
||||
}
|
||||
return Uri.parse(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the HTTP URL of the default server (stendhalgame.org).
|
||||
*
|
||||
* @return
|
||||
* String URL.
|
||||
*/
|
||||
public static String getDefaultServer() {
|
||||
return UrlHelper.defaultServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the URI of the default server (stendhalgame.org).
|
||||
*
|
||||
* @return
|
||||
* `android.net.Uri` of default server.
|
||||
*/
|
||||
public static Uri getDefaultServerUri() {
|
||||
return UrlHelper.toUri(UrlHelper.defaultServer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default host.
|
||||
*
|
||||
* @return
|
||||
* Host portion of the default URI.
|
||||
*/
|
||||
public static String getDefaultHost() {
|
||||
return UrlHelper.getDefaultServerUri().getHost();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes protocol & "www" prefixes from a URL.
|
||||
*
|
||||
* FIXME: rename to "trimProtocol"
|
||||
*
|
||||
* @return
|
||||
* Trimmed URL string.
|
||||
*/
|
||||
public static String stripHost(final String url) {
|
||||
if (url == null) {
|
||||
return "";
|
||||
}
|
||||
return url.replaceAll("^https://", "").replaceAll("^http://", "")
|
||||
.replaceAll("^www\\.", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts character name from URL fragment identifier & converts to query string.
|
||||
*
|
||||
* Deprecated. Should use `UrlHelper.formatCharName(Uri, Uri.Builder).
|
||||
*
|
||||
* @param url
|
||||
* HTTP string to be formatted.
|
||||
* @return
|
||||
* Formatted URL.
|
||||
*/
|
||||
@Deprecated
|
||||
public static String formatCharName(String url) {
|
||||
final int idx = url.indexOf("#");
|
||||
if (idx > -1) {
|
||||
url = url.substring(0, idx) + "?char=" + url.substring(idx+1);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts URL fragment identifier to character name query string parameter.
|
||||
*
|
||||
* @param uri
|
||||
* URI being checked for fragment identifier.
|
||||
* @param builder
|
||||
* URI builder to update.
|
||||
*/
|
||||
public static void formatCharName(final Uri uri, final Uri.Builder builder) {
|
||||
final String name = uri.getFragment();
|
||||
if (name != null) {
|
||||
builder.appendQueryParameter("char", name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats client URL for currently selected server.
|
||||
*
|
||||
* @param url
|
||||
* URL to be checked.
|
||||
* @return
|
||||
* URL to be loaded.
|
||||
*/
|
||||
public static String checkClientUrl(String url) {
|
||||
final Uri uri = UrlHelper.toUri(url);
|
||||
final Uri.Builder builder = uri.buildUpon();
|
||||
if (UrlHelper.isClientUrl(url)) {
|
||||
final ClientView client = MainActivity.get().getActiveClientView();
|
||||
final boolean testClient = client.isTestClient();
|
||||
String replaceSuffix = "/testclient/";
|
||||
if (testClient) {
|
||||
replaceSuffix = "/client/";
|
||||
}
|
||||
// ensure website directs to configured client
|
||||
builder.path(uri.getPath().replace(replaceSuffix, "/" + client.getClientUrlSuffix() + "/"));
|
||||
UrlHelper.formatCharName(uri, builder);
|
||||
if (testClient && !client.isTestServer() && uri.getQueryParameter("server") == null) {
|
||||
// connect test client to main server
|
||||
builder.appendQueryParameter("server", "main");
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a URL is a link to one of the web clients.
|
||||
*
|
||||
* @param url
|
||||
* HTTP URL to be checked.
|
||||
* @return
|
||||
* `true` if `url` links to "client" or "testclient".
|
||||
*/
|
||||
public static boolean isClientUrl(final String url) {
|
||||
final String custom_client = PreferencesActivity.getString("client_url").trim();
|
||||
if (!custom_client.equals("")) {
|
||||
return url.contains(custom_client);
|
||||
}
|
||||
final String defaultHost = UrlHelper.stripHost(UrlHelper.getDefaultServerUri().getHost());
|
||||
return url.contains(defaultHost + "/client/") || url.contains(defaultHost + "/testclient/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves client URL.
|
||||
*
|
||||
* @return
|
||||
* Client URL string.
|
||||
*/
|
||||
public static String getClientUrl() {
|
||||
final String custom_client = PreferencesActivity.getString("client_url").trim();
|
||||
if (!custom_client.equals("")) {
|
||||
return custom_client;
|
||||
}
|
||||
return UrlHelper.defaultServer + "client/stendhal.html";
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves URL string for initial page to be loaded from server.
|
||||
*
|
||||
* If custom client URL is used then client URL is returned.
|
||||
*
|
||||
* @return
|
||||
* HTTP URL string.
|
||||
*/
|
||||
public static String getInitialPageUrl() {
|
||||
final String custom_client = PreferencesActivity.getString("client_url").trim();
|
||||
if (!custom_client.equals("")) {
|
||||
return custom_client;
|
||||
}
|
||||
return UrlHelper.defaultServer + "account/mycharacters.html";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if requested URL is whitelisted to be opened within WebView client.
|
||||
*
|
||||
* @param uri
|
||||
* `android.net.Uri` to be checked.
|
||||
* @return
|
||||
* `true` if URI is under default domain (stendhalgame.org) or localhost.
|
||||
*/
|
||||
public static boolean isInternalUri(final Uri uri) {
|
||||
if (UrlHelper.isLoginUri(uri)) {
|
||||
// open login page in external browser
|
||||
return false;
|
||||
}
|
||||
final String defaultHost = UrlHelper.stripHost(UrlHelper.getDefaultHost());
|
||||
final String host = UrlHelper.stripHost(uri.getHost());
|
||||
if (defaultHost.equals(host)) {
|
||||
// allow pages from stendhalgame.org
|
||||
return true;
|
||||
}
|
||||
final String cs = MainActivity.get().getActiveClientView().checkCustomServer();
|
||||
if (cs != null) {
|
||||
return UrlHelper.stripHost(cs).equals(host);
|
||||
}
|
||||
return "localhost".equals(host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if requested URL is whitelisted to be opened within WebView client.
|
||||
*
|
||||
* @param url
|
||||
* HTTP URL string to be checked.
|
||||
* @return
|
||||
* `true` if URL is under default domain (stendhalgame.org) or localhost.
|
||||
*/
|
||||
public static boolean isInternal(final String url) {
|
||||
return UrlHelper.isInternalUri(UrlHelper.toUri(url));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a URI matches the intent URL scheme.
|
||||
*
|
||||
* @param uri
|
||||
* URI to be check.
|
||||
* @return
|
||||
* `true` if URI's host is the intent scheme.
|
||||
*/
|
||||
public static boolean isIntentUri(final Uri uri) {
|
||||
return AppInfo.getIntentUrlScheme().equals(UrlHelper.stripHost(uri.getHost()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a URL matches the intent URL scheme.
|
||||
*
|
||||
* @param url
|
||||
* HTTP URL string to be check.
|
||||
* @return
|
||||
* `true` if URI's host is the intent scheme.
|
||||
*/
|
||||
public static boolean isIntentUrl(final String url) {
|
||||
return UrlHelper.isIntentUri(UrlHelper.toUri(url));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a URI represents a login page.
|
||||
*
|
||||
* @param uri
|
||||
* Page URI to check.
|
||||
* @return
|
||||
* `true` if URI path equals "/account/login.html" or `id` parameter of query string equals
|
||||
* "content/account/login".
|
||||
*/
|
||||
public static boolean isLoginUri(final Uri uri) {
|
||||
if ("/account/login.html".equals(uri.getPath())) {
|
||||
return true;
|
||||
}
|
||||
if (!uri.isHierarchical()) {
|
||||
return false;
|
||||
}
|
||||
final String id = uri.getQueryParameter("id");
|
||||
if (id == null) {
|
||||
return false;
|
||||
}
|
||||
return "content/account/login".equals(id);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
https://sourceforge.net/projects/arianne/support
|
||||
|
|
@ -1 +0,0 @@
|
|||
https://stendhalgame.org/
|
||||