Compare commits
20 commits
master
...
VERSION_01
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9cd7e774b | ||
|
|
2793ab2382 | ||
|
|
e462173cd7 | ||
|
|
b2f1d21f5f | ||
|
|
8a1e83f573 | ||
|
|
47cc43572f | ||
|
|
93178b8f09 | ||
|
|
c15aa34830 | ||
|
|
6966df0328 | ||
|
|
a18c93b323 | ||
|
|
197c85541b | ||
|
|
abbe0bac01 | ||
|
|
54068a796f | ||
|
|
83077c591e | ||
|
|
741bface9b | ||
|
|
2605e35066 | ||
|
|
9cc0648272 | ||
|
|
e964352fe9 | ||
|
|
bef30954e8 | ||
|
|
a44c60c08d |
|
|
@ -25,5 +25,5 @@
|
|||
<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="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
|
||||
12
.github/workflows/codeql-analysis.yml
vendored
|
|
@ -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@v2
|
||||
# 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@v2
|
||||
|
||||
# ℹ️ 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@v2
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
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>
|
||||
|
|
|
|||
48
README.md
|
|
@ -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 are 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-2023, 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.jar')
|
||||
}
|
||||
7
android/app/src/debug/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.arianne.stendhal.client">
|
||||
<application android:usesCleartextTraffic="true">
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
@ -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>
|
||||
41
android/app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?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.READ_EXTERNAL_STORAGE" />
|
||||
<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,572 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2022-2023 - 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 testClient = false;
|
||||
private boolean testServer = 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);
|
||||
|
||||
// allow autoplay of music
|
||||
viewSettings.setMediaPlaybackRequiresUserGesture(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)) {
|
||||
// open external links in default browser/app
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public String getSelectedClient() {
|
||||
if (ClientView.onTitleScreen()) {
|
||||
return "none";
|
||||
}
|
||||
return testClient ? "test" : "main";
|
||||
}
|
||||
|
||||
public String getSelectedServer() {
|
||||
if (ClientView.onTitleScreen()) {
|
||||
return "none";
|
||||
}
|
||||
return testServer ? "test" : "main";
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
testClient = false;
|
||||
testServer = false;
|
||||
clientUrlSuffix = "client";
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows initial title screen.
|
||||
*/
|
||||
public void loadTitleScreen() {
|
||||
reset();
|
||||
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 client/server
|
||||
selectClient();
|
||||
} 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 clients.
|
||||
*/
|
||||
private void selectClient() {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.get());
|
||||
builder.setMessage("Select client");
|
||||
|
||||
builder.setPositiveButton("Main", new DialogInterface.OnClickListener() {
|
||||
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() {
|
||||
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(MainActivity.get());
|
||||
builder.setMessage("Select server");
|
||||
|
||||
builder.setPositiveButton("Main", new DialogInterface.OnClickListener() {
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
testServer = false;
|
||||
dialog.cancel();
|
||||
onSelectServer();
|
||||
}
|
||||
});
|
||||
|
||||
builder.setNegativeButton("Test", new DialogInterface.OnClickListener() {
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
testServer = true;
|
||||
dialog.cancel();
|
||||
onSelectServer();
|
||||
}
|
||||
});
|
||||
|
||||
builder.create().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 (testServer) {
|
||||
DebugLog.debug("Connecting to test server");
|
||||
} else {
|
||||
DebugLog.debug("Connecting to main server");
|
||||
|
||||
Notifier.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 (testClient) {
|
||||
replaceSuffix = "/client/";
|
||||
}
|
||||
// ensure website directs to configured client
|
||||
url = url.replace(replaceSuffix, "/" + clientUrlSuffix + "/");
|
||||
url = formatCharName(url);
|
||||
if (testClient && !testServer) {
|
||||
// connect test client to main server
|
||||
url += "&server=main";
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts character name from URL fragment identifier & converts to query string.
|
||||
*/
|
||||
private String formatCharName(String url) {
|
||||
final int idx = url.indexOf("#");
|
||||
if (idx > -1) {
|
||||
url = url.substring(0, idx) + "?char=" + url.substring(idx+1);
|
||||
}
|
||||
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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,161 @@
|
|||
/***************************************************************************
|
||||
* Copyright © 2022-2023 - 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
|
||||
// TODO: remove on-screen joystick/dpad support after 1.45 release as it is now built
|
||||
// into the web client
|
||||
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.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();
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
}
|
||||
});
|
||||
|
||||
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-2023 - 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?",
|
||||
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,7 +132,6 @@ 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);
|
||||
|
||||
|
|
@ -168,7 +139,7 @@ public class Menu {
|
|||
new Notifier.Action() {
|
||||
@Override
|
||||
protected void onCall() {
|
||||
MainActivity.get().getActiveClientView().reload();
|
||||
ClientView.get().reload();
|
||||
}
|
||||
},
|
||||
new Notifier.Action() {
|
||||
|
|
@ -182,57 +153,43 @@ 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";
|
||||
}
|
||||
// FIXME: how to find server version if connected?
|
||||
|
||||
final ClientView client = ClientView.get();
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder((Activity) ctx);
|
||||
String msg = "WebView client version: " + BuildConfig.VERSION_NAME;
|
||||
if (client.debugEnabled()) {
|
||||
msg += "\nClient type: " + client.getSelectedClient()
|
||||
+ "\nServer type: " + client.getSelectedServer()
|
||||
+ "\nLogs directory: " + Logger.getLogsDir();
|
||||
msg += "\nSelected client: " + client.getSelectedClient()
|
||||
+ "\nSelected server: " + client.getSelectedServer();
|
||||
}
|
||||
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";
|
||||
msg += "\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.setMessage(msg);
|
||||
|
||||
builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
dialog.cancel();
|
||||
}
|
||||
|
|
@ -241,11 +198,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 +210,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-2023 - 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;
|
||||
|
|
@ -53,7 +53,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();
|
||||
}
|
||||
|
|
@ -93,7 +92,6 @@ public class Notifier {
|
|||
}
|
||||
|
||||
builder.setPositiveButton(labelY, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
dialog.cancel();
|
||||
|
||||
|
|
@ -113,7 +111,6 @@ public class Notifier {
|
|||
}
|
||||
|
||||
builder.setNegativeButton(labelN, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
dialog.cancel();
|
||||
|
||||
|
|
@ -134,7 +131,6 @@ public class Notifier {
|
|||
}
|
||||
|
||||
builder.setNeutralButton(labelO, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int id) {
|
||||
dialog.cancel();
|
||||
|
||||
|
|
@ -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-2023 - 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.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?",
|
||||
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_left;
|
||||
id_active = R.drawable.dpad_left_active;
|
||||
break;
|
||||
case RIGHT:
|
||||
id = R.drawable.dpad_right;
|
||||
id_active = R.drawable.dpad_right_active;
|
||||
break;
|
||||
case UP:
|
||||
id = R.drawable.dpad_up;
|
||||
id_active = R.drawable.dpad_up_active;
|
||||
break;
|
||||
default:
|
||||
id = R.drawable.dpad_down;
|
||||
id_active = R.drawable.dpad_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.joystick_outer);
|
||||
|
||||
innerButton = new InnerButton(ctx);
|
||||
innerButton.setBackgroundColor(android.graphics.Color.TRANSPARENT);
|
||||
innerButton.setImageResource(R.drawable.joystick_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.joystick_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.joystick_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 © 2024 - Faiumoni e. V. *
|
||||
* Copyright © 2022 - Arianne *
|
||||
***************************************************************************
|
||||
***************************************************************************
|
||||
* *
|
||||
|
|
@ -9,31 +9,29 @@
|
|||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
package org.stendhalgame.client;
|
||||
package org.arianne.stendhal.client.js;
|
||||
|
||||
import android.webkit.JavascriptInterface;
|
||||
|
||||
|
||||
/**
|
||||
* Logger verbosity levels.
|
||||
*/
|
||||
public enum LogLevel {
|
||||
|
||||
// available verbosity levels
|
||||
INFO("INFO"),
|
||||
WARN("WARN"),
|
||||
ERROR("ERROR"),
|
||||
DEBUG("DEBUG");
|
||||
public class JSInterface {
|
||||
|
||||
/** String identifier. */
|
||||
public final String label;
|
||||
/** Singleton instance. */
|
||||
private static JSInterface instance;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new logging verbosity level.
|
||||
*
|
||||
* @param label
|
||||
* String identifier.
|
||||
* Retrieves the singleton instance.
|
||||
*/
|
||||
private LogLevel(final String label) {
|
||||
this.label = label;
|
||||
public static JSInterface get() {
|
||||
if (instance == null) {
|
||||
instance = new JSInterface();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
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_down.png
Normal file
|
After Width: | Height: | Size: 364 B |
BIN
android/app/src/main/res/drawable/dpad_down_active.png
Normal file
|
After Width: | Height: | Size: 375 B |
BIN
android/app/src/main/res/drawable/dpad_left.png
Normal file
|
After Width: | Height: | Size: 397 B |
BIN
android/app/src/main/res/drawable/dpad_left_active.png
Normal file
|
After Width: | Height: | Size: 411 B |
BIN
android/app/src/main/res/drawable/dpad_right.png
Normal file
|
After Width: | Height: | Size: 393 B |
BIN
android/app/src/main/res/drawable/dpad_right_active.png
Normal file
|
After Width: | Height: | Size: 400 B |
BIN
android/app/src/main/res/drawable/dpad_up.png
Normal file
|
After Width: | Height: | Size: 316 B |
BIN
android/app/src/main/res/drawable/dpad_up_active.png
Normal file
|
After Width: | Height: | Size: 324 B |
BIN
android/app/src/main/res/drawable/joystick_inner.png
Normal file
|
After Width: | Height: | Size: 472 B |
BIN
android/app/src/main/res/drawable/joystick_inner_active.png
Normal file
|
After Width: | Height: | Size: 482 B |
BIN
android/app/src/main/res/drawable/joystick_outer.png
Normal file
|
After Width: | Height: | Size: 1 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,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/
|
||||
|
|
@ -1 +0,0 @@
|
|||
en-US
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
Stendhal has a huge and rich world. You can explore cities, forest, mountains, plains and dungeons.
|
||||
You can fight monsters and become a hero.
|
||||
|
||||
You will meet a wide variety of characters. Many will give you tasks and quests for valuable
|
||||
experience. You may be asked to help protect land, feed the hungry, heal the sick, make someone
|
||||
happy, solve a puzzle or simply lend a hand.
|
||||
|
||||
So what are you waiting for? A whole new world awaits... And if you like, get the source code, and
|
||||
add your own ideas!
|
||||